@payconductor/houndsman 0.6.0 → 0.8.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/README.md +20 -0
- package/package.json +1 -1
- package/src/cli/commands/command.ts +10 -1
- package/src/cli/commands/context-command.ts +40 -2
- package/src/cli/commands/hunt-command.ts +28 -2
- package/src/cli/commands/review-command.ts +30 -2
- package/src/cli/commands/server-command.ts +7 -1
- package/src/cli/commands/worktree-command.ts +118 -0
- package/src/cli/help.ts +13 -3
- package/src/cli/index.ts +14 -4
- package/src/core/ast/service.ts +7 -2
- package/src/core/brief.ts +1 -0
- package/src/core/commands/context.ts +9 -1
- package/src/core/commands/findings.ts +17 -6
- package/src/core/commands/review.ts +13 -0
- package/src/core/commands/scaffold.ts +4 -2
- package/src/core/config.ts +13 -0
- package/src/core/defaults.ts +8 -1
- package/src/core/engine/houndsman-engine.ts +26 -3
- package/src/core/engine/review-scope.ts +17 -22
- package/src/core/engine/review-session.ts +25 -2
- package/src/core/engine/review-workspace.ts +204 -0
- package/src/core/git-runner.ts +25 -13
- package/src/core/github-review.ts +10 -7
- package/src/core/jev/jev-detector.ts +10 -2
- package/src/core/orchestrator.ts +62 -13
- package/src/core/project-config.ts +12 -0
- package/src/core/services/gh-client.ts +38 -22
- package/src/core/services/git-repository.ts +46 -0
- package/src/core/services/pr-resolver.ts +147 -0
- package/src/core/services/worktree-manager.ts +269 -0
- package/src/core/storage/file/file-storage.ts +14 -1
- package/src/core/storage/paths.ts +30 -1
- package/src/core/storage/sqlite/mappers.ts +4 -0
- package/src/core/storage/sqlite/schema.ts +13 -3
- package/src/core/storage/sqlite/sqlite-storage.ts +28 -8
- package/src/core/storage/types.ts +12 -0
- package/src/core/storage/work-area.ts +20 -1
- package/src/core/types.ts +2 -0
- package/src/harness/hunt/hunt-options.ts +17 -0
- package/src/harness/hunt/hunt-session.ts +7 -20
- package/src/index.ts +31 -0
- package/src/server/client/App.tsx +86 -22
- package/src/server/client/CommentsPanel.tsx +255 -0
- package/src/server/client/DeleteRunModal.tsx +163 -0
- package/src/server/client/FindingCard.tsx +12 -4
- package/src/server/client/FindingsList.tsx +4 -1
- package/src/server/client/ResumePanel.tsx +8 -8
- package/src/server/client/Sidebar.tsx +452 -0
- package/src/server/client/SubmitModal.tsx +61 -5
- package/src/server/client/icons.tsx +29 -0
- package/src/server/client/index.tsx +1 -1
- package/src/server/client/utils/constants.ts +2 -0
- package/src/server/client/utils/format.ts +47 -2
- package/src/server/client/utils/types.ts +108 -0
- package/src/server/editor.ts +129 -0
- package/src/server/index.ts +205 -3
- package/src/server/pr-comments.ts +256 -0
- package/src/server/pr-inbox.ts +213 -0
- package/src/server/run-repository.ts +136 -3
- package/src/skill/README.md +11 -3
- package/src/skill/SKILL.md +44 -10
- package/src/skill/hunt.md +33 -7
- package/src/tests/__snapshots__/baseline.test.ts.snap +94 -24
- package/src/tests/ai-core.test.ts +2 -2
- package/src/tests/cli.test.ts +35 -1
- package/src/tests/github-review.test.ts +9 -0
- package/src/tests/server-editor.test.ts +100 -0
- package/src/tests/server.test.ts +333 -2
- package/src/tests/worktree.test.ts +369 -0
- package/src/server/client/RunList.tsx +0 -75
|
@@ -83,6 +83,9 @@ export type ArtifactMeta = {
|
|
|
83
83
|
export type RunSummary = {
|
|
84
84
|
slug: string;
|
|
85
85
|
branch: string;
|
|
86
|
+
/** Worktree onde a run foi executada (ausente = clone principal). */
|
|
87
|
+
worktreePath?: string;
|
|
88
|
+
prNumber?: number;
|
|
86
89
|
updatedAt?: string;
|
|
87
90
|
startedAt?: string;
|
|
88
91
|
/** Último sinal de vida de uma execução ativa (revisão/correção). */
|
|
@@ -97,6 +100,109 @@ export type FindingsResponse = {
|
|
|
97
100
|
summary: Summary;
|
|
98
101
|
};
|
|
99
102
|
|
|
103
|
+
/** Editor detectado pelo servidor (base dos links de código). */
|
|
104
|
+
export type EditorKind = "vscode" | "jetbrains" | "none";
|
|
105
|
+
|
|
106
|
+
export type EditorContext = {
|
|
107
|
+
kind: EditorKind;
|
|
108
|
+
project?: string;
|
|
109
|
+
product?: string;
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
/** Estado da árvore de trabalho (bloqueia o checkout quando suja). */
|
|
113
|
+
export type WorktreeState = {
|
|
114
|
+
available: boolean;
|
|
115
|
+
clean: boolean;
|
|
116
|
+
changes: number;
|
|
117
|
+
sample: string[];
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/** PR aberta exibida na sidebar. */
|
|
121
|
+
export type PrListItem = {
|
|
122
|
+
number: number;
|
|
123
|
+
title: string;
|
|
124
|
+
headRefName: string;
|
|
125
|
+
baseRefName: string;
|
|
126
|
+
author: string;
|
|
127
|
+
updatedAt?: string;
|
|
128
|
+
isDraft: boolean;
|
|
129
|
+
reviewDecision?: string;
|
|
130
|
+
url?: string;
|
|
131
|
+
runSlug?: string;
|
|
132
|
+
/** Worktree ativo da PR (revisão paralela). */
|
|
133
|
+
worktreePath?: string;
|
|
134
|
+
};
|
|
135
|
+
|
|
136
|
+
export type PrListResponse = {
|
|
137
|
+
available: boolean;
|
|
138
|
+
reason?: string;
|
|
139
|
+
currentBranch: string | null;
|
|
140
|
+
worktree: WorktreeState;
|
|
141
|
+
prs: PrListItem[];
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
export type CheckoutResponse =
|
|
145
|
+
| { ok: true; number: number; branch: string | null }
|
|
146
|
+
| {
|
|
147
|
+
ok: false;
|
|
148
|
+
reason: "dirty" | "git" | "gh";
|
|
149
|
+
detail: string;
|
|
150
|
+
changes?: string[];
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/** Resultado de `DELETE /api/runs/:slug`. */
|
|
154
|
+
export type DeleteRunResponse = {
|
|
155
|
+
deleted: boolean;
|
|
156
|
+
worktreeRequested: boolean;
|
|
157
|
+
worktreeRemoved: boolean;
|
|
158
|
+
worktreePath?: string;
|
|
159
|
+
warning?: string;
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
export type PrWorktreeResponse =
|
|
163
|
+
| {
|
|
164
|
+
ok: true;
|
|
165
|
+
number: number;
|
|
166
|
+
branch: string;
|
|
167
|
+
path: string;
|
|
168
|
+
head: string;
|
|
169
|
+
created: boolean;
|
|
170
|
+
warning?: string;
|
|
171
|
+
}
|
|
172
|
+
| { ok: false; reason: string; detail: string };
|
|
173
|
+
|
|
174
|
+
/** Comentário normalizado da PR (thread ou geral). */
|
|
175
|
+
export type PrCommentView = {
|
|
176
|
+
id: number;
|
|
177
|
+
author: string;
|
|
178
|
+
authorType: string;
|
|
179
|
+
body: string;
|
|
180
|
+
createdAt?: string;
|
|
181
|
+
url?: string;
|
|
182
|
+
path?: string;
|
|
183
|
+
line: number | null;
|
|
184
|
+
absFile?: string;
|
|
185
|
+
replyTo?: number | null;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
export type PrCommentThread = {
|
|
189
|
+
id: number;
|
|
190
|
+
path: string;
|
|
191
|
+
line: number | null;
|
|
192
|
+
absFile: string;
|
|
193
|
+
comments: PrCommentView[];
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export type PrCommentsResponse = {
|
|
197
|
+
available: boolean;
|
|
198
|
+
reason?: string;
|
|
199
|
+
target?: { number: number; repo: string; url?: string };
|
|
200
|
+
fetchedAt: string;
|
|
201
|
+
threads: PrCommentThread[];
|
|
202
|
+
issueComments: PrCommentView[];
|
|
203
|
+
counts: { threads: number; inline: number; issue: number };
|
|
204
|
+
};
|
|
205
|
+
|
|
100
206
|
/** Manifesto da run (subset consumido pela aba de resumo). */
|
|
101
207
|
export type RunManifest = {
|
|
102
208
|
branch?: string;
|
|
@@ -149,6 +255,8 @@ export type SubmitCandidate = {
|
|
|
149
255
|
file: string;
|
|
150
256
|
line: number | null;
|
|
151
257
|
severity: Severity;
|
|
258
|
+
/** Decisão da triagem (`execute` = "a executar"). */
|
|
259
|
+
decision: Decision;
|
|
152
260
|
execState: ExecState;
|
|
153
261
|
rule: string;
|
|
154
262
|
message: string;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Detecção do editor que hospeda o terminal do servidor (biblioteca do servidor).
|
|
3
|
+
*
|
|
4
|
+
* VS Code e JetBrains expõem variáveis próprias no terminal integrado. O
|
|
5
|
+
* contexto alimenta os links de código da UI (`vscode://` / `jetbrains://`).
|
|
6
|
+
* A detecção é pura e fail-open: sem pistas, o editor fica `none`.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { basename, resolve } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Editores com esquema de URL próprio. */
|
|
12
|
+
export type EditorKind = "vscode" | "jetbrains" | "none";
|
|
13
|
+
|
|
14
|
+
/** Contexto usado pelo cliente para montar os links de código. */
|
|
15
|
+
export type EditorContext = {
|
|
16
|
+
kind: EditorKind;
|
|
17
|
+
/** Nome do projeto aberto no IDE (basename da raiz do repositório). */
|
|
18
|
+
project?: string;
|
|
19
|
+
/** Código do produto JetBrains (`idea`, `webstorm`, `pycharm`…). */
|
|
20
|
+
product?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
type Env = Record<string, string | undefined>;
|
|
24
|
+
|
|
25
|
+
/** Bundle macOS → código do produto usado no esquema `jetbrains://`. */
|
|
26
|
+
const BUNDLE_PRODUCTS: Record<string, string> = {
|
|
27
|
+
"com.jetbrains.intellij": "idea",
|
|
28
|
+
"com.jetbrains.intellij.ce": "idea",
|
|
29
|
+
"com.jetbrains.pycharm": "pycharm",
|
|
30
|
+
"com.jetbrains.pycharm.ce": "pycharm",
|
|
31
|
+
"com.jetbrains.webstorm": "webstorm",
|
|
32
|
+
"com.jetbrains.goland": "goland",
|
|
33
|
+
"com.jetbrains.clion": "clion",
|
|
34
|
+
"com.jetbrains.rider": "rider",
|
|
35
|
+
"com.jetbrains.phpstorm": "phpstorm",
|
|
36
|
+
"com.jetbrains.rubymine": "rubymine",
|
|
37
|
+
"com.jetbrains.datagrip": "datagrip",
|
|
38
|
+
"com.jetbrains.appcode": "appcode",
|
|
39
|
+
"com.jetbrains.rustrover": "rustrover",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** Variáveis `*_INITIAL_DIRECTORY` → produto JetBrains. */
|
|
43
|
+
const DIR_PRODUCTS: [string, string][] = [
|
|
44
|
+
["IDEA_INITIAL_DIRECTORY", "idea"],
|
|
45
|
+
["PYCHARM_INITIAL_DIRECTORY", "pycharm"],
|
|
46
|
+
["WEBSTORM_INITIAL_DIRECTORY", "webstorm"],
|
|
47
|
+
["GOLAND_INITIAL_DIRECTORY", "goland"],
|
|
48
|
+
["CLION_INITIAL_DIRECTORY", "clion"],
|
|
49
|
+
["RIDER_INITIAL_DIRECTORY", "rider"],
|
|
50
|
+
["PHPSTORM_INITIAL_DIRECTORY", "phpstorm"],
|
|
51
|
+
["RUBYMINE_INITIAL_DIRECTORY", "rubymine"],
|
|
52
|
+
["DATAGRIP_INITIAL_DIRECTORY", "datagrip"],
|
|
53
|
+
["RUSTROVER_INITIAL_DIRECTORY", "rustrover"],
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
function projectName(repoRoot: string): string {
|
|
57
|
+
return basename(resolve(repoRoot));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Produto JetBrains detectado no ambiente (undefined quando ambíguo). */
|
|
61
|
+
export function detectJetBrainsProduct(env: Env): string | undefined {
|
|
62
|
+
const bundle = env.__CFBundleIdentifier?.toLowerCase();
|
|
63
|
+
if (bundle) {
|
|
64
|
+
const exact = BUNDLE_PRODUCTS[bundle];
|
|
65
|
+
if (exact) return exact;
|
|
66
|
+
const prefix = Object.keys(BUNDLE_PRODUCTS).find((key) => bundle.startsWith(key));
|
|
67
|
+
if (prefix) return BUNDLE_PRODUCTS[prefix];
|
|
68
|
+
}
|
|
69
|
+
for (const [name, product] of DIR_PRODUCTS) {
|
|
70
|
+
if (env[name]) return product;
|
|
71
|
+
}
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function isJetBrains(env: Env): boolean {
|
|
76
|
+
const bundle = env.__CFBundleIdentifier?.toLowerCase() ?? "";
|
|
77
|
+
return (
|
|
78
|
+
/jetbrains/i.test(env.TERMINAL_EMULATOR ?? "") ||
|
|
79
|
+
bundle.startsWith("com.jetbrains.") ||
|
|
80
|
+
DIR_PRODUCTS.some(([name]) => Boolean(env[name]))
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function isVsCode(env: Env): boolean {
|
|
85
|
+
return (
|
|
86
|
+
env.TERM_PROGRAM === "vscode" ||
|
|
87
|
+
env.TERM_PROGRAM === "vscode-insiders" ||
|
|
88
|
+
Boolean(env.VSCODE_PID || env.VSCODE_IPC_HOOK_CLI || env.VSCODE_GIT_IPC_HANDLE)
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/** Detecta o editor a partir do ambiente do terminal integrado. */
|
|
93
|
+
export function detectEditor(env: Env, repoRoot: string): EditorContext {
|
|
94
|
+
const project = projectName(repoRoot);
|
|
95
|
+
if (isJetBrains(env)) {
|
|
96
|
+
return {
|
|
97
|
+
kind: "jetbrains",
|
|
98
|
+
project,
|
|
99
|
+
product: detectJetBrainsProduct(env) ?? "idea",
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
if (isVsCode(env)) return { kind: "vscode", project };
|
|
103
|
+
return { kind: "none", project };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Resolve a preferência do `--editor`.
|
|
108
|
+
*
|
|
109
|
+
* `auto` (ou ausente) detecta do ambiente; `vscode`/`jetbrains`/`none` forçam
|
|
110
|
+
* o comportamento. Valores desconhecidos caem na detecção automática.
|
|
111
|
+
*/
|
|
112
|
+
export function resolveEditor(
|
|
113
|
+
preference: string | undefined,
|
|
114
|
+
env: Env,
|
|
115
|
+
repoRoot: string,
|
|
116
|
+
): EditorContext {
|
|
117
|
+
const value = preference?.trim().toLowerCase();
|
|
118
|
+
const project = projectName(repoRoot);
|
|
119
|
+
if (value === "vscode") return { kind: "vscode", project };
|
|
120
|
+
if (value === "jetbrains") {
|
|
121
|
+
return {
|
|
122
|
+
kind: "jetbrains",
|
|
123
|
+
project,
|
|
124
|
+
product: detectJetBrainsProduct(env) ?? "idea",
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
if (value === "none") return { kind: "none", project };
|
|
128
|
+
return detectEditor(env, repoRoot);
|
|
129
|
+
}
|
package/src/server/index.ts
CHANGED
|
@@ -9,16 +9,23 @@
|
|
|
9
9
|
import { mediaPath } from "../core/assets";
|
|
10
10
|
import type { HoundsmanConfig } from "../core/config";
|
|
11
11
|
import type { FindingUpdate } from "../core/findings-state";
|
|
12
|
+
import { createGitRunner, type GitRunner } from "../core/git-runner";
|
|
12
13
|
import {
|
|
13
14
|
defaultGhRunner,
|
|
14
15
|
type GhRunner,
|
|
15
16
|
type SubmitSelection,
|
|
16
17
|
} from "../core/github-review";
|
|
17
18
|
import { logger } from "../core/logging";
|
|
19
|
+
import { GitRepository } from "../core/services/git-repository";
|
|
20
|
+
import { PrResolver } from "../core/services/pr-resolver";
|
|
21
|
+
import { WorktreeManager } from "../core/services/worktree-manager";
|
|
18
22
|
import { createStorage } from "../core/storage/factory";
|
|
23
|
+
import { resolveWorktreesRoot } from "../core/storage/paths";
|
|
19
24
|
import { resolveRepoIdentity } from "../core/storage/repo-identity";
|
|
20
25
|
import type { RepoIdentity, StorageProvider } from "../core/storage/types";
|
|
21
26
|
import { buildClientBundle } from "./client-build";
|
|
27
|
+
import { type EditorContext, resolveEditor } from "./editor";
|
|
28
|
+
import { PrInbox } from "./pr-inbox";
|
|
22
29
|
import { RunRepository } from "./run-repository";
|
|
23
30
|
|
|
24
31
|
type BunServer = ReturnType<typeof Bun.serve>;
|
|
@@ -59,8 +66,13 @@ function notFound(detail = "não encontrado"): Response {
|
|
|
59
66
|
/** Repositório das runs (dados) — ver `run-repository.ts`. */
|
|
60
67
|
type ServerDeps = {
|
|
61
68
|
ghRunner?: GhRunner;
|
|
69
|
+
gitRunner?: GitRunner;
|
|
62
70
|
storage?: StorageProvider;
|
|
63
71
|
repo?: RepoIdentity;
|
|
72
|
+
/** Contexto do editor injetado (testes). */
|
|
73
|
+
editor?: EditorContext;
|
|
74
|
+
/** Preferência do `--editor` (CLI). */
|
|
75
|
+
editorPreference?: string;
|
|
64
76
|
};
|
|
65
77
|
|
|
66
78
|
// ---------------------------------------------------------------------------
|
|
@@ -83,6 +95,10 @@ async function route(
|
|
|
83
95
|
request: Request,
|
|
84
96
|
runs: RunRepository,
|
|
85
97
|
ghRunner: GhRunner,
|
|
98
|
+
prs: PrInbox,
|
|
99
|
+
editor: EditorContext,
|
|
100
|
+
worktrees: WorktreeManager,
|
|
101
|
+
git: GitRepository,
|
|
86
102
|
): Promise<Response> {
|
|
87
103
|
const url = new URL(request.url);
|
|
88
104
|
const path = url.pathname;
|
|
@@ -95,6 +111,75 @@ async function route(
|
|
|
95
111
|
}
|
|
96
112
|
if (path === `/assets/${LOGO_FILE}`) return serveLogo();
|
|
97
113
|
if (path === "/api/health") return json({ ok: true });
|
|
114
|
+
if (path === "/api/editor" && request.method === "GET") return json(editor);
|
|
115
|
+
if (path === "/api/prs" && request.method === "GET") {
|
|
116
|
+
return json(prs.list(runs.workspaceIndex()));
|
|
117
|
+
}
|
|
118
|
+
if (path === "/api/worktrees" && request.method === "GET") {
|
|
119
|
+
return json({
|
|
120
|
+
worktrees: worktrees.managed().map((entry) => ({
|
|
121
|
+
path: entry.path,
|
|
122
|
+
branch: entry.branch?.replace(/^refs\/heads\//, "") ?? null,
|
|
123
|
+
head: entry.head ?? null,
|
|
124
|
+
prunable: entry.prunable,
|
|
125
|
+
})),
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
if (path === "/api/prs/worktree" && request.method === "POST") {
|
|
129
|
+
const body = (await request.json().catch(() => ({}))) as { number?: unknown };
|
|
130
|
+
const number = Number(body.number);
|
|
131
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
132
|
+
return json(
|
|
133
|
+
{ ok: false, reason: "invalid", detail: "Número de PR inválido." },
|
|
134
|
+
400,
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
const resolver = new PrResolver(ghRunner, git);
|
|
138
|
+
const resolved = resolver.resolve(number);
|
|
139
|
+
if (!resolved.ok) {
|
|
140
|
+
return json({ ok: false, reason: resolved.reason, detail: resolved.detail });
|
|
141
|
+
}
|
|
142
|
+
const refs = resolver.prepareRefs(resolved.pr);
|
|
143
|
+
try {
|
|
144
|
+
const worktree = worktrees.ensure({
|
|
145
|
+
branch: resolved.pr.headRefName,
|
|
146
|
+
ref: refs.head,
|
|
147
|
+
});
|
|
148
|
+
return json({
|
|
149
|
+
ok: true,
|
|
150
|
+
number,
|
|
151
|
+
branch: resolved.pr.headRefName,
|
|
152
|
+
path: worktree.path,
|
|
153
|
+
head: worktree.head,
|
|
154
|
+
created: worktree.created,
|
|
155
|
+
warning: refs.warning,
|
|
156
|
+
});
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return json({
|
|
159
|
+
ok: false,
|
|
160
|
+
reason: "git",
|
|
161
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const worktreeMatch = path.match(/^\/api\/worktrees\/(.+)$/);
|
|
166
|
+
if (worktreeMatch && request.method === "DELETE") {
|
|
167
|
+
const branch = decodeURIComponent(worktreeMatch[1]);
|
|
168
|
+
const force = url.searchParams.get("force") === "1";
|
|
169
|
+
const result = worktrees.remove(branch, { force });
|
|
170
|
+
return json(result, result.removed ? 200 : 409);
|
|
171
|
+
}
|
|
172
|
+
if (path === "/api/prs/checkout" && request.method === "POST") {
|
|
173
|
+
const body = (await request.json().catch(() => ({}))) as { number?: unknown };
|
|
174
|
+
const number = Number(body.number);
|
|
175
|
+
if (!Number.isInteger(number) || number <= 0) {
|
|
176
|
+
return json(
|
|
177
|
+
{ ok: false, reason: "invalid", detail: "Número de PR inválido." },
|
|
178
|
+
400,
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
return json(prs.checkout(number));
|
|
182
|
+
}
|
|
98
183
|
|
|
99
184
|
if (path === "/api/runs" && request.method === "GET") {
|
|
100
185
|
return json({ runs: runs.list() });
|
|
@@ -106,8 +191,21 @@ async function route(
|
|
|
106
191
|
const rest = match[2] ?? "";
|
|
107
192
|
if (!runs.exists(slug)) return notFound(`run '${slug}' não encontrada`);
|
|
108
193
|
|
|
194
|
+
if (rest === "" && request.method === "DELETE") {
|
|
195
|
+
const removeWorktree = ["1", "true"].includes(
|
|
196
|
+
url.searchParams.get("worktree") ?? "",
|
|
197
|
+
);
|
|
198
|
+
const result = runs.delete(slug, { removeWorktree });
|
|
199
|
+
return result ? json(result) : notFound(`run '${slug}' não encontrada`);
|
|
200
|
+
}
|
|
201
|
+
|
|
109
202
|
if (rest === "" || rest === "/") return json(runs.detail(slug));
|
|
110
203
|
|
|
204
|
+
if (rest === "/comments" && request.method === "GET") {
|
|
205
|
+
return json(
|
|
206
|
+
runs.comments(slug, { refresh: url.searchParams.get("refresh") === "1" }),
|
|
207
|
+
);
|
|
208
|
+
}
|
|
111
209
|
if (rest === "/findings" && request.method === "GET") {
|
|
112
210
|
return json(runs.findings(slug, url.searchParams));
|
|
113
211
|
}
|
|
@@ -119,6 +217,7 @@ async function route(
|
|
|
119
217
|
const body = (await request.json().catch(() => ({}))) as SubmitSelection;
|
|
120
218
|
const selection: SubmitSelection = {
|
|
121
219
|
severities: body.severities,
|
|
220
|
+
decisions: body.decisions,
|
|
122
221
|
execStates: body.execStates,
|
|
123
222
|
includeSent: body.includeSent,
|
|
124
223
|
};
|
|
@@ -161,19 +260,46 @@ async function route(
|
|
|
161
260
|
export class HoundsmanServer {
|
|
162
261
|
private readonly runs: RunRepository;
|
|
163
262
|
private readonly ghRunner: GhRunner;
|
|
263
|
+
private readonly prs: PrInbox;
|
|
264
|
+
private readonly editor: EditorContext;
|
|
265
|
+
private readonly worktrees: WorktreeManager;
|
|
266
|
+
private readonly git: GitRepository;
|
|
164
267
|
|
|
165
268
|
constructor(config: HoundsmanConfig, deps: ServerDeps = {}) {
|
|
269
|
+
const repo = deps.repo ?? resolveRepoIdentity();
|
|
270
|
+
this.ghRunner = deps.ghRunner ?? defaultGhRunner;
|
|
271
|
+
this.git = new GitRepository(deps.gitRunner ?? createGitRunner(repo.root));
|
|
272
|
+
this.prs = new PrInbox(this.ghRunner, this.git);
|
|
273
|
+
this.worktrees = new WorktreeManager(
|
|
274
|
+
new GitRepository(createGitRunner(repo.root)),
|
|
275
|
+
{
|
|
276
|
+
repoKey: repo.key,
|
|
277
|
+
repoRoot: repo.root,
|
|
278
|
+
baseDir: resolveWorktreesRoot(config.worktrees?.location, repo.root),
|
|
279
|
+
},
|
|
280
|
+
);
|
|
166
281
|
this.runs = new RunRepository(
|
|
167
282
|
deps.storage ?? createStorage(config),
|
|
168
|
-
|
|
283
|
+
repo,
|
|
284
|
+
this.ghRunner,
|
|
285
|
+
this.worktrees,
|
|
169
286
|
);
|
|
170
|
-
this.
|
|
287
|
+
this.editor =
|
|
288
|
+
deps.editor ?? resolveEditor(deps.editorPreference, process.env, repo.root);
|
|
171
289
|
}
|
|
172
290
|
|
|
173
291
|
/** Handler de requisições (testável sem abrir porta). */
|
|
174
292
|
readonly handler = async (request: Request): Promise<Response> => {
|
|
175
293
|
try {
|
|
176
|
-
return await route(
|
|
294
|
+
return await route(
|
|
295
|
+
request,
|
|
296
|
+
this.runs,
|
|
297
|
+
this.ghRunner,
|
|
298
|
+
this.prs,
|
|
299
|
+
this.editor,
|
|
300
|
+
this.worktrees,
|
|
301
|
+
this.git,
|
|
302
|
+
);
|
|
177
303
|
} catch (error) {
|
|
178
304
|
return json(
|
|
179
305
|
{ error: error instanceof Error ? error.message : String(error) },
|
|
@@ -223,17 +349,26 @@ export type ServerOptions = {
|
|
|
223
349
|
host?: string;
|
|
224
350
|
/** Runner do `gh` (injetável nos testes). */
|
|
225
351
|
ghRunner?: GhRunner;
|
|
352
|
+
/** Runner do git (injetável nos testes). */
|
|
353
|
+
gitRunner?: GitRunner;
|
|
226
354
|
/** Provider/storage injetável (testes). */
|
|
227
355
|
storage?: StorageProvider;
|
|
228
356
|
repo?: RepoIdentity;
|
|
357
|
+
/** Contexto de editor injetado (testes). */
|
|
358
|
+
editor?: EditorContext;
|
|
359
|
+
/** Preferência do `--editor` (auto|vscode|jetbrains|none). */
|
|
360
|
+
editorPreference?: string;
|
|
229
361
|
};
|
|
230
362
|
|
|
231
363
|
/** Sobe o servidor e devolve a instância + URL. */
|
|
232
364
|
export function startServer(options: ServerOptions): { server: BunServer; url: string } {
|
|
233
365
|
return new HoundsmanServer(options.config, {
|
|
234
366
|
ghRunner: options.ghRunner,
|
|
367
|
+
gitRunner: options.gitRunner,
|
|
235
368
|
storage: options.storage,
|
|
236
369
|
repo: options.repo,
|
|
370
|
+
editor: options.editor,
|
|
371
|
+
editorPreference: options.editorPreference,
|
|
237
372
|
}).start({
|
|
238
373
|
port: options.port,
|
|
239
374
|
host: options.host,
|
|
@@ -295,6 +430,66 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
|
|
|
295
430
|
.status-review_done { color:var(--suggestion); }
|
|
296
431
|
.status-fix_done { color:var(--success); }
|
|
297
432
|
.status-timeout { color:var(--blocking); }
|
|
433
|
+
.side-tabs { display:flex; gap:4px; padding:4px; background:var(--surface-2); border:1px solid var(--border); border-radius:999px; }
|
|
434
|
+
.side-tab { flex:1; display:inline-flex; align-items:center; justify-content:center; gap:7px; font:inherit; font-size:12px; font-weight:600; cursor:pointer; color:var(--muted);
|
|
435
|
+
padding:7px 10px; background:transparent; border:1px solid transparent; border-radius:999px; transition:color .15s ease, background .15s ease; }
|
|
436
|
+
.side-tab:hover { color:var(--fg); }
|
|
437
|
+
.side-tab.active { color:#1b1305; background:linear-gradient(180deg, var(--accent-strong), var(--accent)); }
|
|
438
|
+
.side-count { padding:1px 6px; font-family:var(--font-mono); font-size:10px; color:var(--muted); background:rgba(255,255,255,.08); border-radius:999px; }
|
|
439
|
+
.side-tab.active .side-count { color:#1b1305; background:rgba(0,0,0,.14); }
|
|
440
|
+
.icon-btn { display:inline-grid; place-items:center; width:26px; height:26px; color:var(--muted); background:transparent; border:1px solid var(--border); border-radius:8px; cursor:pointer; text-decoration:none; }
|
|
441
|
+
.icon-btn:hover { color:var(--fg); border-color:var(--border-strong); }
|
|
442
|
+
.icon-btn:disabled { opacity:.5; cursor:not-allowed; }
|
|
443
|
+
.pr-toolbar { display:flex; align-items:center; justify-content:space-between; gap:8px; }
|
|
444
|
+
.pr-toolbar .section-label { margin:0; }
|
|
445
|
+
.prs { display:flex; flex-direction:column; gap:8px; }
|
|
446
|
+
.pr { display:flex; flex-direction:column; gap:7px; padding:11px 12px; background:var(--card); border:1px solid var(--border); border-radius:var(--radius); }
|
|
447
|
+
.pr.current { background:var(--accent-soft); border-color:var(--accent-line); }
|
|
448
|
+
.pr-head { display:flex; align-items:center; gap:7px; font-family:var(--font-mono); font-size:11px; color:var(--muted); }
|
|
449
|
+
.pr-icon { color:var(--muted-2); }
|
|
450
|
+
.pr.current .pr-icon { color:var(--accent); }
|
|
451
|
+
.pr-number { font-weight:600; color:var(--fg); }
|
|
452
|
+
.pr-current { padding:1px 6px; font-size:9px; font-weight:700; letter-spacing:.08em; text-transform:uppercase; color:var(--accent); background:rgba(240,165,44,.12); border-radius:999px; }
|
|
453
|
+
.pr-title { margin:0; font-size:12.5px; font-weight:600; line-height:1.4; word-break:break-word; }
|
|
454
|
+
.pr-meta { display:flex; flex-wrap:wrap; align-items:center; gap:1px 5px; margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
|
|
455
|
+
.pr-branch { color:var(--suggestion); }
|
|
456
|
+
.pr-review { margin:0; font-size:10.5px; font-weight:600; }
|
|
457
|
+
.pr-review-APPROVED { color:var(--success); }
|
|
458
|
+
.pr-review-CHANGES_REQUESTED { color:var(--blocking); }
|
|
459
|
+
.pr-review-REVIEW_REQUIRED { color:var(--attention); }
|
|
460
|
+
.pr-actions { display:flex; flex-wrap:wrap; gap:6px; margin-top:2px; }
|
|
461
|
+
.pr-btn { display:inline-flex; align-items:center; gap:5px; font:inherit; font-size:10.5px; font-weight:600; cursor:pointer; padding:4px 9px; color:var(--muted);
|
|
462
|
+
background:transparent; border:1px solid var(--border-strong); border-radius:999px; text-decoration:none; }
|
|
463
|
+
.pr-btn:hover { color:var(--fg); border-color:var(--accent-line); }
|
|
464
|
+
.pr-btn:disabled { opacity:.45; cursor:not-allowed; }
|
|
465
|
+
.pr-btn.danger { color:var(--accent); background:var(--accent-soft); border-color:var(--accent-line); }
|
|
466
|
+
.pr-warning { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--attention);
|
|
467
|
+
background:rgba(237,161,58,.08); border:1px solid rgba(237,161,58,.24); border-radius:10px; }
|
|
468
|
+
.pr-notice { margin:0; padding:8px 10px; font-size:11px; line-height:1.5; color:var(--success);
|
|
469
|
+
background:rgba(69,192,122,.08); border:1px solid rgba(69,192,122,.24); border-radius:10px; }
|
|
470
|
+
.pr-current-branch { margin:0; font-family:var(--font-mono); font-size:10.5px; color:var(--muted-2); }
|
|
471
|
+
.pr-current-branch code { color:var(--fg); }
|
|
472
|
+
.comments { display:flex; flex-direction:column; gap:18px; }
|
|
473
|
+
.comments-toolbar { display:flex; align-items:center; justify-content:space-between; gap:12px; }
|
|
474
|
+
.comments-counts { display:flex; flex-wrap:wrap; align-items:center; gap:12px; font-size:11.5px; color:var(--muted); }
|
|
475
|
+
.comments-counts b { font-family:var(--font-mono); font-weight:600; color:var(--fg); }
|
|
476
|
+
.comment-group { display:flex; flex-direction:column; gap:10px; }
|
|
477
|
+
.comment-group-title { margin:0; font-size:11px; font-weight:700; letter-spacing:.16em; text-transform:uppercase; color:var(--muted-2); }
|
|
478
|
+
.comment-file-group { display:flex; flex-direction:column; gap:8px; }
|
|
479
|
+
.comment-group-label { display:flex; align-items:center; gap:8px; margin:0; font-family:var(--font-mono); font-size:11.5px; color:var(--suggestion); }
|
|
480
|
+
.comment-thread { display:flex; flex-direction:column; overflow:hidden; background:var(--card); border:1px solid var(--border); border-radius:var(--radius-lg); }
|
|
481
|
+
.comment-thread-head { display:flex; align-items:center; gap:8px; padding:9px 13px; font-family:var(--font-mono); font-size:11px; color:var(--muted);
|
|
482
|
+
background:var(--surface-2); border-bottom:1px solid var(--border); }
|
|
483
|
+
.comment-file { overflow:hidden; color:var(--fg); text-overflow:ellipsis; white-space:nowrap; }
|
|
484
|
+
.pr-comment { display:flex; flex-direction:column; gap:7px; padding:12px 14px; border-top:1px solid var(--border); }
|
|
485
|
+
.comment-thread .pr-comment:first-of-type { border-top:0; }
|
|
486
|
+
.pr-comment.reply { margin-left:26px; background:rgba(255,255,255,.012); border-left:2px solid var(--accent-line); }
|
|
487
|
+
.pr-comment-head { display:flex; align-items:center; gap:9px; font-size:11px; color:var(--muted-2); }
|
|
488
|
+
.pr-comment-author { font-size:12px; font-weight:600; color:var(--fg); }
|
|
489
|
+
.pr-comment-link { display:inline-flex; align-items:center; gap:5px; font-size:11px; color:var(--muted); text-decoration:none; }
|
|
490
|
+
.pr-comment-link:hover { color:var(--accent); }
|
|
491
|
+
.pr-comment-body { font-size:12.5px; line-height:1.6; color:#f0ece6; }
|
|
492
|
+
.pr-comment-body .md-p, .pr-comment-body .md-list { font-size:12.5px; }
|
|
298
493
|
.main { flex:1; min-width:0; display:flex; flex-direction:column; overflow-y:auto; }
|
|
299
494
|
.topbar { position:sticky; top:0; z-index:10; display:flex; align-items:center; justify-content:space-between; gap:16px;
|
|
300
495
|
padding:13px 24px; border-bottom:1px solid var(--border); background:rgba(16,16,20,.74);
|
|
@@ -314,6 +509,13 @@ code, pre, .mono { font-family:var(--font-mono); tab-size:4; }
|
|
|
314
509
|
transition:background .15s ease, border-color .15s ease, color .15s ease; }
|
|
315
510
|
.btn:hover { background:#202027; border-color:var(--accent-line); }
|
|
316
511
|
.btn:disabled { opacity:.55; cursor:not-allowed; }
|
|
512
|
+
.btn.danger { color:#ffb3b3; background:rgba(255,86,86,.12); border-color:rgba(255,86,86,.4); }
|
|
513
|
+
.btn.danger:hover { background:rgba(255,86,86,.2); border-color:rgba(255,86,86,.62); }
|
|
514
|
+
.modal-check { display:flex; align-items:flex-start; gap:10px; padding:12px 14px; font-size:12.5px; line-height:1.55;
|
|
515
|
+
color:var(--fg); background:var(--surface-2); border:1px solid var(--border); border-radius:12px; cursor:pointer; }
|
|
516
|
+
.modal-check input { margin:3px 0 0; flex:none; accent-color:var(--accent); }
|
|
517
|
+
.modal-check code { font-family:var(--font-mono); font-size:11.5px; color:var(--muted); word-break:break-all; }
|
|
518
|
+
.modal-warn { margin:0; font-size:12px; line-height:1.55; color:var(--muted); }
|
|
317
519
|
.icon.spin { animation:spin .8s linear infinite; }
|
|
318
520
|
.content { width:100%; max-width:1180px; margin:0 auto; padding:22px 24px 64px; }
|
|
319
521
|
.page-header { display:flex; align-items:flex-start; justify-content:space-between; gap:18px; margin-bottom:16px; }
|