@imdeadpool/guardex 7.0.41 → 7.0.43
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +68 -13
- package/package.json +2 -1
- package/skills/gitguardex/SKILL.md +13 -0
- package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
- package/src/agents/cleanup-sessions.js +126 -0
- package/src/agents/detect.js +160 -0
- package/src/agents/finish.js +172 -0
- package/src/agents/inspect.js +189 -0
- package/src/agents/launch.js +240 -0
- package/src/agents/registry.js +133 -0
- package/src/agents/selection-panel.js +571 -0
- package/src/agents/sessions.js +151 -0
- package/src/agents/start.js +591 -0
- package/src/agents/status.js +143 -0
- package/src/agents/terminal.js +152 -0
- package/src/budget/index.js +343 -0
- package/src/ci-init/index.js +265 -0
- package/src/cli/args.js +305 -1
- package/src/cli/main.js +262 -132
- package/src/cockpit/action-runner.js +3 -0
- package/src/cockpit/actions.js +80 -0
- package/src/cockpit/control.js +1121 -0
- package/src/cockpit/index.js +426 -0
- package/src/cockpit/keybindings.js +224 -0
- package/src/cockpit/kitty-layout.js +549 -0
- package/src/cockpit/kitty-tree.js +144 -0
- package/src/cockpit/layout.js +224 -0
- package/src/cockpit/logs-reader.js +182 -0
- package/src/cockpit/menu.js +204 -0
- package/src/cockpit/pane-actions.js +597 -0
- package/src/cockpit/pane-menu.js +387 -0
- package/src/cockpit/projects-finder.js +178 -0
- package/src/cockpit/render.js +215 -0
- package/src/cockpit/settings-render.js +128 -0
- package/src/cockpit/settings.js +124 -0
- package/src/cockpit/shortcuts.js +24 -0
- package/src/cockpit/sidebar.js +311 -0
- package/src/cockpit/state.js +72 -0
- package/src/cockpit/theme.js +128 -0
- package/src/cockpit/welcome.js +266 -0
- package/src/context.js +76 -33
- package/src/doctor/index.js +3 -2
- package/src/finish/index.js +39 -2
- package/src/git/index.js +65 -0
- package/src/kitty/command.js +101 -0
- package/src/kitty/runtime.js +250 -0
- package/src/output/index.js +1 -1
- package/src/pr-review.js +241 -0
- package/src/scaffold/index.js +19 -0
- package/src/submodule/index.js +288 -0
- package/src/terminal/index.js +120 -0
- package/src/terminal/kitty.js +622 -0
- package/src/terminal/tmux.js +126 -0
- package/src/tmux/command.js +27 -0
- package/src/tmux/session.js +89 -0
- package/templates/AGENTS.multiagent-safety.md +27 -1
- package/templates/codex/skills/gitguardex/SKILL.md +2 -0
- package/templates/githooks/pre-commit +22 -1
- package/templates/github/workflows/README.md +87 -0
- package/templates/github/workflows/ci-full.yml +55 -0
- package/templates/github/workflows/ci.yml +56 -0
- package/templates/github/workflows/cr.yml +20 -1
- package/templates/scripts/agent-branch-finish.sh +544 -26
- package/templates/scripts/agent-branch-start.sh +89 -22
- package/templates/scripts/agent-preflight.sh +89 -0
- package/templates/scripts/agent-worktree-prune.sh +96 -5
- package/templates/scripts/codex-agent.sh +41 -6
- package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
- package/templates/scripts/review-bot-watch.sh +31 -2
- package/templates/scripts/agent-session-state.js +0 -171
- package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
- package/templates/vscode/guardex-active-agents/README.md +0 -34
- package/templates/vscode/guardex-active-agents/extension.js +0 -3782
- package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
- package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
- package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
- package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
- package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
- package/templates/vscode/guardex-active-agents/icon.png +0 -0
- package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
- package/templates/vscode/guardex-active-agents/package.json +0 -169
- package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cp = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_KITTY_BIN = 'kitty';
|
|
6
|
+
|
|
7
|
+
function text(value, fallback = '') {
|
|
8
|
+
if (typeof value === 'string') return value.trim() || fallback;
|
|
9
|
+
if (value === null || value === undefined) return fallback;
|
|
10
|
+
return String(value).trim() || fallback;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function resolveKittyBin(options = {}) {
|
|
14
|
+
return text(options.kittyBin || options.bin || process.env.GUARDEX_KITTY_BIN, DEFAULT_KITTY_BIN);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function assertArgs(args) {
|
|
18
|
+
if (!Array.isArray(args)) {
|
|
19
|
+
throw new TypeError('kitty args must be an array');
|
|
20
|
+
}
|
|
21
|
+
for (const arg of args) {
|
|
22
|
+
if (typeof arg !== 'string') {
|
|
23
|
+
throw new TypeError('kitty args must contain only strings');
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function commandShape(cmd, args, options = {}) {
|
|
29
|
+
const command = {
|
|
30
|
+
cmd,
|
|
31
|
+
args: [...args],
|
|
32
|
+
};
|
|
33
|
+
if (Object.prototype.hasOwnProperty.call(options, 'input')) {
|
|
34
|
+
command.input = options.input === undefined || options.input === null ? '' : String(options.input);
|
|
35
|
+
}
|
|
36
|
+
return command;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runnerOptions(options = {}) {
|
|
40
|
+
return {
|
|
41
|
+
cwd: options.cwd,
|
|
42
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
input: options.input,
|
|
45
|
+
stdio: options.stdio || 'pipe',
|
|
46
|
+
timeout: options.timeout,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function defaultRunner(cmd, args, options = {}) {
|
|
51
|
+
return cp.spawnSync(cmd, args, runnerOptions(options));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function runnerFor(options = {}) {
|
|
55
|
+
if (typeof options.runner === 'function') return options.runner;
|
|
56
|
+
if (options.runtime && typeof options.runtime.run === 'function') return options.runtime.run;
|
|
57
|
+
return defaultRunner;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function dryRunOptions(options = {}) {
|
|
61
|
+
const result = {};
|
|
62
|
+
for (const key of ['cwd', 'env', 'input', 'stdio', 'timeout']) {
|
|
63
|
+
if (Object.prototype.hasOwnProperty.call(options, key) && options[key] !== undefined) {
|
|
64
|
+
result[key] = options[key];
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function runKitty(args, options = {}) {
|
|
71
|
+
assertArgs(args);
|
|
72
|
+
const cmd = resolveKittyBin(options);
|
|
73
|
+
const command = commandShape(cmd, args, options);
|
|
74
|
+
|
|
75
|
+
if (options.dryRun) {
|
|
76
|
+
const result = {
|
|
77
|
+
dryRun: true,
|
|
78
|
+
commands: [command],
|
|
79
|
+
};
|
|
80
|
+
const optionsForReport = dryRunOptions(options);
|
|
81
|
+
if (optionsForReport) result.options = optionsForReport;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return runnerFor(options)(cmd, [...args], runnerOptions(options));
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isKittyAvailable(options = {}) {
|
|
89
|
+
const result = runKitty(['@', 'ls'], {
|
|
90
|
+
...options,
|
|
91
|
+
stdio: 'pipe',
|
|
92
|
+
});
|
|
93
|
+
if (result && result.dryRun) return result;
|
|
94
|
+
return Boolean(result && result.status === 0 && !result.error);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
module.exports = {
|
|
98
|
+
isKittyAvailable,
|
|
99
|
+
runKitty,
|
|
100
|
+
resolveKittyBin,
|
|
101
|
+
};
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const {
|
|
4
|
+
isKittyAvailable,
|
|
5
|
+
resolveKittyBin,
|
|
6
|
+
runKitty,
|
|
7
|
+
} = require('./command');
|
|
8
|
+
|
|
9
|
+
function text(value, fallback = '') {
|
|
10
|
+
if (typeof value === 'string') return value.trim() || fallback;
|
|
11
|
+
if (value === null || value === undefined) return fallback;
|
|
12
|
+
return String(value).trim() || fallback;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function requireText(value, name) {
|
|
16
|
+
const normalized = text(value);
|
|
17
|
+
if (!normalized) {
|
|
18
|
+
throw new TypeError(`${name} must be a non-empty string`);
|
|
19
|
+
}
|
|
20
|
+
return normalized;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function appendOption(args, flag, value) {
|
|
24
|
+
const normalized = text(value);
|
|
25
|
+
if (normalized) args.push(flag, normalized);
|
|
26
|
+
return args;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeEnvEntries(env = {}) {
|
|
30
|
+
if (!env) return [];
|
|
31
|
+
if (Array.isArray(env)) {
|
|
32
|
+
return env.map((entry) => {
|
|
33
|
+
if (Array.isArray(entry)) {
|
|
34
|
+
const key = requireText(entry[0], 'kitty env key');
|
|
35
|
+
const value = entry.length > 1 && entry[1] !== undefined && entry[1] !== null ? String(entry[1]) : '';
|
|
36
|
+
return `${key}=${value}`;
|
|
37
|
+
}
|
|
38
|
+
return requireText(entry, 'kitty env');
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
if (typeof env !== 'object') {
|
|
42
|
+
throw new TypeError('kitty env must be an object, array, or undefined');
|
|
43
|
+
}
|
|
44
|
+
return Object.keys(env)
|
|
45
|
+
.sort()
|
|
46
|
+
.map((key) => `${requireText(key, 'kitty env key')}=${env[key] === undefined || env[key] === null ? '' : String(env[key])}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function appendEnv(args, env) {
|
|
50
|
+
for (const entry of normalizeEnvEntries(env)) {
|
|
51
|
+
args.push('--env', entry);
|
|
52
|
+
}
|
|
53
|
+
return args;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function normalizeCommandArgv(options = {}) {
|
|
57
|
+
if (options.argv === undefined && options.commandArgv === undefined && Object.prototype.hasOwnProperty.call(options, 'command') && !Array.isArray(options.command)) {
|
|
58
|
+
throw new TypeError('kitty command argv must be an array');
|
|
59
|
+
}
|
|
60
|
+
const commandArgv = options.argv || options.commandArgv || (Array.isArray(options.command) ? options.command : undefined);
|
|
61
|
+
if (commandArgv === undefined || commandArgv === null) return [];
|
|
62
|
+
if (!Array.isArray(commandArgv)) {
|
|
63
|
+
throw new TypeError('kitty command argv must be an array');
|
|
64
|
+
}
|
|
65
|
+
return commandArgv.map((arg) => {
|
|
66
|
+
if (arg === undefined || arg === null) {
|
|
67
|
+
throw new TypeError('kitty command argv values must be strings');
|
|
68
|
+
}
|
|
69
|
+
return String(arg);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function appendCommandArgv(args, options = {}) {
|
|
74
|
+
const commandArgv = normalizeCommandArgv(options);
|
|
75
|
+
if (commandArgv.length > 0) args.push('--', ...commandArgv);
|
|
76
|
+
return args;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function commandShape(args, options = {}) {
|
|
80
|
+
const command = {
|
|
81
|
+
cmd: resolveKittyBin(options),
|
|
82
|
+
args,
|
|
83
|
+
};
|
|
84
|
+
if (Object.prototype.hasOwnProperty.call(options, 'input')) {
|
|
85
|
+
command.input = options.input === undefined || options.input === null ? '' : String(options.input);
|
|
86
|
+
}
|
|
87
|
+
return command;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildKittyLaunchCommand(options = {}) {
|
|
91
|
+
const args = ['@', 'launch'];
|
|
92
|
+
appendOption(args, '--type', text(options.type, 'window'));
|
|
93
|
+
appendOption(args, '--location', options.location);
|
|
94
|
+
appendOption(args, '--cwd', options.cwd);
|
|
95
|
+
appendOption(args, '--title', options.title);
|
|
96
|
+
appendEnv(args, options.env);
|
|
97
|
+
appendCommandArgv(args, options);
|
|
98
|
+
return commandShape(args, options);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function runCommand(command, action, options = {}) {
|
|
102
|
+
const runOptions = {
|
|
103
|
+
...options,
|
|
104
|
+
kittyBin: command.cmd,
|
|
105
|
+
action,
|
|
106
|
+
};
|
|
107
|
+
if (Object.prototype.hasOwnProperty.call(command, 'input')) {
|
|
108
|
+
runOptions.input = command.input;
|
|
109
|
+
} else if (Object.prototype.hasOwnProperty.call(options, 'input')) {
|
|
110
|
+
runOptions.input = options.input;
|
|
111
|
+
}
|
|
112
|
+
return runKitty(command.args, runOptions);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function cloneCommand(command) {
|
|
116
|
+
if (!command || typeof command !== 'object' || !Array.isArray(command.args)) {
|
|
117
|
+
throw new TypeError('kitty cockpit command must include args');
|
|
118
|
+
}
|
|
119
|
+
const clone = {
|
|
120
|
+
cmd: requireText(command.cmd, 'kitty cockpit command cmd'),
|
|
121
|
+
args: command.args.map((arg) => {
|
|
122
|
+
if (arg === undefined || arg === null) {
|
|
123
|
+
throw new TypeError('kitty cockpit command args must be strings');
|
|
124
|
+
}
|
|
125
|
+
return String(arg);
|
|
126
|
+
}),
|
|
127
|
+
};
|
|
128
|
+
if (Object.prototype.hasOwnProperty.call(command, 'input')) {
|
|
129
|
+
clone.input = command.input === undefined || command.input === null ? '' : String(command.input);
|
|
130
|
+
}
|
|
131
|
+
return clone;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function cockpitCommands(plan = {}) {
|
|
135
|
+
if (!plan || typeof plan !== 'object') {
|
|
136
|
+
throw new TypeError('kitty cockpit plan must be an object');
|
|
137
|
+
}
|
|
138
|
+
const commands = Array.isArray(plan.commands)
|
|
139
|
+
? plan.commands
|
|
140
|
+
: Array.isArray(plan.steps)
|
|
141
|
+
? plan.steps.map((step) => step && step.command).filter(Boolean)
|
|
142
|
+
: [];
|
|
143
|
+
return commands.map(cloneCommand);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function assertCommandResult(command, result) {
|
|
147
|
+
if (result && result.error) throw result.error;
|
|
148
|
+
if (!result || result.status === 0) return result;
|
|
149
|
+
const detail = String(result.stderr || result.stdout || '').trim();
|
|
150
|
+
throw new Error(`kitty cockpit command failed: ${command.cmd} ${command.args.join(' ')}${detail ? `: ${detail}` : ''}`);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function openKittyCockpit(options = {}) {
|
|
154
|
+
const plan = options.plan && typeof options.plan === 'object' ? options.plan : options;
|
|
155
|
+
const commands = cockpitCommands(plan);
|
|
156
|
+
const dryRun = Boolean(options.dryRun || plan.dryRun);
|
|
157
|
+
|
|
158
|
+
if (dryRun) {
|
|
159
|
+
return {
|
|
160
|
+
dryRun: true,
|
|
161
|
+
action: 'open-kitty-cockpit',
|
|
162
|
+
commands,
|
|
163
|
+
plan,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const results = commands.map((command) => (
|
|
168
|
+
assertCommandResult(command, runCommand(command, 'open-kitty-cockpit', options))
|
|
169
|
+
));
|
|
170
|
+
return {
|
|
171
|
+
action: 'open-kitty-cockpit',
|
|
172
|
+
commands,
|
|
173
|
+
results,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function launchKittyWindow(options = {}) {
|
|
178
|
+
return runCommand(
|
|
179
|
+
buildKittyLaunchCommand({
|
|
180
|
+
...options,
|
|
181
|
+
type: text(options.type, 'window'),
|
|
182
|
+
}),
|
|
183
|
+
'launch-window',
|
|
184
|
+
options,
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
function launchKittyTab(options = {}) {
|
|
189
|
+
return runCommand(
|
|
190
|
+
buildKittyLaunchCommand({
|
|
191
|
+
...options,
|
|
192
|
+
type: 'tab',
|
|
193
|
+
}),
|
|
194
|
+
'launch-tab',
|
|
195
|
+
options,
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function launchKittyPane(options = {}) {
|
|
200
|
+
return runCommand(
|
|
201
|
+
buildKittyLaunchCommand({
|
|
202
|
+
...options,
|
|
203
|
+
type: 'window',
|
|
204
|
+
location: text(options.location, 'vsplit'),
|
|
205
|
+
}),
|
|
206
|
+
'launch-pane',
|
|
207
|
+
options,
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function targetMatch(target) {
|
|
212
|
+
if (target && typeof target === 'object') {
|
|
213
|
+
const explicit = text(target.match || target.kittyMatch);
|
|
214
|
+
if (explicit) return explicit;
|
|
215
|
+
|
|
216
|
+
const id = text(target.id || target.windowId || target.kittyWindowId || target.paneId || target.target);
|
|
217
|
+
if (id) return `id:${id}`;
|
|
218
|
+
|
|
219
|
+
const title = text(target.title || target.windowTitle || target.kittyTitle);
|
|
220
|
+
if (title) return `title:${title}`;
|
|
221
|
+
} else {
|
|
222
|
+
const id = text(target);
|
|
223
|
+
if (id) return `id:${id}`;
|
|
224
|
+
}
|
|
225
|
+
throw new TypeError('kitty target must include id, title, or match');
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
function sendTextToKitty(target, value, options = {}) {
|
|
229
|
+
return runKitty(['@', 'send-text', '--match', targetMatch(target), '--stdin'], {
|
|
230
|
+
...options,
|
|
231
|
+
input: value === undefined || value === null ? '' : String(value),
|
|
232
|
+
stdio: options.stdio || 'pipe',
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function setKittyWindowTitle(target, title, options = {}) {
|
|
237
|
+
return runKitty(['@', 'set-window-title', '--match', targetMatch(target), requireText(title, 'kitty window title')], options);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
module.exports = {
|
|
241
|
+
isKittyAvailable,
|
|
242
|
+
runKitty,
|
|
243
|
+
buildKittyLaunchCommand,
|
|
244
|
+
launchKittyWindow,
|
|
245
|
+
launchKittyTab,
|
|
246
|
+
launchKittyPane,
|
|
247
|
+
openKittyCockpit,
|
|
248
|
+
sendTextToKitty,
|
|
249
|
+
setKittyWindowTitle,
|
|
250
|
+
};
|
package/src/output/index.js
CHANGED
|
@@ -347,7 +347,7 @@ REPO TOGGLE
|
|
|
347
347
|
${repoToggleLines().join('\n')}
|
|
348
348
|
|
|
349
349
|
NOTES
|
|
350
|
-
- No command = ${invoked} status (compact
|
|
350
|
+
- No command = ${invoked} status (compact by default; pass --verbose for full services + help tree).
|
|
351
351
|
- ${invoked} init is an alias of ${invoked} setup.
|
|
352
352
|
- Global installs need Y/N approval; GitHub CLI (gh) is required for PR automation.
|
|
353
353
|
- Target another repo: ${invoked} <cmd> --target <repo-path>.
|
package/src/pr-review.js
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
const {
|
|
2
|
+
fs,
|
|
3
|
+
path,
|
|
4
|
+
os,
|
|
5
|
+
GH_BIN,
|
|
6
|
+
} = require('./context');
|
|
7
|
+
const { run } = require('./core/runtime');
|
|
8
|
+
|
|
9
|
+
const TOOL_PREFIX = '[gitguardex]';
|
|
10
|
+
const VALID_SEVERITIES = new Set(['low', 'medium', 'high', 'critical']);
|
|
11
|
+
|
|
12
|
+
function normalizeProvider(raw) {
|
|
13
|
+
const provider = String(raw || 'codex').trim().toLowerCase();
|
|
14
|
+
if (!['codex', 'claude'].includes(provider)) {
|
|
15
|
+
throw new Error(`Invalid provider: ${raw}`);
|
|
16
|
+
}
|
|
17
|
+
return provider;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function commandForProvider(provider, prompt) {
|
|
21
|
+
if (provider === 'claude') {
|
|
22
|
+
return { cmd: 'claude', args: ['-p', prompt] };
|
|
23
|
+
}
|
|
24
|
+
return { cmd: 'codex', args: ['exec', prompt] };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function compactReviewPrompt(diff) {
|
|
28
|
+
return [
|
|
29
|
+
'You are gitguardex-code-assist, a PR review runner.',
|
|
30
|
+
'Review this GitHub PR diff for correctness bugs, regressions, security issues, and missing tests.',
|
|
31
|
+
'Return JSON only. Shape:',
|
|
32
|
+
'{"findings":[{"path":"file","line":123,"severity":"low|medium|high|critical","message":"concise finding","suggestion":"optional replacement or fix"}]}',
|
|
33
|
+
'Rules: path and line must point to changed lines in the diff. Use an empty findings array when nothing is worth commenting.',
|
|
34
|
+
'',
|
|
35
|
+
'PR diff:',
|
|
36
|
+
diff,
|
|
37
|
+
].join('\n');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function extractJsonPayload(text) {
|
|
41
|
+
const raw = String(text || '').trim();
|
|
42
|
+
if (!raw) return { findings: [] };
|
|
43
|
+
const fenced = raw.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
44
|
+
const candidate = fenced ? fenced[1].trim() : raw;
|
|
45
|
+
try {
|
|
46
|
+
return JSON.parse(candidate);
|
|
47
|
+
} catch (_error) {
|
|
48
|
+
const objectStart = candidate.indexOf('{');
|
|
49
|
+
const objectEnd = candidate.lastIndexOf('}');
|
|
50
|
+
if (objectStart >= 0 && objectEnd > objectStart) {
|
|
51
|
+
return JSON.parse(candidate.slice(objectStart, objectEnd + 1));
|
|
52
|
+
}
|
|
53
|
+
const arrayStart = candidate.indexOf('[');
|
|
54
|
+
const arrayEnd = candidate.lastIndexOf(']');
|
|
55
|
+
if (arrayStart >= 0 && arrayEnd > arrayStart) {
|
|
56
|
+
return { findings: JSON.parse(candidate.slice(arrayStart, arrayEnd + 1)) };
|
|
57
|
+
}
|
|
58
|
+
throw new Error('Review provider did not return parseable JSON findings');
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeFinding(rawFinding) {
|
|
63
|
+
const pathValue = String(rawFinding?.path || '').trim();
|
|
64
|
+
const line = Number.parseInt(String(rawFinding?.line || ''), 10);
|
|
65
|
+
const severity = String(rawFinding?.severity || 'medium').trim().toLowerCase();
|
|
66
|
+
const message = String(rawFinding?.message || '').trim();
|
|
67
|
+
const suggestion = String(rawFinding?.suggestion || '').trim();
|
|
68
|
+
if (!pathValue || !Number.isInteger(line) || line <= 0 || !message) {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
path: pathValue,
|
|
73
|
+
line,
|
|
74
|
+
severity: VALID_SEVERITIES.has(severity) ? severity : 'medium',
|
|
75
|
+
message,
|
|
76
|
+
suggestion,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeFindings(providerOutput) {
|
|
81
|
+
const payload = extractJsonPayload(providerOutput);
|
|
82
|
+
const rawFindings = Array.isArray(payload) ? payload : payload.findings;
|
|
83
|
+
if (!Array.isArray(rawFindings)) {
|
|
84
|
+
throw new Error('Review provider JSON must contain a findings array');
|
|
85
|
+
}
|
|
86
|
+
return rawFindings.map(normalizeFinding).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function findingBody(finding) {
|
|
90
|
+
const lines = [`**${finding.severity.toUpperCase()}** ${finding.message}`];
|
|
91
|
+
if (finding.suggestion) {
|
|
92
|
+
lines.push('', 'Suggested fix:', '```suggestion', finding.suggestion, '```');
|
|
93
|
+
}
|
|
94
|
+
return lines.join('\n');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function renderMarkdownReview({ pr, provider, findings }) {
|
|
98
|
+
const lines = [
|
|
99
|
+
`# GitGuardex PR Review`,
|
|
100
|
+
'',
|
|
101
|
+
`- PR: #${pr}`,
|
|
102
|
+
`- Provider: ${provider}`,
|
|
103
|
+
`- Findings: ${findings.length}`,
|
|
104
|
+
'',
|
|
105
|
+
];
|
|
106
|
+
if (findings.length === 0) {
|
|
107
|
+
lines.push('No findings.');
|
|
108
|
+
} else {
|
|
109
|
+
for (const finding of findings) {
|
|
110
|
+
lines.push(`## ${finding.severity.toUpperCase()} ${finding.path}:${finding.line}`);
|
|
111
|
+
lines.push('');
|
|
112
|
+
lines.push(finding.message);
|
|
113
|
+
if (finding.suggestion) {
|
|
114
|
+
lines.push('', '```suggestion', finding.suggestion, '```');
|
|
115
|
+
}
|
|
116
|
+
lines.push('');
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return `${lines.join('\n').replace(/\n{3,}/g, '\n\n')}\n`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function defaultArtifactPath(repoRoot, pr) {
|
|
123
|
+
return path.join(repoRoot, '.gitguardex', 'pr-reviews', `pr-${pr}.md`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function writeArtifact(repoRoot, artifactPath, payload) {
|
|
127
|
+
const outputPath = artifactPath
|
|
128
|
+
? path.resolve(repoRoot, artifactPath)
|
|
129
|
+
: defaultArtifactPath(repoRoot, payload.pr);
|
|
130
|
+
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
|
131
|
+
fs.writeFileSync(outputPath, renderMarkdownReview(payload), 'utf8');
|
|
132
|
+
return outputPath;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function githubAuthAvailable(env = process.env, runner = run) {
|
|
136
|
+
if (env.GITHUB_TOKEN || env.GH_TOKEN) return true;
|
|
137
|
+
const result = runner(GH_BIN, ['auth', 'status'], { timeout: 15_000 });
|
|
138
|
+
return result.status === 0;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function fetchPrDiff(pr, repoRoot, runner = run) {
|
|
142
|
+
const result = runner(GH_BIN, ['pr', 'diff', String(pr)], { cwd: repoRoot, timeout: 120_000 });
|
|
143
|
+
if (result.status !== 0) {
|
|
144
|
+
throw new Error(`gh pr diff ${pr} failed${result.stderr ? `\n${result.stderr.trim()}` : ''}`);
|
|
145
|
+
}
|
|
146
|
+
return result.stdout || '';
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function runProviderReview(provider, diff, repoRoot, timeoutMs, runner = run) {
|
|
150
|
+
const prompt = compactReviewPrompt(diff);
|
|
151
|
+
const command = commandForProvider(provider, prompt);
|
|
152
|
+
const result = runner(command.cmd, command.args, { cwd: repoRoot, timeout: timeoutMs });
|
|
153
|
+
if (result.status !== 0) {
|
|
154
|
+
throw new Error(`${provider} review failed${result.stderr ? `\n${result.stderr.trim()}` : ''}`);
|
|
155
|
+
}
|
|
156
|
+
return normalizeFindings(result.stdout || '');
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function postGithubReview(pr, findings, repoRoot, runner = run) {
|
|
160
|
+
const comments = findings.map((finding) => ({
|
|
161
|
+
path: finding.path,
|
|
162
|
+
line: finding.line,
|
|
163
|
+
side: 'RIGHT',
|
|
164
|
+
body: findingBody(finding),
|
|
165
|
+
}));
|
|
166
|
+
const body = findings.length > 0
|
|
167
|
+
? `GitGuardex code-assist found ${findings.length} issue(s).`
|
|
168
|
+
: 'GitGuardex code-assist found no issues worth inline comments.';
|
|
169
|
+
const payload = {
|
|
170
|
+
event: 'COMMENT',
|
|
171
|
+
body,
|
|
172
|
+
comments,
|
|
173
|
+
};
|
|
174
|
+
const inputPath = path.join(os.tmpdir(), `gitguardex-pr-review-${process.pid}-${Date.now()}.json`);
|
|
175
|
+
fs.writeFileSync(inputPath, JSON.stringify(payload), 'utf8');
|
|
176
|
+
try {
|
|
177
|
+
const result = runner(GH_BIN, [
|
|
178
|
+
'api',
|
|
179
|
+
`repos/:owner/:repo/pulls/${pr}/reviews`,
|
|
180
|
+
'--method',
|
|
181
|
+
'POST',
|
|
182
|
+
'--input',
|
|
183
|
+
inputPath,
|
|
184
|
+
], { cwd: repoRoot, timeout: 120_000 });
|
|
185
|
+
if (result.status !== 0) {
|
|
186
|
+
throw new Error(`gh api review post failed${result.stderr ? `\n${result.stderr.trim()}` : ''}`);
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
} finally {
|
|
190
|
+
try {
|
|
191
|
+
fs.unlinkSync(inputPath);
|
|
192
|
+
} catch (_error) {
|
|
193
|
+
// best effort cleanup for temp API payload
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function runPrReview(options, deps = {}) {
|
|
199
|
+
const runner = deps.run || run;
|
|
200
|
+
const repoRoot = path.resolve(options.target || process.cwd());
|
|
201
|
+
const provider = normalizeProvider(options.provider);
|
|
202
|
+
const pr = String(options.pr);
|
|
203
|
+
const diff = fetchPrDiff(pr, repoRoot, runner);
|
|
204
|
+
const findings = runProviderReview(provider, diff, repoRoot, options.timeoutMs, runner);
|
|
205
|
+
const payload = { pr, provider, findings };
|
|
206
|
+
|
|
207
|
+
if (!options.post) {
|
|
208
|
+
const artifactPath = writeArtifact(repoRoot, options.artifact, payload);
|
|
209
|
+
return { posted: false, artifactPath, findings };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (!githubAuthAvailable(process.env, runner)) {
|
|
213
|
+
const artifactPath = writeArtifact(repoRoot, options.artifact, payload);
|
|
214
|
+
return { posted: false, artifactPath, findings, reason: 'github-auth-unavailable' };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
postGithubReview(pr, findings, repoRoot, runner);
|
|
218
|
+
return { posted: true, artifactPath: '', findings };
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function printPrReviewResult(result) {
|
|
222
|
+
if (result.posted) {
|
|
223
|
+
console.log(`${TOOL_PREFIX} Posted PR review with ${result.findings.length} finding(s).`);
|
|
224
|
+
return;
|
|
225
|
+
}
|
|
226
|
+
if (result.reason === 'github-auth-unavailable') {
|
|
227
|
+
console.log(`${TOOL_PREFIX} GitHub auth unavailable; wrote PR review artifact: ${result.artifactPath}`);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
console.log(`${TOOL_PREFIX} Wrote PR review artifact: ${result.artifactPath}`);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
module.exports = {
|
|
234
|
+
compactReviewPrompt,
|
|
235
|
+
commandForProvider,
|
|
236
|
+
extractJsonPayload,
|
|
237
|
+
normalizeFindings,
|
|
238
|
+
renderMarkdownReview,
|
|
239
|
+
runPrReview,
|
|
240
|
+
printPrReviewResult,
|
|
241
|
+
};
|
package/src/scaffold/index.js
CHANGED
|
@@ -539,6 +539,24 @@ function ensureAgentsSnippet(repoRoot, dryRun) {
|
|
|
539
539
|
return { status: 'updated', file: 'AGENTS.md' };
|
|
540
540
|
}
|
|
541
541
|
|
|
542
|
+
function ensureClaudeAgentsLink(repoRoot, dryRun) {
|
|
543
|
+
const claudePath = path.join(repoRoot, 'CLAUDE.md');
|
|
544
|
+
try {
|
|
545
|
+
fs.lstatSync(claudePath);
|
|
546
|
+
return { status: 'unchanged', file: 'CLAUDE.md', note: 'existing path preserved' };
|
|
547
|
+
} catch (error) {
|
|
548
|
+
if (error.code !== 'ENOENT') {
|
|
549
|
+
throw error;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!dryRun) {
|
|
554
|
+
fs.symlinkSync('AGENTS.md', claudePath);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return { status: dryRun ? 'would-create' : 'created', file: 'CLAUDE.md', note: 'symlink to AGENTS.md' };
|
|
558
|
+
}
|
|
559
|
+
|
|
542
560
|
function ensureManagedGitignore(repoRoot, dryRun) {
|
|
543
561
|
const gitignorePath = path.join(repoRoot, '.gitignore');
|
|
544
562
|
const managedBlock = [
|
|
@@ -763,6 +781,7 @@ module.exports = {
|
|
|
763
781
|
installUserLevelAsset,
|
|
764
782
|
removeLegacyManagedRepoFile,
|
|
765
783
|
ensureAgentsSnippet,
|
|
784
|
+
ensureClaudeAgentsLink,
|
|
766
785
|
ensureManagedGitignore,
|
|
767
786
|
parseJsonObjectLikeFile,
|
|
768
787
|
buildRepoVscodeSettings,
|