revdeck 0.1.0 → 0.2.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/CHANGELOG.md +25 -0
- package/CONTRIBUTING.md +17 -7
- package/README.md +45 -6
- package/README.ru.md +44 -5
- package/dist/cli.js +626 -69
- package/dist/cli.js.map +4 -4
- package/dist/daemon.js +338 -51
- package/dist/daemon.js.map +4 -4
- package/dist/mcp.js +20 -4
- package/dist/mcp.js.map +2 -2
- package/dist/ui/assets/index-BAejn_X6.js +10 -0
- package/dist/ui/assets/{index-ByfGHwCb.css → index-xv-zA2Sf.css} +1 -1
- package/dist/ui/index.html +2 -2
- package/docs/adr/0001-daemon-first.md +2 -1
- package/docs/architecture.md +2 -1
- package/docs/configuration.md +2 -0
- package/docs/integrations.md +50 -11
- package/docs/protocol.md +6 -0
- package/docs/ru/adr/0001-daemon-first.md +2 -1
- package/docs/ru/architecture.md +2 -1
- package/docs/ru/configuration.md +2 -0
- package/docs/ru/integrations.md +50 -10
- package/docs/ru/protocol.md +6 -0
- package/docs/ru/threat-model.md +3 -1
- package/docs/threat-model.md +3 -1
- package/package.json +1 -1
- package/skills/revdeck/en/agents/openai.yaml +4 -0
- package/skills/revdeck/ru/agents/openai.yaml +4 -0
- package/dist/ui/assets/index-mvPA8Avw.js +0 -10
package/dist/cli.js
CHANGED
|
@@ -50,6 +50,15 @@ function daemonInfoPath() {
|
|
|
50
50
|
function logPath() {
|
|
51
51
|
return path.join(stateDir(), "daemon.log");
|
|
52
52
|
}
|
|
53
|
+
async function ensureDir(dir, mode = 448) {
|
|
54
|
+
await mkdir(dir, { recursive: true, mode });
|
|
55
|
+
const info = await lstat(dir);
|
|
56
|
+
if (!info.isDirectory()) throw new Error(`${dir}: expected a directory`);
|
|
57
|
+
if (process.platform !== "win32") {
|
|
58
|
+
const { chmod } = await import("node:fs/promises");
|
|
59
|
+
await chmod(dir, mode);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
53
62
|
async function readTextNoFollow(file, maxBytes = 4 * 1024 * 1024) {
|
|
54
63
|
const info = await lstat(file);
|
|
55
64
|
if (info.isSymbolicLink()) throw new Error(`${path.basename(file)}: symbolic link, read refused`);
|
|
@@ -146,7 +155,7 @@ var VERSION;
|
|
|
146
155
|
var init_version = __esm({
|
|
147
156
|
"src/version.ts"() {
|
|
148
157
|
"use strict";
|
|
149
|
-
VERSION = "0.
|
|
158
|
+
VERSION = "0.2.0";
|
|
150
159
|
}
|
|
151
160
|
});
|
|
152
161
|
|
|
@@ -182,6 +191,19 @@ async function daemonInfo() {
|
|
|
182
191
|
return null;
|
|
183
192
|
}
|
|
184
193
|
}
|
|
194
|
+
function isDevContainer() {
|
|
195
|
+
return process.env.REVDECK_DEV_CONTAINER === "1";
|
|
196
|
+
}
|
|
197
|
+
function devcontainerHint() {
|
|
198
|
+
const runtime = process.env.REVDECK_RUNTIME_DIR ?? "(REVDECK_RUNTIME_DIR)";
|
|
199
|
+
const state = process.env.REVDECK_STATE_DIR ?? "(REVDECK_STATE_DIR)";
|
|
200
|
+
return [
|
|
201
|
+
"\u0434\u0435\u043C\u043E\u043D revdeck \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0430 \u0445\u043E\u0441\u0442\u0435",
|
|
202
|
+
"\u043D\u0430 \u0445\u043E\u0441\u0442\u0435: revdeck daemon start",
|
|
203
|
+
`\u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 mounts \u0438\u0437 revdeck devcontainer --write: ${runtime} \u0438 ${state}`,
|
|
204
|
+
"gh/glab \u0434\u043E\u043B\u0436\u043D\u044B \u043E\u0441\u0442\u0430\u0432\u0430\u0442\u044C\u0441\u044F \u043D\u0430 \u0445\u043E\u0441\u0442\u0435: \u0432\u043D\u0443\u0442\u0440\u0438 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0438\u0445 \u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0442\u044C \u043D\u0435 \u043D\u0443\u0436\u043D\u043E"
|
|
205
|
+
].join("; ");
|
|
206
|
+
}
|
|
185
207
|
async function ensureDaemon(options = {}) {
|
|
186
208
|
const url = process.env.REVDECK_URL;
|
|
187
209
|
if (url) {
|
|
@@ -192,9 +214,12 @@ async function ensureDaemon(options = {}) {
|
|
|
192
214
|
try {
|
|
193
215
|
return { client: await connectSocket(), started: false };
|
|
194
216
|
} catch (error) {
|
|
217
|
+
if (isDevContainer()) {
|
|
218
|
+
throw new RpcError(`devcontainer: ${devcontainerHint()}`, "DEVCONTAINER", "revdeck devcontainer --write \u0438 \u043F\u0435\u0440\u0435\u0441\u043E\u0431\u0435\u0440\u0438\u0442\u0435 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440");
|
|
219
|
+
}
|
|
195
220
|
if (options.autoStart === false) throw error;
|
|
196
221
|
}
|
|
197
|
-
await spawnDaemon();
|
|
222
|
+
await spawnDaemon(options.daemonArgs ?? []);
|
|
198
223
|
const deadline = Date.now() + 15e3;
|
|
199
224
|
let lastError = null;
|
|
200
225
|
while (Date.now() < deadline) {
|
|
@@ -211,9 +236,9 @@ async function ensureDaemon(options = {}) {
|
|
|
211
236
|
"\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: revdeck daemon start --foreground"
|
|
212
237
|
);
|
|
213
238
|
}
|
|
214
|
-
async function spawnDaemon() {
|
|
239
|
+
async function spawnDaemon(extraArgs = []) {
|
|
215
240
|
const entry = daemonEntry();
|
|
216
|
-
const child = spawn(entry.command, entry.args, {
|
|
241
|
+
const child = spawn(entry.command, [...entry.args, ...extraArgs], {
|
|
217
242
|
detached: true,
|
|
218
243
|
stdio: "ignore",
|
|
219
244
|
cwd: process.cwd(),
|
|
@@ -234,6 +259,11 @@ function daemonEntry() {
|
|
|
234
259
|
if (existsSync4(sibling)) return { command: process.execPath, args: [sibling] };
|
|
235
260
|
throw new RpcError("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u0432\u0445\u043E\u0434 \u0434\u0435\u043C\u043E\u043D\u0430 (dist/daemon.js)", "DAEMON");
|
|
236
261
|
}
|
|
262
|
+
function uiBaseUrl(info) {
|
|
263
|
+
const host = process.env.REVDECK_UI_HOST;
|
|
264
|
+
if (!isDevContainer() || !host) return info.url;
|
|
265
|
+
return `http://${host}:${info.port}`;
|
|
266
|
+
}
|
|
237
267
|
async function isDaemonRunning() {
|
|
238
268
|
try {
|
|
239
269
|
const client2 = await connectSocket(void 0, 400);
|
|
@@ -1058,10 +1088,10 @@ ${failed.map((item) => `- ${item.id}: ${item.error}`).join("\n")}` : ""),
|
|
|
1058
1088
|
|
|
1059
1089
|
// src/cli/main.ts
|
|
1060
1090
|
init_paths();
|
|
1061
|
-
import { readFile as
|
|
1091
|
+
import { readFile as readFile7, stat } from "node:fs/promises";
|
|
1062
1092
|
import { readFileSync } from "node:fs";
|
|
1063
1093
|
import { parseArgs } from "node:util";
|
|
1064
|
-
import
|
|
1094
|
+
import path9 from "node:path";
|
|
1065
1095
|
|
|
1066
1096
|
// src/core/config.ts
|
|
1067
1097
|
init_paths();
|
|
@@ -1134,8 +1164,8 @@ function globToRegExp(glob) {
|
|
|
1134
1164
|
function escapeRegExp(value) {
|
|
1135
1165
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1136
1166
|
}
|
|
1137
|
-
function classifyFile(
|
|
1138
|
-
const lower =
|
|
1167
|
+
function classifyFile(path10) {
|
|
1168
|
+
const lower = path10.toLowerCase();
|
|
1139
1169
|
if (/(^|\/)(node_modules|dist|build|coverage|vendor)\//.test(lower)) return "generated";
|
|
1140
1170
|
if (/\.(md|mdx|rst|txt|adoc)$/.test(lower) || /(^|\/)(docs?|adr)\//.test(lower)) return "docs";
|
|
1141
1171
|
if (/(^|\/)(__tests__|tests?|spec|e2e)\//.test(lower) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(lower) || /_test\.go$/.test(lower)) return "test";
|
|
@@ -1556,7 +1586,7 @@ init_format();
|
|
|
1556
1586
|
// src/cli/init.ts
|
|
1557
1587
|
init_paths();
|
|
1558
1588
|
import { existsSync as existsSync5 } from "node:fs";
|
|
1559
|
-
import { appendFile, cp, mkdir as mkdir2, readFile as readFile3, writeFile } from "node:fs/promises";
|
|
1589
|
+
import { appendFile, cp, lstat as lstat2, mkdir as mkdir2, readdir, readFile as readFile3, readlink, rm, symlink, writeFile } from "node:fs/promises";
|
|
1560
1590
|
import path5 from "node:path";
|
|
1561
1591
|
init_redact();
|
|
1562
1592
|
init_locale();
|
|
@@ -1604,8 +1634,9 @@ async function runInit(rootInput, options) {
|
|
|
1604
1634
|
await ensureGitignore(root);
|
|
1605
1635
|
const adapters = options.adapters.length ? options.adapters : detectAdapters(root);
|
|
1606
1636
|
const mcp = mcpServer(options.locale);
|
|
1637
|
+
const canonicalSkill = await installSkill(root, options.locale);
|
|
1607
1638
|
for (const adapter of adapters) {
|
|
1608
|
-
await installAdapter(root, adapter, mcp, options.locale);
|
|
1639
|
+
await installAdapter(root, adapter, mcp, options.locale, canonicalSkill);
|
|
1609
1640
|
}
|
|
1610
1641
|
if (!options.noService) {
|
|
1611
1642
|
writers.out("");
|
|
@@ -1618,6 +1649,7 @@ async function runInit(rootInput, options) {
|
|
|
1618
1649
|
writers.out(` revdeck open ${tr("<\u043D\u043E\u043C\u0435\u0440 \u0438\u043B\u0438 \u0441\u0441\u044B\u043B\u043A\u0430>", "<number or URL>")}`);
|
|
1619
1650
|
writers.out(" revdeck ui");
|
|
1620
1651
|
}
|
|
1652
|
+
var CANONICAL_SKILL = ".agents/skills/revdeck";
|
|
1621
1653
|
function commandMd(locale2) {
|
|
1622
1654
|
if (locale2 === "ru") {
|
|
1623
1655
|
return `---
|
|
@@ -1643,10 +1675,10 @@ function commandToml(locale2) {
|
|
|
1643
1675
|
return 'description = "Open a MR/PR review and work through the discussion"\nprompt = """Run `revdeck context <number>` (or MCP revdeck_context) and follow the revdeck skill."""\n';
|
|
1644
1676
|
}
|
|
1645
1677
|
var SPECS = {
|
|
1646
|
-
agents: {
|
|
1647
|
-
dsh: {
|
|
1678
|
+
agents: {},
|
|
1679
|
+
dsh: {},
|
|
1648
1680
|
claude: {
|
|
1649
|
-
|
|
1681
|
+
linkSkill: true,
|
|
1650
1682
|
mcp: ".mcp.json",
|
|
1651
1683
|
files: [{ path: ".claude/commands/revdeck.md", content: commandMd }]
|
|
1652
1684
|
},
|
|
@@ -1670,25 +1702,13 @@ function detectAdapters(root) {
|
|
|
1670
1702
|
if (existsSync5(path5.join(root, ".opencode"))) adapters.add("opencode");
|
|
1671
1703
|
return [...adapters];
|
|
1672
1704
|
}
|
|
1673
|
-
async function installAdapter(root, adapter, mcp, locale2) {
|
|
1705
|
+
async function installAdapter(root, adapter, mcp, locale2, canonicalSkill) {
|
|
1674
1706
|
const spec = SPECS[adapter];
|
|
1675
1707
|
if (!spec) {
|
|
1676
1708
|
writers.err(`! ${tr("\u043D\u0435\u0438\u0437\u0432\u0435\u0441\u0442\u043D\u044B\u0439 \u0430\u0434\u0430\u043F\u0442\u0435\u0440", "unknown adapter")}: ${adapter} (${Object.keys(SPECS).join(", ")})`);
|
|
1677
1709
|
return;
|
|
1678
1710
|
}
|
|
1679
|
-
if (spec.
|
|
1680
|
-
const skills = path5.join(packageRoot(), "skills", "revdeck", locale2);
|
|
1681
|
-
if (!existsSync5(skills)) {
|
|
1682
|
-
writers.err(`! ${tr("\u043A\u0430\u0442\u0430\u043B\u043E\u0433 skills/revdeck \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: \u043F\u0430\u043A\u0435\u0442 \u0441\u043E\u0431\u0440\u0430\u043D \u043D\u0435 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", "skills/revdeck not found: the package is incomplete")}`);
|
|
1683
|
-
} else {
|
|
1684
|
-
const target = path5.join(root, spec.skills);
|
|
1685
|
-
await mkdir2(target, { recursive: true });
|
|
1686
|
-
for (const entry of await skillFiles(skills)) {
|
|
1687
|
-
await cp(path5.join(skills, entry), path5.join(target, entry), { force: true });
|
|
1688
|
-
}
|
|
1689
|
-
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: ${path5.relative(root, target)}`);
|
|
1690
|
-
}
|
|
1691
|
-
}
|
|
1711
|
+
if (spec.linkSkill && canonicalSkill) await linkSkill(root, canonicalSkill);
|
|
1692
1712
|
if (spec.mcp) await mergeMcpJson(path5.join(root, spec.mcp), root, mcp);
|
|
1693
1713
|
for (const file of spec.files ?? []) {
|
|
1694
1714
|
const target = path5.join(root, file.path);
|
|
@@ -1714,9 +1734,67 @@ async function installAdapter(root, adapter, mcp, locale2) {
|
|
|
1714
1734
|
writers.out(` ${tr("\u041A\u043E\u043C\u0430\u043D\u0434\u0430 /revdeck \u0432 DSH \u043F\u043E\u044F\u0432\u0438\u0442\u0441\u044F \u0438\u0437 \u043F\u043B\u0430\u0433\u0438\u043D\u0430-\u043E\u0431\u0451\u0440\u0442\u043A\u0438; MCP-\u0438\u043D\u0441\u0442\u0440\u0443\u043C\u0435\u043D\u0442\u044B \u0434\u043E\u0441\u0442\u0443\u043F\u043D\u044B \u0441\u0440\u0430\u0437\u0443.", "A /revdeck command in DSH requires a host plugin; the MCP tools are available immediately.")}`);
|
|
1715
1735
|
}
|
|
1716
1736
|
}
|
|
1717
|
-
async function
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1737
|
+
async function installSkill(root, locale2) {
|
|
1738
|
+
const source = path5.join(packageRoot(), "skills", "revdeck", locale2);
|
|
1739
|
+
if (!existsSync5(source)) {
|
|
1740
|
+
writers.err(`! ${tr("\u043A\u0430\u0442\u0430\u043B\u043E\u0433 skills/revdeck \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: \u043F\u0430\u043A\u0435\u0442 \u0441\u043E\u0431\u0440\u0430\u043D \u043D\u0435 \u043F\u043E\u043B\u043D\u043E\u0441\u0442\u044C\u044E", "skills/revdeck not found: the package is incomplete")}`);
|
|
1741
|
+
return null;
|
|
1742
|
+
}
|
|
1743
|
+
const target = path5.join(root, CANONICAL_SKILL);
|
|
1744
|
+
await copySkill(source, target);
|
|
1745
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: ${CANONICAL_SKILL}`);
|
|
1746
|
+
return target;
|
|
1747
|
+
}
|
|
1748
|
+
async function copySkill(source, target) {
|
|
1749
|
+
await mkdir2(target, { recursive: true });
|
|
1750
|
+
for (const entry of await skillFiles(source)) {
|
|
1751
|
+
const file = path5.join(target, entry);
|
|
1752
|
+
await mkdir2(path5.dirname(file), { recursive: true });
|
|
1753
|
+
await cp(path5.join(source, entry), file, { force: true });
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
async function linkSkill(root, canonicalSkill) {
|
|
1757
|
+
const target = path5.join(root, ".claude", "skills", "revdeck");
|
|
1758
|
+
const relative = path5.relative(path5.dirname(target), canonicalSkill);
|
|
1759
|
+
await mkdir2(path5.dirname(target), { recursive: true });
|
|
1760
|
+
const existing = await lstat2(target).catch(() => null);
|
|
1761
|
+
if (existing) {
|
|
1762
|
+
if (existing.isSymbolicLink()) {
|
|
1763
|
+
const current2 = await readlink(target).catch(() => "");
|
|
1764
|
+
if (current2 === relative) {
|
|
1765
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: .claude/skills/revdeck -> ${CANONICAL_SKILL}`);
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
await rm(target, { force: true });
|
|
1769
|
+
} else if (existing.isDirectory() && await isManagedSkillDir(target)) {
|
|
1770
|
+
await rm(target, { recursive: true, force: true });
|
|
1771
|
+
} else {
|
|
1772
|
+
writers.out(`! .claude/skills/revdeck ${tr("\u0443\u0436\u0435 \u0435\u0441\u0442\u044C: \u043D\u0435 \u043F\u0435\u0440\u0435\u0437\u0430\u043F\u0438\u0441\u044B\u0432\u0430\u044E", "already exists: not overwriting")}`);
|
|
1773
|
+
return;
|
|
1774
|
+
}
|
|
1775
|
+
}
|
|
1776
|
+
if (process.platform === "win32") {
|
|
1777
|
+
await copySkill(canonicalSkill, target);
|
|
1778
|
+
} else {
|
|
1779
|
+
await symlink(relative, target, "dir");
|
|
1780
|
+
}
|
|
1781
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: .claude/skills/revdeck -> ${CANONICAL_SKILL}`);
|
|
1782
|
+
}
|
|
1783
|
+
async function isManagedSkillDir(dir) {
|
|
1784
|
+
const skill = path5.join(dir, "SKILL.md");
|
|
1785
|
+
if (!existsSync5(skill)) return false;
|
|
1786
|
+
const text = await readFile3(skill, "utf8").catch(() => "");
|
|
1787
|
+
return /^name:\s*revdeck\s*$/m.test(text);
|
|
1788
|
+
}
|
|
1789
|
+
async function skillFiles(dir, prefix = "") {
|
|
1790
|
+
const entries = await readdir(path5.join(dir, prefix), { withFileTypes: true });
|
|
1791
|
+
const files = [];
|
|
1792
|
+
for (const entry of entries) {
|
|
1793
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1794
|
+
if (entry.isDirectory()) files.push(...await skillFiles(dir, relative));
|
|
1795
|
+
else if (entry.isFile()) files.push(relative);
|
|
1796
|
+
}
|
|
1797
|
+
return files.sort();
|
|
1720
1798
|
}
|
|
1721
1799
|
async function mergeMcpJson(file, root, mcp) {
|
|
1722
1800
|
let data = {};
|
|
@@ -1802,12 +1880,368 @@ function describeError(error) {
|
|
|
1802
1880
|
return redactLine(error instanceof Error ? error.message : error);
|
|
1803
1881
|
}
|
|
1804
1882
|
|
|
1883
|
+
// src/cli/eject.ts
|
|
1884
|
+
init_paths();
|
|
1885
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1886
|
+
import { lstat as lstat3, readFile as readFile5, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1887
|
+
import path7 from "node:path";
|
|
1888
|
+
|
|
1889
|
+
// src/core/jsonc.ts
|
|
1890
|
+
function parseJsonc(text) {
|
|
1891
|
+
let withoutComments = "";
|
|
1892
|
+
let inString = false;
|
|
1893
|
+
let escaped = false;
|
|
1894
|
+
let lineComment = false;
|
|
1895
|
+
let blockComment = false;
|
|
1896
|
+
for (let index = 0; index < text.length; index++) {
|
|
1897
|
+
const char = text[index];
|
|
1898
|
+
const next = text[index + 1];
|
|
1899
|
+
if (lineComment) {
|
|
1900
|
+
if (char === "\n") {
|
|
1901
|
+
lineComment = false;
|
|
1902
|
+
withoutComments += char;
|
|
1903
|
+
}
|
|
1904
|
+
continue;
|
|
1905
|
+
}
|
|
1906
|
+
if (blockComment) {
|
|
1907
|
+
if (char === "*" && next === "/") {
|
|
1908
|
+
blockComment = false;
|
|
1909
|
+
index += 1;
|
|
1910
|
+
} else if (char === "\n") {
|
|
1911
|
+
withoutComments += char;
|
|
1912
|
+
}
|
|
1913
|
+
continue;
|
|
1914
|
+
}
|
|
1915
|
+
if (inString) {
|
|
1916
|
+
withoutComments += char;
|
|
1917
|
+
if (escaped) escaped = false;
|
|
1918
|
+
else if (char === "\\") escaped = true;
|
|
1919
|
+
else if (char === '"') inString = false;
|
|
1920
|
+
continue;
|
|
1921
|
+
}
|
|
1922
|
+
if (char === '"') {
|
|
1923
|
+
inString = true;
|
|
1924
|
+
withoutComments += char;
|
|
1925
|
+
continue;
|
|
1926
|
+
}
|
|
1927
|
+
if (char === "/" && next === "/") {
|
|
1928
|
+
lineComment = true;
|
|
1929
|
+
index += 1;
|
|
1930
|
+
continue;
|
|
1931
|
+
}
|
|
1932
|
+
if (char === "/" && next === "*") {
|
|
1933
|
+
blockComment = true;
|
|
1934
|
+
index += 1;
|
|
1935
|
+
continue;
|
|
1936
|
+
}
|
|
1937
|
+
withoutComments += char;
|
|
1938
|
+
}
|
|
1939
|
+
let cleaned = "";
|
|
1940
|
+
inString = false;
|
|
1941
|
+
escaped = false;
|
|
1942
|
+
for (let index = 0; index < withoutComments.length; index++) {
|
|
1943
|
+
const char = withoutComments[index];
|
|
1944
|
+
if (inString) {
|
|
1945
|
+
cleaned += char;
|
|
1946
|
+
if (escaped) escaped = false;
|
|
1947
|
+
else if (char === "\\") escaped = true;
|
|
1948
|
+
else if (char === '"') inString = false;
|
|
1949
|
+
continue;
|
|
1950
|
+
}
|
|
1951
|
+
if (char === '"') {
|
|
1952
|
+
inString = true;
|
|
1953
|
+
cleaned += char;
|
|
1954
|
+
continue;
|
|
1955
|
+
}
|
|
1956
|
+
if (char === ",") {
|
|
1957
|
+
let lookahead = index + 1;
|
|
1958
|
+
while (lookahead < withoutComments.length && /\s/.test(withoutComments[lookahead])) lookahead += 1;
|
|
1959
|
+
const next = withoutComments[lookahead];
|
|
1960
|
+
if (next === "}" || next === "]") continue;
|
|
1961
|
+
}
|
|
1962
|
+
cleaned += char;
|
|
1963
|
+
}
|
|
1964
|
+
return JSON.parse(cleaned);
|
|
1965
|
+
}
|
|
1966
|
+
function isRecord(value) {
|
|
1967
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1968
|
+
}
|
|
1969
|
+
|
|
1970
|
+
// src/cli/devcontainer.ts
|
|
1971
|
+
init_paths();
|
|
1972
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1973
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
1974
|
+
import path6 from "node:path";
|
|
1975
|
+
init_locale();
|
|
1976
|
+
init_format();
|
|
1977
|
+
var CONTAINER_RUNTIME_DIR = "/run/revdeck";
|
|
1978
|
+
var CONTAINER_STATE_DIR = "/var/lib/revdeck";
|
|
1979
|
+
var CONTAINER_UI_HOST = "host.docker.internal";
|
|
1980
|
+
function devcontainerPatch() {
|
|
1981
|
+
const mountSource = { runtime: runtimeDir(), state: stateDir() };
|
|
1982
|
+
return {
|
|
1983
|
+
mounts: [
|
|
1984
|
+
{ type: "bind", source: mountSource.runtime, target: CONTAINER_RUNTIME_DIR },
|
|
1985
|
+
{ type: "bind", source: mountSource.state, target: CONTAINER_STATE_DIR, readonly: true }
|
|
1986
|
+
],
|
|
1987
|
+
containerEnv: {
|
|
1988
|
+
REVDECK_RUNTIME_DIR: CONTAINER_RUNTIME_DIR,
|
|
1989
|
+
REVDECK_STATE_DIR: CONTAINER_STATE_DIR,
|
|
1990
|
+
REVDECK_DEV_CONTAINER: "1",
|
|
1991
|
+
REVDECK_UI_HOST: CONTAINER_UI_HOST
|
|
1992
|
+
},
|
|
1993
|
+
remoteEnv: {
|
|
1994
|
+
REVDECK_RUNTIME_DIR: CONTAINER_RUNTIME_DIR,
|
|
1995
|
+
REVDECK_STATE_DIR: CONTAINER_STATE_DIR,
|
|
1996
|
+
REVDECK_DEV_CONTAINER: "1",
|
|
1997
|
+
REVDECK_UI_HOST: CONTAINER_UI_HOST
|
|
1998
|
+
},
|
|
1999
|
+
runArgs: [`--add-host=${CONTAINER_UI_HOST}:host-gateway`]
|
|
2000
|
+
};
|
|
2001
|
+
}
|
|
2002
|
+
function mergeDevcontainerConfig(existing, patch) {
|
|
2003
|
+
const result = { ...existing };
|
|
2004
|
+
const currentMounts = Array.isArray(existing.mounts) ? [...existing.mounts] : [];
|
|
2005
|
+
for (const mount of patch.mounts) {
|
|
2006
|
+
const index = currentMounts.findIndex((item) => isRecord(item) && item.target === mount.target);
|
|
2007
|
+
if (index >= 0) currentMounts[index] = mount;
|
|
2008
|
+
else currentMounts.push(mount);
|
|
2009
|
+
}
|
|
2010
|
+
result.mounts = currentMounts;
|
|
2011
|
+
const containerEnv = isRecord(existing.containerEnv) ? { ...existing.containerEnv } : {};
|
|
2012
|
+
Object.assign(containerEnv, patch.containerEnv);
|
|
2013
|
+
result.containerEnv = containerEnv;
|
|
2014
|
+
const remoteEnv = isRecord(existing.remoteEnv) ? { ...existing.remoteEnv } : {};
|
|
2015
|
+
Object.assign(remoteEnv, patch.remoteEnv);
|
|
2016
|
+
result.remoteEnv = remoteEnv;
|
|
2017
|
+
const runArgs = Array.isArray(existing.runArgs) ? existing.runArgs.filter((item) => typeof item === "string") : [];
|
|
2018
|
+
for (const arg of patch.runArgs) if (!runArgs.includes(arg)) runArgs.push(arg);
|
|
2019
|
+
result.runArgs = runArgs;
|
|
2020
|
+
return result;
|
|
2021
|
+
}
|
|
2022
|
+
async function runDevcontainer(rootInput, options) {
|
|
2023
|
+
const root = findRepoRoot(rootInput);
|
|
2024
|
+
const patch = devcontainerPatch();
|
|
2025
|
+
await ensureDir(runtimeDir());
|
|
2026
|
+
await ensureDir(stateDir());
|
|
2027
|
+
const dir = path6.join(root, ".devcontainer");
|
|
2028
|
+
const file = path6.join(dir, "devcontainer.json");
|
|
2029
|
+
writers.out(tr("\u0425\u043E\u0441\u0442-\u043A\u0430\u0442\u0430\u043B\u043E\u0433\u0438 revdeck:", "revdeck host directories:"));
|
|
2030
|
+
writers.out(` ${tr("\u0441\u043E\u043A\u0435\u0442", "socket")}: ${runtimeDir()} -> ${CONTAINER_RUNTIME_DIR}`);
|
|
2031
|
+
writers.out(` ${tr("\u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435", "state")}: ${stateDir()} -> ${CONTAINER_STATE_DIR} (${tr("\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u0435\u043D\u0438\u0435", "read-only")})`);
|
|
2032
|
+
writers.out(tr("\u0412 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435: \u0434\u0435\u043C\u043E\u043D, gh \u0438 glab \u043E\u0441\u0442\u0430\u044E\u0442\u0441\u044F \u043D\u0430 \u0445\u043E\u0441\u0442\u0435.", "In the container: the daemon, gh and glab stay on the host."));
|
|
2033
|
+
if (!options.write) {
|
|
2034
|
+
writers.out("");
|
|
2035
|
+
writers.out(tr("\u0424\u0440\u0430\u0433\u043C\u0435\u043D\u0442 \u0434\u043B\u044F .devcontainer/devcontainer.json:", "Snippet for .devcontainer/devcontainer.json:"));
|
|
2036
|
+
writers.out(JSON.stringify(patch, null, 2));
|
|
2037
|
+
writers.out("");
|
|
2038
|
+
writers.out(tr("\u041F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C \u0430\u0432\u0442\u043E\u043C\u0430\u0442\u0438\u0447\u0435\u0441\u043A\u0438: revdeck devcontainer --write", "Apply automatically: revdeck devcontainer --write"));
|
|
2039
|
+
return;
|
|
2040
|
+
}
|
|
2041
|
+
await mkdir3(dir, { recursive: true });
|
|
2042
|
+
const existed = existsSync6(file);
|
|
2043
|
+
let existing = {};
|
|
2044
|
+
if (existed) {
|
|
2045
|
+
const text = await readFile4(file, "utf8");
|
|
2046
|
+
let parsed;
|
|
2047
|
+
try {
|
|
2048
|
+
parsed = parseJsonc(text);
|
|
2049
|
+
} catch (error) {
|
|
2050
|
+
throw new Error(
|
|
2051
|
+
tr(
|
|
2052
|
+
`${path6.relative(root, file)}: \u043D\u0435 \u0443\u0434\u0430\u043B\u043E\u0441\u044C \u0440\u0430\u0437\u043E\u0431\u0440\u0430\u0442\u044C JSONC (${error.message}); \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 mounts \u0432\u0440\u0443\u0447\u043D\u0443\u044E`,
|
|
2053
|
+
`${path6.relative(root, file)}: could not parse JSONC (${error.message}); add the mounts manually`
|
|
2054
|
+
)
|
|
2055
|
+
);
|
|
2056
|
+
}
|
|
2057
|
+
if (!isRecord(parsed)) throw new Error(`${path6.relative(root, file)}: ${tr("\u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u043E\u0431\u044A\u0435\u043A\u0442", "expected an object")}`);
|
|
2058
|
+
existing = parsed;
|
|
2059
|
+
} else {
|
|
2060
|
+
existing = { image: "mcr.microsoft.com/devcontainers/base:bookworm" };
|
|
2061
|
+
}
|
|
2062
|
+
const merged = mergeDevcontainerConfig(existing, patch);
|
|
2063
|
+
await writeFile2(file, `${JSON.stringify(merged, null, 2)}
|
|
2064
|
+
`);
|
|
2065
|
+
writers.out(`\u2714 ${path6.relative(root, file)}`);
|
|
2066
|
+
if (existed) {
|
|
2067
|
+
writers.out(tr(" \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u0432 \u0441\u0443\u0449\u0435\u0441\u0442\u0432\u0443\u044E\u0449\u0435\u043C JSONC \u043D\u0435 \u0441\u043E\u0445\u0440\u0430\u043D\u044F\u044E\u0442\u0441\u044F: \u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 diff.", " comments in an existing JSONC are not preserved: review the diff."));
|
|
2068
|
+
}
|
|
2069
|
+
writers.out("");
|
|
2070
|
+
writers.out(tr("\u0414\u0430\u043B\u044C\u0448\u0435:", "Next:"));
|
|
2071
|
+
writers.out(tr(" 1. \u041F\u0435\u0440\u0435\u0441\u043E\u0431\u0435\u0440\u0438\u0442\u0435/\u043F\u0435\u0440\u0435\u043E\u0442\u043A\u0440\u043E\u0439\u0442\u0435 devcontainer.", " 1. Rebuild/reopen the devcontainer."));
|
|
2072
|
+
writers.out(tr(" 2. \u041D\u0430 \u0445\u043E\u0441\u0442\u0435: revdeck daemon start.", " 2. On the host: revdeck daemon start."));
|
|
2073
|
+
writers.out(tr(" 3. \u0412 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435: revdeck doctor \u2014 \u043E\u043D \u043D\u0435 \u0431\u0443\u0434\u0435\u0442 \u0438\u0441\u043A\u0430\u0442\u044C glab/gh \u0432\u043D\u0443\u0442\u0440\u0438.", " 3. In the container: revdeck doctor \u2014 it will not look for glab/gh inside."));
|
|
2074
|
+
writers.out(tr(" \u0414\u043B\u044F UI \u0438\u0437 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430 \u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u0434\u0435\u043C\u043E\u043D \u043D\u0430 \u0445\u043E\u0441\u0442\u0435 \u0441 --host 0.0.0.0.", " For the UI from the container, start the daemon on the host with --host 0.0.0.0."));
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
// src/cli/eject.ts
|
|
2078
|
+
init_locale();
|
|
2079
|
+
init_format();
|
|
2080
|
+
var CONFIG_FILES2 = [".revdeck.yml", ".revdeck.yaml"];
|
|
2081
|
+
var COMMAND_FILES = [
|
|
2082
|
+
".claude/commands/revdeck.md",
|
|
2083
|
+
".cursor/commands/revdeck.md",
|
|
2084
|
+
".codex/prompts/revdeck.md",
|
|
2085
|
+
".gemini/commands/revdeck.toml",
|
|
2086
|
+
".opencode/command/revdeck.md",
|
|
2087
|
+
".github/prompts/revdeck.prompt.md"
|
|
2088
|
+
];
|
|
2089
|
+
var SKILL_DIRS = [".agents/skills/revdeck", ".claude/skills/revdeck"];
|
|
2090
|
+
var MCP_FILES = [".mcp.json", ".cursor/mcp.json"];
|
|
2091
|
+
async function runEject(rootInput, options) {
|
|
2092
|
+
const root = findRepoRoot(rootInput);
|
|
2093
|
+
const changes = [];
|
|
2094
|
+
const warnings = [];
|
|
2095
|
+
if (!options.keepConfig) {
|
|
2096
|
+
for (const name of CONFIG_FILES2) {
|
|
2097
|
+
const file = path7.join(root, name);
|
|
2098
|
+
if (existsSync7(file)) changes.push({ path: name, action: tr("\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u043E\u043D\u0444\u0438\u0433", "remove config"), remove: () => rm2(file, { force: true }) });
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
for (const name of SKILL_DIRS) {
|
|
2102
|
+
const target = path7.join(root, name);
|
|
2103
|
+
if (!existsSync7(target)) continue;
|
|
2104
|
+
const managed = await isManagedSkill(target);
|
|
2105
|
+
if (!managed) {
|
|
2106
|
+
warnings.push(`${name}: ${tr("\u0441\u043A\u0438\u043B\u043B \u043D\u0435 \u043F\u043E\u0445\u043E\u0436 \u043D\u0430 revdeck \u2014 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E", "does not look like the revdeck skill \u2014 leaving it")}`);
|
|
2107
|
+
continue;
|
|
2108
|
+
}
|
|
2109
|
+
changes.push({ path: name, action: tr("\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u0441\u043A\u0438\u043B\u043B", "remove skill"), remove: () => rm2(target, { recursive: true, force: true }) });
|
|
2110
|
+
}
|
|
2111
|
+
for (const name of COMMAND_FILES) {
|
|
2112
|
+
const file = path7.join(root, name);
|
|
2113
|
+
if (!existsSync7(file)) continue;
|
|
2114
|
+
const text = await readFile5(file, "utf8").catch(() => "");
|
|
2115
|
+
if (!/revdeck/i.test(text)) {
|
|
2116
|
+
warnings.push(`${name}: ${tr("\u0444\u0430\u0439\u043B \u043D\u0435 \u0441\u043E\u0434\u0435\u0440\u0436\u0438\u0442 revdeck \u2014 \u043E\u0441\u0442\u0430\u0432\u043B\u044F\u044E", "file does not mention revdeck \u2014 leaving it")}`);
|
|
2117
|
+
continue;
|
|
2118
|
+
}
|
|
2119
|
+
changes.push({ path: name, action: tr("\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u043A\u043E\u043C\u0430\u043D\u0434\u0443", "remove command"), remove: () => rm2(file, { force: true }) });
|
|
2120
|
+
}
|
|
2121
|
+
for (const name of MCP_FILES) {
|
|
2122
|
+
const file = path7.join(root, name);
|
|
2123
|
+
if (!existsSync7(file)) continue;
|
|
2124
|
+
const text = await readFile5(file, "utf8").catch(() => "");
|
|
2125
|
+
let data;
|
|
2126
|
+
try {
|
|
2127
|
+
data = JSON.parse(text);
|
|
2128
|
+
} catch {
|
|
2129
|
+
warnings.push(`${name}: ${tr("\u043D\u0435 JSON \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E", "not JSON \u2014 leaving it untouched")}`);
|
|
2130
|
+
continue;
|
|
2131
|
+
}
|
|
2132
|
+
const servers = data.mcpServers;
|
|
2133
|
+
if (!servers || !("revdeck" in servers)) continue;
|
|
2134
|
+
changes.push({
|
|
2135
|
+
path: name,
|
|
2136
|
+
action: tr("\u0443\u0431\u0440\u0430\u0442\u044C MCP-\u0441\u0435\u0440\u0432\u0435\u0440 revdeck", "remove the revdeck MCP server"),
|
|
2137
|
+
remove: async () => {
|
|
2138
|
+
delete servers.revdeck;
|
|
2139
|
+
await writeFile3(file, `${JSON.stringify(data, null, 2)}
|
|
2140
|
+
`);
|
|
2141
|
+
}
|
|
2142
|
+
});
|
|
2143
|
+
}
|
|
2144
|
+
const devcontainer = path7.join(root, ".devcontainer", "devcontainer.json");
|
|
2145
|
+
if (existsSync7(devcontainer)) {
|
|
2146
|
+
const text = await readFile5(devcontainer, "utf8");
|
|
2147
|
+
try {
|
|
2148
|
+
const parsed = parseJsonc(text);
|
|
2149
|
+
if (isRecord(parsed)) {
|
|
2150
|
+
const next = { ...parsed };
|
|
2151
|
+
let changed = false;
|
|
2152
|
+
if (Array.isArray(next.mounts)) {
|
|
2153
|
+
const mounts = next.mounts.filter((item) => !(isRecord(item) && (item.target === CONTAINER_RUNTIME_DIR || item.target === CONTAINER_STATE_DIR)));
|
|
2154
|
+
if (mounts.length !== next.mounts.length) {
|
|
2155
|
+
next.mounts = mounts;
|
|
2156
|
+
changed = true;
|
|
2157
|
+
}
|
|
2158
|
+
}
|
|
2159
|
+
for (const key of ["containerEnv", "remoteEnv"]) {
|
|
2160
|
+
if (!isRecord(next[key])) continue;
|
|
2161
|
+
const env = { ...next[key] };
|
|
2162
|
+
for (const name of ["REVDECK_RUNTIME_DIR", "REVDECK_STATE_DIR", "REVDECK_DEV_CONTAINER", "REVDECK_UI_HOST"]) delete env[name];
|
|
2163
|
+
if (Object.keys(env).length !== Object.keys(next[key]).length) {
|
|
2164
|
+
next[key] = env;
|
|
2165
|
+
changed = true;
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
if (Array.isArray(next.runArgs)) {
|
|
2169
|
+
const args = next.runArgs.filter((item) => item !== "--add-host=host.docker.internal:host-gateway");
|
|
2170
|
+
if (args.length !== next.runArgs.length) {
|
|
2171
|
+
next.runArgs = args;
|
|
2172
|
+
changed = true;
|
|
2173
|
+
}
|
|
2174
|
+
}
|
|
2175
|
+
if (changed) {
|
|
2176
|
+
changes.push({
|
|
2177
|
+
path: ".devcontainer/devcontainer.json",
|
|
2178
|
+
action: tr("\u0443\u0431\u0440\u0430\u0442\u044C \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0435\u043D\u0438\u0435 \u043A \u0434\u0435\u043C\u043E\u043D\u0443 \u0445\u043E\u0441\u0442\u0430", "remove the host daemon connection"),
|
|
2179
|
+
remove: () => writeFile3(devcontainer, `${JSON.stringify(next, null, 2)}
|
|
2180
|
+
`)
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
}
|
|
2184
|
+
} catch {
|
|
2185
|
+
warnings.push(`.devcontainer/devcontainer.json: ${tr("\u043D\u0435 JSONC \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E", "not JSONC \u2014 leaving it untouched")}`);
|
|
2186
|
+
}
|
|
2187
|
+
}
|
|
2188
|
+
const cache = path7.join(root, ".revdeck");
|
|
2189
|
+
if (existsSync7(cache)) {
|
|
2190
|
+
changes.push({ path: ".revdeck/", action: tr("\u0443\u0434\u0430\u043B\u0438\u0442\u044C \u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u0435\u0448", "remove local cache"), remove: () => rm2(cache, { recursive: true, force: true }) });
|
|
2191
|
+
}
|
|
2192
|
+
const gitignore = path7.join(root, ".gitignore");
|
|
2193
|
+
if (existsSync7(gitignore)) {
|
|
2194
|
+
const text = await readFile5(gitignore, "utf8");
|
|
2195
|
+
if (/(^|\n)\.revdeck\/?\s*(\n|$)/.test(text)) {
|
|
2196
|
+
changes.push({
|
|
2197
|
+
path: ".gitignore",
|
|
2198
|
+
action: tr("\u0443\u0431\u0440\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 .revdeck/", "remove the .revdeck/ line"),
|
|
2199
|
+
remove: async () => {
|
|
2200
|
+
const next = text.split("\n").filter((line) => line.trim() !== ".revdeck/" && line.trim() !== ".revdeck").join("\n");
|
|
2201
|
+
await writeFile3(gitignore, next);
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
}
|
|
2205
|
+
}
|
|
2206
|
+
if (!changes.length) {
|
|
2207
|
+
writers.out(tr("\u0432 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u0438 \u043D\u0435\u0442 \u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D\u043D\u043E\u0433\u043E revdeck", "revdeck is not installed in this repository"));
|
|
2208
|
+
for (const warning of warnings) writers.err(`! ${warning}`);
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
writers.out(tr("\u0411\u0443\u0434\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043E:", "Will change:"));
|
|
2212
|
+
for (const change of changes) writers.out(` ${change.action}: ${change.path}`);
|
|
2213
|
+
for (const warning of warnings) writers.err(`! ${warning}`);
|
|
2214
|
+
if (!options.yes) {
|
|
2215
|
+
writers.out("");
|
|
2216
|
+
writers.out(tr("\u041D\u0438\u0447\u0435\u0433\u043E \u043D\u0435 \u0443\u0434\u0430\u043B\u0435\u043D\u043E: \u0434\u043E\u0431\u0430\u0432\u044C\u0442\u0435 --yes, \u0447\u0442\u043E\u0431\u044B \u043F\u0440\u0438\u043C\u0435\u043D\u0438\u0442\u044C.", "Nothing was removed: add --yes to apply."));
|
|
2217
|
+
return;
|
|
2218
|
+
}
|
|
2219
|
+
for (const change of changes) {
|
|
2220
|
+
await change.remove();
|
|
2221
|
+
writers.out(`\u2714 ${change.action}: ${change.path}`);
|
|
2222
|
+
}
|
|
2223
|
+
writers.out(tr("revdeck \u0443\u0434\u0430\u043B\u0451\u043D \u0438\u0437 \u0440\u0435\u043F\u043E\u0437\u0438\u0442\u043E\u0440\u0438\u044F.", "revdeck was removed from the repository."));
|
|
2224
|
+
}
|
|
2225
|
+
async function isManagedSkill(target) {
|
|
2226
|
+
try {
|
|
2227
|
+
const info = await lstat3(target);
|
|
2228
|
+
if (info.isSymbolicLink()) return true;
|
|
2229
|
+
if (!info.isDirectory()) return false;
|
|
2230
|
+
const skill = path7.join(target, "SKILL.md");
|
|
2231
|
+
if (!existsSync7(skill)) return false;
|
|
2232
|
+
const text = await readFile5(skill, "utf8");
|
|
2233
|
+
return /^name:\s*revdeck\s*$/m.test(text) || /revdeck/i.test(text.slice(0, 2e3));
|
|
2234
|
+
} catch {
|
|
2235
|
+
return false;
|
|
2236
|
+
}
|
|
2237
|
+
}
|
|
2238
|
+
|
|
1805
2239
|
// src/cli/export.ts
|
|
1806
2240
|
init_client();
|
|
1807
2241
|
init_format();
|
|
1808
2242
|
init_locale();
|
|
1809
|
-
import { mkdir as
|
|
1810
|
-
import
|
|
2243
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2244
|
+
import path8 from "node:path";
|
|
1811
2245
|
async function reviewFor(client2, root, arg) {
|
|
1812
2246
|
if (arg && /^(gitlab|github):/.test(arg)) {
|
|
1813
2247
|
const { review } = await client2.request("reviews.get", { review: arg });
|
|
@@ -1863,13 +2297,13 @@ async function exportReview(root, arg, dir) {
|
|
|
1863
2297
|
const { client: client2 } = await ensureDaemon();
|
|
1864
2298
|
try {
|
|
1865
2299
|
const review = await reviewFor(client2, root, arg);
|
|
1866
|
-
const target =
|
|
1867
|
-
await
|
|
2300
|
+
const target = path8.resolve(root, dir);
|
|
2301
|
+
await mkdir4(target, { recursive: true });
|
|
1868
2302
|
const base = `mr-${review.info.ref.number}`;
|
|
1869
2303
|
const files = [];
|
|
1870
2304
|
const write = async (name, data) => {
|
|
1871
|
-
const file =
|
|
1872
|
-
await
|
|
2305
|
+
const file = path8.join(target, name);
|
|
2306
|
+
await writeFile4(file, `${JSON.stringify(data, null, 2)}
|
|
1873
2307
|
`, { mode: 420 });
|
|
1874
2308
|
files.push(name);
|
|
1875
2309
|
};
|
|
@@ -1934,9 +2368,9 @@ async function exportReview(root, arg, dir) {
|
|
|
1934
2368
|
}
|
|
1935
2369
|
}
|
|
1936
2370
|
await write(`${base}-published.json`, published);
|
|
1937
|
-
await
|
|
2371
|
+
await writeFile4(path8.join(target, `${base}.diff`), await serializeDiff(client2, review), { mode: 420 });
|
|
1938
2372
|
files.push(`${base}.diff`);
|
|
1939
|
-
writers.out(`${tr("\u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E \u0432", "exported to")} ${
|
|
2373
|
+
writers.out(`${tr("\u044D\u043A\u0441\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E \u0432", "exported to")} ${path8.relative(root, target)}: ${files.join(", ")}`);
|
|
1940
2374
|
} finally {
|
|
1941
2375
|
client2.close();
|
|
1942
2376
|
}
|
|
@@ -1945,11 +2379,11 @@ async function importReview(root, arg, dir) {
|
|
|
1945
2379
|
const { client: client2 } = await ensureDaemon();
|
|
1946
2380
|
try {
|
|
1947
2381
|
const review = await reviewFor(client2, root, arg);
|
|
1948
|
-
const target =
|
|
2382
|
+
const target = path8.resolve(root, dir);
|
|
1949
2383
|
const base = `mr-${review.info.ref.number}`;
|
|
1950
2384
|
const read = async (name) => {
|
|
1951
2385
|
try {
|
|
1952
|
-
return JSON.parse(await
|
|
2386
|
+
return JSON.parse(await readFile6(path8.join(target, name), "utf8"));
|
|
1953
2387
|
} catch {
|
|
1954
2388
|
return null;
|
|
1955
2389
|
}
|
|
@@ -1997,6 +2431,53 @@ async function importReview(root, arg, dir) {
|
|
|
1997
2431
|
}
|
|
1998
2432
|
}
|
|
1999
2433
|
|
|
2434
|
+
// src/core/attachments.ts
|
|
2435
|
+
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
2436
|
+
var MIME_BY_EXTENSION = {
|
|
2437
|
+
".png": "image/png",
|
|
2438
|
+
".jpg": "image/jpeg",
|
|
2439
|
+
".jpeg": "image/jpeg",
|
|
2440
|
+
".gif": "image/gif",
|
|
2441
|
+
".webp": "image/webp",
|
|
2442
|
+
".avif": "image/avif",
|
|
2443
|
+
".mp4": "video/mp4",
|
|
2444
|
+
".m4v": "video/mp4",
|
|
2445
|
+
".webm": "video/webm",
|
|
2446
|
+
".ogv": "video/ogg",
|
|
2447
|
+
".ogg": "video/ogg",
|
|
2448
|
+
".mov": "video/quicktime"
|
|
2449
|
+
};
|
|
2450
|
+
function extensionOf(name) {
|
|
2451
|
+
const index = name.lastIndexOf(".");
|
|
2452
|
+
return index >= 0 ? name.slice(index).toLowerCase() : "";
|
|
2453
|
+
}
|
|
2454
|
+
function attachmentSpec(name, contentType = "") {
|
|
2455
|
+
if (/[/\\]/.test(name)) return null;
|
|
2456
|
+
const base = name.replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
2457
|
+
if (!base || base.length > 240) return null;
|
|
2458
|
+
const extension = extensionOf(base);
|
|
2459
|
+
const expected = MIME_BY_EXTENSION[extension];
|
|
2460
|
+
if (!expected) return null;
|
|
2461
|
+
const provided = normalizeContentType(String(contentType));
|
|
2462
|
+
if (provided && provided !== expected) return null;
|
|
2463
|
+
return {
|
|
2464
|
+
name: base,
|
|
2465
|
+
contentType: expected,
|
|
2466
|
+
extension,
|
|
2467
|
+
kind: expected.startsWith("image/") ? "image" : "video"
|
|
2468
|
+
};
|
|
2469
|
+
}
|
|
2470
|
+
var CONTENT_TYPE_ALIASES = {
|
|
2471
|
+
"image/jpg": "image/jpeg",
|
|
2472
|
+
"image/pjpeg": "image/jpeg",
|
|
2473
|
+
"video/x-m4v": "video/mp4",
|
|
2474
|
+
"video/x-ogg": "video/ogg"
|
|
2475
|
+
};
|
|
2476
|
+
function normalizeContentType(value) {
|
|
2477
|
+
const type = value.split(";")[0].trim().toLowerCase();
|
|
2478
|
+
return CONTENT_TYPE_ALIASES[type] ?? type;
|
|
2479
|
+
}
|
|
2480
|
+
|
|
2000
2481
|
// src/cli/main.ts
|
|
2001
2482
|
init_context();
|
|
2002
2483
|
var OPTIONS = {
|
|
@@ -2027,10 +2508,13 @@ var OPTIONS = {
|
|
|
2027
2508
|
force: { type: "boolean", default: false },
|
|
2028
2509
|
adapters: { type: "string" },
|
|
2029
2510
|
"no-service": { type: "boolean", default: false },
|
|
2511
|
+
"keep-config": { type: "boolean", default: false },
|
|
2512
|
+
write: { type: "boolean", default: false },
|
|
2030
2513
|
explain: { type: "string" },
|
|
2031
2514
|
limit: { type: "string" },
|
|
2032
2515
|
decision: { type: "string" },
|
|
2033
2516
|
note: { type: "string" },
|
|
2517
|
+
emoji: { type: "string" },
|
|
2034
2518
|
text: { type: "string" },
|
|
2035
2519
|
line: { type: "string" },
|
|
2036
2520
|
id: { type: "string" },
|
|
@@ -2049,6 +2533,8 @@ function parse(argv) {
|
|
|
2049
2533
|
var HELP_RU = `revdeck ${VERSION} \u2014 \u0440\u0435\u0432\u044C\u044E GitLab/GitHub \u0434\u043B\u044F \u043B\u044E\u0434\u0435\u0439 \u0438 \u0430\u0433\u0435\u043D\u0442\u043E\u0432
|
|
2050
2534
|
|
|
2051
2535
|
revdeck init \u043D\u0430\u0441\u0442\u0440\u043E\u0438\u0442\u044C \u043F\u0440\u043E\u0435\u043A\u0442: .revdeck.yml, \u0430\u0434\u0430\u043F\u0442\u0435\u0440\u044B, MCP
|
|
2536
|
+
revdeck eject \u0443\u0434\u0430\u043B\u0438\u0442\u044C revdeck \u0438\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (--yes; --keep-config)
|
|
2537
|
+
revdeck devcontainer \u043F\u043E\u0434\u043A\u043B\u044E\u0447\u0438\u0442\u044C devcontainer \u043A \u0434\u0435\u043C\u043E\u043D\u0443 \u0445\u043E\u0441\u0442\u0430 (--write)
|
|
2052
2538
|
revdeck doctor \u043F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C \u043E\u043A\u0440\u0443\u0436\u0435\u043D\u0438\u0435 \u0438 \u0434\u043E\u0441\u0442\u0443\u043F\u044B
|
|
2053
2539
|
revdeck daemon <start|stop|status|logs|restart>
|
|
2054
2540
|
revdeck list \u043E\u0442\u043A\u0440\u044B\u0442\u044B\u0435 \u0440\u0435\u0432\u044C\u044E (\u043B\u043E\u043A\u0430\u043B\u044C\u043D\u044B\u0439 \u043A\u0435\u0448), --remote \u2014 \u0441 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u044B
|
|
@@ -2061,6 +2547,8 @@ var HELP_RU = `revdeck ${VERSION} \u2014 \u0440\u0435\u0432\u044C\u044E GitLab/G
|
|
|
2061
2547
|
revdeck summary <N> [--file <json>] \u0432\u044B\u0436\u0438\u043C\u043A\u0430 MR
|
|
2062
2548
|
revdeck reply <N> --thread <id> (--body <\u0442\u0435\u043A\u0441\u0442> | --body-file <\u0444\u0430\u0439\u043B> | -) [--resolve] [--publish --yes]
|
|
2063
2549
|
revdeck notes <N> <list|add|update|delete|reply>
|
|
2550
|
+
revdeck react <N> --note <id> --emoji <name> \u043F\u043E\u0441\u0442\u0430\u0432\u0438\u0442\u044C \u0438\u043B\u0438 \u0441\u043D\u044F\u0442\u044C \u0440\u0435\u0430\u043A\u0446\u0438\u044E
|
|
2551
|
+
revdeck attach <N> --file <\u043F\u0443\u0442\u044C> \u0437\u0430\u0433\u0440\u0443\u0437\u0438\u0442\u044C \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u0443/\u0432\u0438\u0434\u0435\u043E \u0432 \u043F\u043B\u0430\u0442\u0444\u043E\u0440\u043C\u0443
|
|
2064
2552
|
revdeck publish <N> [--kind reply|finding|note] [--ids a,b] [--yes]
|
|
2065
2553
|
revdeck rules [<N>] [--explain <\u043F\u0443\u0442\u044C>]
|
|
2066
2554
|
revdeck checks <N> [--run --yes]
|
|
@@ -2074,6 +2562,8 @@ var HELP_RU = `revdeck ${VERSION} \u2014 \u0440\u0435\u0432\u044C\u044E GitLab/G
|
|
|
2074
2562
|
var HELP_EN = `revdeck ${VERSION} \u2014 code review for GitLab/GitHub, for humans and agents
|
|
2075
2563
|
|
|
2076
2564
|
revdeck init set up a project: .revdeck.yml, adapters, MCP
|
|
2565
|
+
revdeck eject remove revdeck from the project (--yes; --keep-config)
|
|
2566
|
+
revdeck devcontainer connect a devcontainer to the host daemon (--write)
|
|
2077
2567
|
revdeck doctor check the environment and access
|
|
2078
2568
|
revdeck daemon <start|stop|status|logs|restart>
|
|
2079
2569
|
revdeck list open reviews (local cache), --remote \u2014 from the platform
|
|
@@ -2086,6 +2576,8 @@ var HELP_EN = `revdeck ${VERSION} \u2014 code review for GitLab/GitHub, for huma
|
|
|
2086
2576
|
revdeck summary <N> [--file <json>] review summary
|
|
2087
2577
|
revdeck reply <N> --thread <id> (--body <text> | --body-file <file> | -) [--resolve] [--publish --yes]
|
|
2088
2578
|
revdeck notes <N> <list|add|update|delete|reply>
|
|
2579
|
+
revdeck react <N> --note <id> --emoji <name> add or remove a reaction
|
|
2580
|
+
revdeck attach <N> --file <path> upload an image/video to the platform
|
|
2089
2581
|
revdeck publish <N> [--kind reply|finding|note] [--ids a,b] [--yes]
|
|
2090
2582
|
revdeck rules [<N>] [--explain <path>]
|
|
2091
2583
|
revdeck checks <N> [--run --yes]
|
|
@@ -2120,10 +2612,14 @@ async function main2() {
|
|
|
2120
2612
|
switch (command) {
|
|
2121
2613
|
case "init":
|
|
2122
2614
|
return runInit(root, { force: values.force === true, adapters: parseList(values.adapters), noService: values["no-service"] === true, locale: values.lang === "ru" || values.lang === "en" ? values.lang : detectLocale(), outputLanguage: values.lang === "ru" || values.lang === "en" ? values.lang : "auto" });
|
|
2615
|
+
case "eject":
|
|
2616
|
+
return runEject(root, { yes: values.yes === true && values["dry-run"] !== true, keepConfig: values["keep-config"] === true });
|
|
2617
|
+
case "devcontainer":
|
|
2618
|
+
return runDevcontainer(root, { write: values.write === true });
|
|
2123
2619
|
case "doctor":
|
|
2124
2620
|
return doctor(root);
|
|
2125
2621
|
case "daemon":
|
|
2126
|
-
return daemon(positionals[0] ?? "status");
|
|
2622
|
+
return daemon(positionals[0] ?? "status", values);
|
|
2127
2623
|
case "list":
|
|
2128
2624
|
return list(root, values);
|
|
2129
2625
|
case "open":
|
|
@@ -2147,6 +2643,10 @@ async function main2() {
|
|
|
2147
2643
|
return reply(positionals[0], values);
|
|
2148
2644
|
case "notes":
|
|
2149
2645
|
return notes(positionals[0], positionals[1] ?? "list", values);
|
|
2646
|
+
case "react":
|
|
2647
|
+
return react(positionals[0], values);
|
|
2648
|
+
case "attach":
|
|
2649
|
+
return attach(positionals[0], values);
|
|
2150
2650
|
case "publish":
|
|
2151
2651
|
return publish(positionals[0], values);
|
|
2152
2652
|
case "rules":
|
|
@@ -2259,13 +2759,14 @@ async function ui(arg, values) {
|
|
|
2259
2759
|
});
|
|
2260
2760
|
const info = await daemonInfo();
|
|
2261
2761
|
if (!info) fail(tr("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D", "the daemon is not running"), "revdeck daemon start");
|
|
2262
|
-
|
|
2263
|
-
|
|
2762
|
+
const url = `${uiBaseUrl(info)}/?t=${info.token}`;
|
|
2763
|
+
writers.out(url);
|
|
2764
|
+
if (values.open) openBrowser(url);
|
|
2264
2765
|
}
|
|
2265
2766
|
function printUi(reviewId, values) {
|
|
2266
2767
|
const info = daemonInfoSync();
|
|
2267
2768
|
if (!info) return writers.err(tr("\u0434\u0435\u043C\u043E\u043D \u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D: revdeck daemon start", "the daemon is not running: revdeck daemon start"));
|
|
2268
|
-
const url = `${info
|
|
2769
|
+
const url = `${uiBaseUrl(info)}/?t=${info.token}${reviewId ? `&review=${encodeURIComponent(reviewId)}` : ""}`;
|
|
2269
2770
|
writers.out(`UI: ${url}`);
|
|
2270
2771
|
if (values.open) openBrowser(url);
|
|
2271
2772
|
}
|
|
@@ -2283,14 +2784,16 @@ function openBrowser(url) {
|
|
|
2283
2784
|
spawn2(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
2284
2785
|
});
|
|
2285
2786
|
}
|
|
2286
|
-
async function daemon(action) {
|
|
2787
|
+
async function daemon(action, values = {}) {
|
|
2287
2788
|
if (action === "start") {
|
|
2288
|
-
const { client: client2, started } = await ensureDaemon(
|
|
2789
|
+
const { client: client2, started } = await ensureDaemon({
|
|
2790
|
+
...values.host ? { daemonArgs: ["--host", String(values.host)] } : {}
|
|
2791
|
+
});
|
|
2289
2792
|
const ping = await client2.request("ping");
|
|
2290
2793
|
client2.close();
|
|
2291
2794
|
const info = await daemonInfo();
|
|
2292
2795
|
writers.out(`${started ? tr("\u0437\u0430\u043F\u0443\u0449\u0435\u043D", "started") : tr("\u0443\u0436\u0435 \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442", "already running")} ${tr("\u0434\u0435\u043C\u043E\u043D", "daemon")} pid ${ping.pid}, ${tr("\u0432\u0435\u0440\u0441\u0438\u044F", "version")} ${ping.version}`);
|
|
2293
|
-
if (info) writers.out(`${info
|
|
2796
|
+
if (info) writers.out(`${uiBaseUrl(info)}/?t=${info.token}`);
|
|
2294
2797
|
return;
|
|
2295
2798
|
}
|
|
2296
2799
|
if (action === "status") {
|
|
@@ -2330,11 +2833,11 @@ async function daemon(action) {
|
|
|
2330
2833
|
}
|
|
2331
2834
|
if (action === "restart") {
|
|
2332
2835
|
await daemon("stop");
|
|
2333
|
-
return daemon("start");
|
|
2836
|
+
return daemon("start", values);
|
|
2334
2837
|
}
|
|
2335
2838
|
if (action === "logs") {
|
|
2336
2839
|
try {
|
|
2337
|
-
const text = await
|
|
2840
|
+
const text = await readFile7(logPath(), "utf8");
|
|
2338
2841
|
writers.out(text.split("\n").slice(-80).join("\n"));
|
|
2339
2842
|
} catch {
|
|
2340
2843
|
writers.out(tr("\u043B\u043E\u0433 \u043F\u0443\u0441\u0442", "the log is empty"));
|
|
@@ -2399,7 +2902,7 @@ async function findings(arg, action, values) {
|
|
|
2399
2902
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
2400
2903
|
if (action === "set") {
|
|
2401
2904
|
if (!values.file) fail(tr("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 --file \u0441 JSON \u0437\u0430\u043C\u0435\u0447\u0430\u043D\u0438\u0439", "pass --file with findings JSON"));
|
|
2402
|
-
const raw = JSON.parse(await
|
|
2905
|
+
const raw = JSON.parse(await readFile7(String(values.file), "utf8"));
|
|
2403
2906
|
const list2 = Array.isArray(raw) ? raw : raw.findings;
|
|
2404
2907
|
if (!Array.isArray(list2)) fail(tr("\u043E\u0436\u0438\u0434\u0430\u043B\u0441\u044F \u043C\u0430\u0441\u0441\u0438\u0432 \u0438\u043B\u0438 {findings: [...]}", "expected an array or {findings: [...]}"));
|
|
2405
2908
|
const { findings: saved } = await client2.request("findings.put", { review: review.id, findings: list2 });
|
|
@@ -2426,7 +2929,7 @@ async function findings(arg, action, values) {
|
|
|
2426
2929
|
}
|
|
2427
2930
|
async function summary(arg, values) {
|
|
2428
2931
|
if (!values.file && process.stdin.isTTY) fail(tr("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 --file \u0441 JSON \u0432\u044B\u0436\u0438\u043C\u043A\u0438 \u0438\u043B\u0438 \u043F\u0435\u0440\u0435\u0434\u0430\u0439\u0442\u0435 \u0435\u0451 \u0447\u0435\u0440\u0435\u0437 stdin", "pass --file with summary JSON or pipe it via stdin"));
|
|
2429
|
-
const raw = values.file ? JSON.parse(await
|
|
2932
|
+
const raw = values.file ? JSON.parse(await readFile7(String(values.file), "utf8")) : JSON.parse(await readStdin());
|
|
2430
2933
|
await withClient(async ({ client: client2 }) => {
|
|
2431
2934
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
2432
2935
|
const result = await client2.request("summary.put", { review: review.id, summary: raw });
|
|
@@ -2498,6 +3001,45 @@ async function notes(arg, action, values) {
|
|
|
2498
3001
|
}
|
|
2499
3002
|
});
|
|
2500
3003
|
}
|
|
3004
|
+
async function react(arg, values) {
|
|
3005
|
+
if (!values.note || !values.emoji) fail(tr("\u043D\u0443\u0436\u043D\u044B --note <id> \u0438 --emoji <name>", "--note <id> and --emoji <name> are required"));
|
|
3006
|
+
await withClient(async ({ client: client2 }) => {
|
|
3007
|
+
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
3008
|
+
const result = await client2.request("reactions.toggle", {
|
|
3009
|
+
review: review.id,
|
|
3010
|
+
noteId: String(values.note),
|
|
3011
|
+
emoji: String(values.emoji)
|
|
3012
|
+
});
|
|
3013
|
+
if (values.json) return printJson(result);
|
|
3014
|
+
writers.out(`${result.active ? "\u2714" : "\u25CB"} ${values.emoji}${result.active ? ` \xB7 ${tr("\u043F\u043E\u0441\u0442\u0430\u0432\u043B\u0435\u043D\u0430", "added")}` : ` \xB7 ${tr("\u0441\u043D\u044F\u0442\u0430", "removed")}`}`);
|
|
3015
|
+
for (const reaction of result.reactions) writers.out(` ${reaction.name}${reaction.mine ? " \xB7" : ""} ${reaction.count}`);
|
|
3016
|
+
});
|
|
3017
|
+
}
|
|
3018
|
+
async function attach(arg, values) {
|
|
3019
|
+
if (!values.file) fail(tr("\u0443\u043A\u0430\u0436\u0438\u0442\u0435 --file \u0441 \u043A\u0430\u0440\u0442\u0438\u043D\u043A\u043E\u0439 \u0438\u043B\u0438 \u0432\u0438\u0434\u0435\u043E", "pass --file with an image or video"));
|
|
3020
|
+
const name = path9.basename(String(values.file));
|
|
3021
|
+
const spec = attachmentSpec(name);
|
|
3022
|
+
if (!spec) fail(tr("\u0440\u0430\u0437\u0440\u0435\u0448\u0435\u043D\u044B PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, OGV \u0438 MOV", "PNG, JPEG, GIF, WebP, AVIF, MP4, WebM, OGV and MOV are allowed"));
|
|
3023
|
+
const file = String(values.file);
|
|
3024
|
+
const info = await stat(file).catch(() => null);
|
|
3025
|
+
if (!info?.isFile()) fail(tr(`\u043D\u0435\u0442 \u0444\u0430\u0439\u043B\u0430 ${file}`, `file ${file} does not exist`));
|
|
3026
|
+
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
3027
|
+
fail(tr(`\u0444\u0430\u0439\u043B \u0431\u043E\u043B\u044C\u0448\u0435 ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))} \u041C\u0411`, `file is larger than ${Math.floor(MAX_ATTACHMENT_BYTES / (1024 * 1024))} MB`));
|
|
3028
|
+
}
|
|
3029
|
+
const data = await readFile7(file);
|
|
3030
|
+
await withClient(async ({ client: client2 }) => {
|
|
3031
|
+
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
3032
|
+
const result = await client2.request("attachments.upload", {
|
|
3033
|
+
review: review.id,
|
|
3034
|
+
name,
|
|
3035
|
+
contentType: spec.contentType,
|
|
3036
|
+
data: data.toString("base64")
|
|
3037
|
+
});
|
|
3038
|
+
if (values.json) return printJson(result.attachment);
|
|
3039
|
+
writers.out(`${tr("\u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E", "uploaded")}: ${result.attachment.url}`);
|
|
3040
|
+
writers.out(result.attachment.markdown);
|
|
3041
|
+
});
|
|
3042
|
+
}
|
|
2501
3043
|
async function publish(arg, values) {
|
|
2502
3044
|
await withClient(async ({ client: client2 }) => {
|
|
2503
3045
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
@@ -2606,7 +3148,7 @@ async function readBodyInput(values) {
|
|
|
2606
3148
|
if (values["body-file"] !== void 0) {
|
|
2607
3149
|
const file = String(values["body-file"]);
|
|
2608
3150
|
if (file === "-") return readStdin();
|
|
2609
|
-
return
|
|
3151
|
+
return readFile7(file, "utf8");
|
|
2610
3152
|
}
|
|
2611
3153
|
if (process.stdin.isTTY) return null;
|
|
2612
3154
|
return readStdin();
|
|
@@ -2618,12 +3160,20 @@ async function readStdin() {
|
|
|
2618
3160
|
}
|
|
2619
3161
|
async function doctor(root) {
|
|
2620
3162
|
const rows = [];
|
|
3163
|
+
const container = isDevContainer();
|
|
2621
3164
|
const major = Number(process.version.slice(1).split(".")[0]);
|
|
2622
3165
|
rows.push(["node", major >= 22 ? "ok" : "fail", process.version]);
|
|
3166
|
+
if (container) {
|
|
3167
|
+
rows.push(["mode", "ok", tr("devcontainer: revdeck \u0438 gh/glab \u0440\u0430\u0431\u043E\u0442\u0430\u044E\u0442 \u043D\u0430 \u0445\u043E\u0441\u0442\u0435", "devcontainer: revdeck and gh/glab run on the host")]);
|
|
3168
|
+
}
|
|
2623
3169
|
const binaries = {};
|
|
2624
3170
|
for (const bin of ["glab", "gh", "git"]) {
|
|
2625
3171
|
const present = await which(bin);
|
|
2626
3172
|
binaries[bin] = present;
|
|
3173
|
+
if (container && (bin === "glab" || bin === "gh")) {
|
|
3174
|
+
rows.push([bin, "ok", present ? tr("\u0435\u0441\u0442\u044C \u0432 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435, \u043D\u043E \u043D\u0435 \u0438\u0441\u043F\u043E\u043B\u044C\u0437\u0443\u0435\u0442\u0441\u044F", "present in the container but unused") : tr("\u043D\u0435 \u043D\u0443\u0436\u0435\u043D \u0432 \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0435: \u0440\u0430\u0431\u043E\u0442\u0430\u0435\u0442 \u043D\u0430 \u0445\u043E\u0441\u0442\u0435", "not needed in the container: the host runs it")]);
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
2627
3177
|
rows.push([bin, present ? "ok" : "warn", present ? tr("\u043D\u0430\u0439\u0434\u0435\u043D", "found") : tr("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D", "not found")]);
|
|
2628
3178
|
}
|
|
2629
3179
|
let host;
|
|
@@ -2635,28 +3185,35 @@ async function doctor(root) {
|
|
|
2635
3185
|
} catch (error) {
|
|
2636
3186
|
rows.push(["config", "fail", redactLine(error)]);
|
|
2637
3187
|
}
|
|
2638
|
-
if (
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3188
|
+
if (!container) {
|
|
3189
|
+
if (host && binaries.glab) {
|
|
3190
|
+
try {
|
|
3191
|
+
await run("glab", ["auth", "status", "--hostname", host], { timeoutMs: 15e3 });
|
|
3192
|
+
rows.push(["glab auth", "ok", host]);
|
|
3193
|
+
} catch (error) {
|
|
3194
|
+
rows.push(["glab auth", "warn", `${tr("\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A", "no access to")} ${host}: ${redactLine(error).slice(0, 120)}`]);
|
|
3195
|
+
}
|
|
2644
3196
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
3197
|
+
if (host && binaries.gh) {
|
|
3198
|
+
try {
|
|
3199
|
+
await run("gh", ["auth", "status"], { timeoutMs: 15e3, env: { GH_HOST: host } });
|
|
3200
|
+
rows.push(["gh auth", "ok", host]);
|
|
3201
|
+
} catch (error) {
|
|
3202
|
+
rows.push(["gh auth", "warn", `${tr("\u043D\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0430 \u043A", "no access to")} ${host}: ${redactLine(error).slice(0, 120)}`]);
|
|
3203
|
+
}
|
|
2652
3204
|
}
|
|
2653
3205
|
}
|
|
2654
3206
|
const running = await isDaemonRunning();
|
|
2655
3207
|
const info = await daemonInfo();
|
|
2656
|
-
|
|
2657
|
-
rows.push([
|
|
2658
|
-
|
|
2659
|
-
|
|
3208
|
+
const daemonStatus = running ? "ok" : container ? "fail" : "warn";
|
|
3209
|
+
rows.push([
|
|
3210
|
+
"daemon",
|
|
3211
|
+
daemonStatus,
|
|
3212
|
+
running && info ? `${uiBaseUrl(info)} (pid ${info.pid})` : container ? tr("\u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D: \u043D\u0430 \u0445\u043E\u0441\u0442\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 revdeck daemon start", "not found: run revdeck daemon start on the host") : tr("\u043D\u0435 \u0437\u0430\u043F\u0443\u0449\u0435\u043D (\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0441\u044F \u0441\u0430\u043C)", "not running (starts on demand)")
|
|
3213
|
+
]);
|
|
3214
|
+
rows.push(["ui", running ? "ok" : "warn", info ? uiBaseUrl(info) : tr("\u0431\u0443\u0434\u0435\u0442 \u0434\u043E\u0441\u0442\u0443\u043F\u0435\u043D \u043F\u043E\u0441\u043B\u0435 \u0437\u0430\u043F\u0443\u0441\u043A\u0430", "available after the daemon starts")]);
|
|
3215
|
+
const { existsSync: existsSync8 } = await import("node:fs");
|
|
3216
|
+
const skill = existsSync8(path9.join(root, ".agents", "skills", "revdeck", "SKILL.md")) || existsSync8(path9.join(root, ".claude", "skills", "revdeck", "SKILL.md")) || existsSync8(path9.join(root, ".cursor", "commands", "revdeck.md"));
|
|
2660
3217
|
rows.push(["adapter", skill ? "ok" : "warn", skill ? tr("\u0443\u0441\u0442\u0430\u043D\u043E\u0432\u043B\u0435\u043D", "installed") : tr("\u043D\u0435\u0442: revdeck init", "missing: revdeck init")]);
|
|
2661
3218
|
for (const [name, status2, detail] of rows) {
|
|
2662
3219
|
writers.out(`${status2 === "ok" ? "\u2714" : status2 === "warn" ? "!" : "\u2716"} ${name.padEnd(10)} ${detail}`);
|