revdeck 0.1.0 → 0.2.1
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 +34 -0
- package/CONTRIBUTING.md +17 -7
- package/README.md +50 -6
- package/README.ru.md +50 -5
- package/dist/cli.js +687 -69
- package/dist/cli.js.map +4 -4
- package/dist/daemon.js +386 -54
- 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 +3 -1
- package/docs/architecture.md +3 -1
- package/docs/configuration.md +2 -0
- package/docs/integrations.md +53 -11
- package/docs/protocol.md +11 -2
- package/docs/ru/adr/0001-daemon-first.md +3 -1
- package/docs/ru/architecture.md +3 -1
- package/docs/ru/configuration.md +2 -0
- package/docs/ru/integrations.md +52 -10
- package/docs/ru/protocol.md +11 -2
- package/docs/ru/threat-model.md +3 -1
- package/docs/threat-model.md +3 -1
- package/package.json +2 -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
|
@@ -47,9 +47,31 @@ function socketPath() {
|
|
|
47
47
|
function daemonInfoPath() {
|
|
48
48
|
return path.join(stateDir(), "daemon.json");
|
|
49
49
|
}
|
|
50
|
+
function devcontainerBridgeDir() {
|
|
51
|
+
if (process.env.REVDECK_DEVCONTAINER_DIR) return path.resolve(process.env.REVDECK_DEVCONTAINER_DIR);
|
|
52
|
+
return path.join(os.homedir(), ".local", "state", "revdeck", "devcontainer");
|
|
53
|
+
}
|
|
54
|
+
function devcontainerRunDir() {
|
|
55
|
+
return path.join(devcontainerBridgeDir(), "run");
|
|
56
|
+
}
|
|
57
|
+
function devcontainerStateDir() {
|
|
58
|
+
return path.join(devcontainerBridgeDir(), "state");
|
|
59
|
+
}
|
|
60
|
+
function devcontainerSocketPath() {
|
|
61
|
+
return path.join(devcontainerRunDir(), "daemon.sock");
|
|
62
|
+
}
|
|
50
63
|
function logPath() {
|
|
51
64
|
return path.join(stateDir(), "daemon.log");
|
|
52
65
|
}
|
|
66
|
+
async function ensureDir(dir, mode = 448) {
|
|
67
|
+
await mkdir(dir, { recursive: true, mode });
|
|
68
|
+
const info = await lstat(dir);
|
|
69
|
+
if (!info.isDirectory()) throw new Error(`${dir}: expected a directory`);
|
|
70
|
+
if (process.platform !== "win32") {
|
|
71
|
+
const { chmod } = await import("node:fs/promises");
|
|
72
|
+
await chmod(dir, mode);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
53
75
|
async function readTextNoFollow(file, maxBytes = 4 * 1024 * 1024) {
|
|
54
76
|
const info = await lstat(file);
|
|
55
77
|
if (info.isSymbolicLink()) throw new Error(`${path.basename(file)}: symbolic link, read refused`);
|
|
@@ -146,7 +168,7 @@ var VERSION;
|
|
|
146
168
|
var init_version = __esm({
|
|
147
169
|
"src/version.ts"() {
|
|
148
170
|
"use strict";
|
|
149
|
-
VERSION = "0.1
|
|
171
|
+
VERSION = "0.2.1";
|
|
150
172
|
}
|
|
151
173
|
});
|
|
152
174
|
|
|
@@ -182,6 +204,19 @@ async function daemonInfo() {
|
|
|
182
204
|
return null;
|
|
183
205
|
}
|
|
184
206
|
}
|
|
207
|
+
function isDevContainer() {
|
|
208
|
+
return process.env.REVDECK_DEV_CONTAINER === "1";
|
|
209
|
+
}
|
|
210
|
+
function devcontainerHint() {
|
|
211
|
+
const runtime = process.env.REVDECK_RUNTIME_DIR ?? "(REVDECK_RUNTIME_DIR)";
|
|
212
|
+
const state = process.env.REVDECK_STATE_DIR ?? "(REVDECK_STATE_DIR)";
|
|
213
|
+
return [
|
|
214
|
+
"\u0434\u0435\u043C\u043E\u043D revdeck \u043D\u0435 \u043D\u0430\u0439\u0434\u0435\u043D \u043D\u0430 \u0445\u043E\u0441\u0442\u0435",
|
|
215
|
+
"\u043D\u0430 \u0445\u043E\u0441\u0442\u0435: revdeck daemon start",
|
|
216
|
+
`\u043F\u0440\u043E\u0432\u0435\u0440\u044C\u0442\u0435 mounts \u0438\u0437 revdeck devcontainer --write: ${runtime} \u0438 ${state}`,
|
|
217
|
+
"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"
|
|
218
|
+
].join("; ");
|
|
219
|
+
}
|
|
185
220
|
async function ensureDaemon(options = {}) {
|
|
186
221
|
const url = process.env.REVDECK_URL;
|
|
187
222
|
if (url) {
|
|
@@ -192,9 +227,12 @@ async function ensureDaemon(options = {}) {
|
|
|
192
227
|
try {
|
|
193
228
|
return { client: await connectSocket(), started: false };
|
|
194
229
|
} catch (error) {
|
|
230
|
+
if (isDevContainer()) {
|
|
231
|
+
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");
|
|
232
|
+
}
|
|
195
233
|
if (options.autoStart === false) throw error;
|
|
196
234
|
}
|
|
197
|
-
await spawnDaemon();
|
|
235
|
+
await spawnDaemon(options.daemonArgs ?? []);
|
|
198
236
|
const deadline = Date.now() + 15e3;
|
|
199
237
|
let lastError = null;
|
|
200
238
|
while (Date.now() < deadline) {
|
|
@@ -211,9 +249,9 @@ async function ensureDaemon(options = {}) {
|
|
|
211
249
|
"\u0437\u0430\u043F\u0443\u0441\u0442\u0438\u0442\u0435 \u0432\u0440\u0443\u0447\u043D\u0443\u044E: revdeck daemon start --foreground"
|
|
212
250
|
);
|
|
213
251
|
}
|
|
214
|
-
async function spawnDaemon() {
|
|
252
|
+
async function spawnDaemon(extraArgs = []) {
|
|
215
253
|
const entry = daemonEntry();
|
|
216
|
-
const child = spawn(entry.command, entry.args, {
|
|
254
|
+
const child = spawn(entry.command, [...entry.args, ...extraArgs], {
|
|
217
255
|
detached: true,
|
|
218
256
|
stdio: "ignore",
|
|
219
257
|
cwd: process.cwd(),
|
|
@@ -234,6 +272,11 @@ function daemonEntry() {
|
|
|
234
272
|
if (existsSync4(sibling)) return { command: process.execPath, args: [sibling] };
|
|
235
273
|
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
274
|
}
|
|
275
|
+
function uiBaseUrl(info) {
|
|
276
|
+
const host = process.env.REVDECK_UI_HOST;
|
|
277
|
+
if (!isDevContainer() || !host) return info.url;
|
|
278
|
+
return `http://${host}:${info.port}`;
|
|
279
|
+
}
|
|
237
280
|
async function isDaemonRunning() {
|
|
238
281
|
try {
|
|
239
282
|
const client2 = await connectSocket(void 0, 400);
|
|
@@ -1058,10 +1101,10 @@ ${failed.map((item) => `- ${item.id}: ${item.error}`).join("\n")}` : ""),
|
|
|
1058
1101
|
|
|
1059
1102
|
// src/cli/main.ts
|
|
1060
1103
|
init_paths();
|
|
1061
|
-
import { readFile as
|
|
1104
|
+
import { readFile as readFile7, stat } from "node:fs/promises";
|
|
1062
1105
|
import { readFileSync } from "node:fs";
|
|
1063
1106
|
import { parseArgs } from "node:util";
|
|
1064
|
-
import
|
|
1107
|
+
import path9 from "node:path";
|
|
1065
1108
|
|
|
1066
1109
|
// src/core/config.ts
|
|
1067
1110
|
init_paths();
|
|
@@ -1134,8 +1177,8 @@ function globToRegExp(glob) {
|
|
|
1134
1177
|
function escapeRegExp(value) {
|
|
1135
1178
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1136
1179
|
}
|
|
1137
|
-
function classifyFile(
|
|
1138
|
-
const lower =
|
|
1180
|
+
function classifyFile(path10) {
|
|
1181
|
+
const lower = path10.toLowerCase();
|
|
1139
1182
|
if (/(^|\/)(node_modules|dist|build|coverage|vendor)\//.test(lower)) return "generated";
|
|
1140
1183
|
if (/\.(md|mdx|rst|txt|adoc)$/.test(lower) || /(^|\/)(docs?|adr)\//.test(lower)) return "docs";
|
|
1141
1184
|
if (/(^|\/)(__tests__|tests?|spec|e2e)\//.test(lower) || /\.(test|spec)\.[cm]?[jt]sx?$/.test(lower) || /_test\.go$/.test(lower)) return "test";
|
|
@@ -1556,7 +1599,7 @@ init_format();
|
|
|
1556
1599
|
// src/cli/init.ts
|
|
1557
1600
|
init_paths();
|
|
1558
1601
|
import { existsSync as existsSync5 } from "node:fs";
|
|
1559
|
-
import { appendFile, cp, mkdir as mkdir2, readFile as readFile3, writeFile } from "node:fs/promises";
|
|
1602
|
+
import { appendFile, cp, lstat as lstat2, mkdir as mkdir2, readdir, readFile as readFile3, readlink, rm, symlink, writeFile } from "node:fs/promises";
|
|
1560
1603
|
import path5 from "node:path";
|
|
1561
1604
|
init_redact();
|
|
1562
1605
|
init_locale();
|
|
@@ -1604,8 +1647,9 @@ async function runInit(rootInput, options) {
|
|
|
1604
1647
|
await ensureGitignore(root);
|
|
1605
1648
|
const adapters = options.adapters.length ? options.adapters : detectAdapters(root);
|
|
1606
1649
|
const mcp = mcpServer(options.locale);
|
|
1650
|
+
const canonicalSkill = await installSkill(root, options.locale);
|
|
1607
1651
|
for (const adapter of adapters) {
|
|
1608
|
-
await installAdapter(root, adapter, mcp, options.locale);
|
|
1652
|
+
await installAdapter(root, adapter, mcp, options.locale, canonicalSkill);
|
|
1609
1653
|
}
|
|
1610
1654
|
if (!options.noService) {
|
|
1611
1655
|
writers.out("");
|
|
@@ -1618,6 +1662,7 @@ async function runInit(rootInput, options) {
|
|
|
1618
1662
|
writers.out(` revdeck open ${tr("<\u043D\u043E\u043C\u0435\u0440 \u0438\u043B\u0438 \u0441\u0441\u044B\u043B\u043A\u0430>", "<number or URL>")}`);
|
|
1619
1663
|
writers.out(" revdeck ui");
|
|
1620
1664
|
}
|
|
1665
|
+
var CANONICAL_SKILL = ".agents/skills/revdeck";
|
|
1621
1666
|
function commandMd(locale2) {
|
|
1622
1667
|
if (locale2 === "ru") {
|
|
1623
1668
|
return `---
|
|
@@ -1643,10 +1688,10 @@ function commandToml(locale2) {
|
|
|
1643
1688
|
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
1689
|
}
|
|
1645
1690
|
var SPECS = {
|
|
1646
|
-
agents: {
|
|
1647
|
-
dsh: {
|
|
1691
|
+
agents: {},
|
|
1692
|
+
dsh: {},
|
|
1648
1693
|
claude: {
|
|
1649
|
-
|
|
1694
|
+
linkSkill: true,
|
|
1650
1695
|
mcp: ".mcp.json",
|
|
1651
1696
|
files: [{ path: ".claude/commands/revdeck.md", content: commandMd }]
|
|
1652
1697
|
},
|
|
@@ -1670,25 +1715,13 @@ function detectAdapters(root) {
|
|
|
1670
1715
|
if (existsSync5(path5.join(root, ".opencode"))) adapters.add("opencode");
|
|
1671
1716
|
return [...adapters];
|
|
1672
1717
|
}
|
|
1673
|
-
async function installAdapter(root, adapter, mcp, locale2) {
|
|
1718
|
+
async function installAdapter(root, adapter, mcp, locale2, canonicalSkill) {
|
|
1674
1719
|
const spec = SPECS[adapter];
|
|
1675
1720
|
if (!spec) {
|
|
1676
1721
|
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
1722
|
return;
|
|
1678
1723
|
}
|
|
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
|
-
}
|
|
1724
|
+
if (spec.linkSkill && canonicalSkill) await linkSkill(root, canonicalSkill);
|
|
1692
1725
|
if (spec.mcp) await mergeMcpJson(path5.join(root, spec.mcp), root, mcp);
|
|
1693
1726
|
for (const file of spec.files ?? []) {
|
|
1694
1727
|
const target = path5.join(root, file.path);
|
|
@@ -1714,9 +1747,67 @@ async function installAdapter(root, adapter, mcp, locale2) {
|
|
|
1714
1747
|
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
1748
|
}
|
|
1716
1749
|
}
|
|
1717
|
-
async function
|
|
1718
|
-
const
|
|
1719
|
-
|
|
1750
|
+
async function installSkill(root, locale2) {
|
|
1751
|
+
const source = path5.join(packageRoot(), "skills", "revdeck", locale2);
|
|
1752
|
+
if (!existsSync5(source)) {
|
|
1753
|
+
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")}`);
|
|
1754
|
+
return null;
|
|
1755
|
+
}
|
|
1756
|
+
const target = path5.join(root, CANONICAL_SKILL);
|
|
1757
|
+
await copySkill(source, target);
|
|
1758
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: ${CANONICAL_SKILL}`);
|
|
1759
|
+
return target;
|
|
1760
|
+
}
|
|
1761
|
+
async function copySkill(source, target) {
|
|
1762
|
+
await mkdir2(target, { recursive: true });
|
|
1763
|
+
for (const entry of await skillFiles(source)) {
|
|
1764
|
+
const file = path5.join(target, entry);
|
|
1765
|
+
await mkdir2(path5.dirname(file), { recursive: true });
|
|
1766
|
+
await cp(path5.join(source, entry), file, { force: true });
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
async function linkSkill(root, canonicalSkill) {
|
|
1770
|
+
const target = path5.join(root, ".claude", "skills", "revdeck");
|
|
1771
|
+
const relative = path5.relative(path5.dirname(target), canonicalSkill);
|
|
1772
|
+
await mkdir2(path5.dirname(target), { recursive: true });
|
|
1773
|
+
const existing = await lstat2(target).catch(() => null);
|
|
1774
|
+
if (existing) {
|
|
1775
|
+
if (existing.isSymbolicLink()) {
|
|
1776
|
+
const current2 = await readlink(target).catch(() => "");
|
|
1777
|
+
if (current2 === relative) {
|
|
1778
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: .claude/skills/revdeck -> ${CANONICAL_SKILL}`);
|
|
1779
|
+
return;
|
|
1780
|
+
}
|
|
1781
|
+
await rm(target, { force: true });
|
|
1782
|
+
} else if (existing.isDirectory() && await isManagedSkillDir(target)) {
|
|
1783
|
+
await rm(target, { recursive: true, force: true });
|
|
1784
|
+
} else {
|
|
1785
|
+
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")}`);
|
|
1786
|
+
return;
|
|
1787
|
+
}
|
|
1788
|
+
}
|
|
1789
|
+
if (process.platform === "win32") {
|
|
1790
|
+
await copySkill(canonicalSkill, target);
|
|
1791
|
+
} else {
|
|
1792
|
+
await symlink(relative, target, "dir");
|
|
1793
|
+
}
|
|
1794
|
+
writers.out(`\u2714 ${tr("\u0441\u043A\u0438\u043B\u043B", "skill")}: .claude/skills/revdeck -> ${CANONICAL_SKILL}`);
|
|
1795
|
+
}
|
|
1796
|
+
async function isManagedSkillDir(dir) {
|
|
1797
|
+
const skill = path5.join(dir, "SKILL.md");
|
|
1798
|
+
if (!existsSync5(skill)) return false;
|
|
1799
|
+
const text = await readFile3(skill, "utf8").catch(() => "");
|
|
1800
|
+
return /^name:\s*revdeck\s*$/m.test(text);
|
|
1801
|
+
}
|
|
1802
|
+
async function skillFiles(dir, prefix = "") {
|
|
1803
|
+
const entries = await readdir(path5.join(dir, prefix), { withFileTypes: true });
|
|
1804
|
+
const files = [];
|
|
1805
|
+
for (const entry of entries) {
|
|
1806
|
+
const relative = prefix ? `${prefix}/${entry.name}` : entry.name;
|
|
1807
|
+
if (entry.isDirectory()) files.push(...await skillFiles(dir, relative));
|
|
1808
|
+
else if (entry.isFile()) files.push(relative);
|
|
1809
|
+
}
|
|
1810
|
+
return files.sort();
|
|
1720
1811
|
}
|
|
1721
1812
|
async function mergeMcpJson(file, root, mcp) {
|
|
1722
1813
|
let data = {};
|
|
@@ -1802,12 +1893,407 @@ function describeError(error) {
|
|
|
1802
1893
|
return redactLine(error instanceof Error ? error.message : error);
|
|
1803
1894
|
}
|
|
1804
1895
|
|
|
1896
|
+
// src/cli/eject.ts
|
|
1897
|
+
init_paths();
|
|
1898
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1899
|
+
import { lstat as lstat3, readFile as readFile5, rm as rm2, writeFile as writeFile3 } from "node:fs/promises";
|
|
1900
|
+
import path7 from "node:path";
|
|
1901
|
+
|
|
1902
|
+
// src/core/jsonc.ts
|
|
1903
|
+
function parseJsonc(text) {
|
|
1904
|
+
let withoutComments = "";
|
|
1905
|
+
let inString = false;
|
|
1906
|
+
let escaped = false;
|
|
1907
|
+
let lineComment = false;
|
|
1908
|
+
let blockComment = false;
|
|
1909
|
+
for (let index = 0; index < text.length; index++) {
|
|
1910
|
+
const char = text[index];
|
|
1911
|
+
const next = text[index + 1];
|
|
1912
|
+
if (lineComment) {
|
|
1913
|
+
if (char === "\n") {
|
|
1914
|
+
lineComment = false;
|
|
1915
|
+
withoutComments += char;
|
|
1916
|
+
}
|
|
1917
|
+
continue;
|
|
1918
|
+
}
|
|
1919
|
+
if (blockComment) {
|
|
1920
|
+
if (char === "*" && next === "/") {
|
|
1921
|
+
blockComment = false;
|
|
1922
|
+
index += 1;
|
|
1923
|
+
} else if (char === "\n") {
|
|
1924
|
+
withoutComments += char;
|
|
1925
|
+
}
|
|
1926
|
+
continue;
|
|
1927
|
+
}
|
|
1928
|
+
if (inString) {
|
|
1929
|
+
withoutComments += char;
|
|
1930
|
+
if (escaped) escaped = false;
|
|
1931
|
+
else if (char === "\\") escaped = true;
|
|
1932
|
+
else if (char === '"') inString = false;
|
|
1933
|
+
continue;
|
|
1934
|
+
}
|
|
1935
|
+
if (char === '"') {
|
|
1936
|
+
inString = true;
|
|
1937
|
+
withoutComments += char;
|
|
1938
|
+
continue;
|
|
1939
|
+
}
|
|
1940
|
+
if (char === "/" && next === "/") {
|
|
1941
|
+
lineComment = true;
|
|
1942
|
+
index += 1;
|
|
1943
|
+
continue;
|
|
1944
|
+
}
|
|
1945
|
+
if (char === "/" && next === "*") {
|
|
1946
|
+
blockComment = true;
|
|
1947
|
+
index += 1;
|
|
1948
|
+
continue;
|
|
1949
|
+
}
|
|
1950
|
+
withoutComments += char;
|
|
1951
|
+
}
|
|
1952
|
+
let cleaned = "";
|
|
1953
|
+
inString = false;
|
|
1954
|
+
escaped = false;
|
|
1955
|
+
for (let index = 0; index < withoutComments.length; index++) {
|
|
1956
|
+
const char = withoutComments[index];
|
|
1957
|
+
if (inString) {
|
|
1958
|
+
cleaned += char;
|
|
1959
|
+
if (escaped) escaped = false;
|
|
1960
|
+
else if (char === "\\") escaped = true;
|
|
1961
|
+
else if (char === '"') inString = false;
|
|
1962
|
+
continue;
|
|
1963
|
+
}
|
|
1964
|
+
if (char === '"') {
|
|
1965
|
+
inString = true;
|
|
1966
|
+
cleaned += char;
|
|
1967
|
+
continue;
|
|
1968
|
+
}
|
|
1969
|
+
if (char === ",") {
|
|
1970
|
+
let lookahead = index + 1;
|
|
1971
|
+
while (lookahead < withoutComments.length && /\s/.test(withoutComments[lookahead])) lookahead += 1;
|
|
1972
|
+
const next = withoutComments[lookahead];
|
|
1973
|
+
if (next === "}" || next === "]") continue;
|
|
1974
|
+
}
|
|
1975
|
+
cleaned += char;
|
|
1976
|
+
}
|
|
1977
|
+
return JSON.parse(cleaned);
|
|
1978
|
+
}
|
|
1979
|
+
function isRecord(value) {
|
|
1980
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
1981
|
+
}
|
|
1982
|
+
|
|
1983
|
+
// src/cli/eject.ts
|
|
1984
|
+
import { applyEdits as applyEdits2, modify as modify2 } from "jsonc-parser";
|
|
1985
|
+
|
|
1986
|
+
// src/cli/devcontainer.ts
|
|
1987
|
+
init_paths();
|
|
1988
|
+
import { existsSync as existsSync6 } from "node:fs";
|
|
1989
|
+
import { mkdir as mkdir3, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
1990
|
+
import path6 from "node:path";
|
|
1991
|
+
import { applyEdits, modify, parse as parseJsonc2 } from "jsonc-parser";
|
|
1992
|
+
init_locale();
|
|
1993
|
+
init_format();
|
|
1994
|
+
var CONTAINER_RUNTIME_DIR = "/run/revdeck";
|
|
1995
|
+
var CONTAINER_STATE_DIR = "/var/lib/revdeck";
|
|
1996
|
+
var CONTAINER_UI_HOST = "host.docker.internal";
|
|
1997
|
+
function devcontainerPatch() {
|
|
1998
|
+
const bridge = "${localEnv:HOME}${localEnv:USERPROFILE}/.local/state/revdeck/devcontainer";
|
|
1999
|
+
return {
|
|
2000
|
+
mounts: [
|
|
2001
|
+
{ type: "bind", source: `${bridge}/run`, target: CONTAINER_RUNTIME_DIR },
|
|
2002
|
+
{ type: "bind", source: `${bridge}/state`, target: CONTAINER_STATE_DIR, readonly: true }
|
|
2003
|
+
],
|
|
2004
|
+
containerEnv: {
|
|
2005
|
+
REVDECK_RUNTIME_DIR: CONTAINER_RUNTIME_DIR,
|
|
2006
|
+
REVDECK_STATE_DIR: CONTAINER_STATE_DIR,
|
|
2007
|
+
REVDECK_DEV_CONTAINER: "1",
|
|
2008
|
+
REVDECK_UI_HOST: CONTAINER_UI_HOST
|
|
2009
|
+
},
|
|
2010
|
+
remoteEnv: {
|
|
2011
|
+
REVDECK_RUNTIME_DIR: CONTAINER_RUNTIME_DIR,
|
|
2012
|
+
REVDECK_STATE_DIR: CONTAINER_STATE_DIR,
|
|
2013
|
+
REVDECK_DEV_CONTAINER: "1",
|
|
2014
|
+
REVDECK_UI_HOST: CONTAINER_UI_HOST
|
|
2015
|
+
},
|
|
2016
|
+
runArgs: [`--add-host=${CONTAINER_UI_HOST}:host-gateway`]
|
|
2017
|
+
};
|
|
2018
|
+
}
|
|
2019
|
+
function mergeDevcontainerConfig(existing, patch) {
|
|
2020
|
+
const result = { ...existing };
|
|
2021
|
+
const currentMounts = Array.isArray(existing.mounts) ? [...existing.mounts] : [];
|
|
2022
|
+
for (const mount of patch.mounts) {
|
|
2023
|
+
const index = currentMounts.findIndex((item) => isRecord(item) && item.target === mount.target);
|
|
2024
|
+
if (index >= 0) currentMounts[index] = mount;
|
|
2025
|
+
else currentMounts.push(mount);
|
|
2026
|
+
}
|
|
2027
|
+
result.mounts = currentMounts;
|
|
2028
|
+
const containerEnv = isRecord(existing.containerEnv) ? { ...existing.containerEnv } : {};
|
|
2029
|
+
Object.assign(containerEnv, patch.containerEnv);
|
|
2030
|
+
result.containerEnv = containerEnv;
|
|
2031
|
+
const remoteEnv = isRecord(existing.remoteEnv) ? { ...existing.remoteEnv } : {};
|
|
2032
|
+
Object.assign(remoteEnv, patch.remoteEnv);
|
|
2033
|
+
result.remoteEnv = remoteEnv;
|
|
2034
|
+
const runArgs = Array.isArray(existing.runArgs) ? existing.runArgs.filter((item) => typeof item === "string") : [];
|
|
2035
|
+
for (const arg of patch.runArgs) if (!runArgs.includes(arg)) runArgs.push(arg);
|
|
2036
|
+
result.runArgs = runArgs;
|
|
2037
|
+
return result;
|
|
2038
|
+
}
|
|
2039
|
+
function applyDevcontainerPatch(text, patch) {
|
|
2040
|
+
let current2 = text;
|
|
2041
|
+
const parsed = parseJsonc2(current2);
|
|
2042
|
+
if (!isRecord(parsed)) throw new Error("devcontainer.json: expected an object");
|
|
2043
|
+
const formatting = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
2044
|
+
const mounts = Array.isArray(parsed.mounts) ? [...parsed.mounts] : [];
|
|
2045
|
+
for (const mount of patch.mounts) {
|
|
2046
|
+
const index = mounts.findIndex((item) => isRecord(item) && item.target === mount.target);
|
|
2047
|
+
const path10 = ["mounts", index >= 0 ? index : mounts.length];
|
|
2048
|
+
current2 = applyEdits(current2, modify(current2, path10, mount, formatting));
|
|
2049
|
+
if (index >= 0) mounts[index] = mount;
|
|
2050
|
+
else mounts.push(mount);
|
|
2051
|
+
}
|
|
2052
|
+
for (const [key, env] of [
|
|
2053
|
+
["containerEnv", patch.containerEnv],
|
|
2054
|
+
["remoteEnv", patch.remoteEnv]
|
|
2055
|
+
]) {
|
|
2056
|
+
for (const [name, value] of Object.entries(env)) {
|
|
2057
|
+
current2 = applyEdits(current2, modify(current2, [key, name], value, formatting));
|
|
2058
|
+
}
|
|
2059
|
+
}
|
|
2060
|
+
const runArgs = Array.isArray(parsed.runArgs) ? parsed.runArgs.filter((item) => typeof item === "string") : [];
|
|
2061
|
+
for (const arg of patch.runArgs) {
|
|
2062
|
+
if (runArgs.includes(arg)) continue;
|
|
2063
|
+
current2 = applyEdits(current2, modify(current2, ["runArgs", runArgs.length], arg, formatting));
|
|
2064
|
+
runArgs.push(arg);
|
|
2065
|
+
}
|
|
2066
|
+
return current2.endsWith("\n") ? current2 : `${current2}
|
|
2067
|
+
`;
|
|
2068
|
+
}
|
|
2069
|
+
async function runDevcontainer(rootInput, options) {
|
|
2070
|
+
const root = findRepoRoot(rootInput);
|
|
2071
|
+
const patch = devcontainerPatch();
|
|
2072
|
+
await ensureDir(devcontainerRunDir());
|
|
2073
|
+
await ensureDir(devcontainerStateDir());
|
|
2074
|
+
const dir = path6.join(root, ".devcontainer");
|
|
2075
|
+
const file = path6.join(dir, "devcontainer.json");
|
|
2076
|
+
writers.out(tr("\u041C\u043E\u0441\u0442 revdeck \u0434\u043B\u044F \u043A\u043E\u043D\u0442\u0435\u0439\u043D\u0435\u0440\u0430:", "revdeck bridge for the container:"));
|
|
2077
|
+
writers.out(` ${tr("\u0441\u043E\u043A\u0435\u0442", "socket")}: ${devcontainerRunDir()} -> ${CONTAINER_RUNTIME_DIR}`);
|
|
2078
|
+
writers.out(` ${tr("\u0441\u043E\u0441\u0442\u043E\u044F\u043D\u0438\u0435", "state")}: ${devcontainerStateDir()} -> ${CONTAINER_STATE_DIR} (${tr("\u0442\u043E\u043B\u044C\u043A\u043E \u0447\u0442\u0435\u043D\u0438\u0435", "read-only")})`);
|
|
2079
|
+
if (!existsSync6(devcontainerSocketPath())) {
|
|
2080
|
+
writers.err(
|
|
2081
|
+
tr(
|
|
2082
|
+
"! \u0441\u043E\u043A\u0435\u0442 \u043C\u043E\u0441\u0442\u0430 \u0435\u0449\u0451 \u043D\u0435 \u0441\u043E\u0437\u0434\u0430\u043D: \u043D\u0430 \u0445\u043E\u0441\u0442\u0435 \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 revdeck daemon restart",
|
|
2083
|
+
"! the bridge socket does not exist yet: run revdeck daemon restart on the host"
|
|
2084
|
+
)
|
|
2085
|
+
);
|
|
2086
|
+
}
|
|
2087
|
+
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."));
|
|
2088
|
+
if (!options.write) {
|
|
2089
|
+
writers.out("");
|
|
2090
|
+
writers.out(tr("\u0424\u0440\u0430\u0433\u043C\u0435\u043D\u0442 \u0434\u043B\u044F .devcontainer/devcontainer.json:", "Snippet for .devcontainer/devcontainer.json:"));
|
|
2091
|
+
writers.out(JSON.stringify(patch, null, 2));
|
|
2092
|
+
writers.out("");
|
|
2093
|
+
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"));
|
|
2094
|
+
return;
|
|
2095
|
+
}
|
|
2096
|
+
await mkdir3(dir, { recursive: true });
|
|
2097
|
+
const existed = existsSync6(file);
|
|
2098
|
+
let output;
|
|
2099
|
+
if (existed) {
|
|
2100
|
+
const text = await readFile4(file, "utf8");
|
|
2101
|
+
try {
|
|
2102
|
+
output = applyDevcontainerPatch(text, patch);
|
|
2103
|
+
} catch (error) {
|
|
2104
|
+
throw new Error(
|
|
2105
|
+
tr(
|
|
2106
|
+
`${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`,
|
|
2107
|
+
`${path6.relative(root, file)}: could not parse JSONC (${error.message}); add the mounts manually`
|
|
2108
|
+
)
|
|
2109
|
+
);
|
|
2110
|
+
}
|
|
2111
|
+
} else {
|
|
2112
|
+
const existing = { image: "mcr.microsoft.com/devcontainers/base:bookworm" };
|
|
2113
|
+
output = `${JSON.stringify(mergeDevcontainerConfig(existing, patch), null, 2)}
|
|
2114
|
+
`;
|
|
2115
|
+
}
|
|
2116
|
+
await writeFile2(file, output);
|
|
2117
|
+
writers.out(`\u2714 ${path6.relative(root, file)}`);
|
|
2118
|
+
if (existed) writers.out(tr(" \u043A\u043E\u043C\u043C\u0435\u043D\u0442\u0430\u0440\u0438\u0438 \u0441\u043E\u0445\u0440\u0430\u043D\u0435\u043D\u044B.", " comments were preserved."));
|
|
2119
|
+
writers.out("");
|
|
2120
|
+
writers.out(tr("\u0414\u0430\u043B\u044C\u0448\u0435:", "Next:"));
|
|
2121
|
+
writers.out(tr(" 1. \u041D\u0430 \u0445\u043E\u0441\u0442\u0435: revdeck daemon restart \u2014 \u043E\u043D \u0441\u043E\u0437\u0434\u0430\u0441\u0442 \u0441\u043E\u043A\u0435\u0442 \u043C\u043E\u0441\u0442\u0430.", " 1. On the host: revdeck daemon restart \u2014 it creates the bridge socket."));
|
|
2122
|
+
writers.out(tr(" 2. \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.", " 2. Rebuild/reopen the devcontainer."));
|
|
2123
|
+
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."));
|
|
2124
|
+
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."));
|
|
2125
|
+
}
|
|
2126
|
+
|
|
2127
|
+
// src/cli/eject.ts
|
|
2128
|
+
init_locale();
|
|
2129
|
+
init_format();
|
|
2130
|
+
var CONFIG_FILES2 = [".revdeck.yml", ".revdeck.yaml"];
|
|
2131
|
+
var COMMAND_FILES = [
|
|
2132
|
+
".claude/commands/revdeck.md",
|
|
2133
|
+
".cursor/commands/revdeck.md",
|
|
2134
|
+
".codex/prompts/revdeck.md",
|
|
2135
|
+
".gemini/commands/revdeck.toml",
|
|
2136
|
+
".opencode/command/revdeck.md",
|
|
2137
|
+
".github/prompts/revdeck.prompt.md"
|
|
2138
|
+
];
|
|
2139
|
+
var SKILL_DIRS = [".agents/skills/revdeck", ".claude/skills/revdeck"];
|
|
2140
|
+
var MCP_FILES = [".mcp.json", ".cursor/mcp.json"];
|
|
2141
|
+
async function runEject(rootInput, options) {
|
|
2142
|
+
const root = findRepoRoot(rootInput);
|
|
2143
|
+
const changes = [];
|
|
2144
|
+
const warnings = [];
|
|
2145
|
+
if (!options.keepConfig) {
|
|
2146
|
+
for (const name of CONFIG_FILES2) {
|
|
2147
|
+
const file = path7.join(root, name);
|
|
2148
|
+
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 }) });
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
for (const name of SKILL_DIRS) {
|
|
2152
|
+
const target = path7.join(root, name);
|
|
2153
|
+
if (!existsSync7(target)) continue;
|
|
2154
|
+
const managed = await isManagedSkill(target);
|
|
2155
|
+
if (!managed) {
|
|
2156
|
+
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")}`);
|
|
2157
|
+
continue;
|
|
2158
|
+
}
|
|
2159
|
+
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 }) });
|
|
2160
|
+
}
|
|
2161
|
+
for (const name of COMMAND_FILES) {
|
|
2162
|
+
const file = path7.join(root, name);
|
|
2163
|
+
if (!existsSync7(file)) continue;
|
|
2164
|
+
const text = await readFile5(file, "utf8").catch(() => "");
|
|
2165
|
+
if (!/revdeck/i.test(text)) {
|
|
2166
|
+
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")}`);
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
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 }) });
|
|
2170
|
+
}
|
|
2171
|
+
for (const name of MCP_FILES) {
|
|
2172
|
+
const file = path7.join(root, name);
|
|
2173
|
+
if (!existsSync7(file)) continue;
|
|
2174
|
+
const text = await readFile5(file, "utf8").catch(() => "");
|
|
2175
|
+
let data;
|
|
2176
|
+
try {
|
|
2177
|
+
data = JSON.parse(text);
|
|
2178
|
+
} catch {
|
|
2179
|
+
warnings.push(`${name}: ${tr("\u043D\u0435 JSON \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E", "not JSON \u2014 leaving it untouched")}`);
|
|
2180
|
+
continue;
|
|
2181
|
+
}
|
|
2182
|
+
const servers = data.mcpServers;
|
|
2183
|
+
if (!servers || !("revdeck" in servers)) continue;
|
|
2184
|
+
changes.push({
|
|
2185
|
+
path: name,
|
|
2186
|
+
action: tr("\u0443\u0431\u0440\u0430\u0442\u044C MCP-\u0441\u0435\u0440\u0432\u0435\u0440 revdeck", "remove the revdeck MCP server"),
|
|
2187
|
+
remove: async () => {
|
|
2188
|
+
delete servers.revdeck;
|
|
2189
|
+
await writeFile3(file, `${JSON.stringify(data, null, 2)}
|
|
2190
|
+
`);
|
|
2191
|
+
}
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
const devcontainer = path7.join(root, ".devcontainer", "devcontainer.json");
|
|
2195
|
+
if (existsSync7(devcontainer)) {
|
|
2196
|
+
const text = await readFile5(devcontainer, "utf8");
|
|
2197
|
+
try {
|
|
2198
|
+
const parsed = parseJsonc(text);
|
|
2199
|
+
if (isRecord(parsed)) {
|
|
2200
|
+
const formatting = { formattingOptions: { insertSpaces: true, tabSize: 2 } };
|
|
2201
|
+
let current2 = text;
|
|
2202
|
+
let changed = false;
|
|
2203
|
+
if (Array.isArray(parsed.mounts)) {
|
|
2204
|
+
const mounts = parsed.mounts.filter((item) => !(isRecord(item) && (item.target === CONTAINER_RUNTIME_DIR || item.target === CONTAINER_STATE_DIR)));
|
|
2205
|
+
if (mounts.length !== parsed.mounts.length) {
|
|
2206
|
+
current2 = applyEdits2(current2, modify2(current2, ["mounts"], mounts, formatting));
|
|
2207
|
+
changed = true;
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
for (const key of ["containerEnv", "remoteEnv"]) {
|
|
2211
|
+
if (!isRecord(parsed[key])) continue;
|
|
2212
|
+
const env = { ...parsed[key] };
|
|
2213
|
+
for (const name of ["REVDECK_RUNTIME_DIR", "REVDECK_STATE_DIR", "REVDECK_DEV_CONTAINER", "REVDECK_UI_HOST"]) delete env[name];
|
|
2214
|
+
if (Object.keys(env).length !== Object.keys(parsed[key]).length) {
|
|
2215
|
+
current2 = applyEdits2(current2, modify2(current2, [key], env, formatting));
|
|
2216
|
+
changed = true;
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
if (Array.isArray(parsed.runArgs)) {
|
|
2220
|
+
const args = parsed.runArgs.filter((item) => item !== "--add-host=host.docker.internal:host-gateway");
|
|
2221
|
+
if (args.length !== parsed.runArgs.length) {
|
|
2222
|
+
current2 = applyEdits2(current2, modify2(current2, ["runArgs"], args, formatting));
|
|
2223
|
+
changed = true;
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
2226
|
+
if (changed) {
|
|
2227
|
+
const output = current2.endsWith("\n") ? current2 : `${current2}
|
|
2228
|
+
`;
|
|
2229
|
+
changes.push({
|
|
2230
|
+
path: ".devcontainer/devcontainer.json",
|
|
2231
|
+
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"),
|
|
2232
|
+
remove: () => writeFile3(devcontainer, output)
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
}
|
|
2236
|
+
} catch {
|
|
2237
|
+
warnings.push(`.devcontainer/devcontainer.json: ${tr("\u043D\u0435 JSONC \u2014 \u043D\u0435 \u0442\u0440\u043E\u0433\u0430\u044E", "not JSONC \u2014 leaving it untouched")}`);
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
const cache = path7.join(root, ".revdeck");
|
|
2241
|
+
if (existsSync7(cache)) {
|
|
2242
|
+
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 }) });
|
|
2243
|
+
}
|
|
2244
|
+
const gitignore = path7.join(root, ".gitignore");
|
|
2245
|
+
if (existsSync7(gitignore)) {
|
|
2246
|
+
const text = await readFile5(gitignore, "utf8");
|
|
2247
|
+
if (/(^|\n)\.revdeck\/?\s*(\n|$)/.test(text)) {
|
|
2248
|
+
changes.push({
|
|
2249
|
+
path: ".gitignore",
|
|
2250
|
+
action: tr("\u0443\u0431\u0440\u0430\u0442\u044C \u0441\u0442\u0440\u043E\u043A\u0443 .revdeck/", "remove the .revdeck/ line"),
|
|
2251
|
+
remove: async () => {
|
|
2252
|
+
const next = text.split("\n").filter((line) => line.trim() !== ".revdeck/" && line.trim() !== ".revdeck").join("\n");
|
|
2253
|
+
await writeFile3(gitignore, next);
|
|
2254
|
+
}
|
|
2255
|
+
});
|
|
2256
|
+
}
|
|
2257
|
+
}
|
|
2258
|
+
if (!changes.length) {
|
|
2259
|
+
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"));
|
|
2260
|
+
for (const warning of warnings) writers.err(`! ${warning}`);
|
|
2261
|
+
return;
|
|
2262
|
+
}
|
|
2263
|
+
writers.out(tr("\u0411\u0443\u0434\u0435\u0442 \u0438\u0437\u043C\u0435\u043D\u0435\u043D\u043E:", "Will change:"));
|
|
2264
|
+
for (const change of changes) writers.out(` ${change.action}: ${change.path}`);
|
|
2265
|
+
for (const warning of warnings) writers.err(`! ${warning}`);
|
|
2266
|
+
if (!options.yes) {
|
|
2267
|
+
writers.out("");
|
|
2268
|
+
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."));
|
|
2269
|
+
return;
|
|
2270
|
+
}
|
|
2271
|
+
for (const change of changes) {
|
|
2272
|
+
await change.remove();
|
|
2273
|
+
writers.out(`\u2714 ${change.action}: ${change.path}`);
|
|
2274
|
+
}
|
|
2275
|
+
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."));
|
|
2276
|
+
}
|
|
2277
|
+
async function isManagedSkill(target) {
|
|
2278
|
+
try {
|
|
2279
|
+
const info = await lstat3(target);
|
|
2280
|
+
if (info.isSymbolicLink()) return true;
|
|
2281
|
+
if (!info.isDirectory()) return false;
|
|
2282
|
+
const skill = path7.join(target, "SKILL.md");
|
|
2283
|
+
if (!existsSync7(skill)) return false;
|
|
2284
|
+
const text = await readFile5(skill, "utf8");
|
|
2285
|
+
return /^name:\s*revdeck\s*$/m.test(text) || /revdeck/i.test(text.slice(0, 2e3));
|
|
2286
|
+
} catch {
|
|
2287
|
+
return false;
|
|
2288
|
+
}
|
|
2289
|
+
}
|
|
2290
|
+
|
|
1805
2291
|
// src/cli/export.ts
|
|
1806
2292
|
init_client();
|
|
1807
2293
|
init_format();
|
|
1808
2294
|
init_locale();
|
|
1809
|
-
import { mkdir as
|
|
1810
|
-
import
|
|
2295
|
+
import { mkdir as mkdir4, readFile as readFile6, writeFile as writeFile4 } from "node:fs/promises";
|
|
2296
|
+
import path8 from "node:path";
|
|
1811
2297
|
async function reviewFor(client2, root, arg) {
|
|
1812
2298
|
if (arg && /^(gitlab|github):/.test(arg)) {
|
|
1813
2299
|
const { review } = await client2.request("reviews.get", { review: arg });
|
|
@@ -1863,13 +2349,13 @@ async function exportReview(root, arg, dir) {
|
|
|
1863
2349
|
const { client: client2 } = await ensureDaemon();
|
|
1864
2350
|
try {
|
|
1865
2351
|
const review = await reviewFor(client2, root, arg);
|
|
1866
|
-
const target =
|
|
1867
|
-
await
|
|
2352
|
+
const target = path8.resolve(root, dir);
|
|
2353
|
+
await mkdir4(target, { recursive: true });
|
|
1868
2354
|
const base = `mr-${review.info.ref.number}`;
|
|
1869
2355
|
const files = [];
|
|
1870
2356
|
const write = async (name, data) => {
|
|
1871
|
-
const file =
|
|
1872
|
-
await
|
|
2357
|
+
const file = path8.join(target, name);
|
|
2358
|
+
await writeFile4(file, `${JSON.stringify(data, null, 2)}
|
|
1873
2359
|
`, { mode: 420 });
|
|
1874
2360
|
files.push(name);
|
|
1875
2361
|
};
|
|
@@ -1934,9 +2420,9 @@ async function exportReview(root, arg, dir) {
|
|
|
1934
2420
|
}
|
|
1935
2421
|
}
|
|
1936
2422
|
await write(`${base}-published.json`, published);
|
|
1937
|
-
await
|
|
2423
|
+
await writeFile4(path8.join(target, `${base}.diff`), await serializeDiff(client2, review), { mode: 420 });
|
|
1938
2424
|
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")} ${
|
|
2425
|
+
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
2426
|
} finally {
|
|
1941
2427
|
client2.close();
|
|
1942
2428
|
}
|
|
@@ -1945,11 +2431,11 @@ async function importReview(root, arg, dir) {
|
|
|
1945
2431
|
const { client: client2 } = await ensureDaemon();
|
|
1946
2432
|
try {
|
|
1947
2433
|
const review = await reviewFor(client2, root, arg);
|
|
1948
|
-
const target =
|
|
2434
|
+
const target = path8.resolve(root, dir);
|
|
1949
2435
|
const base = `mr-${review.info.ref.number}`;
|
|
1950
2436
|
const read = async (name) => {
|
|
1951
2437
|
try {
|
|
1952
|
-
return JSON.parse(await
|
|
2438
|
+
return JSON.parse(await readFile6(path8.join(target, name), "utf8"));
|
|
1953
2439
|
} catch {
|
|
1954
2440
|
return null;
|
|
1955
2441
|
}
|
|
@@ -1997,6 +2483,53 @@ async function importReview(root, arg, dir) {
|
|
|
1997
2483
|
}
|
|
1998
2484
|
}
|
|
1999
2485
|
|
|
2486
|
+
// src/core/attachments.ts
|
|
2487
|
+
var MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
|
|
2488
|
+
var MIME_BY_EXTENSION = {
|
|
2489
|
+
".png": "image/png",
|
|
2490
|
+
".jpg": "image/jpeg",
|
|
2491
|
+
".jpeg": "image/jpeg",
|
|
2492
|
+
".gif": "image/gif",
|
|
2493
|
+
".webp": "image/webp",
|
|
2494
|
+
".avif": "image/avif",
|
|
2495
|
+
".mp4": "video/mp4",
|
|
2496
|
+
".m4v": "video/mp4",
|
|
2497
|
+
".webm": "video/webm",
|
|
2498
|
+
".ogv": "video/ogg",
|
|
2499
|
+
".ogg": "video/ogg",
|
|
2500
|
+
".mov": "video/quicktime"
|
|
2501
|
+
};
|
|
2502
|
+
function extensionOf(name) {
|
|
2503
|
+
const index = name.lastIndexOf(".");
|
|
2504
|
+
return index >= 0 ? name.slice(index).toLowerCase() : "";
|
|
2505
|
+
}
|
|
2506
|
+
function attachmentSpec(name, contentType = "") {
|
|
2507
|
+
if (/[/\\]/.test(name)) return null;
|
|
2508
|
+
const base = name.replace(/[\u0000-\u001f\u007f]/g, "").trim();
|
|
2509
|
+
if (!base || base.length > 240) return null;
|
|
2510
|
+
const extension = extensionOf(base);
|
|
2511
|
+
const expected = MIME_BY_EXTENSION[extension];
|
|
2512
|
+
if (!expected) return null;
|
|
2513
|
+
const provided = normalizeContentType(String(contentType));
|
|
2514
|
+
if (provided && provided !== expected) return null;
|
|
2515
|
+
return {
|
|
2516
|
+
name: base,
|
|
2517
|
+
contentType: expected,
|
|
2518
|
+
extension,
|
|
2519
|
+
kind: expected.startsWith("image/") ? "image" : "video"
|
|
2520
|
+
};
|
|
2521
|
+
}
|
|
2522
|
+
var CONTENT_TYPE_ALIASES = {
|
|
2523
|
+
"image/jpg": "image/jpeg",
|
|
2524
|
+
"image/pjpeg": "image/jpeg",
|
|
2525
|
+
"video/x-m4v": "video/mp4",
|
|
2526
|
+
"video/x-ogg": "video/ogg"
|
|
2527
|
+
};
|
|
2528
|
+
function normalizeContentType(value) {
|
|
2529
|
+
const type = value.split(";")[0].trim().toLowerCase();
|
|
2530
|
+
return CONTENT_TYPE_ALIASES[type] ?? type;
|
|
2531
|
+
}
|
|
2532
|
+
|
|
2000
2533
|
// src/cli/main.ts
|
|
2001
2534
|
init_context();
|
|
2002
2535
|
var OPTIONS = {
|
|
@@ -2027,10 +2560,13 @@ var OPTIONS = {
|
|
|
2027
2560
|
force: { type: "boolean", default: false },
|
|
2028
2561
|
adapters: { type: "string" },
|
|
2029
2562
|
"no-service": { type: "boolean", default: false },
|
|
2563
|
+
"keep-config": { type: "boolean", default: false },
|
|
2564
|
+
write: { type: "boolean", default: false },
|
|
2030
2565
|
explain: { type: "string" },
|
|
2031
2566
|
limit: { type: "string" },
|
|
2032
2567
|
decision: { type: "string" },
|
|
2033
2568
|
note: { type: "string" },
|
|
2569
|
+
emoji: { type: "string" },
|
|
2034
2570
|
text: { type: "string" },
|
|
2035
2571
|
line: { type: "string" },
|
|
2036
2572
|
id: { type: "string" },
|
|
@@ -2049,6 +2585,8 @@ function parse(argv) {
|
|
|
2049
2585
|
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
2586
|
|
|
2051
2587
|
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
|
|
2588
|
+
revdeck eject \u0443\u0434\u0430\u043B\u0438\u0442\u044C revdeck \u0438\u0437 \u043F\u0440\u043E\u0435\u043A\u0442\u0430 (--yes; --keep-config)
|
|
2589
|
+
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
2590
|
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
2591
|
revdeck daemon <start|stop|status|logs|restart>
|
|
2054
2592
|
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 +2599,8 @@ var HELP_RU = `revdeck ${VERSION} \u2014 \u0440\u0435\u0432\u044C\u044E GitLab/G
|
|
|
2061
2599
|
revdeck summary <N> [--file <json>] \u0432\u044B\u0436\u0438\u043C\u043A\u0430 MR
|
|
2062
2600
|
revdeck reply <N> --thread <id> (--body <\u0442\u0435\u043A\u0441\u0442> | --body-file <\u0444\u0430\u0439\u043B> | -) [--resolve] [--publish --yes]
|
|
2063
2601
|
revdeck notes <N> <list|add|update|delete|reply>
|
|
2602
|
+
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
|
|
2603
|
+
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
2604
|
revdeck publish <N> [--kind reply|finding|note] [--ids a,b] [--yes]
|
|
2065
2605
|
revdeck rules [<N>] [--explain <\u043F\u0443\u0442\u044C>]
|
|
2066
2606
|
revdeck checks <N> [--run --yes]
|
|
@@ -2074,6 +2614,8 @@ var HELP_RU = `revdeck ${VERSION} \u2014 \u0440\u0435\u0432\u044C\u044E GitLab/G
|
|
|
2074
2614
|
var HELP_EN = `revdeck ${VERSION} \u2014 code review for GitLab/GitHub, for humans and agents
|
|
2075
2615
|
|
|
2076
2616
|
revdeck init set up a project: .revdeck.yml, adapters, MCP
|
|
2617
|
+
revdeck eject remove revdeck from the project (--yes; --keep-config)
|
|
2618
|
+
revdeck devcontainer connect a devcontainer to the host daemon (--write)
|
|
2077
2619
|
revdeck doctor check the environment and access
|
|
2078
2620
|
revdeck daemon <start|stop|status|logs|restart>
|
|
2079
2621
|
revdeck list open reviews (local cache), --remote \u2014 from the platform
|
|
@@ -2086,6 +2628,8 @@ var HELP_EN = `revdeck ${VERSION} \u2014 code review for GitLab/GitHub, for huma
|
|
|
2086
2628
|
revdeck summary <N> [--file <json>] review summary
|
|
2087
2629
|
revdeck reply <N> --thread <id> (--body <text> | --body-file <file> | -) [--resolve] [--publish --yes]
|
|
2088
2630
|
revdeck notes <N> <list|add|update|delete|reply>
|
|
2631
|
+
revdeck react <N> --note <id> --emoji <name> add or remove a reaction
|
|
2632
|
+
revdeck attach <N> --file <path> upload an image/video to the platform
|
|
2089
2633
|
revdeck publish <N> [--kind reply|finding|note] [--ids a,b] [--yes]
|
|
2090
2634
|
revdeck rules [<N>] [--explain <path>]
|
|
2091
2635
|
revdeck checks <N> [--run --yes]
|
|
@@ -2120,10 +2664,14 @@ async function main2() {
|
|
|
2120
2664
|
switch (command) {
|
|
2121
2665
|
case "init":
|
|
2122
2666
|
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" });
|
|
2667
|
+
case "eject":
|
|
2668
|
+
return runEject(root, { yes: values.yes === true && values["dry-run"] !== true, keepConfig: values["keep-config"] === true });
|
|
2669
|
+
case "devcontainer":
|
|
2670
|
+
return runDevcontainer(root, { write: values.write === true });
|
|
2123
2671
|
case "doctor":
|
|
2124
2672
|
return doctor(root);
|
|
2125
2673
|
case "daemon":
|
|
2126
|
-
return daemon(positionals[0] ?? "status");
|
|
2674
|
+
return daemon(positionals[0] ?? "status", values);
|
|
2127
2675
|
case "list":
|
|
2128
2676
|
return list(root, values);
|
|
2129
2677
|
case "open":
|
|
@@ -2147,6 +2695,10 @@ async function main2() {
|
|
|
2147
2695
|
return reply(positionals[0], values);
|
|
2148
2696
|
case "notes":
|
|
2149
2697
|
return notes(positionals[0], positionals[1] ?? "list", values);
|
|
2698
|
+
case "react":
|
|
2699
|
+
return react(positionals[0], values);
|
|
2700
|
+
case "attach":
|
|
2701
|
+
return attach(positionals[0], values);
|
|
2150
2702
|
case "publish":
|
|
2151
2703
|
return publish(positionals[0], values);
|
|
2152
2704
|
case "rules":
|
|
@@ -2259,13 +2811,14 @@ async function ui(arg, values) {
|
|
|
2259
2811
|
});
|
|
2260
2812
|
const info = await daemonInfo();
|
|
2261
2813
|
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
|
-
|
|
2814
|
+
const url = `${uiBaseUrl(info)}/?t=${info.token}`;
|
|
2815
|
+
writers.out(url);
|
|
2816
|
+
if (values.open) openBrowser(url);
|
|
2264
2817
|
}
|
|
2265
2818
|
function printUi(reviewId, values) {
|
|
2266
2819
|
const info = daemonInfoSync();
|
|
2267
2820
|
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
|
|
2821
|
+
const url = `${uiBaseUrl(info)}/?t=${info.token}${reviewId ? `&review=${encodeURIComponent(reviewId)}` : ""}`;
|
|
2269
2822
|
writers.out(`UI: ${url}`);
|
|
2270
2823
|
if (values.open) openBrowser(url);
|
|
2271
2824
|
}
|
|
@@ -2283,14 +2836,25 @@ function openBrowser(url) {
|
|
|
2283
2836
|
spawn2(command, args, { detached: true, stdio: "ignore" }).unref();
|
|
2284
2837
|
});
|
|
2285
2838
|
}
|
|
2286
|
-
async function daemon(action) {
|
|
2839
|
+
async function daemon(action, values = {}) {
|
|
2840
|
+
if (isDevContainer() && ["start", "stop", "restart", "install", "uninstall"].includes(action)) {
|
|
2841
|
+
fail(
|
|
2842
|
+
tr(
|
|
2843
|
+
"\u0432 devcontainer \u0434\u0435\u043C\u043E\u043D\u043E\u043C \u0443\u043F\u0440\u0430\u0432\u043B\u044F\u0435\u0442 \u0445\u043E\u0441\u0442: \u0432\u044B\u043F\u043E\u043B\u043D\u0438\u0442\u0435 \u043A\u043E\u043C\u0430\u043D\u0434\u0443 \u043D\u0430 \u0445\u043E\u0441\u0442\u0435",
|
|
2844
|
+
"the daemon is managed by the host in a devcontainer: run this command on the host"
|
|
2845
|
+
),
|
|
2846
|
+
tr("revdeck daemon restart \u043D\u0430 \u0445\u043E\u0441\u0442\u0435", "revdeck daemon restart on the host")
|
|
2847
|
+
);
|
|
2848
|
+
}
|
|
2287
2849
|
if (action === "start") {
|
|
2288
|
-
const { client: client2, started } = await ensureDaemon(
|
|
2850
|
+
const { client: client2, started } = await ensureDaemon({
|
|
2851
|
+
...values.host ? { daemonArgs: ["--host", String(values.host)] } : {}
|
|
2852
|
+
});
|
|
2289
2853
|
const ping = await client2.request("ping");
|
|
2290
2854
|
client2.close();
|
|
2291
2855
|
const info = await daemonInfo();
|
|
2292
2856
|
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
|
|
2857
|
+
if (info) writers.out(`${uiBaseUrl(info)}/?t=${info.token}`);
|
|
2294
2858
|
return;
|
|
2295
2859
|
}
|
|
2296
2860
|
if (action === "status") {
|
|
@@ -2330,11 +2894,11 @@ async function daemon(action) {
|
|
|
2330
2894
|
}
|
|
2331
2895
|
if (action === "restart") {
|
|
2332
2896
|
await daemon("stop");
|
|
2333
|
-
return daemon("start");
|
|
2897
|
+
return daemon("start", values);
|
|
2334
2898
|
}
|
|
2335
2899
|
if (action === "logs") {
|
|
2336
2900
|
try {
|
|
2337
|
-
const text = await
|
|
2901
|
+
const text = await readFile7(logPath(), "utf8");
|
|
2338
2902
|
writers.out(text.split("\n").slice(-80).join("\n"));
|
|
2339
2903
|
} catch {
|
|
2340
2904
|
writers.out(tr("\u043B\u043E\u0433 \u043F\u0443\u0441\u0442", "the log is empty"));
|
|
@@ -2399,7 +2963,7 @@ async function findings(arg, action, values) {
|
|
|
2399
2963
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
2400
2964
|
if (action === "set") {
|
|
2401
2965
|
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
|
|
2966
|
+
const raw = JSON.parse(await readFile7(String(values.file), "utf8"));
|
|
2403
2967
|
const list2 = Array.isArray(raw) ? raw : raw.findings;
|
|
2404
2968
|
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
2969
|
const { findings: saved } = await client2.request("findings.put", { review: review.id, findings: list2 });
|
|
@@ -2426,7 +2990,7 @@ async function findings(arg, action, values) {
|
|
|
2426
2990
|
}
|
|
2427
2991
|
async function summary(arg, values) {
|
|
2428
2992
|
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
|
|
2993
|
+
const raw = values.file ? JSON.parse(await readFile7(String(values.file), "utf8")) : JSON.parse(await readStdin());
|
|
2430
2994
|
await withClient(async ({ client: client2 }) => {
|
|
2431
2995
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
2432
2996
|
const result = await client2.request("summary.put", { review: review.id, summary: raw });
|
|
@@ -2498,6 +3062,45 @@ async function notes(arg, action, values) {
|
|
|
2498
3062
|
}
|
|
2499
3063
|
});
|
|
2500
3064
|
}
|
|
3065
|
+
async function react(arg, values) {
|
|
3066
|
+
if (!values.note || !values.emoji) fail(tr("\u043D\u0443\u0436\u043D\u044B --note <id> \u0438 --emoji <name>", "--note <id> and --emoji <name> are required"));
|
|
3067
|
+
await withClient(async ({ client: client2 }) => {
|
|
3068
|
+
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
3069
|
+
const result = await client2.request("reactions.toggle", {
|
|
3070
|
+
review: review.id,
|
|
3071
|
+
noteId: String(values.note),
|
|
3072
|
+
emoji: String(values.emoji)
|
|
3073
|
+
});
|
|
3074
|
+
if (values.json) return printJson(result);
|
|
3075
|
+
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")}`}`);
|
|
3076
|
+
for (const reaction of result.reactions) writers.out(` ${reaction.name}${reaction.mine ? " \xB7" : ""} ${reaction.count}`);
|
|
3077
|
+
});
|
|
3078
|
+
}
|
|
3079
|
+
async function attach(arg, values) {
|
|
3080
|
+
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"));
|
|
3081
|
+
const name = path9.basename(String(values.file));
|
|
3082
|
+
const spec = attachmentSpec(name);
|
|
3083
|
+
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"));
|
|
3084
|
+
const file = String(values.file);
|
|
3085
|
+
const info = await stat(file).catch(() => null);
|
|
3086
|
+
if (!info?.isFile()) fail(tr(`\u043D\u0435\u0442 \u0444\u0430\u0439\u043B\u0430 ${file}`, `file ${file} does not exist`));
|
|
3087
|
+
if (info.size > MAX_ATTACHMENT_BYTES) {
|
|
3088
|
+
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`));
|
|
3089
|
+
}
|
|
3090
|
+
const data = await readFile7(file);
|
|
3091
|
+
await withClient(async ({ client: client2 }) => {
|
|
3092
|
+
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
3093
|
+
const result = await client2.request("attachments.upload", {
|
|
3094
|
+
review: review.id,
|
|
3095
|
+
name,
|
|
3096
|
+
contentType: spec.contentType,
|
|
3097
|
+
data: data.toString("base64")
|
|
3098
|
+
});
|
|
3099
|
+
if (values.json) return printJson(result.attachment);
|
|
3100
|
+
writers.out(`${tr("\u0437\u0430\u0433\u0440\u0443\u0436\u0435\u043D\u043E", "uploaded")}: ${result.attachment.url}`);
|
|
3101
|
+
writers.out(result.attachment.markdown);
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
2501
3104
|
async function publish(arg, values) {
|
|
2502
3105
|
await withClient(async ({ client: client2 }) => {
|
|
2503
3106
|
const { review } = await resolveReview(client2, findRepoRoot(), reviewArg(values, arg), values);
|
|
@@ -2606,7 +3209,7 @@ async function readBodyInput(values) {
|
|
|
2606
3209
|
if (values["body-file"] !== void 0) {
|
|
2607
3210
|
const file = String(values["body-file"]);
|
|
2608
3211
|
if (file === "-") return readStdin();
|
|
2609
|
-
return
|
|
3212
|
+
return readFile7(file, "utf8");
|
|
2610
3213
|
}
|
|
2611
3214
|
if (process.stdin.isTTY) return null;
|
|
2612
3215
|
return readStdin();
|
|
@@ -2618,12 +3221,20 @@ async function readStdin() {
|
|
|
2618
3221
|
}
|
|
2619
3222
|
async function doctor(root) {
|
|
2620
3223
|
const rows = [];
|
|
3224
|
+
const container = isDevContainer();
|
|
2621
3225
|
const major = Number(process.version.slice(1).split(".")[0]);
|
|
2622
3226
|
rows.push(["node", major >= 22 ? "ok" : "fail", process.version]);
|
|
3227
|
+
if (container) {
|
|
3228
|
+
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")]);
|
|
3229
|
+
}
|
|
2623
3230
|
const binaries = {};
|
|
2624
3231
|
for (const bin of ["glab", "gh", "git"]) {
|
|
2625
3232
|
const present = await which(bin);
|
|
2626
3233
|
binaries[bin] = present;
|
|
3234
|
+
if (container && (bin === "glab" || bin === "gh")) {
|
|
3235
|
+
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")]);
|
|
3236
|
+
continue;
|
|
3237
|
+
}
|
|
2627
3238
|
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
3239
|
}
|
|
2629
3240
|
let host;
|
|
@@ -2635,28 +3246,35 @@ async function doctor(root) {
|
|
|
2635
3246
|
} catch (error) {
|
|
2636
3247
|
rows.push(["config", "fail", redactLine(error)]);
|
|
2637
3248
|
}
|
|
2638
|
-
if (
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
|
|
2642
|
-
|
|
2643
|
-
|
|
3249
|
+
if (!container) {
|
|
3250
|
+
if (host && binaries.glab) {
|
|
3251
|
+
try {
|
|
3252
|
+
await run("glab", ["auth", "status", "--hostname", host], { timeoutMs: 15e3 });
|
|
3253
|
+
rows.push(["glab auth", "ok", host]);
|
|
3254
|
+
} catch (error) {
|
|
3255
|
+
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)}`]);
|
|
3256
|
+
}
|
|
2644
3257
|
}
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
3258
|
+
if (host && binaries.gh) {
|
|
3259
|
+
try {
|
|
3260
|
+
await run("gh", ["auth", "status"], { timeoutMs: 15e3, env: { GH_HOST: host } });
|
|
3261
|
+
rows.push(["gh auth", "ok", host]);
|
|
3262
|
+
} catch (error) {
|
|
3263
|
+
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)}`]);
|
|
3264
|
+
}
|
|
2652
3265
|
}
|
|
2653
3266
|
}
|
|
2654
3267
|
const running = await isDaemonRunning();
|
|
2655
3268
|
const info = await daemonInfo();
|
|
2656
|
-
|
|
2657
|
-
rows.push([
|
|
2658
|
-
|
|
2659
|
-
|
|
3269
|
+
const daemonStatus = running ? "ok" : container ? "fail" : "warn";
|
|
3270
|
+
rows.push([
|
|
3271
|
+
"daemon",
|
|
3272
|
+
daemonStatus,
|
|
3273
|
+
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)")
|
|
3274
|
+
]);
|
|
3275
|
+
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")]);
|
|
3276
|
+
const { existsSync: existsSync8 } = await import("node:fs");
|
|
3277
|
+
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
3278
|
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
3279
|
for (const [name, status2, detail] of rows) {
|
|
2662
3280
|
writers.out(`${status2 === "ok" ? "\u2714" : status2 === "warn" ? "!" : "\u2716"} ${name.padEnd(10)} ${detail}`);
|