@backburner/cli 0.1.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 +329 -0
- package/dist/src/agents/antigravity-launch.js +162 -0
- package/dist/src/agents/antigravity.js +251 -0
- package/dist/src/agents/claude-launch.js +117 -0
- package/dist/src/agents/claude.js +271 -0
- package/dist/src/agents/codex-launch.js +82 -0
- package/dist/src/agents/codex.js +261 -0
- package/dist/src/agents/composite-runner.js +13 -0
- package/dist/src/agents/extraction.js +442 -0
- package/dist/src/agents/failure-feedback.js +132 -0
- package/dist/src/agents/gemini-launch.js +96 -0
- package/dist/src/agents/gemini.js +392 -0
- package/dist/src/agents/job-agent-assignments.js +180 -0
- package/dist/src/agents/lifecycle-feedback.js +147 -0
- package/dist/src/agents/limit-detector.js +83 -0
- package/dist/src/agents/opencode-launch.js +138 -0
- package/dist/src/agents/opencode.js +156 -0
- package/dist/src/agents/plan-breakdown-feedback.js +97 -0
- package/dist/src/agents/pool.js +168 -0
- package/dist/src/agents/prompt.js +462 -0
- package/dist/src/agents/provider-sessions.js +105 -0
- package/dist/src/agents/review-feedback.js +255 -0
- package/dist/src/agents/selection.js +113 -0
- package/dist/src/agents/service.js +1087 -0
- package/dist/src/agents/types.js +1 -0
- package/dist/src/agents/wave-assessment-feedback.js +69 -0
- package/dist/src/attention/derive.js +337 -0
- package/dist/src/capabilities/index.js +2 -0
- package/dist/src/capabilities/projection.js +183 -0
- package/dist/src/capabilities/types.js +1 -0
- package/dist/src/capabilities/validator.js +87 -0
- package/dist/src/cli/commands/broker-smoke.js +96 -0
- package/dist/src/cli/commands/broker.js +56 -0
- package/dist/src/cli/commands/init.js +4 -0
- package/dist/src/cli/commands/journal.js +43 -0
- package/dist/src/cli/commands/run-loop.js +56 -0
- package/dist/src/cli/commands/run.js +1241 -0
- package/dist/src/cli/commands/tui.js +187 -0
- package/dist/src/cli/dispatcher.js +51 -0
- package/dist/src/cli/errors.js +2 -0
- package/dist/src/cli/init.js +92 -0
- package/dist/src/cli/options.js +86 -0
- package/dist/src/cli/output.js +231 -0
- package/dist/src/cli/run-journal.js +342 -0
- package/dist/src/cli/run-types.js +1 -0
- package/dist/src/cli/run.js +32 -0
- package/dist/src/cli/runtime/dispatch-lifecycle.js +772 -0
- package/dist/src/cli/runtime/provider-runtime-types.js +1 -0
- package/dist/src/cli/runtime/provider-runtime.js +152 -0
- package/dist/src/cli/runtime/provider-tools.js +73 -0
- package/dist/src/cli/runtime/providers/antigravity.js +26 -0
- package/dist/src/cli/runtime/providers/claude.js +20 -0
- package/dist/src/cli/runtime/providers/codex.js +20 -0
- package/dist/src/cli/runtime/providers/gemini.js +20 -0
- package/dist/src/cli/runtime/providers/opencode.js +20 -0
- package/dist/src/cli/runtime/run-context.js +131 -0
- package/dist/src/cli/tui/index.js +3 -0
- package/dist/src/cli/tui/screen.js +388 -0
- package/dist/src/cli/tui/tracker.js +558 -0
- package/dist/src/cli/tui/types.js +1 -0
- package/dist/src/cli/tui/views.js +1006 -0
- package/dist/src/config/loader.js +136 -0
- package/dist/src/config/schemas.js +403 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/context/types.js +331 -0
- package/dist/src/domain/entities.js +1 -0
- package/dist/src/git/backburner-git-tool-service.js +680 -0
- package/dist/src/git/gitops-lifecycle.js +56 -0
- package/dist/src/git/merge-prep-workspace.js +357 -0
- package/dist/src/git/types.js +484 -0
- package/dist/src/github/broker-model-content.js +215 -0
- package/dist/src/github/broker-server.js +237 -0
- package/dist/src/github/broker.js +638 -0
- package/dist/src/github/composed.js +53 -0
- package/dist/src/github/gateway.js +478 -0
- package/dist/src/github/normalize.js +222 -0
- package/dist/src/github/pr-classification.js +73 -0
- package/dist/src/github/scoped-broker.js +35 -0
- package/dist/src/github/security.js +126 -0
- package/dist/src/github/service.js +332 -0
- package/dist/src/github/types.js +1 -0
- package/dist/src/github/utils.js +16 -0
- package/dist/src/journal/index.js +4 -0
- package/dist/src/journal/reader.js +62 -0
- package/dist/src/journal/renderer.js +50 -0
- package/dist/src/journal/service.js +72 -0
- package/dist/src/journal/types.js +1 -0
- package/dist/src/journal/writer.js +19 -0
- package/dist/src/mcps/bridge.js +166 -0
- package/dist/src/mcps/index.js +5 -0
- package/dist/src/mcps/reconcile.js +113 -0
- package/dist/src/mcps/smithery-client.js +186 -0
- package/dist/src/mcps/smithery.js +164 -0
- package/dist/src/mcps/types.js +1 -0
- package/dist/src/memory/json-store.js +71 -0
- package/dist/src/memory/prompt.js +16 -0
- package/dist/src/memory/scope.js +31 -0
- package/dist/src/memory/store.js +1 -0
- package/dist/src/memory/tool-service.js +142 -0
- package/dist/src/memory/types.js +12 -0
- package/dist/src/onboarding/config-writer.js +96 -0
- package/dist/src/onboarding/engine.js +86 -0
- package/dist/src/onboarding/prompt.js +102 -0
- package/dist/src/onboarding/services/provider-discovery.js +58 -0
- package/dist/src/onboarding/services/repo-discovery.js +246 -0
- package/dist/src/onboarding/steps/check-github-auth.js +32 -0
- package/dist/src/onboarding/steps/check-requirements.js +34 -0
- package/dist/src/onboarding/steps/configure-models.js +105 -0
- package/dist/src/onboarding/steps/configure-paths.js +33 -0
- package/dist/src/onboarding/steps/discover-providers.js +38 -0
- package/dist/src/onboarding/steps/discover-repos.js +39 -0
- package/dist/src/onboarding/steps/generate-config.js +106 -0
- package/dist/src/onboarding/steps/readiness-validation.js +158 -0
- package/dist/src/onboarding/steps/select-repos.js +76 -0
- package/dist/src/onboarding/steps/show-summary.js +97 -0
- package/dist/src/onboarding/steps/welcome.js +27 -0
- package/dist/src/onboarding/types.js +1 -0
- package/dist/src/onboarding/validators/git-access.js +63 -0
- package/dist/src/onboarding/validators/github-auth.js +85 -0
- package/dist/src/onboarding/validators/tool-checker.js +153 -0
- package/dist/src/onboarding/validators/workspace.js +83 -0
- package/dist/src/prompts/resolver.js +106 -0
- package/dist/src/retrospectives/candidates.js +224 -0
- package/dist/src/retrospectives/derive.js +169 -0
- package/dist/src/retrospectives/ingest.js +321 -0
- package/dist/src/retrospectives/management-prs.js +380 -0
- package/dist/src/retrospectives/proposals.js +199 -0
- package/dist/src/retrospectives/scopes.js +272 -0
- package/dist/src/retrospectives/scoring.js +171 -0
- package/dist/src/retrospectives/types.js +1 -0
- package/dist/src/state/loader.js +473 -0
- package/dist/src/state/types.js +15 -0
- package/dist/src/tasks/derivation/builders.js +70 -0
- package/dist/src/tasks/derivation/handlers/comment-response.js +169 -0
- package/dist/src/tasks/derivation/handlers/implement-plan.js +103 -0
- package/dist/src/tasks/derivation/handlers/index.js +54 -0
- package/dist/src/tasks/derivation/handlers/packet-pr-control.js +92 -0
- package/dist/src/tasks/derivation/handlers/packet-worktree.js +29 -0
- package/dist/src/tasks/derivation/handlers/plan-breakdown.js +45 -0
- package/dist/src/tasks/derivation/handlers/prepare-for-merge.js +91 -0
- package/dist/src/tasks/derivation/handlers/product-discovery.js +36 -0
- package/dist/src/tasks/derivation/handlers/retry-failed-task.js +125 -0
- package/dist/src/tasks/derivation/handlers/review-pr.js +28 -0
- package/dist/src/tasks/derivation/handlers/shared.js +24 -0
- package/dist/src/tasks/derivation/handlers/sync-parent-branch.js +100 -0
- package/dist/src/tasks/derivation/handlers/wave-assessment.js +223 -0
- package/dist/src/tasks/derivation/handlers/write-plan.js +101 -0
- package/dist/src/tasks/derivation/handlers/write-product-spec.js +49 -0
- package/dist/src/tasks/derivation/runner.js +18 -0
- package/dist/src/tasks/derivation/types.js +1 -0
- package/dist/src/tasks/derive.js +810 -0
- package/dist/src/tasks/mcp-skip.js +10 -0
- package/dist/src/tasks/retry.js +10 -0
- package/dist/src/tasks/task.js +184 -0
- package/dist/src/tasks/types.js +1 -0
- package/dist/src/utils/command-runner.js +192 -0
- package/dist/src/utils/json.js +38 -0
- package/dist/src/utils/logger.js +43 -0
- package/dist/src/utils/paths.js +38 -0
- package/dist/src/utils/slug.js +10 -0
- package/dist/src/workflows/registry.js +174 -0
- package/dist/src/workflows/triage.js +90 -0
- package/dist/src/workflows/workstream-machine/derive-events.js +121 -0
- package/dist/src/workflows/workstream-machine/evaluate.js +24 -0
- package/dist/src/workflows/workstream-machine/events.js +1 -0
- package/dist/src/workflows/workstream-machine/machine.js +355 -0
- package/dist/src/workflows/workstream-machine/selectors.js +19 -0
- package/dist/src/workflows/workstream-machine/types.js +1 -0
- package/dist/src/workstreams/branch-sync.js +87 -0
- package/dist/src/workstreams/derive.js +629 -0
- package/dist/src/workstreams/mcp-blockers.js +420 -0
- package/dist/src/workstreams/packet-candidates.js +42 -0
- package/dist/src/workstreams/packet-lifecycle-feedback.js +60 -0
- package/dist/src/workstreams/packet-lifecycle-report.js +199 -0
- package/dist/src/workstreams/packet-plan.js +597 -0
- package/dist/src/workstreams/packet-projections.js +190 -0
- package/dist/src/workstreams/parent-branch-sync-action.js +312 -0
- package/dist/src/workstreams/parent-branch-sync.js +148 -0
- package/dist/src/workstreams/phases.js +4 -0
- package/dist/src/workstreams/plan-breakdown-generator/export.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/index.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/inspect.js +50 -0
- package/dist/src/workstreams/plan-breakdown-generator/patch.js +139 -0
- package/dist/src/workstreams/plan-breakdown-generator/session.js +19 -0
- package/dist/src/workstreams/plan-breakdown-generator/store.js +27 -0
- package/dist/src/workstreams/plan-breakdown-generator/tools.js +407 -0
- package/dist/src/workstreams/plan-breakdown-generator/types.js +6 -0
- package/dist/src/workstreams/plan-breakdown-generator/validate.js +212 -0
- package/dist/src/workstreams/plan-breakdown-schema.js +175 -0
- package/dist/src/workstreams/utils.js +15 -0
- package/dist/src/workstreams/wave-assessment/checkpoint.js +172 -0
- package/dist/src/workstreams/wave-assessment/index.js +3 -0
- package/dist/src/workstreams/wave-assessment/session.js +84 -0
- package/dist/src/workstreams/wave-assessment/store.js +51 -0
- package/dist/src/workstreams/wave-assessment/tools.js +419 -0
- package/dist/src/workstreams/wave-assessment/types.js +1 -0
- package/dist/src/workstreams/wave-assessment/validate.js +78 -0
- package/dist/src/worktrees/service.js +839 -0
- package/dist/src/worktrees/workspace-ownership.js +34 -0
- package/dist/src/worktrees/workspace-recovery.js +177 -0
- package/package.json +61 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Matches: "mcp-skip: <taskId> <mcpHandle>"
|
|
2
|
+
// Case-insensitive prefix; tolerant of leading/trailing whitespace.
|
|
3
|
+
export const MCP_SKIP_PATTERN = /^\s*mcp-skip:\s+(\S+)\s+(\S+)\s*$/i;
|
|
4
|
+
export function extractMcpSkipCommand(body) {
|
|
5
|
+
const match = MCP_SKIP_PATTERN.exec(body);
|
|
6
|
+
if (!match) {
|
|
7
|
+
return undefined;
|
|
8
|
+
}
|
|
9
|
+
return { taskId: match[1], mcpHandle: match[2] };
|
|
10
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
// Matches exactly: "retry: <github issuecomment URL>"
|
|
2
|
+
// Captures the numeric comment ID from the URL fragment.
|
|
3
|
+
export const RETRY_LINK_PATTERN = /^\s*retry:\s+https:\/\/github\.com\/[^/\s]+\/[^/\s]+\/(?:issues|pull)\/\d+#issuecomment-(\d+)\s*$/i;
|
|
4
|
+
export function extractRetryCommentId(body) {
|
|
5
|
+
const match = RETRY_LINK_PATTERN.exec(body);
|
|
6
|
+
return match ? match[1] : undefined;
|
|
7
|
+
}
|
|
8
|
+
export function isRetryLinkCommand(body) {
|
|
9
|
+
return RETRY_LINK_PATTERN.test(body);
|
|
10
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
export class Task {
|
|
2
|
+
record;
|
|
3
|
+
constructor(record) {
|
|
4
|
+
this.record = record;
|
|
5
|
+
}
|
|
6
|
+
static from(record) {
|
|
7
|
+
return new Task(record);
|
|
8
|
+
}
|
|
9
|
+
get id() {
|
|
10
|
+
return this.record.id;
|
|
11
|
+
}
|
|
12
|
+
get waitingOnProvider() {
|
|
13
|
+
return this.record.waitingOnProvider;
|
|
14
|
+
}
|
|
15
|
+
get kind() {
|
|
16
|
+
return this.record.kind;
|
|
17
|
+
}
|
|
18
|
+
get sourceKind() {
|
|
19
|
+
return this.record.sourceKind;
|
|
20
|
+
}
|
|
21
|
+
get sourceId() {
|
|
22
|
+
return this.record.sourceId;
|
|
23
|
+
}
|
|
24
|
+
get parentSourceKind() {
|
|
25
|
+
return this.record.parentSourceKind;
|
|
26
|
+
}
|
|
27
|
+
get parentSourceId() {
|
|
28
|
+
return this.record.parentSourceId;
|
|
29
|
+
}
|
|
30
|
+
getOwningThreadKind() {
|
|
31
|
+
if (this.record.parentSourceKind) {
|
|
32
|
+
return this.record.parentSourceKind;
|
|
33
|
+
}
|
|
34
|
+
if (this.record.sourceKind === "issue" || this.record.sourceKind === "pull_request") {
|
|
35
|
+
return this.record.sourceKind;
|
|
36
|
+
}
|
|
37
|
+
return undefined;
|
|
38
|
+
}
|
|
39
|
+
getOwningThreadId() {
|
|
40
|
+
return this.record.parentSourceId ?? (this.record.sourceKind === "comment" ? undefined : this.record.sourceId);
|
|
41
|
+
}
|
|
42
|
+
owningPullRequestNumber() {
|
|
43
|
+
const pullRequestId = this.getOwningThreadKind() === "pull_request" ? this.getOwningThreadId() : undefined;
|
|
44
|
+
if (!pullRequestId) {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const match = pullRequestId.match(/:(\d+)$/);
|
|
48
|
+
return match ? Number(match[1]) : undefined;
|
|
49
|
+
}
|
|
50
|
+
owningThreadNumber() {
|
|
51
|
+
const owningThreadId = this.getOwningThreadId() ?? this.record.sourceId;
|
|
52
|
+
const match = owningThreadId.match(/:(\d+)$/);
|
|
53
|
+
return match ? Number(match[1]) : undefined;
|
|
54
|
+
}
|
|
55
|
+
resolveSourceIssue(lookups) {
|
|
56
|
+
switch (this.record.sourceKind) {
|
|
57
|
+
case "issue":
|
|
58
|
+
return lookups.issuesById?.get(this.record.sourceId);
|
|
59
|
+
case "pull_request":
|
|
60
|
+
return undefined;
|
|
61
|
+
case "comment":
|
|
62
|
+
return this.resolveCommentParentIssue(lookups, this.record.sourceId);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
resolveOwningIssue(lookups) {
|
|
66
|
+
const owningThreadId = this.getOwningThreadId();
|
|
67
|
+
if (this.getOwningThreadKind() === "issue" && owningThreadId) {
|
|
68
|
+
return lookups.issuesById?.get(owningThreadId);
|
|
69
|
+
}
|
|
70
|
+
if (this.record.sourceKind === "issue") {
|
|
71
|
+
return lookups.issuesById?.get(this.record.sourceId);
|
|
72
|
+
}
|
|
73
|
+
if (this.record.sourceKind === "comment") {
|
|
74
|
+
return this.resolveCommentParentIssue(lookups, this.record.sourceId);
|
|
75
|
+
}
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
resolveSourcePullRequest(lookups) {
|
|
79
|
+
switch (this.record.sourceKind) {
|
|
80
|
+
case "issue":
|
|
81
|
+
return undefined;
|
|
82
|
+
case "pull_request":
|
|
83
|
+
return lookups.pullRequestsById?.get(this.record.sourceId);
|
|
84
|
+
case "comment":
|
|
85
|
+
return this.resolveCommentParentPullRequest(lookups, this.record.sourceId);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
resolveOwningPullRequest(lookups) {
|
|
89
|
+
const owningThreadId = this.getOwningThreadId();
|
|
90
|
+
if (this.getOwningThreadKind() === "pull_request" && owningThreadId) {
|
|
91
|
+
return lookups.pullRequestsById?.get(owningThreadId);
|
|
92
|
+
}
|
|
93
|
+
if (this.record.sourceKind === "pull_request") {
|
|
94
|
+
return lookups.pullRequestsById?.get(this.record.sourceId);
|
|
95
|
+
}
|
|
96
|
+
if (this.record.sourceKind === "comment") {
|
|
97
|
+
return this.resolveCommentParentPullRequest(lookups, this.record.sourceId);
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
resolveSourceComment(lookups) {
|
|
102
|
+
if (this.record.sourceKind !== "comment") {
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
return lookups.commentsById?.get(this.record.sourceId);
|
|
106
|
+
}
|
|
107
|
+
resolveOwningComment(lookups) {
|
|
108
|
+
if (this.record.sourceKind !== "comment") {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return lookups.commentsById?.get(this.record.sourceId);
|
|
112
|
+
}
|
|
113
|
+
isSourceIssue(issue) {
|
|
114
|
+
return this.record.sourceKind === "issue" && this.record.sourceId === issue.id;
|
|
115
|
+
}
|
|
116
|
+
isOwningIssue(issue) {
|
|
117
|
+
return this.getOwningThreadKind() === "issue" && this.getOwningThreadId() === issue.id;
|
|
118
|
+
}
|
|
119
|
+
isSourcePullRequest(pullRequest) {
|
|
120
|
+
return this.record.sourceKind === "pull_request" && this.record.sourceId === pullRequest.id;
|
|
121
|
+
}
|
|
122
|
+
isOwningPullRequest(pullRequest) {
|
|
123
|
+
return this.getOwningThreadKind() === "pull_request" && this.getOwningThreadId() === pullRequest.id;
|
|
124
|
+
}
|
|
125
|
+
isSourceComment(comment) {
|
|
126
|
+
return this.record.sourceKind === "comment" && this.record.sourceId === comment.id;
|
|
127
|
+
}
|
|
128
|
+
isOwningComment(comment) {
|
|
129
|
+
return this.record.sourceKind === "comment" && this.record.sourceId === comment.id;
|
|
130
|
+
}
|
|
131
|
+
isRelatedToIssue(issue) {
|
|
132
|
+
return this.isSourceIssue(issue) || this.isOwningIssue(issue);
|
|
133
|
+
}
|
|
134
|
+
isRelatedToPullRequest(pullRequest) {
|
|
135
|
+
return this.isSourcePullRequest(pullRequest) || this.isOwningPullRequest(pullRequest);
|
|
136
|
+
}
|
|
137
|
+
isCommentOriginated() {
|
|
138
|
+
return this.record.sourceKind === "comment";
|
|
139
|
+
}
|
|
140
|
+
getSourceCommentId() {
|
|
141
|
+
if (this.record.sourceKind !== "comment") {
|
|
142
|
+
return undefined;
|
|
143
|
+
}
|
|
144
|
+
if (this.record.sourceReactionTargetId) {
|
|
145
|
+
return this.record.sourceReactionTargetId;
|
|
146
|
+
}
|
|
147
|
+
const match = this.record.sourceId.match(/:(\d+)$/);
|
|
148
|
+
if (match) {
|
|
149
|
+
return match[1];
|
|
150
|
+
}
|
|
151
|
+
return /^\d+$/.test(this.record.sourceId) ? this.record.sourceId : undefined;
|
|
152
|
+
}
|
|
153
|
+
getSourceCommentKind() {
|
|
154
|
+
if (this.record.sourceKind !== "comment") {
|
|
155
|
+
return undefined;
|
|
156
|
+
}
|
|
157
|
+
const match = this.record.sourceId.match(/^comment:[^:]+\/[^:]+:(issue_comment|pull_request_comment|pull_request_review|pull_request_review_comment):/);
|
|
158
|
+
return match?.[1];
|
|
159
|
+
}
|
|
160
|
+
getSourceEntityNumber() {
|
|
161
|
+
// If it's an issue or PR, its ID ends with :number
|
|
162
|
+
if (this.record.sourceKind === "issue" || this.record.sourceKind === "pull_request") {
|
|
163
|
+
const match = this.record.sourceId.match(/:(\d+)$/);
|
|
164
|
+
return match ? Number(match[1]) : undefined;
|
|
165
|
+
}
|
|
166
|
+
// For comments, we usually want the parent's number for reactions/feedback if no commentId is used
|
|
167
|
+
// but the lifecycle notifier will use the precise source.
|
|
168
|
+
return undefined;
|
|
169
|
+
}
|
|
170
|
+
resolveCommentParentIssue(lookups, commentId) {
|
|
171
|
+
const comment = lookups.commentsById?.get(commentId);
|
|
172
|
+
if (!comment || comment.parentType !== "issue") {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
return lookups.issuesByKey?.get(comment.parentKey);
|
|
176
|
+
}
|
|
177
|
+
resolveCommentParentPullRequest(lookups, commentId) {
|
|
178
|
+
const comment = lookups.commentsById?.get(commentId);
|
|
179
|
+
if (!comment || comment.parentType !== "pull_request") {
|
|
180
|
+
return undefined;
|
|
181
|
+
}
|
|
182
|
+
return lookups.pullRequestsByKey?.get(comment.parentKey);
|
|
183
|
+
}
|
|
184
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
const require = createRequire(import.meta.url);
|
|
3
|
+
const crossSpawn = require("cross-spawn");
|
|
4
|
+
export class CommandExecutionError extends Error {
|
|
5
|
+
command;
|
|
6
|
+
args;
|
|
7
|
+
exitCode;
|
|
8
|
+
stdout;
|
|
9
|
+
stderr;
|
|
10
|
+
constructor(message, command, args, exitCode, stdout, stderr) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.command = command;
|
|
13
|
+
this.args = args;
|
|
14
|
+
this.exitCode = exitCode;
|
|
15
|
+
this.stdout = stdout;
|
|
16
|
+
this.stderr = stderr;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
const COMMAND_TIMEOUT_EXIT_CODE = 124;
|
|
20
|
+
const PROCESS_GROUP_TERMINATION_GRACE_MS = 250;
|
|
21
|
+
const TIMEOUT_SETTLE_FALLBACK_MS = 1000;
|
|
22
|
+
export class ExecFileCommandRunner {
|
|
23
|
+
spawnProcess;
|
|
24
|
+
killProcess;
|
|
25
|
+
platform;
|
|
26
|
+
constructor(dependencies = {}) {
|
|
27
|
+
this.spawnProcess = dependencies.spawnProcess ?? crossSpawn;
|
|
28
|
+
this.killProcess = dependencies.killProcess ?? process.kill.bind(process);
|
|
29
|
+
this.platform = dependencies.platform ?? process.platform;
|
|
30
|
+
}
|
|
31
|
+
run(command, args, options = {}) {
|
|
32
|
+
return new Promise((resolve, reject) => {
|
|
33
|
+
const child = this.spawnProcess(command, args, {
|
|
34
|
+
cwd: options.cwd,
|
|
35
|
+
env: options.env === undefined ? process.env : { ...process.env, ...options.env },
|
|
36
|
+
detached: this.platform !== "win32",
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
windowsHide: this.platform === "win32"
|
|
39
|
+
});
|
|
40
|
+
let stdout = "";
|
|
41
|
+
let stderr = "";
|
|
42
|
+
let settled = false;
|
|
43
|
+
let timedOut = false;
|
|
44
|
+
let timeoutHandle;
|
|
45
|
+
let killGraceHandle;
|
|
46
|
+
let timeoutFallbackHandle;
|
|
47
|
+
const settleResolve = (result) => {
|
|
48
|
+
if (settled) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
settled = true;
|
|
52
|
+
clearTimers();
|
|
53
|
+
resolve(result);
|
|
54
|
+
};
|
|
55
|
+
const settleReject = (error) => {
|
|
56
|
+
if (settled) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
settled = true;
|
|
60
|
+
clearTimers();
|
|
61
|
+
reject(error);
|
|
62
|
+
};
|
|
63
|
+
const clearTimers = () => {
|
|
64
|
+
if (timeoutHandle !== undefined) {
|
|
65
|
+
clearTimeout(timeoutHandle);
|
|
66
|
+
timeoutHandle = undefined;
|
|
67
|
+
}
|
|
68
|
+
if (killGraceHandle !== undefined) {
|
|
69
|
+
clearTimeout(killGraceHandle);
|
|
70
|
+
killGraceHandle = undefined;
|
|
71
|
+
}
|
|
72
|
+
if (timeoutFallbackHandle !== undefined) {
|
|
73
|
+
clearTimeout(timeoutFallbackHandle);
|
|
74
|
+
timeoutFallbackHandle = undefined;
|
|
75
|
+
}
|
|
76
|
+
};
|
|
77
|
+
child.stdout?.setEncoding("utf8");
|
|
78
|
+
child.stderr?.setEncoding("utf8");
|
|
79
|
+
child.stdout?.on("data", (chunk) => {
|
|
80
|
+
stdout += chunk;
|
|
81
|
+
if (!settled) {
|
|
82
|
+
options.onOutputChunk?.("stdout", chunk);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
child.stderr?.on("data", (chunk) => {
|
|
86
|
+
stderr += chunk;
|
|
87
|
+
if (!settled) {
|
|
88
|
+
options.onOutputChunk?.("stderr", chunk);
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
child.once("error", (error) => {
|
|
92
|
+
settleReject(new CommandExecutionError(formatCommandError(command, args, error.message, stderr), command, args, 1, stdout, stderr));
|
|
93
|
+
});
|
|
94
|
+
child.once("close", (code, signal) => {
|
|
95
|
+
if (timedOut) {
|
|
96
|
+
settleReject(new CommandExecutionError(formatTimeoutError(command, args, options.timeoutMs ?? 0, stderr), command, args, COMMAND_TIMEOUT_EXIT_CODE, stdout, stderr));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
const exitCode = extractExitCodeFromClose(code, signal);
|
|
100
|
+
if (exitCode === 0) {
|
|
101
|
+
settleResolve({
|
|
102
|
+
stdout,
|
|
103
|
+
stderr,
|
|
104
|
+
exitCode: 0
|
|
105
|
+
});
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
settleReject(new CommandExecutionError(formatCommandError(command, args, `command exited with code ${exitCode}`, stderr), command, args, exitCode, stdout, stderr));
|
|
109
|
+
});
|
|
110
|
+
if (options.input !== undefined) {
|
|
111
|
+
child.stdin?.end(options.input);
|
|
112
|
+
}
|
|
113
|
+
if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
|
|
114
|
+
timeoutHandle = setTimeout(() => {
|
|
115
|
+
if (settled || child.pid === undefined) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
timedOut = true;
|
|
119
|
+
terminateProcessTree(child, "SIGTERM", {
|
|
120
|
+
spawnProcess: this.spawnProcess,
|
|
121
|
+
killProcess: this.killProcess,
|
|
122
|
+
platform: this.platform
|
|
123
|
+
});
|
|
124
|
+
killGraceHandle = setTimeout(() => {
|
|
125
|
+
if (settled || child.pid === undefined) {
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
terminateProcessTree(child, "SIGKILL", {
|
|
129
|
+
spawnProcess: this.spawnProcess,
|
|
130
|
+
killProcess: this.killProcess,
|
|
131
|
+
platform: this.platform
|
|
132
|
+
});
|
|
133
|
+
}, PROCESS_GROUP_TERMINATION_GRACE_MS);
|
|
134
|
+
timeoutFallbackHandle = setTimeout(() => {
|
|
135
|
+
if (settled) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
settleReject(new CommandExecutionError(formatTimeoutError(command, args, options.timeoutMs ?? 0, stderr), command, args, COMMAND_TIMEOUT_EXIT_CODE, stdout, stderr));
|
|
139
|
+
}, TIMEOUT_SETTLE_FALLBACK_MS);
|
|
140
|
+
}, options.timeoutMs);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function extractExitCodeFromClose(code, signal) {
|
|
146
|
+
if (typeof code === "number") {
|
|
147
|
+
return code;
|
|
148
|
+
}
|
|
149
|
+
if (signal !== null) {
|
|
150
|
+
return 1;
|
|
151
|
+
}
|
|
152
|
+
return 1;
|
|
153
|
+
}
|
|
154
|
+
export function terminateProcessTree(child, signal, dependencies = {}) {
|
|
155
|
+
if (child.pid === undefined) {
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const platform = dependencies.platform ?? process.platform;
|
|
159
|
+
const spawnProcess = dependencies.spawnProcess ?? crossSpawn;
|
|
160
|
+
const killProcess = dependencies.killProcess ?? process.kill.bind(process);
|
|
161
|
+
if (platform === "win32") {
|
|
162
|
+
const taskkill = spawnProcess("taskkill", ["/pid", String(child.pid), "/T", "/F"], {
|
|
163
|
+
stdio: "ignore",
|
|
164
|
+
windowsHide: true
|
|
165
|
+
});
|
|
166
|
+
taskkill.once("error", () => undefined);
|
|
167
|
+
taskkill.unref();
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
killProcess(-child.pid, signal);
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
try {
|
|
175
|
+
killProcess(child.pid, signal);
|
|
176
|
+
}
|
|
177
|
+
catch {
|
|
178
|
+
// Ignore termination failures. The fallback timeout will surface the timeout error.
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
function formatCommandError(command, args, message, stderr) {
|
|
183
|
+
const renderedCommand = [command, ...args].join(" ");
|
|
184
|
+
const suffix = stderr.trim() === "" ? message : stderr.trim();
|
|
185
|
+
return `${renderedCommand}: ${suffix}`;
|
|
186
|
+
}
|
|
187
|
+
function formatTimeoutError(command, args, timeoutMs, stderr) {
|
|
188
|
+
const renderedCommand = [command, ...args].join(" ");
|
|
189
|
+
const timeoutMessage = `timed out after ${timeoutMs}ms`;
|
|
190
|
+
const suffix = stderr.trim() === "" ? timeoutMessage : `${timeoutMessage}: ${stderr.trim()}`;
|
|
191
|
+
return `${renderedCommand}: ${suffix}`;
|
|
192
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { mkdir, readFile, rename, writeFile } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
export async function readJsonFile(filePath) {
|
|
4
|
+
try {
|
|
5
|
+
const raw = await readFile(filePath, "utf8");
|
|
6
|
+
return JSON.parse(raw);
|
|
7
|
+
}
|
|
8
|
+
catch (error) {
|
|
9
|
+
if (error instanceof SyntaxError) {
|
|
10
|
+
throw new Error(`${filePath}: invalid JSON (${error.message})`);
|
|
11
|
+
}
|
|
12
|
+
if (error.code === "ENOENT") {
|
|
13
|
+
throw new Error(`${filePath}: file not found`);
|
|
14
|
+
}
|
|
15
|
+
throw error;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function readJsonFileIfExists(filePath) {
|
|
19
|
+
try {
|
|
20
|
+
return await readJsonFile(filePath);
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
if (error instanceof Error && error.message === `${filePath}: file not found`) {
|
|
24
|
+
return undefined;
|
|
25
|
+
}
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
export async function writeJsonFileAtomic(filePath, value) {
|
|
30
|
+
const directory = path.dirname(filePath);
|
|
31
|
+
await mkdir(directory, { recursive: true });
|
|
32
|
+
const temporaryFile = path.join(directory, `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
|
|
33
|
+
await writeFile(temporaryFile, toPrettyJson(value), "utf8");
|
|
34
|
+
await rename(temporaryFile, filePath);
|
|
35
|
+
}
|
|
36
|
+
export function toPrettyJson(value) {
|
|
37
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
38
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { createWriteStream } from "node:fs";
|
|
2
|
+
import { mkdirSync } from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
export const defaultStderrLogger = {
|
|
5
|
+
info: (message) => process.stderr.write(`${message}\n`),
|
|
6
|
+
warn: (message) => process.stderr.write(`${message}\n`),
|
|
7
|
+
error: (message) => process.stderr.write(`${message}\n`)
|
|
8
|
+
};
|
|
9
|
+
export function createTeeFileLogger(baseLogger, filePath) {
|
|
10
|
+
mkdirSync(path.dirname(filePath), { recursive: true });
|
|
11
|
+
const stream = createWriteStream(filePath, {
|
|
12
|
+
flags: "a",
|
|
13
|
+
encoding: "utf8"
|
|
14
|
+
});
|
|
15
|
+
const writeLine = (message) => {
|
|
16
|
+
stream.write(`${message}\n`);
|
|
17
|
+
};
|
|
18
|
+
return {
|
|
19
|
+
info(message) {
|
|
20
|
+
baseLogger.info(message);
|
|
21
|
+
writeLine(message);
|
|
22
|
+
},
|
|
23
|
+
warn(message) {
|
|
24
|
+
baseLogger.warn(message);
|
|
25
|
+
writeLine(message);
|
|
26
|
+
},
|
|
27
|
+
error(message) {
|
|
28
|
+
baseLogger.error?.(message);
|
|
29
|
+
writeLine(message);
|
|
30
|
+
},
|
|
31
|
+
async close() {
|
|
32
|
+
await new Promise((resolve, reject) => {
|
|
33
|
+
stream.end((error) => {
|
|
34
|
+
if (error) {
|
|
35
|
+
reject(error);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
resolve();
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import * as path from "node:path";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
const currentFile = fileURLToPath(import.meta.url);
|
|
5
|
+
const currentDirectory = path.dirname(currentFile);
|
|
6
|
+
export const projectRoot = path.resolve(currentDirectory, "../../..");
|
|
7
|
+
export const managementRoot = path.join(projectRoot, "management");
|
|
8
|
+
export const outputsRoot = path.join(projectRoot, "outputs");
|
|
9
|
+
export const outputsStateRoot = path.join(outputsRoot, "state");
|
|
10
|
+
export const outputsLogsRoot = path.join(outputsRoot, "logs");
|
|
11
|
+
export function resolveOutputsStateRoot(outputsDir) {
|
|
12
|
+
return path.join(outputsDir, "state");
|
|
13
|
+
}
|
|
14
|
+
export function resolveOutputsLogsRoot(outputsDir) {
|
|
15
|
+
return path.join(outputsDir, "logs");
|
|
16
|
+
}
|
|
17
|
+
export const outputsJournalRoot = path.join(outputsRoot, "journal");
|
|
18
|
+
export function resolveOutputsJournalRoot(outputsDir) {
|
|
19
|
+
return path.join(outputsDir, "journal");
|
|
20
|
+
}
|
|
21
|
+
export function defaultBackburnerRoot() {
|
|
22
|
+
if (process.env.BACKBURNER_ROOT !== undefined && process.env.BACKBURNER_ROOT.trim() !== "") {
|
|
23
|
+
return process.env.BACKBURNER_ROOT;
|
|
24
|
+
}
|
|
25
|
+
return path.join(os.homedir(), ".backburner");
|
|
26
|
+
}
|
|
27
|
+
export function defaultBackburnerCodeRoot() {
|
|
28
|
+
return path.join(defaultBackburnerRoot(), "code");
|
|
29
|
+
}
|
|
30
|
+
export function defaultBackburnerManagementRoot() {
|
|
31
|
+
return path.join(defaultBackburnerRoot(), "management");
|
|
32
|
+
}
|
|
33
|
+
export function defaultBackburnerOutputsRoot() {
|
|
34
|
+
return path.join(defaultBackburnerRoot(), "outputs");
|
|
35
|
+
}
|
|
36
|
+
export function defaultWorkspacesRootForCodeRoot(codeRoot) {
|
|
37
|
+
return path.join(codeRoot, "_worktrees");
|
|
38
|
+
}
|