@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,484 @@
|
|
|
1
|
+
import { mkdir, rm, stat } from "node:fs/promises";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { CommandExecutionError } from "../utils/command-runner.js";
|
|
4
|
+
export class GitCliGateway {
|
|
5
|
+
commandRunner;
|
|
6
|
+
constructor(commandRunner) {
|
|
7
|
+
this.commandRunner = commandRunner;
|
|
8
|
+
}
|
|
9
|
+
async cloneRepository(repo, destinationPath) {
|
|
10
|
+
const parentDir = path.dirname(destinationPath);
|
|
11
|
+
await mkdir(parentDir, { recursive: true });
|
|
12
|
+
await this.commandRunner.run("git", ["clone", `git@github.com:${repo.owner}/${repo.name}.git`, destinationPath], { cwd: parentDir });
|
|
13
|
+
}
|
|
14
|
+
async fetchRepository(localPath) {
|
|
15
|
+
await this.commandRunner.run("git", ["fetch", "--prune", "origin"], {
|
|
16
|
+
cwd: localPath
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
async getCurrentBranch(localPath) {
|
|
20
|
+
try {
|
|
21
|
+
const result = await this.commandRunner.run("git", ["symbolic-ref", "--quiet", "--short", "HEAD"], { cwd: localPath });
|
|
22
|
+
return result.stdout.trim() || null;
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
async getRemoteDefaultBranch(localPath) {
|
|
29
|
+
try {
|
|
30
|
+
const result = await this.commandRunner.run("git", ["symbolic-ref", "--quiet", "--short", "refs/remotes/origin/HEAD"], { cwd: localPath });
|
|
31
|
+
return stripOriginPrefix(result.stdout.trim());
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
const fallback = await this.commandRunner.run("git", ["remote", "show", "origin"], {
|
|
35
|
+
cwd: localPath
|
|
36
|
+
});
|
|
37
|
+
const match = fallback.stdout.match(/HEAD branch:\s+([^\s]+)/);
|
|
38
|
+
if (!match?.[1]) {
|
|
39
|
+
throw new Error(`Could not determine remote default branch for ${localPath}`);
|
|
40
|
+
}
|
|
41
|
+
return match[1];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
async isWorkingTreeClean(localPath) {
|
|
45
|
+
const result = await this.commandRunner.run("git", ["status", "--porcelain", "."], {
|
|
46
|
+
cwd: localPath
|
|
47
|
+
});
|
|
48
|
+
return result.stdout.trim() === "";
|
|
49
|
+
}
|
|
50
|
+
async ensureDefaultBranchCheckedOut(localPath, branch) {
|
|
51
|
+
const hasLocalBranch = await this.hasLocalBranch(localPath, branch);
|
|
52
|
+
if (hasLocalBranch) {
|
|
53
|
+
await this.commandRunner.run("git", ["checkout", branch], { cwd: localPath });
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
await this.commandRunner.run("git", ["checkout", "-b", branch, "--track", `origin/${branch}`], { cwd: localPath });
|
|
57
|
+
}
|
|
58
|
+
async fastForwardDefaultBranch(localPath, branch) {
|
|
59
|
+
await this.commandRunner.run("git", ["branch", "--set-upstream-to", `origin/${branch}`, branch], { cwd: localPath });
|
|
60
|
+
await this.commandRunner.run("git", ["merge", "--ff-only", `origin/${branch}`], {
|
|
61
|
+
cwd: localPath
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
validatePathWithinCodeRoot(codeRoot, candidatePath) {
|
|
65
|
+
return resolvePathWithinCodeRoot(codeRoot, candidatePath);
|
|
66
|
+
}
|
|
67
|
+
async compareBranchSyncStatus(localPath, parentBranch, defaultBranch) {
|
|
68
|
+
const defaultRef = this.remoteBranchRef(defaultBranch);
|
|
69
|
+
const parentRef = this.remoteBranchRef(parentBranch);
|
|
70
|
+
const baseComparison = {
|
|
71
|
+
parentBranch,
|
|
72
|
+
defaultBranch,
|
|
73
|
+
parentRef,
|
|
74
|
+
defaultRef
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
if (!(await this.hasRef(localPath, defaultRef))) {
|
|
78
|
+
return {
|
|
79
|
+
...baseComparison,
|
|
80
|
+
status: "missing_default_branch",
|
|
81
|
+
message: "Default branch ref was not found locally."
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (!(await this.hasRef(localPath, parentRef))) {
|
|
85
|
+
return {
|
|
86
|
+
...baseComparison,
|
|
87
|
+
status: "missing_parent_branch",
|
|
88
|
+
message: "Parent branch ref was not found locally."
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const result = await this.commandRunner.run("git", ["rev-list", "--left-right", "--count", `${defaultRef}...${parentRef}`], { cwd: localPath });
|
|
92
|
+
const tokens = result.stdout.trim().split(/\s+/);
|
|
93
|
+
if (tokens.length < 2) {
|
|
94
|
+
return {
|
|
95
|
+
...baseComparison,
|
|
96
|
+
status: "unknown",
|
|
97
|
+
message: "Could not compare branch sync status."
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
const behind = Number(tokens[0]);
|
|
101
|
+
const ahead = Number(tokens[1]);
|
|
102
|
+
if (isNaN(behind) || isNaN(ahead)) {
|
|
103
|
+
return {
|
|
104
|
+
...baseComparison,
|
|
105
|
+
status: "unknown",
|
|
106
|
+
message: "Could not compare branch sync status."
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
const comparison = {
|
|
110
|
+
...baseComparison,
|
|
111
|
+
parentAheadCount: ahead,
|
|
112
|
+
parentBehindCount: behind,
|
|
113
|
+
status: "unknown",
|
|
114
|
+
message: ""
|
|
115
|
+
};
|
|
116
|
+
if (behind === 0) {
|
|
117
|
+
comparison.status = "up_to_date";
|
|
118
|
+
comparison.message = "Parent branch contains the default branch.";
|
|
119
|
+
}
|
|
120
|
+
else if (ahead === 0) {
|
|
121
|
+
comparison.status = "behind_default";
|
|
122
|
+
comparison.message = "Parent branch is behind the default branch.";
|
|
123
|
+
}
|
|
124
|
+
else {
|
|
125
|
+
comparison.status = "diverged";
|
|
126
|
+
comparison.message = "Parent branch and default branch have diverged.";
|
|
127
|
+
}
|
|
128
|
+
return comparison;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return {
|
|
132
|
+
...baseComparison,
|
|
133
|
+
status: "unknown",
|
|
134
|
+
message: "Could not compare branch sync status."
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
async listWorktrees(repoPath) {
|
|
139
|
+
const result = await this.commandRunner.run("git", ["worktree", "list", "--porcelain"], {
|
|
140
|
+
cwd: repoPath
|
|
141
|
+
});
|
|
142
|
+
const worktrees = [];
|
|
143
|
+
let currentPath = "";
|
|
144
|
+
for (const line of result.stdout.split(/\r?\n/)) {
|
|
145
|
+
if (line.startsWith("worktree ")) {
|
|
146
|
+
currentPath = line.slice("worktree ".length).trim();
|
|
147
|
+
}
|
|
148
|
+
else if (line.startsWith("branch ") && currentPath) {
|
|
149
|
+
const branchRef = line.slice("branch ".length).trim();
|
|
150
|
+
const branchName = branchRef.startsWith("refs/heads/")
|
|
151
|
+
? branchRef.slice("refs/heads/".length)
|
|
152
|
+
: branchRef;
|
|
153
|
+
worktrees.push({ path: currentPath, branch: branchName });
|
|
154
|
+
currentPath = "";
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return worktrees;
|
|
158
|
+
}
|
|
159
|
+
async getWorktreeForBranch(repoPath, branch) {
|
|
160
|
+
const worktrees = await this.listWorktrees(repoPath);
|
|
161
|
+
const match = worktrees.find((wt) => wt.branch === branch);
|
|
162
|
+
return match ? match.path : null;
|
|
163
|
+
}
|
|
164
|
+
async createWorktree(repoPath, codeRoot, worktreePath, branch, startPoint) {
|
|
165
|
+
const safeWorktreePath = resolvePathWithinCodeRoot(codeRoot, worktreePath);
|
|
166
|
+
await this.commandRunner.run("git", ["worktree", "add", "-b", branch, safeWorktreePath, startPoint], { cwd: repoPath });
|
|
167
|
+
return safeWorktreePath;
|
|
168
|
+
}
|
|
169
|
+
async hasCommitsAhead(localPath, baseRef) {
|
|
170
|
+
const result = await this.commandRunner.run("git", ["rev-list", "--count", `${baseRef}..HEAD`], { cwd: localPath });
|
|
171
|
+
return Number(result.stdout.trim() || "0") > 0;
|
|
172
|
+
}
|
|
173
|
+
async commitAll(localPath, message) {
|
|
174
|
+
await this.commandRunner.run("git", ["add", "--all", "."], { cwd: localPath });
|
|
175
|
+
const status = await this.commandRunner.run("git", ["status", "--porcelain", "."], {
|
|
176
|
+
cwd: localPath
|
|
177
|
+
});
|
|
178
|
+
if (status.stdout.trim() === "") {
|
|
179
|
+
return false;
|
|
180
|
+
}
|
|
181
|
+
await this.commandRunner.run("git", ["commit", "-m", message, "."], { cwd: localPath });
|
|
182
|
+
return true;
|
|
183
|
+
}
|
|
184
|
+
async pushBranch(localPath, branch) {
|
|
185
|
+
await this.commandRunner.run("git", ["push", "-u", "origin", branch], { cwd: localPath });
|
|
186
|
+
}
|
|
187
|
+
async checkoutBranch(localPath, branch) {
|
|
188
|
+
await this.commandRunner.run("git", ["checkout", branch], { cwd: localPath });
|
|
189
|
+
}
|
|
190
|
+
async mergeRemoteBranch(localPath, remote, branch) {
|
|
191
|
+
await this.commandRunner.run("git", ["merge", "--no-edit", `${remote}/${branch}`], {
|
|
192
|
+
cwd: localPath
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
async abortMerge(localPath) {
|
|
196
|
+
await this.commandRunner.run("git", ["merge", "--abort"], { cwd: localPath });
|
|
197
|
+
}
|
|
198
|
+
async abortRebase(localPath) {
|
|
199
|
+
await this.commandRunner.run("git", ["rebase", "--abort"], { cwd: localPath });
|
|
200
|
+
}
|
|
201
|
+
async abortCherryPick(localPath) {
|
|
202
|
+
await this.commandRunner.run("git", ["cherry-pick", "--abort"], { cwd: localPath });
|
|
203
|
+
}
|
|
204
|
+
async abortRevert(localPath) {
|
|
205
|
+
await this.commandRunner.run("git", ["revert", "--abort"], { cwd: localPath });
|
|
206
|
+
}
|
|
207
|
+
async isInsideWorkingTree(localPath) {
|
|
208
|
+
try {
|
|
209
|
+
await stat(localPath);
|
|
210
|
+
}
|
|
211
|
+
catch (err) {
|
|
212
|
+
if (err instanceof Error && err.code === "ENOENT") {
|
|
213
|
+
return false;
|
|
214
|
+
}
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
try {
|
|
218
|
+
const result = await this.commandRunner.run("git", ["rev-parse", "--is-inside-work-tree"], {
|
|
219
|
+
cwd: localPath
|
|
220
|
+
});
|
|
221
|
+
return result.stdout.trim() === "true";
|
|
222
|
+
}
|
|
223
|
+
catch (error) {
|
|
224
|
+
if (error instanceof CommandExecutionError &&
|
|
225
|
+
error.stderr.includes("not a git repository")) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
async pullFastForwardOnly(localPath) {
|
|
232
|
+
await this.commandRunner.run("git", ["pull", "--ff-only"], { cwd: localPath });
|
|
233
|
+
}
|
|
234
|
+
async getHeadCommitSha(localPath) {
|
|
235
|
+
const result = await this.commandRunner.run("git", ["rev-parse", "HEAD"], {
|
|
236
|
+
cwd: localPath
|
|
237
|
+
});
|
|
238
|
+
return result.stdout.trim();
|
|
239
|
+
}
|
|
240
|
+
async cloneRepositoryFromUrl(sshUrl, destinationPath) {
|
|
241
|
+
const parentDir = path.dirname(destinationPath);
|
|
242
|
+
await mkdir(parentDir, { recursive: true });
|
|
243
|
+
await this.commandRunner.run("git", ["clone", sshUrl, destinationPath], { cwd: parentDir });
|
|
244
|
+
}
|
|
245
|
+
async checkoutBranchAtRemote(localPath, branch, options) {
|
|
246
|
+
if (!options?.skipFetch) {
|
|
247
|
+
await this.commandRunner.run("git", ["fetch", "origin"], { cwd: localPath });
|
|
248
|
+
}
|
|
249
|
+
await this.commandRunner.run("git", ["checkout", "-B", branch, `origin/${branch}`], { cwd: localPath });
|
|
250
|
+
}
|
|
251
|
+
async getAheadBehindVsRemote(localPath, branch) {
|
|
252
|
+
// Output format: "<behind>\t<ahead>" (left = behind, right = ahead)
|
|
253
|
+
const result = await this.commandRunner.run("git", ["rev-list", "--left-right", "--count", `origin/${branch}...HEAD`], { cwd: localPath });
|
|
254
|
+
const tokens = result.stdout.trim().split(/\s+/);
|
|
255
|
+
const behind = Number(tokens[0] ?? "0");
|
|
256
|
+
const ahead = Number(tokens[1] ?? "0");
|
|
257
|
+
return {
|
|
258
|
+
behind: isNaN(behind) ? 0 : behind,
|
|
259
|
+
ahead: isNaN(ahead) ? 0 : ahead
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
async isAncestorOf(localPath, ancestorSha, descendantSha) {
|
|
263
|
+
if (ancestorSha === descendantSha)
|
|
264
|
+
return true;
|
|
265
|
+
try {
|
|
266
|
+
await this.commandRunner.run("git", ["merge-base", "--is-ancestor", ancestorSha, descendantSha], { cwd: localPath });
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
// git merge-base --is-ancestor exits non-zero when ancestorSha is NOT an ancestor
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
async countCommitsAheadOf(localPath, baseSha) {
|
|
275
|
+
const result = await this.commandRunner.run("git", ["rev-list", "--count", `${baseSha}..HEAD`], { cwd: localPath });
|
|
276
|
+
return Number(result.stdout.trim()) || 0;
|
|
277
|
+
}
|
|
278
|
+
async getActiveMergeState(localPath) {
|
|
279
|
+
// Resolve .git directory path (works in both normal clones and linked worktrees)
|
|
280
|
+
const gitDirResult = await this.commandRunner.run("git", ["rev-parse", "--git-dir"], { cwd: localPath });
|
|
281
|
+
const rawGitDir = gitDirResult.stdout.trim();
|
|
282
|
+
const gitDir = path.isAbsolute(rawGitDir) ? rawGitDir : path.resolve(localPath, rawGitDir);
|
|
283
|
+
// Check file-based state indicators
|
|
284
|
+
const fileChecks = [
|
|
285
|
+
[path.join(gitDir, "MERGE_HEAD"), "merging"],
|
|
286
|
+
[path.join(gitDir, "REBASE_HEAD"), "rebasing"],
|
|
287
|
+
[path.join(gitDir, "CHERRY_PICK_HEAD"), "cherry-picking"],
|
|
288
|
+
[path.join(gitDir, "REVERT_HEAD"), "reverting"]
|
|
289
|
+
];
|
|
290
|
+
for (const [filePath, state] of fileChecks) {
|
|
291
|
+
try {
|
|
292
|
+
await stat(filePath);
|
|
293
|
+
return state;
|
|
294
|
+
}
|
|
295
|
+
catch {
|
|
296
|
+
// file does not exist, continue
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
// Check directory-based rebase state indicators
|
|
300
|
+
const dirChecks = [
|
|
301
|
+
[path.join(gitDir, "rebase-merge"), "rebasing"],
|
|
302
|
+
[path.join(gitDir, "rebase-apply"), "rebasing"]
|
|
303
|
+
];
|
|
304
|
+
for (const [dirPath, state] of dirChecks) {
|
|
305
|
+
try {
|
|
306
|
+
await stat(dirPath);
|
|
307
|
+
return state;
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// directory does not exist, continue
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return "none";
|
|
314
|
+
}
|
|
315
|
+
async pushWithLease(localPath, branch, expectedOldSha) {
|
|
316
|
+
await this.commandRunner.run("git", [
|
|
317
|
+
"push",
|
|
318
|
+
`--force-with-lease=refs/heads/${branch}:${expectedOldSha}`,
|
|
319
|
+
"origin",
|
|
320
|
+
`HEAD:refs/heads/${branch}`
|
|
321
|
+
], { cwd: localPath });
|
|
322
|
+
}
|
|
323
|
+
async removeDirectory(dirPath) {
|
|
324
|
+
await rm(dirPath, { recursive: true, force: true });
|
|
325
|
+
}
|
|
326
|
+
async getCommitLogSince(localPath, baseSha) {
|
|
327
|
+
const result = await this.commandRunner.run("git", ["log", "--oneline", `${baseSha}..HEAD`], { cwd: localPath });
|
|
328
|
+
return result.stdout.trim();
|
|
329
|
+
}
|
|
330
|
+
async getDiffStatSince(localPath, baseSha) {
|
|
331
|
+
const result = await this.commandRunner.run("git", ["diff", "--stat", `${baseSha}..HEAD`], { cwd: localPath });
|
|
332
|
+
return result.stdout.trim();
|
|
333
|
+
}
|
|
334
|
+
async resetHard(localPath, ref) {
|
|
335
|
+
await this.commandRunner.run("git", ["reset", "--hard", ref], { cwd: localPath });
|
|
336
|
+
}
|
|
337
|
+
async cleanUntrackedFiles(localPath) {
|
|
338
|
+
await this.commandRunner.run("git", ["clean", "-fd"], { cwd: localPath });
|
|
339
|
+
}
|
|
340
|
+
remoteBranchRef(branch) {
|
|
341
|
+
return `refs/remotes/origin/${branch}`;
|
|
342
|
+
}
|
|
343
|
+
async hasRef(localPath, ref) {
|
|
344
|
+
try {
|
|
345
|
+
await this.commandRunner.run("git", ["show-ref", "--verify", "--quiet", ref], {
|
|
346
|
+
cwd: localPath
|
|
347
|
+
});
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
catch (error) {
|
|
351
|
+
if (error instanceof CommandExecutionError && error.exitCode === 1) {
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
throw error;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
async hasLocalBranch(localPath, branch) {
|
|
358
|
+
try {
|
|
359
|
+
await this.commandRunner.run("git", ["show-ref", "--verify", "--quiet", `refs/heads/${branch}`], { cwd: localPath });
|
|
360
|
+
return true;
|
|
361
|
+
}
|
|
362
|
+
catch (error) {
|
|
363
|
+
if (error instanceof CommandExecutionError && error.exitCode === 1) {
|
|
364
|
+
return false;
|
|
365
|
+
}
|
|
366
|
+
throw error;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export class LocalRepoSyncService {
|
|
371
|
+
gitGateway;
|
|
372
|
+
gitHubGateway;
|
|
373
|
+
now;
|
|
374
|
+
constructor(gitGateway, gitHubGateway, now = () => new Date().toISOString()) {
|
|
375
|
+
this.gitGateway = gitGateway;
|
|
376
|
+
this.gitHubGateway = gitHubGateway;
|
|
377
|
+
this.now = now;
|
|
378
|
+
}
|
|
379
|
+
async syncRepository(repo, codeRoot) {
|
|
380
|
+
const localPath = resolvePathWithinCodeRoot(codeRoot, repo.localPath);
|
|
381
|
+
const repoSlug = `${repo.owner}/${repo.name}`;
|
|
382
|
+
const existedBeforeSync = await pathExists(localPath);
|
|
383
|
+
let currentBranchBefore = null;
|
|
384
|
+
let currentBranchAfter = null;
|
|
385
|
+
let defaultBranch = repo.defaultBranch ?? null;
|
|
386
|
+
let defaultBranchSource = repo.defaultBranch ? "config" : "unknown";
|
|
387
|
+
try {
|
|
388
|
+
if (!existedBeforeSync) {
|
|
389
|
+
await this.gitGateway.cloneRepository(repo, localPath);
|
|
390
|
+
}
|
|
391
|
+
await this.gitGateway.fetchRepository(localPath);
|
|
392
|
+
currentBranchBefore = await this.gitGateway.getCurrentBranch(localPath);
|
|
393
|
+
if (!defaultBranch) {
|
|
394
|
+
try {
|
|
395
|
+
defaultBranch = await this.gitHubGateway.getRepositoryDefaultBranch(repo.owner, repo.name);
|
|
396
|
+
defaultBranchSource = "github";
|
|
397
|
+
}
|
|
398
|
+
catch {
|
|
399
|
+
defaultBranch = await this.gitGateway.getRemoteDefaultBranch(localPath);
|
|
400
|
+
defaultBranchSource = "git";
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const clean = await this.gitGateway.isWorkingTreeClean(localPath);
|
|
404
|
+
if (!clean) {
|
|
405
|
+
throw new Error("working tree is dirty; refusing to switch or fast-forward the default branch");
|
|
406
|
+
}
|
|
407
|
+
await this.gitGateway.ensureDefaultBranchCheckedOut(localPath, defaultBranch);
|
|
408
|
+
await this.gitGateway.fastForwardDefaultBranch(localPath, defaultBranch);
|
|
409
|
+
currentBranchAfter = await this.gitGateway.getCurrentBranch(localPath);
|
|
410
|
+
return {
|
|
411
|
+
repoId: repo.id,
|
|
412
|
+
repoName: repo.name,
|
|
413
|
+
repoSlug,
|
|
414
|
+
localPath,
|
|
415
|
+
codeRoot,
|
|
416
|
+
status: "synced",
|
|
417
|
+
existedBeforeSync,
|
|
418
|
+
cloned: !existedBeforeSync,
|
|
419
|
+
fetched: true,
|
|
420
|
+
defaultBranch,
|
|
421
|
+
defaultBranchSource,
|
|
422
|
+
currentBranchBefore,
|
|
423
|
+
currentBranchAfter,
|
|
424
|
+
syncedAt: this.now(),
|
|
425
|
+
message: !existedBeforeSync
|
|
426
|
+
? "Repository cloned, fetched, and synchronized to the latest default branch."
|
|
427
|
+
: "Repository fetched and synchronized to the latest default branch."
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
catch (error) {
|
|
431
|
+
currentBranchAfter = await safeGetCurrentBranch(this.gitGateway, localPath);
|
|
432
|
+
return {
|
|
433
|
+
repoId: repo.id,
|
|
434
|
+
repoName: repo.name,
|
|
435
|
+
repoSlug,
|
|
436
|
+
localPath,
|
|
437
|
+
codeRoot,
|
|
438
|
+
status: "failed",
|
|
439
|
+
existedBeforeSync,
|
|
440
|
+
cloned: !existedBeforeSync && currentBranchAfter !== null,
|
|
441
|
+
fetched: existedBeforeSync,
|
|
442
|
+
defaultBranch,
|
|
443
|
+
defaultBranchSource,
|
|
444
|
+
currentBranchBefore,
|
|
445
|
+
currentBranchAfter,
|
|
446
|
+
syncedAt: this.now(),
|
|
447
|
+
message: "Repository synchronization failed.",
|
|
448
|
+
error: {
|
|
449
|
+
step: "sync",
|
|
450
|
+
message: error instanceof Error ? error.message : String(error)
|
|
451
|
+
}
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function safeGetCurrentBranch(gitGateway, localPath) {
|
|
457
|
+
try {
|
|
458
|
+
return await gitGateway.getCurrentBranch(localPath);
|
|
459
|
+
}
|
|
460
|
+
catch {
|
|
461
|
+
return null;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
async function pathExists(filePath) {
|
|
465
|
+
try {
|
|
466
|
+
await stat(filePath);
|
|
467
|
+
return true;
|
|
468
|
+
}
|
|
469
|
+
catch {
|
|
470
|
+
return false;
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
function resolvePathWithinCodeRoot(codeRoot, localPath) {
|
|
474
|
+
const resolvedCodeRoot = path.resolve(codeRoot);
|
|
475
|
+
const resolvedRepoPath = path.resolve(resolvedCodeRoot, localPath);
|
|
476
|
+
const relativePath = path.relative(resolvedCodeRoot, resolvedRepoPath);
|
|
477
|
+
if (relativePath === ".." || relativePath.startsWith(`..${path.sep}`)) {
|
|
478
|
+
throw new Error(`Configured repo path "${localPath}" escapes the code root`);
|
|
479
|
+
}
|
|
480
|
+
return resolvedRepoPath;
|
|
481
|
+
}
|
|
482
|
+
function stripOriginPrefix(value) {
|
|
483
|
+
return value.startsWith("origin/") ? value.slice("origin/".length) : value;
|
|
484
|
+
}
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
const MODEL_VISIBLE_BODY_LENGTH = 1_500;
|
|
2
|
+
const MODEL_VISIBLE_LIST_LENGTH = 10;
|
|
3
|
+
const MODEL_VISIBLE_READ_TOOLS = new Set([
|
|
4
|
+
"get_issue",
|
|
5
|
+
"get_issue_comments",
|
|
6
|
+
"get_pr",
|
|
7
|
+
"get_pr_comments",
|
|
8
|
+
"get_pr_checks",
|
|
9
|
+
"search_code"
|
|
10
|
+
]);
|
|
11
|
+
export function createModelVisibleSuccessText(result) {
|
|
12
|
+
if (!MODEL_VISIBLE_READ_TOOLS.has(result.tool)) {
|
|
13
|
+
return result.summary;
|
|
14
|
+
}
|
|
15
|
+
return [
|
|
16
|
+
`${result.summary}. Model-visible result projection follows; full untruncated result is available in structuredContent.`,
|
|
17
|
+
JSON.stringify(createReadToolProjection(result.tool, result.result), null, 2)
|
|
18
|
+
].join("\n");
|
|
19
|
+
}
|
|
20
|
+
function createReadToolProjection(tool, result) {
|
|
21
|
+
switch (tool) {
|
|
22
|
+
case "get_issue":
|
|
23
|
+
return { repo: result.repo, issue: projectIssue(result.issue) };
|
|
24
|
+
case "get_issue_comments":
|
|
25
|
+
return {
|
|
26
|
+
repo: result.repo,
|
|
27
|
+
issueNumber: result.issueNumber,
|
|
28
|
+
comments: projectList(result.comments, projectComment)
|
|
29
|
+
};
|
|
30
|
+
case "get_pr":
|
|
31
|
+
return {
|
|
32
|
+
repo: result.repo,
|
|
33
|
+
pullRequestNumber: getRecordProperty(result.pullRequest, "number"),
|
|
34
|
+
pullRequest: projectPullRequest(result.pullRequest)
|
|
35
|
+
};
|
|
36
|
+
case "get_pr_comments":
|
|
37
|
+
return {
|
|
38
|
+
repo: result.repo,
|
|
39
|
+
pullRequestNumber: result.pullRequestNumber,
|
|
40
|
+
comments: projectList(result.comments, projectComment),
|
|
41
|
+
reviews: projectList(result.reviews, projectReview),
|
|
42
|
+
reviewComments: projectList(result.reviewComments, projectReviewComment)
|
|
43
|
+
};
|
|
44
|
+
case "get_pr_checks":
|
|
45
|
+
return {
|
|
46
|
+
repo: result.repo,
|
|
47
|
+
pullRequestNumber: result.pullRequestNumber,
|
|
48
|
+
headSha: result.headSha,
|
|
49
|
+
checkRuns: projectList(result.checkRuns, projectCheckRun)
|
|
50
|
+
};
|
|
51
|
+
case "search_code":
|
|
52
|
+
return {
|
|
53
|
+
repo: result.repo,
|
|
54
|
+
query: result.query,
|
|
55
|
+
pathPrefix: result.pathPrefix,
|
|
56
|
+
totalCount: result.totalCount,
|
|
57
|
+
incompleteResults: result.incompleteResults,
|
|
58
|
+
items: projectList(result.items, projectSearchCodeItem)
|
|
59
|
+
};
|
|
60
|
+
default:
|
|
61
|
+
return { summary: "No model-visible projection is configured for this read tool." };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function projectIssue(value) {
|
|
65
|
+
if (!isRecord(value)) {
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
return {
|
|
69
|
+
id: value.id,
|
|
70
|
+
number: value.number,
|
|
71
|
+
title: value.title,
|
|
72
|
+
state: value.state,
|
|
73
|
+
author: projectActor(value.author),
|
|
74
|
+
url: value.url,
|
|
75
|
+
labels: projectLabels(value.labels),
|
|
76
|
+
createdAt: value.createdAt,
|
|
77
|
+
updatedAt: value.updatedAt,
|
|
78
|
+
...projectBody(value.body)
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function projectPullRequest(value) {
|
|
82
|
+
if (!isRecord(value)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
id: value.id,
|
|
87
|
+
number: value.number,
|
|
88
|
+
title: value.title,
|
|
89
|
+
state: value.state,
|
|
90
|
+
author: projectActor(value.author),
|
|
91
|
+
url: value.url,
|
|
92
|
+
labels: projectLabels(value.labels),
|
|
93
|
+
headRefName: value.headRefName,
|
|
94
|
+
headSha: value.headSha,
|
|
95
|
+
baseRefName: value.baseRefName,
|
|
96
|
+
isDraft: value.isDraft,
|
|
97
|
+
createdAt: value.createdAt,
|
|
98
|
+
updatedAt: value.updatedAt,
|
|
99
|
+
...projectBody(value.body)
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function projectComment(value) {
|
|
103
|
+
if (!isRecord(value)) {
|
|
104
|
+
return undefined;
|
|
105
|
+
}
|
|
106
|
+
return {
|
|
107
|
+
id: value.id,
|
|
108
|
+
author: projectActor(value.author),
|
|
109
|
+
url: value.url,
|
|
110
|
+
createdAt: value.createdAt,
|
|
111
|
+
updatedAt: value.updatedAt,
|
|
112
|
+
...projectBody(value.body)
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
function projectReview(value) {
|
|
116
|
+
if (!isRecord(value)) {
|
|
117
|
+
return undefined;
|
|
118
|
+
}
|
|
119
|
+
return {
|
|
120
|
+
id: value.id,
|
|
121
|
+
author: projectActor(value.author),
|
|
122
|
+
state: value.state,
|
|
123
|
+
url: value.url,
|
|
124
|
+
submittedAt: value.submittedAt,
|
|
125
|
+
updatedAt: value.updatedAt,
|
|
126
|
+
...projectBody(value.body)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
function projectReviewComment(value) {
|
|
130
|
+
if (!isRecord(value)) {
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
return {
|
|
134
|
+
id: value.id,
|
|
135
|
+
author: projectActor(value.user),
|
|
136
|
+
url: value.html_url,
|
|
137
|
+
path: value.path,
|
|
138
|
+
line: value.line,
|
|
139
|
+
originalLine: value.original_line,
|
|
140
|
+
side: value.side,
|
|
141
|
+
startLine: value.start_line,
|
|
142
|
+
startSide: value.start_side,
|
|
143
|
+
createdAt: value.created_at,
|
|
144
|
+
updatedAt: value.updated_at,
|
|
145
|
+
...projectBody(value.body)
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function projectCheckRun(value) {
|
|
149
|
+
if (!isRecord(value)) {
|
|
150
|
+
return undefined;
|
|
151
|
+
}
|
|
152
|
+
return {
|
|
153
|
+
id: value.id,
|
|
154
|
+
name: value.name,
|
|
155
|
+
status: value.status,
|
|
156
|
+
conclusion: value.conclusion,
|
|
157
|
+
url: value.url,
|
|
158
|
+
startedAt: value.startedAt,
|
|
159
|
+
completedAt: value.completedAt
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
function projectSearchCodeItem(value) {
|
|
163
|
+
if (!isRecord(value)) {
|
|
164
|
+
return undefined;
|
|
165
|
+
}
|
|
166
|
+
return {
|
|
167
|
+
name: value.name,
|
|
168
|
+
path: value.path,
|
|
169
|
+
sha: value.sha,
|
|
170
|
+
htmlUrl: value.htmlUrl
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
function projectList(value, projectItem) {
|
|
174
|
+
const items = Array.isArray(value) ? value : [];
|
|
175
|
+
const visibleItems = items.slice(-MODEL_VISIBLE_LIST_LENGTH);
|
|
176
|
+
return {
|
|
177
|
+
totalCount: items.length,
|
|
178
|
+
returnedCount: visibleItems.length,
|
|
179
|
+
truncated: visibleItems.length < items.length,
|
|
180
|
+
...(visibleItems.length < items.length
|
|
181
|
+
? { truncationNote: `Showing the latest ${visibleItems.length} of ${items.length} items.` }
|
|
182
|
+
: {}),
|
|
183
|
+
items: visibleItems.map(projectItem).filter((item) => item !== undefined)
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
function projectBody(value) {
|
|
187
|
+
if (typeof value !== "string") {
|
|
188
|
+
return { body: null };
|
|
189
|
+
}
|
|
190
|
+
if (value.length <= MODEL_VISIBLE_BODY_LENGTH) {
|
|
191
|
+
return { body: value };
|
|
192
|
+
}
|
|
193
|
+
return {
|
|
194
|
+
body: `${value.slice(0, MODEL_VISIBLE_BODY_LENGTH)}\n...[truncated]`,
|
|
195
|
+
bodyTruncated: true,
|
|
196
|
+
bodyOriginalLength: value.length
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
function projectActor(value) {
|
|
200
|
+
return isRecord(value) && typeof value.login === "string" ? value.login : null;
|
|
201
|
+
}
|
|
202
|
+
function projectLabels(value) {
|
|
203
|
+
if (!Array.isArray(value)) {
|
|
204
|
+
return [];
|
|
205
|
+
}
|
|
206
|
+
return value
|
|
207
|
+
.map((label) => (isRecord(label) && typeof label.name === "string" ? label.name : undefined))
|
|
208
|
+
.filter((label) => label !== undefined);
|
|
209
|
+
}
|
|
210
|
+
function isRecord(value) {
|
|
211
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
212
|
+
}
|
|
213
|
+
function getRecordProperty(value, property) {
|
|
214
|
+
return isRecord(value) ? value[property] : undefined;
|
|
215
|
+
}
|