ai-foreman 1.5.0 → 1.6.1
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 +8 -0
- package/dist/adapters/claude.d.ts +6 -0
- package/dist/adapters/claude.js +84 -9
- package/dist/adapters/recovering.d.ts +30 -0
- package/dist/adapters/recovering.js +83 -0
- package/dist/adapters/types.d.ts +13 -0
- package/dist/adapters/types.js +0 -3
- package/dist/agentRun.d.ts +3 -2
- package/dist/agentRun.js +89 -24
- package/dist/branch/git.d.ts +3 -0
- package/dist/branch/git.js +31 -5
- package/dist/branch/github.d.ts +2 -2
- package/dist/branch/github.js +2 -2
- package/dist/branch/gitlab.d.ts +3 -2
- package/dist/branch/gitlab.js +10 -4
- package/dist/branch/planner.d.ts +3 -1
- package/dist/branch/planner.js +24 -2
- package/dist/branch/runner.d.ts +10 -1
- package/dist/branch/runner.js +41 -13
- package/dist/branch/types.d.ts +4 -0
- package/dist/buildRuns.d.ts +35 -0
- package/dist/buildRuns.js +149 -0
- package/dist/cli/doctor.js +91 -4
- package/dist/cli/runtimeAuthPrompt.d.ts +3 -1
- package/dist/cli/runtimeAuthPrompt.js +25 -6
- package/dist/cli/start.js +208 -17
- package/dist/cli/status.d.ts +2 -0
- package/dist/cli/status.js +71 -55
- package/dist/cli/tickets.js +57 -10
- package/dist/foreman.d.ts +3 -1
- package/dist/foreman.js +52 -5
- package/dist/index.js +0 -0
- package/dist/interviews.d.ts +26 -4
- package/dist/interviews.js +71 -4
- package/dist/log.d.ts +1 -1
- package/dist/roles.d.ts +11 -0
- package/dist/roles.js +8 -0
- package/dist/runtimeAuth.d.ts +1 -1
- package/dist/runtimeAuth.js +10 -31
- package/dist/runtimeReadiness.d.ts +17 -0
- package/dist/runtimeReadiness.js +195 -0
- package/dist/ticketPlanning.d.ts +83 -0
- package/dist/ticketPlanning.js +287 -0
- package/dist/tickets/commands.js +16 -0
- package/dist/tickets/delivery.d.ts +38 -0
- package/dist/tickets/delivery.js +152 -0
- package/dist/tickets/importer.js +4 -0
- package/dist/tickets/renderMarkdown.js +9 -0
- package/dist/tickets/setupConfig.d.ts +8 -0
- package/dist/tickets/setupConfig.js +47 -2
- package/dist/tickets/sourceFetch.d.ts +24 -0
- package/dist/tickets/sourceFetch.js +246 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +24 -0
- package/dist/tickets/ticketLoader.js +18 -0
- package/dist/tickets/ticketSchema.d.ts +93 -0
- package/dist/tickets/ticketSchema.js +17 -0
- package/dist/tickets/validate.js +12 -0
- package/package.json +33 -2
package/dist/branch/github.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult, ReviewMergeStatus } from "./types.js";
|
|
1
|
+
import type { BranchPlanNode, GitHubFailure, GitHubOperationResult, GitHubReadinessResult, GitHubRemote, GitHubRemoteResult, PrResult, ReviewMergeStatus, MergeMethod } from "./types.js";
|
|
2
2
|
export declare function inspectGitHubRemote(cwd: string): GitHubRemoteResult;
|
|
3
3
|
export declare function originLooksLikeGitHub(cwd: string): boolean;
|
|
4
4
|
export declare function checkGitHubReadiness(cwd: string): GitHubReadinessResult;
|
|
@@ -17,7 +17,7 @@ export interface CreatePrOptions {
|
|
|
17
17
|
commit?: string;
|
|
18
18
|
}
|
|
19
19
|
export declare function createOrReusePr(cwd: string, opts: CreatePrOptions): PrResult;
|
|
20
|
-
export declare function enableGitHubAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
|
|
20
|
+
export declare function enableGitHubAutoMerge(cwd: string, branch: string, cleanup: boolean, method?: MergeMethod): PrResult;
|
|
21
21
|
export declare function checkGitHubPrMerged(cwd: string, branch: string): ReviewMergeStatus;
|
|
22
22
|
export declare function formatGitHubFailure(failure: GitHubFailure): string;
|
|
23
23
|
export {};
|
package/dist/branch/github.js
CHANGED
|
@@ -157,8 +157,8 @@ export function createOrReusePr(cwd, opts) {
|
|
|
157
157
|
return { status: "created", url: created.stdout };
|
|
158
158
|
return prFailure(classifyPrFailure(created, opts.node.branch, "Failed to create GitHub PR.", remoteForRepair));
|
|
159
159
|
}
|
|
160
|
-
export function enableGitHubAutoMerge(cwd, branch, cleanup) {
|
|
161
|
-
const args = ["pr", "merge", branch, "--auto",
|
|
160
|
+
export function enableGitHubAutoMerge(cwd, branch, cleanup, method = "squash") {
|
|
161
|
+
const args = ["pr", "merge", branch, "--auto", `--${method}`];
|
|
162
162
|
if (cleanup)
|
|
163
163
|
args.push("--delete-branch");
|
|
164
164
|
const result = runCommand(cwd, "gh", args, 30_000);
|
package/dist/branch/gitlab.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BranchPlanNode, GitHubOperationResult, PrResult, ReviewMergeStatus } from "./types.js";
|
|
1
|
+
import type { BranchPlanNode, GitHubOperationResult, PrResult, ReviewMergeStatus, MergeMethod } from "./types.js";
|
|
2
2
|
export declare function preflightGlab(cwd: string): GitHubOperationResult;
|
|
3
3
|
export declare function pushBranchForMr(cwd: string, branch: string): GitHubOperationResult;
|
|
4
4
|
export interface CreateMrOptions {
|
|
@@ -9,7 +9,8 @@ export interface CreateMrOptions {
|
|
|
9
9
|
commit?: string;
|
|
10
10
|
autoMerge?: boolean;
|
|
11
11
|
cleanup?: boolean;
|
|
12
|
+
mergeMethod?: MergeMethod;
|
|
12
13
|
}
|
|
13
14
|
export declare function createOrReuseMr(cwd: string, opts: CreateMrOptions): PrResult;
|
|
14
|
-
export declare function enableGitLabAutoMerge(cwd: string, branch: string, cleanup: boolean): PrResult;
|
|
15
|
+
export declare function enableGitLabAutoMerge(cwd: string, branch: string, cleanup: boolean, method?: MergeMethod): PrResult;
|
|
15
16
|
export declare function checkGitLabMrMerged(cwd: string, branch: string): ReviewMergeStatus;
|
package/dist/branch/gitlab.js
CHANGED
|
@@ -77,7 +77,9 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
77
77
|
if (!opts.ready)
|
|
78
78
|
args.push("--draft");
|
|
79
79
|
if (opts.autoMerge)
|
|
80
|
-
args.push("--auto-merge"
|
|
80
|
+
args.push("--auto-merge");
|
|
81
|
+
if (opts.mergeMethod === "squash")
|
|
82
|
+
args.push("--squash-before-merge");
|
|
81
83
|
if (opts.cleanup)
|
|
82
84
|
args.push("--remove-source-branch");
|
|
83
85
|
const created = runCommand(cwd, "glab", args);
|
|
@@ -88,15 +90,19 @@ export function createOrReuseMr(cwd, opts) {
|
|
|
88
90
|
"glab auth status",
|
|
89
91
|
]));
|
|
90
92
|
}
|
|
91
|
-
export function enableGitLabAutoMerge(cwd, branch, cleanup) {
|
|
92
|
-
const args = ["mr", "merge", branch, "--auto-merge", "--
|
|
93
|
+
export function enableGitLabAutoMerge(cwd, branch, cleanup, method = "squash") {
|
|
94
|
+
const args = ["mr", "merge", branch, "--auto-merge", "--yes"];
|
|
95
|
+
if (method === "squash")
|
|
96
|
+
args.push("--squash");
|
|
97
|
+
if (method === "rebase")
|
|
98
|
+
args.push("--rebase");
|
|
93
99
|
if (cleanup)
|
|
94
100
|
args.push("--remove-source-branch");
|
|
95
101
|
const result = runCommand(cwd, "glab", args, 30_000);
|
|
96
102
|
if (result.ok)
|
|
97
103
|
return { status: "auto_merge_enabled", url: result.stdout };
|
|
98
104
|
return prFailure(classifyCommandFailure(result, "mr_create_failed", `Failed to enable GitLab MR auto-merge. Branch: ${branch}`, [
|
|
99
|
-
`glab mr merge ${branch} --auto-merge --squash --yes`,
|
|
105
|
+
`glab mr merge ${branch} --auto-merge${method === "squash" ? " --squash" : method === "rebase" ? " --rebase" : ""} --yes`,
|
|
100
106
|
"glab auth status",
|
|
101
107
|
]));
|
|
102
108
|
}
|
package/dist/branch/planner.d.ts
CHANGED
|
@@ -13,8 +13,10 @@ export interface BuildBranchPlanOptions {
|
|
|
13
13
|
maxBranchDepth: number;
|
|
14
14
|
auditDependencies?: AuditDependency[];
|
|
15
15
|
rootBaseBranches?: boolean;
|
|
16
|
+
ticketIds?: string[];
|
|
16
17
|
}
|
|
17
18
|
export declare function buildBranchPlan(tickets: TicketDef[], states: Map<string, TicketState>, opts: BuildBranchPlanOptions): BranchPlan;
|
|
18
|
-
export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number): TicketDef[];
|
|
19
|
+
export declare function selectTicketsForBranchRun(tickets: TicketDef[], states: Map<string, TicketState>, steps: number, ticketIds?: string[]): TicketDef[];
|
|
20
|
+
export declare function applySharedDeliveryBranch(plan: BranchPlan, unitId: string, allRemainingTicketIds: string[], branchPrefix?: string): BranchPlan;
|
|
19
21
|
export declare function parseAuditDependencies(text: string): AuditDependency[];
|
|
20
22
|
export declare function buildBranchAuditInstruction(tickets: TicketDef[]): string;
|
package/dist/branch/planner.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export function buildBranchPlan(tickets, states, opts) {
|
|
2
|
-
const selected = selectTicketsForBranchRun(tickets, states, opts.steps);
|
|
2
|
+
const selected = selectTicketsForBranchRun(tickets, states, opts.steps, opts.ticketIds);
|
|
3
3
|
const selectedIds = new Set(selected.map((ticket) => ticket.id));
|
|
4
4
|
const issues = [];
|
|
5
5
|
const dependencyMap = new Map();
|
|
@@ -75,9 +75,12 @@ export function buildBranchPlan(tickets, states, opts) {
|
|
|
75
75
|
}
|
|
76
76
|
return { baseRef: opts.baseRef, nodes: selected.map((ticket) => nodeById.get(ticket.id)), issues };
|
|
77
77
|
}
|
|
78
|
-
export function selectTicketsForBranchRun(tickets, states, steps) {
|
|
78
|
+
export function selectTicketsForBranchRun(tickets, states, steps, ticketIds) {
|
|
79
|
+
const allowed = ticketIds ? new Set(ticketIds) : undefined;
|
|
79
80
|
const remaining = tickets
|
|
80
81
|
.filter((ticket) => {
|
|
82
|
+
if (allowed && !allowed.has(ticket.id))
|
|
83
|
+
return false;
|
|
81
84
|
const status = states.get(ticket.id)?.status ?? "planned";
|
|
82
85
|
return status !== "done" && status !== "canceled";
|
|
83
86
|
})
|
|
@@ -96,6 +99,25 @@ export function selectTicketsForBranchRun(tickets, states, steps) {
|
|
|
96
99
|
}
|
|
97
100
|
return selected;
|
|
98
101
|
}
|
|
102
|
+
export function applySharedDeliveryBranch(plan, unitId, allRemainingTicketIds, branchPrefix = "rafi") {
|
|
103
|
+
if (plan.nodes.length === 0)
|
|
104
|
+
return plan;
|
|
105
|
+
const slug = unitId.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "group";
|
|
106
|
+
const branch = `${branchPrefix.replace(/^\/+|\/+$/g, "") || "rafi"}/${slug}`;
|
|
107
|
+
const finalSelected = plan.nodes.at(-1)?.ticket.id;
|
|
108
|
+
const completesUnit = plan.nodes.length === allRemainingTicketIds.length
|
|
109
|
+
&& allRemainingTicketIds.every((id) => plan.nodes.some((node) => node.ticket.id === id));
|
|
110
|
+
return {
|
|
111
|
+
...plan,
|
|
112
|
+
nodes: plan.nodes.map((node) => ({
|
|
113
|
+
...node,
|
|
114
|
+
branch,
|
|
115
|
+
baseBranch: plan.baseRef,
|
|
116
|
+
deliveryUnitId: unitId,
|
|
117
|
+
deliveryUnitFinal: completesUnit && node.ticket.id === finalSelected,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
99
121
|
export function parseAuditDependencies(text) {
|
|
100
122
|
const deps = [];
|
|
101
123
|
for (const line of text.split(/\r?\n/)) {
|
package/dist/branch/runner.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { BuilderAdapter, EffortLevel } from "../adapters/types.js";
|
|
2
2
|
import type { Log } from "../log.js";
|
|
3
|
-
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, ReviewProvider } from "./types.js";
|
|
3
|
+
import type { BranchPlan, BranchPlanNode, BranchRunSummary, CompletionMode, MergeMethod, ReviewProvider } from "./types.js";
|
|
4
|
+
export interface DeliveryUnitSession {
|
|
5
|
+
unitId: string;
|
|
6
|
+
branch: string;
|
|
7
|
+
worktreePath: string;
|
|
8
|
+
sessionId: string;
|
|
9
|
+
ticket: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function readDeliveryUnitSession(projectDir: string, unitId: string): DeliveryUnitSession | undefined;
|
|
4
12
|
export interface BranchRunnerOptions {
|
|
5
13
|
projectDir: string;
|
|
6
14
|
runId: string;
|
|
@@ -20,6 +28,7 @@ export interface BranchRunnerOptions {
|
|
|
20
28
|
cleanupBranches?: boolean;
|
|
21
29
|
autoMergeWait?: boolean;
|
|
22
30
|
autoMergeTimeoutMinutes?: number | null;
|
|
31
|
+
mergeMethod?: MergeMethod;
|
|
23
32
|
allowedBaseDirtyPaths?: string[];
|
|
24
33
|
trackerPaths?: {
|
|
25
34
|
progressDoc: string;
|
package/dist/branch/runner.js
CHANGED
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { Foreman, MARKER_SPEC } from "../foreman.js";
|
|
2
2
|
import { fireNotification } from "../notify.js";
|
|
3
3
|
import { cmdBlock, cmdComplete, cmdUpdate } from "../tickets/commands.js";
|
|
4
|
-
import { commitAll, createTicketWorktree, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree,
|
|
4
|
+
import { commitAll, createTicketWorktree, findWorktreeForBranch, currentWorktreeBranch, deleteLocalBranch, ensureCleanBaseWorktree, ensureForemanExcluded, hasTrackerChanges, hasWorktreeChanges, headCommitIfAhead, removeTicketWorktree, mergeBranchToLocalBase, } from "./git.js";
|
|
5
5
|
import { checkGitHubPrMerged, createOrReusePr, enableGitHubAutoMerge, pushBranchForPr } from "./github.js";
|
|
6
6
|
import { checkGitLabMrMerged, createOrReuseMr, enableGitLabAutoMerge, pushBranchForMr } from "./gitlab.js";
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
export function readDeliveryUnitSession(projectDir, unitId) {
|
|
10
|
+
const path = deliverySessionPath(projectDir, unitId);
|
|
11
|
+
if (!existsSync(path))
|
|
12
|
+
return undefined;
|
|
13
|
+
try {
|
|
14
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
function deliverySessionPath(projectDir, unitId) {
|
|
21
|
+
return join(projectDir, ".foreman", "delivery-sessions", `${unitId}.json`);
|
|
22
|
+
}
|
|
7
23
|
export async function runBranchPlan(opts) {
|
|
8
24
|
ensureCleanBaseWorktree(opts.projectDir, { allowedDirtyPaths: opts.allowedBaseDirtyPaths });
|
|
9
25
|
ensureForemanExcluded(opts.projectDir);
|
|
@@ -29,6 +45,9 @@ export async function runBranchPlan(opts) {
|
|
|
29
45
|
if (opts.plan.issues.some((issue) => issue.blocking))
|
|
30
46
|
return summaries;
|
|
31
47
|
for (const node of orderNodes(opts.plan.nodes)) {
|
|
48
|
+
const sharedUnit = Boolean(node.deliveryUnitId);
|
|
49
|
+
const completesSharedUnit = !sharedUnit || Boolean(node.deliveryUnitFinal);
|
|
50
|
+
const createsReviewForNode = createsReview && completesSharedUnit;
|
|
32
51
|
const missingDependency = node.dependencies.find((dep) => !successfulBranches.has(dep));
|
|
33
52
|
if (missingDependency) {
|
|
34
53
|
const detail = `dependency ${missingDependency} did not complete`;
|
|
@@ -42,7 +61,7 @@ export async function runBranchPlan(opts) {
|
|
|
42
61
|
notifyIssue(opts.notificationsEnabled, detail);
|
|
43
62
|
continue;
|
|
44
63
|
}
|
|
45
|
-
if (createsReview) {
|
|
64
|
+
if (createsReview && !sharedUnit) {
|
|
46
65
|
const missingPush = node.dependencies.find((dep) => {
|
|
47
66
|
const depNode = opts.plan.nodes.find((candidate) => candidate.ticket.id === dep);
|
|
48
67
|
return depNode && !pushedBranches.has(depNode.branch);
|
|
@@ -60,7 +79,7 @@ export async function runBranchPlan(opts) {
|
|
|
60
79
|
continue;
|
|
61
80
|
}
|
|
62
81
|
}
|
|
63
|
-
if (completionMode === "auto-merge" && node.dependencies.length > 0) {
|
|
82
|
+
if (completionMode === "auto-merge" && node.dependencies.length > 0 && !sharedUnit) {
|
|
64
83
|
const mergeReadiness = await waitForAutoMergeDependencies(opts, node, reviewProvider);
|
|
65
84
|
if (!mergeReadiness.ok) {
|
|
66
85
|
summaries.push(summaryFor(node, "skipped", mergeReadiness.message, "skipped"));
|
|
@@ -85,6 +104,7 @@ export async function runBranchPlan(opts) {
|
|
|
85
104
|
});
|
|
86
105
|
const resumeSession = opts.resumeSessions?.get(node.ticket.id);
|
|
87
106
|
const worktreePath = resumeSession?.worktreePath
|
|
107
|
+
?? (sharedUnit ? findWorktreeForBranch(opts.projectDir, node.branch) : undefined)
|
|
88
108
|
?? createTicketWorktree(opts.projectDir, opts.runId, node.branch, node.baseBranch);
|
|
89
109
|
node.worktreePath = worktreePath;
|
|
90
110
|
let builder;
|
|
@@ -127,6 +147,11 @@ export async function runBranchPlan(opts) {
|
|
|
127
147
|
prReady: opts.prReady,
|
|
128
148
|
keepWorktrees: opts.keepWorktrees,
|
|
129
149
|
});
|
|
150
|
+
if (node.deliveryUnitId) {
|
|
151
|
+
const path = deliverySessionPath(opts.projectDir, node.deliveryUnitId);
|
|
152
|
+
mkdirSync(join(opts.projectDir, ".foreman", "delivery-sessions"), { recursive: true });
|
|
153
|
+
writeFileSync(path, `${JSON.stringify({ unitId: node.deliveryUnitId, branch: node.branch, worktreePath, sessionId, ticket: node.ticket.id }, null, 2)}\n`, "utf8");
|
|
154
|
+
}
|
|
130
155
|
}
|
|
131
156
|
opts.log.write("step", {
|
|
132
157
|
index: summaries.length + 1,
|
|
@@ -180,12 +205,12 @@ export async function runBranchPlan(opts) {
|
|
|
180
205
|
if (hasWorktreeChanges(worktreePath)) {
|
|
181
206
|
commit = commitAll(worktreePath, `${node.ticket.id}: ${node.ticket.title}`);
|
|
182
207
|
}
|
|
183
|
-
if (!commit && (
|
|
208
|
+
if (!commit && (createsReviewForNode || (completionMode === "direct-merge" && completesSharedUnit))) {
|
|
184
209
|
commit = headCommitIfAhead(worktreePath, node.baseBranch);
|
|
185
210
|
}
|
|
186
211
|
const summary = summaryFor(node, "done", commit ? undefined : "no_changes");
|
|
187
212
|
summary.commit = commit;
|
|
188
|
-
if (commit &&
|
|
213
|
+
if (commit && createsReviewForNode) {
|
|
189
214
|
const push = reviewProvider === "gitlab"
|
|
190
215
|
? pushBranchForMr(worktreePath, node.branch)
|
|
191
216
|
: pushBranchForPr(worktreePath, node.branch);
|
|
@@ -226,8 +251,9 @@ export async function runBranchPlan(opts) {
|
|
|
226
251
|
runId: opts.runId,
|
|
227
252
|
qaEvidence: qaSummary,
|
|
228
253
|
commit,
|
|
229
|
-
autoMerge:
|
|
254
|
+
autoMerge: false,
|
|
230
255
|
cleanup: opts.cleanupBranches ?? true,
|
|
256
|
+
mergeMethod: opts.mergeMethod ?? "squash",
|
|
231
257
|
})
|
|
232
258
|
: createOrReusePr(opts.projectDir, {
|
|
233
259
|
node,
|
|
@@ -248,7 +274,7 @@ export async function runBranchPlan(opts) {
|
|
|
248
274
|
continue;
|
|
249
275
|
}
|
|
250
276
|
if (completionMode === "auto-merge" && reviewProvider === "github") {
|
|
251
|
-
const autoMerge = enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
|
|
277
|
+
const autoMerge = enableGitHubAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
252
278
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
253
279
|
if (autoMerge.status === "failed") {
|
|
254
280
|
opts.log.write("pr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -258,8 +284,8 @@ export async function runBranchPlan(opts) {
|
|
|
258
284
|
}
|
|
259
285
|
opts.log.write("pr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
260
286
|
}
|
|
261
|
-
else if (completionMode === "auto-merge" && reviewProvider === "gitlab"
|
|
262
|
-
const autoMerge = enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true);
|
|
287
|
+
else if (completionMode === "auto-merge" && reviewProvider === "gitlab") {
|
|
288
|
+
const autoMerge = enableGitLabAutoMerge(opts.projectDir, node.branch, opts.cleanupBranches ?? true, opts.mergeMethod ?? "squash");
|
|
263
289
|
summary.pr = autoMerge.status === "failed" ? autoMerge : { ...pr, status: "auto_merge_enabled", url: autoMerge.url ?? pr.url };
|
|
264
290
|
if (autoMerge.status === "failed") {
|
|
265
291
|
opts.log.write("mr-auto-merge-failed", failureLogFields(node, autoMerge));
|
|
@@ -270,14 +296,14 @@ export async function runBranchPlan(opts) {
|
|
|
270
296
|
opts.log.write("mr-auto-merge-enabled", { ticket: node.ticket.id, branch: node.branch, url: summary.pr.url });
|
|
271
297
|
}
|
|
272
298
|
}
|
|
273
|
-
else if (
|
|
299
|
+
else if (createsReviewForNode) {
|
|
274
300
|
summary.pushStatus = "skipped";
|
|
275
301
|
summary.pr = { status: "skipped", error: commit ? undefined : "no_changes" };
|
|
276
302
|
}
|
|
277
|
-
else if (commit && completionMode === "direct-merge") {
|
|
303
|
+
else if (commit && completionMode === "direct-merge" && completesSharedUnit) {
|
|
278
304
|
if (!opts.keepWorktrees)
|
|
279
305
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
280
|
-
const mergeCommit =
|
|
306
|
+
const mergeCommit = mergeBranchToLocalBase(opts.projectDir, node.branch, node.baseBranch, `${node.ticket.id}: ${node.ticket.title}`, opts.mergeMethod ?? "squash");
|
|
281
307
|
summary.pr = { status: "merged", url: mergeCommit };
|
|
282
308
|
opts.log.write("branch-direct-merge", {
|
|
283
309
|
ticket: node.ticket.id,
|
|
@@ -304,7 +330,9 @@ export async function runBranchPlan(opts) {
|
|
|
304
330
|
status: "done",
|
|
305
331
|
detail: summary.detail,
|
|
306
332
|
});
|
|
307
|
-
if (
|
|
333
|
+
if (node.deliveryUnitId && node.deliveryUnitFinal)
|
|
334
|
+
rmSync(deliverySessionPath(opts.projectDir, node.deliveryUnitId), { force: true });
|
|
335
|
+
if (!opts.keepWorktrees && completionMode !== "direct-merge" && completesSharedUnit)
|
|
308
336
|
removeTicketWorktree(opts.projectDir, worktreePath);
|
|
309
337
|
}
|
|
310
338
|
catch (err) {
|
package/dist/branch/types.d.ts
CHANGED
|
@@ -7,6 +7,9 @@ export interface BranchPlanNode {
|
|
|
7
7
|
dependencies: string[];
|
|
8
8
|
depth: number;
|
|
9
9
|
worktreePath?: string;
|
|
10
|
+
/** Shared delivery units intentionally reuse one isolated branch/worktree. */
|
|
11
|
+
deliveryUnitId?: string;
|
|
12
|
+
deliveryUnitFinal?: boolean;
|
|
10
13
|
}
|
|
11
14
|
export interface BranchIssue {
|
|
12
15
|
ticket?: string;
|
|
@@ -56,6 +59,7 @@ export interface BranchPlan {
|
|
|
56
59
|
}
|
|
57
60
|
export type CompletionMode = "none" | "pr" | "auto-merge" | "direct-merge";
|
|
58
61
|
export type ReviewProvider = "github" | "gitlab";
|
|
62
|
+
export type MergeMethod = "squash" | "merge" | "rebase";
|
|
59
63
|
export interface PrResult {
|
|
60
64
|
status: "created" | "existing" | "failed" | "skipped" | "auto_merge_enabled" | "merged";
|
|
61
65
|
url?: string;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { BuildRunRecordV1, ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
+
export declare const BUILD_RUN_VERSION = 1;
|
|
3
|
+
export declare const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
4
|
+
export declare const BUILD_HEARTBEAT_MS = 10000;
|
|
5
|
+
export declare const BUILD_LEASE_STALE_MS = 45000;
|
|
6
|
+
export interface CreateBuildRunInput {
|
|
7
|
+
tickets: string[];
|
|
8
|
+
deliveryUnit?: string;
|
|
9
|
+
branchMode?: BuildRunRecordV1["branchMode"];
|
|
10
|
+
repositoryRoot: string;
|
|
11
|
+
worktree?: string;
|
|
12
|
+
branch?: string;
|
|
13
|
+
baseHead?: string;
|
|
14
|
+
startHead?: string;
|
|
15
|
+
builder?: ResolvedAgentSettings;
|
|
16
|
+
qa?: ResolvedAgentSettings;
|
|
17
|
+
now?: Date;
|
|
18
|
+
}
|
|
19
|
+
export declare function createBuildRun(input: CreateBuildRunInput): BuildRunRecordV1;
|
|
20
|
+
export declare function saveBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
21
|
+
export declare function checkpointBuildRun(projectDir: string, run: BuildRunRecordV1, checkpoint: string, patch?: Partial<BuildRunRecordV1>): BuildRunRecordV1;
|
|
22
|
+
export declare function persistBuildSession(projectDir: string, run: BuildRunRecordV1, role: "builder" | "qa", sessionId: string): BuildRunRecordV1;
|
|
23
|
+
export declare function recordBuildReceipt(projectDir: string, run: BuildRunRecordV1, operationId: string, detail?: {
|
|
24
|
+
externalId?: string;
|
|
25
|
+
detail?: string;
|
|
26
|
+
}): BuildRunRecordV1;
|
|
27
|
+
export declare function heartbeatBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
28
|
+
export declare function releaseBuildLease(projectDir: string, run: BuildRunRecordV1, status?: BuildRunRecordV1["status"]): BuildRunRecordV1;
|
|
29
|
+
export declare function completeBuildRun(projectDir: string, run: BuildRunRecordV1, now?: Date): BuildRunRecordV1;
|
|
30
|
+
export declare function readBuildRuns(projectDir: string): BuildRunRecordV1[];
|
|
31
|
+
export declare function recoverableBuildRuns(projectDir: string, now?: Date): Array<BuildRunRecordV1 & {
|
|
32
|
+
active: boolean;
|
|
33
|
+
}>;
|
|
34
|
+
export declare function isLeaseActive(run: BuildRunRecordV1, now?: Date): boolean;
|
|
35
|
+
export declare function buildRecoveryPreview(run: BuildRunRecordV1): string[];
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { hostname } from "node:os";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
export const BUILD_RUN_VERSION = 1;
|
|
6
|
+
export const BUILD_RUN_DIRECTORY = ".foreman/runs";
|
|
7
|
+
export const BUILD_HEARTBEAT_MS = 10_000;
|
|
8
|
+
export const BUILD_LEASE_STALE_MS = 45_000;
|
|
9
|
+
export function createBuildRun(input) {
|
|
10
|
+
const now = input.now ?? new Date();
|
|
11
|
+
const stamp = now.toISOString();
|
|
12
|
+
const run = {
|
|
13
|
+
version: 1,
|
|
14
|
+
runId: randomUUID(),
|
|
15
|
+
status: "running",
|
|
16
|
+
tickets: [...input.tickets],
|
|
17
|
+
deliveryUnit: input.deliveryUnit,
|
|
18
|
+
branchMode: input.branchMode ?? "current",
|
|
19
|
+
checkpoint: "created",
|
|
20
|
+
currentTicket: input.tickets[0],
|
|
21
|
+
builder: input.builder ? { settings: input.builder } : undefined,
|
|
22
|
+
qa: input.qa ? { settings: input.qa } : undefined,
|
|
23
|
+
repository: {
|
|
24
|
+
root: resolve(input.repositoryRoot),
|
|
25
|
+
worktree: resolve(input.worktree ?? input.repositoryRoot),
|
|
26
|
+
branch: input.branch,
|
|
27
|
+
baseHead: input.baseHead,
|
|
28
|
+
startHead: input.startHead,
|
|
29
|
+
partialFingerprint: fingerprintWorkingFiles(input.worktree ?? input.repositoryRoot),
|
|
30
|
+
},
|
|
31
|
+
receipts: {},
|
|
32
|
+
lease: currentLease(now),
|
|
33
|
+
createdAt: stamp,
|
|
34
|
+
updatedAt: stamp,
|
|
35
|
+
};
|
|
36
|
+
return saveBuildRun(input.repositoryRoot, run, now);
|
|
37
|
+
}
|
|
38
|
+
export function saveBuildRun(projectDir, run, now = new Date()) {
|
|
39
|
+
validateBuildRun(run);
|
|
40
|
+
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
41
|
+
mkdirSync(directory, { recursive: true });
|
|
42
|
+
const next = { ...run, updatedAt: now.toISOString() };
|
|
43
|
+
const target = join(directory, `${run.runId}.json`);
|
|
44
|
+
const temp = `${target}.${process.pid}.${randomUUID()}.tmp`;
|
|
45
|
+
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
46
|
+
renameSync(temp, target);
|
|
47
|
+
return next;
|
|
48
|
+
}
|
|
49
|
+
export function checkpointBuildRun(projectDir, run, checkpoint, patch = {}) {
|
|
50
|
+
return saveBuildRun(projectDir, { ...run, ...patch, checkpoint, receipts: patch.receipts ?? run.receipts });
|
|
51
|
+
}
|
|
52
|
+
export function persistBuildSession(projectDir, run, role, sessionId) {
|
|
53
|
+
const current = run[role];
|
|
54
|
+
if (!current)
|
|
55
|
+
throw new Error(`${role} settings were not captured for run ${run.runId}`);
|
|
56
|
+
return checkpointBuildRun(projectDir, { ...run, [role]: { ...current, sessionId } }, `${role}-session-ready`);
|
|
57
|
+
}
|
|
58
|
+
export function recordBuildReceipt(projectDir, run, operationId, detail) {
|
|
59
|
+
if (run.receipts[operationId])
|
|
60
|
+
return run;
|
|
61
|
+
return saveBuildRun(projectDir, {
|
|
62
|
+
...run,
|
|
63
|
+
receipts: { ...run.receipts, [operationId]: { completedAt: new Date().toISOString(), ...detail } },
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export function heartbeatBuildRun(projectDir, run, now = new Date()) {
|
|
67
|
+
return saveBuildRun(projectDir, { ...run, lease: { ...(run.lease ?? currentLease(now)), heartbeatAt: now.toISOString() } }, now);
|
|
68
|
+
}
|
|
69
|
+
export function releaseBuildLease(projectDir, run, status = "interrupted") {
|
|
70
|
+
return saveBuildRun(projectDir, { ...run, status, lease: undefined });
|
|
71
|
+
}
|
|
72
|
+
export function completeBuildRun(projectDir, run, now = new Date()) {
|
|
73
|
+
return saveBuildRun(projectDir, { ...run, status: "completed", checkpoint: "complete", lease: undefined, completedAt: now.toISOString() }, now);
|
|
74
|
+
}
|
|
75
|
+
export function readBuildRuns(projectDir) {
|
|
76
|
+
const directory = join(resolve(projectDir), BUILD_RUN_DIRECTORY);
|
|
77
|
+
if (!existsSync(directory))
|
|
78
|
+
return [];
|
|
79
|
+
return readdirSync(directory).filter((name) => name.endsWith(".json")).flatMap((name) => {
|
|
80
|
+
try {
|
|
81
|
+
const run = JSON.parse(readFileSync(join(directory, name), "utf8"));
|
|
82
|
+
validateBuildRun(run);
|
|
83
|
+
return [run];
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
}).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
89
|
+
}
|
|
90
|
+
export function recoverableBuildRuns(projectDir, now = new Date()) {
|
|
91
|
+
return readBuildRuns(projectDir)
|
|
92
|
+
.filter((run) => run.status !== "completed")
|
|
93
|
+
.map((run) => ({ ...run, active: isLeaseActive(run, now) }));
|
|
94
|
+
}
|
|
95
|
+
export function isLeaseActive(run, now = new Date()) {
|
|
96
|
+
if (!run.lease)
|
|
97
|
+
return false;
|
|
98
|
+
const age = now.getTime() - new Date(run.lease.heartbeatAt).getTime();
|
|
99
|
+
if (age > BUILD_LEASE_STALE_MS)
|
|
100
|
+
return false;
|
|
101
|
+
if (run.lease.hostname !== hostname())
|
|
102
|
+
return true;
|
|
103
|
+
try {
|
|
104
|
+
process.kill(run.lease.pid, 0);
|
|
105
|
+
return run.lease.processStart === processStartIdentity(run.lease.pid);
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
export function buildRecoveryPreview(run) {
|
|
112
|
+
const completed = Object.keys(run.receipts).sort();
|
|
113
|
+
return [
|
|
114
|
+
`run ${run.runId}`,
|
|
115
|
+
`ticket ${run.currentTicket ?? run.tickets[0] ?? "unknown"}`,
|
|
116
|
+
`checkpoint ${run.checkpoint}`,
|
|
117
|
+
`worktree ${run.repository.worktree}`,
|
|
118
|
+
`branch ${run.repository.branch ?? "current"}`,
|
|
119
|
+
`partial files fingerprint ${run.repository.partialFingerprint ?? "unavailable"}`,
|
|
120
|
+
`completed operations ${completed.length ? completed.join(", ") : "none"}`,
|
|
121
|
+
`session ${run.builder?.sessionId ? "exact Builder session available" : "fresh Builder session required"}`,
|
|
122
|
+
`QA session ${run.qa?.sessionId ? "exact QA session available" : "fresh QA session required"}`,
|
|
123
|
+
];
|
|
124
|
+
}
|
|
125
|
+
function currentLease(now) {
|
|
126
|
+
return { hostname: hostname(), pid: process.pid, processStart: processStartIdentity(process.pid), heartbeatAt: now.toISOString() };
|
|
127
|
+
}
|
|
128
|
+
function processStartIdentity(pid) {
|
|
129
|
+
try {
|
|
130
|
+
return statSync(`/proc/${pid}`).birthtimeMs.toString();
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
return pid === process.pid ? `${Date.now() - Math.round(process.uptime() * 1000)}` : "unknown";
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function fingerprintWorkingFiles(worktree) {
|
|
137
|
+
try {
|
|
138
|
+
const stat = statSync(resolve(worktree));
|
|
139
|
+
return createHash("sha256").update(`${stat.dev}:${stat.ino}:${stat.mtimeMs}`).digest("hex");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function validateBuildRun(run) {
|
|
146
|
+
if (run.version !== BUILD_RUN_VERSION || !run.runId || !run.repository?.root || !run.repository.worktree || !run.createdAt || !run.updatedAt) {
|
|
147
|
+
throw new Error("invalid build run record");
|
|
148
|
+
}
|
|
149
|
+
}
|
package/dist/cli/doctor.js
CHANGED
|
@@ -6,6 +6,8 @@ import { loadConfig } from "../config.js";
|
|
|
6
6
|
import { isTicketsInitialized } from "../tickets/config.js";
|
|
7
7
|
import { cmdValidate } from "../tickets/commands.js";
|
|
8
8
|
import { checkGitHubReadiness, inspectGitHubRemote } from "../branch/github.js";
|
|
9
|
+
import { requireClaudeSDK } from "../adapters/claude.js";
|
|
10
|
+
import { resolveExecutablePath, sanitizeDiagnostics } from "../runtimeReadiness.js";
|
|
9
11
|
const PACKAGE_VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8"))?.version;
|
|
10
12
|
function commandVersion(command) {
|
|
11
13
|
const result = spawnSync(command, ["--version"], {
|
|
@@ -23,7 +25,8 @@ export function buildDoctorCommand() {
|
|
|
23
25
|
.description("Check Foreman, agent CLIs, config, and optional ticket tracker readiness.")
|
|
24
26
|
.argument("[project]", "path to the project directory", ".")
|
|
25
27
|
.option("--github", "run GitHub PR readiness checks")
|
|
26
|
-
.
|
|
28
|
+
.option("--live-claude", "run a bounded no-tools Claude adapter request (uses account quota)")
|
|
29
|
+
.action(async (project, opts) => {
|
|
27
30
|
const cwd = resolve(project);
|
|
28
31
|
let errors = 0;
|
|
29
32
|
const report = (ok, label, detail) => {
|
|
@@ -46,11 +49,37 @@ export function buildDoctorCommand() {
|
|
|
46
49
|
catch (err) {
|
|
47
50
|
report(false, "foreman.yaml", err instanceof Error ? err.message : String(err));
|
|
48
51
|
}
|
|
49
|
-
const
|
|
52
|
+
const claudeExecutable = resolveExecutablePath("claude");
|
|
53
|
+
const claude = claudeExecutable ? commandVersion(claudeExecutable) : { ok: false };
|
|
50
54
|
if (claude.ok)
|
|
51
|
-
warn("claude CLI found", claude.detail);
|
|
55
|
+
warn("claude CLI found", `${claudeExecutable} (${claude.detail})`);
|
|
52
56
|
else
|
|
53
|
-
warn("claude CLI not found", "Claude
|
|
57
|
+
warn("claude CLI not found", "required for Claude runs; Rafi never falls back to the SDK-bundled binary");
|
|
58
|
+
try {
|
|
59
|
+
await requireClaudeSDK();
|
|
60
|
+
warn("Rafi Claude SDK wrapper", "available; execution uses the system Claude CLI above");
|
|
61
|
+
}
|
|
62
|
+
catch (err) {
|
|
63
|
+
report(false, "Rafi Claude SDK wrapper", err instanceof Error ? err.message.split("\n")[0] : String(err));
|
|
64
|
+
}
|
|
65
|
+
warn("Claude setting sources", "user, project, local, and managed policy");
|
|
66
|
+
const relevantEnvironment = Object.keys(process.env)
|
|
67
|
+
.filter((name) => /^(ANTHROPIC|CLAUDE|HTTP_PROXY|HTTPS_PROXY|NO_PROXY|SSL_CERT_FILE|NODE_EXTRA_CA_CERTS)(_|$)/i.test(name))
|
|
68
|
+
.sort();
|
|
69
|
+
warn("Claude environment names", relevantEnvironment.length > 0 ? relevantEnvironment.join(", ") : "none set");
|
|
70
|
+
const projectSdkRemoval = projectClaudeSdkRemoval(cwd);
|
|
71
|
+
if (projectSdkRemoval) {
|
|
72
|
+
warn("project-local Claude Agent SDK", `Rafi does not use this dependency; keep it if the application does, otherwise: ${projectSdkRemoval}`);
|
|
73
|
+
}
|
|
74
|
+
if (opts.liveClaude) {
|
|
75
|
+
if (!claudeExecutable) {
|
|
76
|
+
report(false, "live Claude adapter", "system Claude executable is missing");
|
|
77
|
+
}
|
|
78
|
+
else {
|
|
79
|
+
const live = await liveClaudeSmoke(cwd, claudeExecutable);
|
|
80
|
+
report(live.ok, "live Claude adapter", live.detail);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
54
83
|
const codex = commandVersion("codex");
|
|
55
84
|
if (codex.ok)
|
|
56
85
|
warn("codex CLI found", codex.detail);
|
|
@@ -91,6 +120,64 @@ export function buildDoctorCommand() {
|
|
|
91
120
|
process.exit(errors === 0 ? 0 : 1);
|
|
92
121
|
});
|
|
93
122
|
}
|
|
123
|
+
async function liveClaudeSmoke(cwd, executable) {
|
|
124
|
+
const abortController = new AbortController();
|
|
125
|
+
const timer = setTimeout(() => abortController.abort(), 45_000);
|
|
126
|
+
timer.unref();
|
|
127
|
+
try {
|
|
128
|
+
const { query } = await requireClaudeSDK();
|
|
129
|
+
const queryInstance = query({
|
|
130
|
+
prompt: "Return exactly OK",
|
|
131
|
+
options: {
|
|
132
|
+
cwd,
|
|
133
|
+
pathToClaudeCodeExecutable: executable,
|
|
134
|
+
settingSources: ["user", "project", "local"],
|
|
135
|
+
tools: [],
|
|
136
|
+
permissionMode: "dontAsk",
|
|
137
|
+
persistSession: false,
|
|
138
|
+
abortController,
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
for await (const message of queryInstance) {
|
|
142
|
+
if (message.type !== "result")
|
|
143
|
+
continue;
|
|
144
|
+
const output = "result" in message ? message.result : message.errors.join("; ");
|
|
145
|
+
return message.is_error
|
|
146
|
+
? { ok: false, detail: sanitizeDiagnostics(output || "Claude returned an error") }
|
|
147
|
+
: { ok: output.trim().toUpperCase() === "OK", detail: sanitizeDiagnostics(output) };
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, detail: "Claude adapter ended without a result" };
|
|
150
|
+
}
|
|
151
|
+
catch (err) {
|
|
152
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
153
|
+
return { ok: false, detail: abortController.signal.aborted ? "timed out after 45 seconds" : sanitizeDiagnostics(message) };
|
|
154
|
+
}
|
|
155
|
+
finally {
|
|
156
|
+
clearTimeout(timer);
|
|
157
|
+
abortController.abort();
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
function projectClaudeSdkRemoval(cwd) {
|
|
161
|
+
const packagePath = join(cwd, "package.json");
|
|
162
|
+
if (!existsSync(packagePath))
|
|
163
|
+
return undefined;
|
|
164
|
+
try {
|
|
165
|
+
const pkg = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
166
|
+
const name = "@anthropic-ai/claude-agent-sdk";
|
|
167
|
+
if (!pkg.dependencies?.[name] && !pkg.devDependencies?.[name] && !pkg.optionalDependencies?.[name])
|
|
168
|
+
return undefined;
|
|
169
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
170
|
+
return `pnpm remove ${name}`;
|
|
171
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
172
|
+
return `yarn remove ${name}`;
|
|
173
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
|
|
174
|
+
return `bun remove ${name}`;
|
|
175
|
+
return `npm uninstall ${name}`;
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
return undefined;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
94
181
|
function truncateOneLine(value) {
|
|
95
182
|
const compact = value.replace(/\s+/g, " ").trim();
|
|
96
183
|
return compact.length <= 180 ? compact : `${compact.slice(0, 180).trimEnd()}...`;
|