dsh-desktop-kit 2.2.1 → 2.7.2
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/CHANGELOG.md +80 -0
- package/README.md +113 -27
- package/client.js +3121 -32
- package/docs/AGENT-HARNESS.md +144 -0
- package/docs/ARCHITECTURE.md +38 -7
- package/docs/COMPATIBILITY.md +4 -2
- package/docs/DEVELOPMENT.md +56 -0
- package/docs/INSTALL.md +20 -11
- package/docs/LAPTOP-INSTALL.md +96 -0
- package/docs/ROADMAP-3.0.md +353 -0
- package/docs/ROADMAP.md +1 -1
- package/docs/SUBAGENTS.md +25 -0
- package/docs/TEST-0.2.2.md +5 -5
- package/docs/TEST-2.3.0.md +49 -0
- package/docs/TEST-2.7.2.md +46 -0
- package/docs/TOKENS.md +2 -2
- package/docs/TROUBLESHOOTING.md +27 -0
- package/docs/UI-QA.md +28 -0
- package/docs/images/peak-toast-peak.png +0 -0
- package/docs/images/peak-toast-soon.png +0 -0
- package/docs/preview/peak-toast.html +155 -0
- package/index.js +18 -1
- package/package.json +26 -6
- package/plugins/dsh-agent-extensions/README.md +99 -0
- package/plugins/dsh-agent-extensions/index.js +167 -0
- package/plugins/dsh-agent-extensions/lib/agents.js +76 -0
- package/plugins/dsh-agent-extensions/lib/checkpoints.js +103 -0
- package/plugins/dsh-agent-extensions/lib/commands.js +255 -0
- package/plugins/dsh-agent-extensions/lib/hooks.js +182 -0
- package/plugins/dsh-agent-extensions/lib/policy.js +84 -0
- package/plugins/dsh-agent-extensions/lib/project.js +144 -0
- package/plugins/dsh-agent-extensions/lib/scaffold.js +151 -0
- package/plugins/dsh-agent-extensions/package.json +9 -0
- package/plugins/dsh-browser-qa/README.md +221 -0
- package/plugins/dsh-browser-qa/cordis.patch.yml +4 -0
- package/plugins/dsh-browser-qa/index.js +165 -0
- package/plugins/dsh-browser-qa/lib/cdp.js +224 -0
- package/plugins/dsh-browser-qa/lib/chrome.js +466 -0
- package/plugins/dsh-browser-qa/lib/pages.js +552 -0
- package/plugins/dsh-browser-qa/lib/qa.js +827 -0
- package/plugins/dsh-browser-qa/lib/ui-gate.js +256 -0
- package/plugins/dsh-browser-qa/package.json +31 -0
- package/plugins/dsh-checkpoints/README.md +22 -0
- package/plugins/dsh-checkpoints/cordis.patch.yml +3 -0
- package/plugins/dsh-checkpoints/index.js +80 -0
- package/plugins/dsh-checkpoints/lib/checkpoints.js +111 -0
- package/plugins/dsh-checkpoints/package.json +25 -0
- package/plugins/dsh-github-review/README.md +131 -0
- package/plugins/dsh-github-review/cordis.patch.yml +3 -0
- package/plugins/dsh-github-review/index.js +536 -0
- package/plugins/dsh-github-review/lib/gh.js +494 -0
- package/plugins/dsh-github-review/lib/normalize.js +662 -0
- package/plugins/dsh-github-review/package.json +28 -0
- package/plugins/dsh-guardrails/README.md +264 -0
- package/plugins/dsh-guardrails/cordis.patch.yml +5 -0
- package/plugins/dsh-guardrails/index.js +389 -0
- package/plugins/dsh-guardrails/lib/hooks.js +262 -0
- package/plugins/dsh-guardrails/lib/modes.js +77 -0
- package/plugins/dsh-guardrails/lib/policy.js +592 -0
- package/plugins/dsh-guardrails/lib/scan.js +451 -0
- package/plugins/dsh-guardrails/lib/secrets.js +274 -0
- package/plugins/dsh-guardrails/package.json +26 -0
- package/plugins/dsh-mcp-manager/README.md +78 -0
- package/plugins/dsh-mcp-manager/client.js +373 -0
- package/plugins/dsh-mcp-manager/index.js +159 -0
- package/plugins/dsh-mcp-manager/lib/api.js +250 -0
- package/plugins/dsh-mcp-manager/lib/client.js +219 -0
- package/plugins/dsh-mcp-manager/lib/config.js +217 -0
- package/plugins/dsh-mcp-manager/lib/manager.js +146 -0
- package/plugins/dsh-mcp-manager/package.json +18 -0
- package/plugins/dsh-mcp-manager/screenshots/modal-390.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-1440.png +0 -0
- package/plugins/dsh-mcp-manager/screenshots/settings-390.png +0 -0
- package/plugins/dsh-memory-bridge/README.md +161 -0
- package/plugins/dsh-memory-bridge/client.js +2264 -0
- package/plugins/dsh-memory-bridge/cordis.patch.yml +5 -0
- package/plugins/dsh-memory-bridge/docs/images/conflicts.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/hero.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-dark.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-light.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/settings-mobile.png +0 -0
- package/plugins/dsh-memory-bridge/docs/images/setup.png +0 -0
- package/plugins/dsh-memory-bridge/icon.svg +13 -0
- package/plugins/dsh-memory-bridge/index.js +378 -0
- package/plugins/dsh-memory-bridge/lib/bridge.js +636 -0
- package/plugins/dsh-memory-bridge/lib/github.js +473 -0
- package/plugins/dsh-memory-bridge/lib/merge.js +205 -0
- package/plugins/dsh-memory-bridge/lib/portable.js +314 -0
- package/plugins/dsh-memory-bridge/lib/state.js +565 -0
- package/plugins/dsh-memory-bridge/lib/sync.js +984 -0
- package/plugins/dsh-memory-bridge/locale/en.json +6 -0
- package/plugins/dsh-memory-bridge/locale/ru.json +6 -0
- package/plugins/dsh-memory-bridge/package.json +67 -0
- package/plugins/dsh-peak-guard/README.md +16 -3
- package/plugins/dsh-peak-guard/client.js +223 -8
- package/plugins/dsh-peak-guard/package.json +1 -1
- package/plugins/dsh-plan-mode/README.md +24 -0
- package/plugins/dsh-plan-mode/cordis.patch.yml +3 -0
- package/plugins/dsh-plan-mode/index.js +103 -0
- package/plugins/dsh-plan-mode/lib/plan.js +89 -0
- package/plugins/dsh-plan-mode/package.json +18 -0
- package/plugins/dsh-profile-transfer/client.js +277 -17
- package/plugins/dsh-profile-transfer/index.js +10 -0
- package/plugins/dsh-profile-transfer/package.json +3 -1
- package/plugins/dsh-profile-transfer/transfer.js +101 -2
- package/plugins/dsh-project-memory/index.js +92 -2
- package/plugins/dsh-project-memory/package.json +1 -1
- package/plugins/dsh-repo-map/README.md +26 -0
- package/plugins/dsh-repo-map/cordis.patch.yml +3 -0
- package/plugins/dsh-repo-map/index.js +99 -0
- package/plugins/dsh-repo-map/lib/repo-map.js +170 -0
- package/plugins/dsh-repo-map/package.json +25 -0
- package/plugins/dsh-skill-autopilot/index.js +135 -11
- package/skills/delivery-gate/SKILL.md +31 -0
- package/skills/git-and-ci/SKILL.md +5 -1
- package/skills/manifest.json +31 -6
- package/skills/requirements-and-planning/SKILL.md +3 -0
- package/skills/research-first/SKILL.md +35 -0
- package/skills/research-first/references/query-patterns.md +20 -0
- package/skills/subagent-playbook/SKILL.md +36 -0
- package/skills/subagent-playbook/references/prompts.md +39 -0
- package/skills/ui-craft/SKILL.md +43 -0
- package/skills/visual-qa/SKILL.md +29 -0
- package/skills/visual-qa/references/commands.md +32 -0
- package/skills-baseline.json +219 -0
|
@@ -0,0 +1,494 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-github-review — `gh` CLI transport.
|
|
3
|
+
*
|
|
4
|
+
* Every call is an argv array with `shell: false` (Node's default), a hard
|
|
5
|
+
* timeout and captured output; nothing is concatenated into a command string,
|
|
6
|
+
* so a PR number, repository name or comment body can never become shell
|
|
7
|
+
* syntax. Output that becomes a tool result is scrubbed of tokens first.
|
|
8
|
+
*
|
|
9
|
+
* Reads use `gh`/`git`; the only mutation is `gh pr comment` and it is exposed
|
|
10
|
+
* only through the explicitly confirmed `comment` action.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
|
|
17
|
+
import {
|
|
18
|
+
LIMITS,
|
|
19
|
+
clampDiffBytes,
|
|
20
|
+
parseChecksText,
|
|
21
|
+
parseGhAuthStatus,
|
|
22
|
+
parseGitRemoteUrl,
|
|
23
|
+
parsePrNumber,
|
|
24
|
+
parseRepoFromPrUrl,
|
|
25
|
+
parseRepoSpec,
|
|
26
|
+
repoArgument,
|
|
27
|
+
scrubOutput,
|
|
28
|
+
snippet,
|
|
29
|
+
stripAnsi,
|
|
30
|
+
} from "./normalize.js";
|
|
31
|
+
|
|
32
|
+
const VERSION_TIMEOUT_MS = 10_000;
|
|
33
|
+
const AUTH_TIMEOUT_MS = 15_000;
|
|
34
|
+
const GIT_TIMEOUT_MS = 8_000;
|
|
35
|
+
const VIEW_TIMEOUT_MS = 30_000;
|
|
36
|
+
const CHECKS_TIMEOUT_MS = 30_000;
|
|
37
|
+
const COMMENTS_TIMEOUT_MS = 35_000;
|
|
38
|
+
const DIFF_TIMEOUT_MS = 45_000;
|
|
39
|
+
const COMMENT_TIMEOUT_MS = 30_000;
|
|
40
|
+
const MAX_BUFFER = 8 * 1024 * 1024;
|
|
41
|
+
const MAX_STDERR = 16 * 1024;
|
|
42
|
+
|
|
43
|
+
/** Fields requested from `gh pr view --json`. */
|
|
44
|
+
export const PR_VIEW_FIELDS = [
|
|
45
|
+
"number",
|
|
46
|
+
"title",
|
|
47
|
+
"body",
|
|
48
|
+
"state",
|
|
49
|
+
"isDraft",
|
|
50
|
+
"url",
|
|
51
|
+
"author",
|
|
52
|
+
"baseRefName",
|
|
53
|
+
"headRefName",
|
|
54
|
+
"baseRefOid",
|
|
55
|
+
"headRefOid",
|
|
56
|
+
"additions",
|
|
57
|
+
"deletions",
|
|
58
|
+
"changedFiles",
|
|
59
|
+
"files",
|
|
60
|
+
"commits",
|
|
61
|
+
"reviewDecision",
|
|
62
|
+
"mergeable",
|
|
63
|
+
"labels",
|
|
64
|
+
"createdAt",
|
|
65
|
+
"updatedAt",
|
|
66
|
+
];
|
|
67
|
+
|
|
68
|
+
/** Non-interactive environment for gh: no prompts, no pager, no colour noise. */
|
|
69
|
+
function buildEnv() {
|
|
70
|
+
return {
|
|
71
|
+
...process.env,
|
|
72
|
+
GH_PROMPT_DISABLED: "1",
|
|
73
|
+
GH_NO_UPDATE_NOTIFIER: "1",
|
|
74
|
+
NO_COLOR: "1",
|
|
75
|
+
CLICOLOR: "0",
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function scrub(value) {
|
|
80
|
+
return scrubOutput(value, process.env);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function shortDetail(value, max = 1_200) {
|
|
84
|
+
const text = scrub(value).trim();
|
|
85
|
+
if (text.length === 0) return "";
|
|
86
|
+
return snippet(text, max);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/* ------------------------------------------------------------------ */
|
|
90
|
+
/* Process primitives */
|
|
91
|
+
/* ------------------------------------------------------------------ */
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Synchronous argv runner. Never uses a shell; `timeout` kills the child.
|
|
95
|
+
* `error.code === "ENOENT"` on a valid cwd means the executable is missing.
|
|
96
|
+
*/
|
|
97
|
+
export function runProcessSync(command, args, { cwd, timeoutMs = GIT_TIMEOUT_MS, input, maxBuffer = MAX_BUFFER } = {}) {
|
|
98
|
+
let result;
|
|
99
|
+
try {
|
|
100
|
+
result = spawnSync(command, args, {
|
|
101
|
+
cwd,
|
|
102
|
+
encoding: "utf8",
|
|
103
|
+
timeout: timeoutMs,
|
|
104
|
+
windowsHide: true,
|
|
105
|
+
maxBuffer,
|
|
106
|
+
input,
|
|
107
|
+
env: buildEnv(),
|
|
108
|
+
});
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return { ok: false, code: "throw", status: null, stdout: "", stderr: scrub(String(error)), missing: false };
|
|
111
|
+
}
|
|
112
|
+
if (result.error) {
|
|
113
|
+
const code = result.error.code;
|
|
114
|
+
const missing = code === "ENOENT";
|
|
115
|
+
return {
|
|
116
|
+
ok: false,
|
|
117
|
+
code: missing ? "missing" : code === "ETIMEDOUT" ? "timeout" : String(code ?? "error"),
|
|
118
|
+
status: result.status ?? null,
|
|
119
|
+
stdout: result.stdout ?? "",
|
|
120
|
+
stderr: scrub(result.error.message ?? String(result.error)),
|
|
121
|
+
missing,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const status = result.status;
|
|
125
|
+
return {
|
|
126
|
+
ok: status === 0,
|
|
127
|
+
code: status === 0 ? "ok" : String(status ?? "signal"),
|
|
128
|
+
status,
|
|
129
|
+
stdout: result.stdout ?? "",
|
|
130
|
+
stderr: result.stderr ?? "",
|
|
131
|
+
missing: false,
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Streaming runner used for `gh pr diff`: keeps at most `maxBytes` of stdout in
|
|
137
|
+
* memory and stops the child once the budget is spent. This is the byte cap
|
|
138
|
+
* from the requirements, enforced before any string is built.
|
|
139
|
+
*/
|
|
140
|
+
export function runProcessStream(command, args, { cwd, timeoutMs = DIFF_TIMEOUT_MS, maxBytes = LIMITS.diffBytes } = {}) {
|
|
141
|
+
return new Promise((resolve) => {
|
|
142
|
+
let child;
|
|
143
|
+
try {
|
|
144
|
+
child = spawn(command, args, { cwd, windowsHide: true, env: buildEnv(), stdio: ["ignore", "pipe", "pipe"] });
|
|
145
|
+
} catch (error) {
|
|
146
|
+
resolve({ ok: false, code: "throw", missing: false, stdout: "", stderr: scrub(String(error)), truncated: false, timedOut: false, bytes: 0 });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const chunks = [];
|
|
150
|
+
let bytes = 0;
|
|
151
|
+
let stderr = "";
|
|
152
|
+
let capped = false;
|
|
153
|
+
let timedOut = false;
|
|
154
|
+
let settled = false;
|
|
155
|
+
|
|
156
|
+
const kill = () => {
|
|
157
|
+
try {
|
|
158
|
+
child.kill();
|
|
159
|
+
} catch {
|
|
160
|
+
/* already gone */
|
|
161
|
+
}
|
|
162
|
+
};
|
|
163
|
+
const timer = setTimeout(() => {
|
|
164
|
+
if (settled) return;
|
|
165
|
+
timedOut = true;
|
|
166
|
+
kill();
|
|
167
|
+
}, timeoutMs);
|
|
168
|
+
timer.unref?.();
|
|
169
|
+
|
|
170
|
+
const finish = (code) => {
|
|
171
|
+
if (settled) return;
|
|
172
|
+
settled = true;
|
|
173
|
+
clearTimeout(timer);
|
|
174
|
+
const buffer = Buffer.concat(chunks);
|
|
175
|
+
resolve({
|
|
176
|
+
ok: !timedOut && !capped && code === 0,
|
|
177
|
+
code: timedOut ? "timeout" : capped ? "capped" : code === 0 ? "ok" : String(code ?? "signal"),
|
|
178
|
+
status: code,
|
|
179
|
+
missing: false,
|
|
180
|
+
stdout: buffer.toString("utf8"),
|
|
181
|
+
stderr: scrub(stderr),
|
|
182
|
+
truncated: capped,
|
|
183
|
+
timedOut,
|
|
184
|
+
bytes,
|
|
185
|
+
});
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
child.stdout?.on("data", (chunk) => {
|
|
189
|
+
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
|
|
190
|
+
const remaining = maxBytes - bytes;
|
|
191
|
+
if (remaining <= 0) {
|
|
192
|
+
capped = true;
|
|
193
|
+
kill();
|
|
194
|
+
return;
|
|
195
|
+
}
|
|
196
|
+
if (buffer.length > remaining) {
|
|
197
|
+
chunks.push(buffer.subarray(0, remaining));
|
|
198
|
+
bytes += remaining;
|
|
199
|
+
capped = true;
|
|
200
|
+
kill();
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
chunks.push(buffer);
|
|
204
|
+
bytes += buffer.length;
|
|
205
|
+
});
|
|
206
|
+
child.stderr?.on("data", (chunk) => {
|
|
207
|
+
stderr = `${stderr}${chunk.toString()}`.slice(-MAX_STDERR);
|
|
208
|
+
});
|
|
209
|
+
child.on("error", (error) => {
|
|
210
|
+
if (settled) return;
|
|
211
|
+
settled = true;
|
|
212
|
+
clearTimeout(timer);
|
|
213
|
+
const missing = error?.code === "ENOENT";
|
|
214
|
+
resolve({ ok: false, code: missing ? "missing" : "error", status: null, missing, stdout: "", stderr: scrub(String(error)), truncated: capped, timedOut, bytes });
|
|
215
|
+
});
|
|
216
|
+
child.on("close", (code) => finish(code));
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function gh(args, options = {}) {
|
|
221
|
+
return runProcessSync("gh", args, { cwd: options.cwd, timeoutMs: options.timeoutMs ?? VIEW_TIMEOUT_MS, input: options.input });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function git(args, cwd) {
|
|
225
|
+
return runProcessSync("git", args, { cwd, timeoutMs: GIT_TIMEOUT_MS });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/** Map a failed gh run to a stable, human-readable error code. */
|
|
229
|
+
export function classifyGhError(result, fallback = "gh завершился с ошибкой") {
|
|
230
|
+
const text = shortDetail(`${result?.stderr ?? ""}\n${result?.stdout ?? ""}`) || fallback;
|
|
231
|
+
if (result?.missing) return { code: "gh-missing", detail: text };
|
|
232
|
+
if (result?.code === "timeout") return { code: "timeout", detail: text };
|
|
233
|
+
if (/not logged in|no oauth token|authentication required|gh auth login|HTTP 401|bad credentials/iu.test(text)) {
|
|
234
|
+
return { code: "not-authenticated", detail: text };
|
|
235
|
+
}
|
|
236
|
+
if (/no pull requests? found|could not find (?:an? )?(?:open )?pull ?request|could not resolve to a pull ?request/iu.test(text)) {
|
|
237
|
+
return { code: "no-pr", detail: text };
|
|
238
|
+
}
|
|
239
|
+
if (/could not resolve to a repository|repository not found|HTTP 404|not found/iu.test(text)) {
|
|
240
|
+
return { code: "repo-not-found", detail: text };
|
|
241
|
+
}
|
|
242
|
+
if (/rate limit|HTTP 403|secondary rate/iu.test(text)) return { code: "rate-limited", detail: text };
|
|
243
|
+
return { code: "gh-failed", detail: text };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function pushRepo(args, repo) {
|
|
247
|
+
const value = repoArgument(repo);
|
|
248
|
+
if (value) args.push("--repo", value);
|
|
249
|
+
return args;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/* ------------------------------------------------------------------ */
|
|
253
|
+
/* Status: version, auth, repository */
|
|
254
|
+
/* ------------------------------------------------------------------ */
|
|
255
|
+
|
|
256
|
+
export function ghVersion() {
|
|
257
|
+
const result = gh(["--version"], { timeoutMs: VERSION_TIMEOUT_MS });
|
|
258
|
+
if (!result.ok) {
|
|
259
|
+
return {
|
|
260
|
+
installed: false,
|
|
261
|
+
version: null,
|
|
262
|
+
detail: result.missing ? "gh CLI не найден в PATH" : shortDetail(result.stderr || result.stdout) || null,
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
const first = stripAnsi(result.stdout).trim().split(/\r?\n/u)[0] ?? "";
|
|
266
|
+
const version = /gh version\s+(\S+)/iu.exec(first)?.[1] ?? null;
|
|
267
|
+
return { installed: true, version, detail: null };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
export function ghAuth() {
|
|
271
|
+
const result = gh(["auth", "status"], { timeoutMs: AUTH_TIMEOUT_MS });
|
|
272
|
+
const parsed = parseGhAuthStatus(`${result.stdout ?? ""}\n${result.stderr ?? ""}`);
|
|
273
|
+
return {
|
|
274
|
+
...parsed,
|
|
275
|
+
exitOk: result.ok,
|
|
276
|
+
missing: result.missing === true,
|
|
277
|
+
detail: result.ok ? "" : shortDetail(result.stderr || result.stdout),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/**
|
|
282
|
+
* Repository for the current session: explicit `repo` argument first, then the
|
|
283
|
+
* local git origin remote. No network call is made for detection.
|
|
284
|
+
*/
|
|
285
|
+
export function detectRepo({ cwd, repo } = {}) {
|
|
286
|
+
const resolvedCwd = path.resolve(cwd ?? process.cwd());
|
|
287
|
+
const result = {
|
|
288
|
+
cwd: resolvedCwd,
|
|
289
|
+
root: undefined,
|
|
290
|
+
branch: undefined,
|
|
291
|
+
repo: null,
|
|
292
|
+
source: null,
|
|
293
|
+
gitInstalled: true,
|
|
294
|
+
error: undefined,
|
|
295
|
+
};
|
|
296
|
+
if (!fs.existsSync(resolvedCwd)) {
|
|
297
|
+
result.error = `рабочий каталог не найден: ${resolvedCwd}`;
|
|
298
|
+
return result;
|
|
299
|
+
}
|
|
300
|
+
const top = git(["rev-parse", "--show-toplevel"], resolvedCwd);
|
|
301
|
+
if (top.missing) {
|
|
302
|
+
result.gitInstalled = false;
|
|
303
|
+
} else if (top.ok) {
|
|
304
|
+
result.root = top.stdout.trim();
|
|
305
|
+
const branch = git(["rev-parse", "--abbrev-ref", "HEAD"], resolvedCwd);
|
|
306
|
+
if (branch.ok) result.branch = branch.stdout.trim() || undefined;
|
|
307
|
+
const remote = git(["remote", "get-url", "origin"], resolvedCwd);
|
|
308
|
+
if (remote.ok) {
|
|
309
|
+
const parsed = parseGitRemoteUrl(remote.stdout.trim());
|
|
310
|
+
if (parsed) {
|
|
311
|
+
result.repo = parsed;
|
|
312
|
+
result.source = "git-remote";
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
const requested = String(repo ?? "").trim();
|
|
317
|
+
if (requested.length > 0) {
|
|
318
|
+
const spec = parseRepoSpec(requested);
|
|
319
|
+
if (!spec) {
|
|
320
|
+
/* An invalid override must not silently fall back to the git remote. */
|
|
321
|
+
result.repo = null;
|
|
322
|
+
result.source = null;
|
|
323
|
+
result.error = `некорректный repo «${requested}»: нужен owner/name или host/owner/name`;
|
|
324
|
+
return result;
|
|
325
|
+
}
|
|
326
|
+
result.repo = spec;
|
|
327
|
+
result.source = "explicit";
|
|
328
|
+
}
|
|
329
|
+
return result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/** Everything `status` needs: gh version, auth state and detected repository. */
|
|
333
|
+
export function probeStatus({ cwd, repo } = {}) {
|
|
334
|
+
const version = ghVersion();
|
|
335
|
+
const auth = version.installed
|
|
336
|
+
? ghAuth()
|
|
337
|
+
: { authenticated: false, notLoggedIn: false, host: "", accounts: [], active: null, scopes: [], exitOk: false, missing: true, detail: null };
|
|
338
|
+
return { gh: version, auth, repo: detectRepo({ cwd, repo }) };
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/* ------------------------------------------------------------------ */
|
|
342
|
+
/* Reads */
|
|
343
|
+
/* ------------------------------------------------------------------ */
|
|
344
|
+
|
|
345
|
+
/**
|
|
346
|
+
* Resolve the PR of the current branch when no explicit number is given.
|
|
347
|
+
*
|
|
348
|
+
* `gh pr view --repo owner/name` refuses to infer the branch (it prints
|
|
349
|
+
* "argument required when using the --repo flag"), so when a repository and a
|
|
350
|
+
* local branch are both known we pass the branch explicitly; without a
|
|
351
|
+
* repository we let gh infer everything from the cwd git context.
|
|
352
|
+
*/
|
|
353
|
+
export function resolvePrNumber({ cwd, repo, branch } = {}) {
|
|
354
|
+
const args = ["pr", "view"];
|
|
355
|
+
if (repo) {
|
|
356
|
+
if (typeof branch !== "string" || branch.length === 0) {
|
|
357
|
+
return { ok: false, failure: { code: "no-pr", detail: "репозиторий задан без локальной ветки — укажите явный pr=<номер>" } };
|
|
358
|
+
}
|
|
359
|
+
args.push(branch);
|
|
360
|
+
}
|
|
361
|
+
args.push("--json", "number");
|
|
362
|
+
if (repo) pushRepo(args, repo);
|
|
363
|
+
const result = gh(args, { cwd, timeoutMs: VIEW_TIMEOUT_MS });
|
|
364
|
+
if (!result.ok) return { ok: false, failure: classifyGhError(result, "не удалось определить PR текущей ветки") };
|
|
365
|
+
let json;
|
|
366
|
+
try {
|
|
367
|
+
json = JSON.parse(result.stdout);
|
|
368
|
+
} catch {
|
|
369
|
+
return { ok: false, failure: { code: "bad-json", detail: "gh pr view вернул невалидный JSON" } };
|
|
370
|
+
}
|
|
371
|
+
const number = parsePrNumber(json?.number);
|
|
372
|
+
if (number === undefined) return { ok: false, failure: { code: "no-pr", detail: "PR для текущей ветки не найден" } };
|
|
373
|
+
return { ok: true, number };
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** `gh pr view --json ...`; `number` optional (current branch PR). */
|
|
377
|
+
export function fetchPrView({ cwd, repo, number } = {}) {
|
|
378
|
+
const args = ["pr", "view"];
|
|
379
|
+
if (number !== undefined && number !== null) args.push(String(number));
|
|
380
|
+
args.push("--json", PR_VIEW_FIELDS.join(","));
|
|
381
|
+
pushRepo(args, repo);
|
|
382
|
+
const result = gh(args, { cwd, timeoutMs: VIEW_TIMEOUT_MS });
|
|
383
|
+
if (!result.ok) return { ok: false, failure: classifyGhError(result, "не удалось получить PR") };
|
|
384
|
+
let json;
|
|
385
|
+
try {
|
|
386
|
+
json = JSON.parse(result.stdout);
|
|
387
|
+
} catch {
|
|
388
|
+
return { ok: false, failure: { code: "bad-json", detail: "gh pr view вернул невалидный JSON" } };
|
|
389
|
+
}
|
|
390
|
+
if (json === null || typeof json !== "object" || Array.isArray(json)) {
|
|
391
|
+
return { ok: false, failure: { code: "bad-json", detail: "gh pr view вернул неожиданный JSON" } };
|
|
392
|
+
}
|
|
393
|
+
return { ok: true, json };
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* `gh pr checks --json ...` with a plain-text fallback for older gh builds
|
|
398
|
+
* that do not support `--json` on this command.
|
|
399
|
+
*/
|
|
400
|
+
export function fetchChecks({ cwd, repo, number } = {}) {
|
|
401
|
+
const jsonArgs = ["pr", "checks"];
|
|
402
|
+
if (number !== undefined && number !== null) jsonArgs.push(String(number));
|
|
403
|
+
jsonArgs.push("--json", "name,state,bucket,description,link,startedAt,completedAt");
|
|
404
|
+
pushRepo(jsonArgs, repo);
|
|
405
|
+
const result = gh(jsonArgs, { cwd, timeoutMs: CHECKS_TIMEOUT_MS });
|
|
406
|
+
const noChecks = /no checks reported/iu.test(`${result.stderr ?? ""}\n${result.stdout ?? ""}`);
|
|
407
|
+
if (!result.ok && noChecks) return { ok: true, format: "json", rows: [] };
|
|
408
|
+
if (result.ok) {
|
|
409
|
+
try {
|
|
410
|
+
const parsed = JSON.parse(result.stdout);
|
|
411
|
+
const rows = Array.isArray(parsed) ? parsed : parsed && typeof parsed === "object" ? [parsed] : [];
|
|
412
|
+
return { ok: true, format: "json", rows };
|
|
413
|
+
} catch {
|
|
414
|
+
return { ok: false, failure: { code: "bad-json", detail: "gh pr checks вернул невалидный JSON" } };
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
const jsonUnsupported = !result.missing && result.code !== "timeout"
|
|
418
|
+
&& /unknown flag:?\s*--json|flag provided but not defined[^\n]*json|unknown shorthand flag/iu.test(`${result.stderr ?? ""}\n${result.stdout ?? ""}`);
|
|
419
|
+
if (!jsonUnsupported) return { ok: false, failure: classifyGhError(result, "не удалось получить CI checks") };
|
|
420
|
+
const textArgs = ["pr", "checks"];
|
|
421
|
+
if (number !== undefined && number !== null) textArgs.push(String(number));
|
|
422
|
+
pushRepo(textArgs, repo);
|
|
423
|
+
const textResult = gh(textArgs, { cwd, timeoutMs: CHECKS_TIMEOUT_MS });
|
|
424
|
+
if (!textResult.ok && /no checks reported/iu.test(`${textResult.stderr ?? ""}\n${textResult.stdout ?? ""}`)) {
|
|
425
|
+
return { ok: true, format: "text", rows: [] };
|
|
426
|
+
}
|
|
427
|
+
if (!textResult.ok) return { ok: false, failure: classifyGhError(textResult, "не удалось получить CI checks") };
|
|
428
|
+
return { ok: true, format: "text", rows: parseChecksText(textResult.stdout) };
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
/** PR view plus inline review comments (threads) from the REST API. */
|
|
432
|
+
export function fetchComments({ cwd, repo, number } = {}) {
|
|
433
|
+
const view = fetchPrView({ cwd, repo, number });
|
|
434
|
+
if (!view.ok) return view;
|
|
435
|
+
const json = view.json;
|
|
436
|
+
const prNumber = parsePrNumber(json.number) ?? number;
|
|
437
|
+
let spec = repo;
|
|
438
|
+
if (!spec) spec = parseRepoFromPrUrl(json.url);
|
|
439
|
+
const inline = { ok: false, rows: [], failure: null };
|
|
440
|
+
if (spec && prNumber !== undefined) {
|
|
441
|
+
const apiPath = `repos/${spec.owner}/${spec.name}/pulls/${prNumber}/comments?per_page=${LIMITS.inlineComments}`;
|
|
442
|
+
const result = gh(["api", "--method", "GET", "-H", "Accept: application/vnd.github+json", apiPath], { cwd, timeoutMs: COMMENTS_TIMEOUT_MS });
|
|
443
|
+
if (result.ok) {
|
|
444
|
+
try {
|
|
445
|
+
const parsed = JSON.parse(result.stdout);
|
|
446
|
+
inline.ok = true;
|
|
447
|
+
inline.rows = Array.isArray(parsed) ? parsed : [];
|
|
448
|
+
} catch {
|
|
449
|
+
inline.failure = { code: "bad-json", detail: "gh api вернул невалидный JSON" };
|
|
450
|
+
}
|
|
451
|
+
} else {
|
|
452
|
+
inline.failure = classifyGhError(result, "не удалось получить inline-комментарии");
|
|
453
|
+
}
|
|
454
|
+
} else {
|
|
455
|
+
inline.failure = { code: "unknown-repo", detail: "репозиторий не определён — inline-комментарии пропущены" };
|
|
456
|
+
}
|
|
457
|
+
return { ok: true, json, inline };
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
/**
|
|
461
|
+
* `gh pr diff` with a streaming byte cap. The stream keeps at most
|
|
462
|
+
* `limit + 4 KiB` so the caller can still cut the tail on a line boundary and
|
|
463
|
+
* return a clean diff of at most `limit` bytes; truncation is always reported.
|
|
464
|
+
*/
|
|
465
|
+
export async function fetchDiff({ cwd, repo, number, maxBytes } = {}) {
|
|
466
|
+
const limit = clampDiffBytes(maxBytes);
|
|
467
|
+
const args = pushRepo(["pr", "diff", String(number)], repo);
|
|
468
|
+
const result = await runProcessStream("gh", args, { cwd, timeoutMs: DIFF_TIMEOUT_MS, maxBytes: limit + 4096 });
|
|
469
|
+
if (result.missing) return { ok: false, failure: { code: "gh-missing", detail: "gh CLI не найден в PATH" } };
|
|
470
|
+
if (result.timedOut) {
|
|
471
|
+
return { ok: false, failure: { code: "timeout", detail: `gh pr diff не ответил за ${Math.round(DIFF_TIMEOUT_MS / 1000)} с` } };
|
|
472
|
+
}
|
|
473
|
+
if (!result.ok && !result.truncated) {
|
|
474
|
+
return { ok: false, failure: classifyGhError({ ...result, code: result.status === 0 ? "gh-failed" : result.code }, "не удалось получить diff PR") };
|
|
475
|
+
}
|
|
476
|
+
return { ok: true, text: result.stdout, truncated: result.truncated === true, bytes: result.bytes, limit };
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
/* ------------------------------------------------------------------ */
|
|
480
|
+
/* The single mutation */
|
|
481
|
+
/* ------------------------------------------------------------------ */
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Post one top-level PR comment. The body is piped through stdin
|
|
485
|
+
* (`--body-file -`), never placed on the command line. Callers must have
|
|
486
|
+
* passed the explicit confirmation gate before reaching this function.
|
|
487
|
+
*/
|
|
488
|
+
export function postComment({ cwd, repo, number, body } = {}) {
|
|
489
|
+
const args = pushRepo(["pr", "comment", String(number), "--body-file", "-"], repo);
|
|
490
|
+
const result = gh(args, { cwd, timeoutMs: COMMENT_TIMEOUT_MS, input: body });
|
|
491
|
+
if (!result.ok) return { ok: false, failure: classifyGhError(result, "не удалось отправить комментарий") };
|
|
492
|
+
const url = /https?:\/\/\S+\/pull\/\d+#issuecomment-\d+/u.exec(scrub(result.stdout))?.[0] ?? "";
|
|
493
|
+
return { ok: true, url, output: snippet(scrub(result.stdout), 300) };
|
|
494
|
+
}
|