@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,426 @@
|
|
|
1
|
+
const { readCockpitState } = require('./state');
|
|
2
|
+
const { readCockpitSettings } = require('./settings');
|
|
3
|
+
const { renderCockpit } = require('./render');
|
|
4
|
+
const { openKittyCockpit } = require('./kitty-layout');
|
|
5
|
+
const control = require('./control');
|
|
6
|
+
const actions = require('./actions');
|
|
7
|
+
const { normalizeBackendName, selectTerminalBackend } = require('../terminal');
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SESSION_NAME = 'guardex';
|
|
10
|
+
const DEFAULT_BACKEND = 'auto';
|
|
11
|
+
const DEFAULT_INTERACTIVE_BACKEND = 'auto';
|
|
12
|
+
|
|
13
|
+
function parseCockpitArgs(rawArgs = []) {
|
|
14
|
+
const options = {
|
|
15
|
+
sessionName: DEFAULT_SESSION_NAME,
|
|
16
|
+
backend: process.env.GUARDEX_COCKPIT_BACKEND || DEFAULT_BACKEND,
|
|
17
|
+
attach: false,
|
|
18
|
+
target: process.cwd(),
|
|
19
|
+
host: undefined,
|
|
20
|
+
socket: undefined,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
24
|
+
const arg = rawArgs[index];
|
|
25
|
+
if (arg === '--attach') {
|
|
26
|
+
options.attach = true;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
if (arg === '--kitty') {
|
|
30
|
+
options.backend = 'kitty';
|
|
31
|
+
continue;
|
|
32
|
+
}
|
|
33
|
+
if (arg === '--host' || arg === '--bootstrap-kitty') {
|
|
34
|
+
options.host = true;
|
|
35
|
+
if (!options.backend || options.backend === 'auto' || options.backend === DEFAULT_BACKEND) {
|
|
36
|
+
options.backend = 'kitty';
|
|
37
|
+
}
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
if (arg === '--no-host' || arg === '--no-bootstrap-kitty') {
|
|
41
|
+
options.host = false;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === '--socket') {
|
|
45
|
+
const next = rawArgs[index + 1];
|
|
46
|
+
if (!next || next.startsWith('-')) {
|
|
47
|
+
throw new Error('--socket requires a path');
|
|
48
|
+
}
|
|
49
|
+
options.socket = next;
|
|
50
|
+
if (options.host !== false) options.host = true;
|
|
51
|
+
if (!options.backend || options.backend === 'auto' || options.backend === DEFAULT_BACKEND) {
|
|
52
|
+
options.backend = 'kitty';
|
|
53
|
+
}
|
|
54
|
+
index += 1;
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
if (arg.startsWith('--socket=')) {
|
|
58
|
+
const next = arg.slice('--socket='.length);
|
|
59
|
+
if (!next) throw new Error('--socket requires a path');
|
|
60
|
+
options.socket = next;
|
|
61
|
+
if (options.host !== false) options.host = true;
|
|
62
|
+
if (!options.backend || options.backend === 'auto' || options.backend === DEFAULT_BACKEND) {
|
|
63
|
+
options.backend = 'kitty';
|
|
64
|
+
}
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
if (arg === '--session') {
|
|
68
|
+
const next = rawArgs[index + 1];
|
|
69
|
+
if (!next || next.startsWith('-')) {
|
|
70
|
+
throw new Error('--session requires a tmux session name');
|
|
71
|
+
}
|
|
72
|
+
options.sessionName = next;
|
|
73
|
+
index += 1;
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (arg.startsWith('--session=')) {
|
|
77
|
+
options.sessionName = arg.slice('--session='.length);
|
|
78
|
+
if (!options.sessionName) {
|
|
79
|
+
throw new Error('--session requires a tmux session name');
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (arg === '--backend') {
|
|
84
|
+
const next = rawArgs[index + 1];
|
|
85
|
+
if (!next || next.startsWith('-')) {
|
|
86
|
+
throw new Error('--backend requires auto, kitty, or tmux');
|
|
87
|
+
}
|
|
88
|
+
options.backend = normalizeBackendName(next);
|
|
89
|
+
index += 1;
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
if (arg.startsWith('--backend=')) {
|
|
93
|
+
const next = arg.slice('--backend='.length);
|
|
94
|
+
if (!next) {
|
|
95
|
+
throw new Error('--backend requires auto, kitty, or tmux');
|
|
96
|
+
}
|
|
97
|
+
options.backend = normalizeBackendName(next);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (arg === '--target' || arg === '-t') {
|
|
101
|
+
const next = rawArgs[index + 1];
|
|
102
|
+
if (!next || next.startsWith('-')) {
|
|
103
|
+
throw new Error(`${arg} requires a repo path`);
|
|
104
|
+
}
|
|
105
|
+
options.target = next;
|
|
106
|
+
index += 1;
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Unknown cockpit option: ${arg}`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return options;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function parseCockpitControlArgs(rawArgs = []) {
|
|
116
|
+
const options = {
|
|
117
|
+
refreshMs: undefined,
|
|
118
|
+
target: process.cwd(),
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
for (let index = 0; index < rawArgs.length; index += 1) {
|
|
122
|
+
const arg = rawArgs[index];
|
|
123
|
+
if (arg === '--target' || arg === '-t') {
|
|
124
|
+
const next = rawArgs[index + 1];
|
|
125
|
+
if (!next || next.startsWith('-')) {
|
|
126
|
+
throw new Error(`${arg} requires a repo path`);
|
|
127
|
+
}
|
|
128
|
+
options.target = next;
|
|
129
|
+
index += 1;
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (arg === '--refresh-ms') {
|
|
133
|
+
const next = rawArgs[index + 1];
|
|
134
|
+
if (!next || next.startsWith('-')) {
|
|
135
|
+
throw new Error('--refresh-ms requires a positive integer');
|
|
136
|
+
}
|
|
137
|
+
options.refreshMs = Number.parseInt(next, 10);
|
|
138
|
+
if (!Number.isFinite(options.refreshMs) || options.refreshMs <= 0) {
|
|
139
|
+
throw new Error('--refresh-ms requires a positive integer');
|
|
140
|
+
}
|
|
141
|
+
index += 1;
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (arg.startsWith('--refresh-ms=')) {
|
|
145
|
+
options.refreshMs = Number.parseInt(arg.slice('--refresh-ms='.length), 10);
|
|
146
|
+
if (!Number.isFinite(options.refreshMs) || options.refreshMs <= 0) {
|
|
147
|
+
throw new Error('--refresh-ms requires a positive integer');
|
|
148
|
+
}
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
throw new Error(`Unknown cockpit control option: ${arg}`);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return options;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function shellQuote(value) {
|
|
158
|
+
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function cockpitControlCommand(repoRoot, options = {}) {
|
|
162
|
+
const refresh = Number.isFinite(options.refreshMs) && options.refreshMs > 0
|
|
163
|
+
? ` --refresh-ms ${Math.floor(options.refreshMs)}`
|
|
164
|
+
: '';
|
|
165
|
+
return `gx cockpit control --target ${shellQuote(repoRoot)}${refresh}`;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
function terminalBackendOptionsFromDeps(deps = {}) {
|
|
169
|
+
const terminalBackendOptions = { ...(deps.terminalBackendOptions || {}) };
|
|
170
|
+
if (deps.terminalBackends && deps.terminalBackends.kitty) {
|
|
171
|
+
terminalBackendOptions.kittyBackend = deps.terminalBackends.kitty;
|
|
172
|
+
}
|
|
173
|
+
if (deps.terminalBackends && deps.terminalBackends.tmux) {
|
|
174
|
+
terminalBackendOptions.tmuxBackend = deps.terminalBackends.tmux;
|
|
175
|
+
}
|
|
176
|
+
if (deps.tmux) {
|
|
177
|
+
terminalBackendOptions.tmux = { tmux: deps.tmux };
|
|
178
|
+
}
|
|
179
|
+
return terminalBackendOptions;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function writeOpenedCockpitMessage({ backend, action, options, repoRoot, controlCommand, stdout, toolName }) {
|
|
183
|
+
if (backend.name === 'tmux' && action === 'attached') {
|
|
184
|
+
stdout.write(`[${toolName}] Attaching tmux session '${options.sessionName}'.\n`);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if (backend.name === 'tmux') {
|
|
189
|
+
stdout.write(`[${toolName}] Created tmux session '${options.sessionName}' in ${repoRoot}.\n`);
|
|
190
|
+
} else {
|
|
191
|
+
stdout.write(`[${toolName}] Created ${backend.name} cockpit window '${options.sessionName}' in ${repoRoot}.\n`);
|
|
192
|
+
}
|
|
193
|
+
stdout.write(`[${toolName}] Control pane: ${controlCommand}\n`);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function shouldAutoHost(options = {}, context = {}) {
|
|
197
|
+
if (options.host === true || options.host === false) return false;
|
|
198
|
+
const env = context.env || process.env;
|
|
199
|
+
const optOut = String(env.GUARDEX_AUTO_HOST || '').trim().toLowerCase();
|
|
200
|
+
if (optOut === '0' || optOut === 'false' || optOut === 'no' || optOut === 'off') return false;
|
|
201
|
+
if (env.KITTY_LISTEN_ON) return false;
|
|
202
|
+
const stdout = context.stdout || process.stdout;
|
|
203
|
+
return Boolean(stdout && stdout.isTTY);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function openWithBackend(backend, options, repoRoot, controlCommand, deps = {}) {
|
|
207
|
+
const stdout = deps.stdout || process.stdout;
|
|
208
|
+
const toolName = deps.toolName || 'gitguardex';
|
|
209
|
+
const env = deps.env || process.env;
|
|
210
|
+
if (backend.name === 'kitty') {
|
|
211
|
+
const autoHost = shouldAutoHost(options, { env, stdout });
|
|
212
|
+
const result = openKittyCockpit({
|
|
213
|
+
repoRoot,
|
|
214
|
+
sessionName: options.sessionName,
|
|
215
|
+
controlCommand,
|
|
216
|
+
state: deps.state,
|
|
217
|
+
settings: deps.settings,
|
|
218
|
+
readState: deps.readState || readCockpitState,
|
|
219
|
+
readSettings: deps.readSettings || readCockpitSettings,
|
|
220
|
+
dryRun: deps.dryRun,
|
|
221
|
+
controlTitle: options.controlTitle || 'gx cockpit',
|
|
222
|
+
focusControl: deps.focusControl === undefined ? true : deps.focusControl,
|
|
223
|
+
runner: deps.kittyRunner || deps.runner,
|
|
224
|
+
kittyBin: deps.kittyBin || env.GUARDEX_KITTY_BIN,
|
|
225
|
+
env,
|
|
226
|
+
backend,
|
|
227
|
+
bootstrap: options.host === true || (options.host === undefined && autoHost)
|
|
228
|
+
? true
|
|
229
|
+
: options.host === false ? false : undefined,
|
|
230
|
+
bootstrapWhenHostless: false,
|
|
231
|
+
socket: options.socket,
|
|
232
|
+
hostRunner: deps.kittyHostRunner,
|
|
233
|
+
hostRuntime: deps.kittyHostRuntime,
|
|
234
|
+
spawn: deps.spawn,
|
|
235
|
+
fs: deps.fs,
|
|
236
|
+
sleep: deps.sleep,
|
|
237
|
+
});
|
|
238
|
+
const action = result && result.action ? result.action : 'created';
|
|
239
|
+
writeOpenedCockpitMessage({ backend, action, options, repoRoot, controlCommand, stdout, toolName });
|
|
240
|
+
return { action, backend: backend.name, sessionName: options.sessionName, repoRoot, plan: result.plan };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const result = backend.openCockpitLayout({
|
|
244
|
+
repoRoot,
|
|
245
|
+
sessionName: options.sessionName,
|
|
246
|
+
command: controlCommand,
|
|
247
|
+
attach: options.attach,
|
|
248
|
+
});
|
|
249
|
+
const action = result && result.action ? result.action : 'created';
|
|
250
|
+
|
|
251
|
+
writeOpenedCockpitMessage({ backend, action, options, repoRoot, controlCommand, stdout, toolName });
|
|
252
|
+
return { action, backend: backend.name, sessionName: options.sessionName, repoRoot };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function backendAvailable(backend) {
|
|
256
|
+
if (!backend || typeof backend.isAvailable !== 'function') return true;
|
|
257
|
+
try {
|
|
258
|
+
return Boolean(backend.isAvailable());
|
|
259
|
+
} catch (_error) {
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function defaultCockpitBackends(preferredBackend, terminalBackendOptions = {}, options = {}) {
|
|
265
|
+
const preferred = normalizeBackendName(preferredBackend || DEFAULT_INTERACTIVE_BACKEND, DEFAULT_INTERACTIVE_BACKEND);
|
|
266
|
+
const seen = new Set();
|
|
267
|
+
const candidates = [];
|
|
268
|
+
const add = (name, addOptions = {}) => {
|
|
269
|
+
if (seen.has(name)) return;
|
|
270
|
+
const backend = selectTerminalBackend(name, terminalBackendOptions);
|
|
271
|
+
if (!backend) return;
|
|
272
|
+
if (addOptions.onlyIfAvailable && !backendAvailable(backend)) return;
|
|
273
|
+
seen.add(name);
|
|
274
|
+
candidates.push(backend);
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
if (preferred === 'auto') {
|
|
278
|
+
if (options.autoHostPermitted) {
|
|
279
|
+
add('kitty');
|
|
280
|
+
} else {
|
|
281
|
+
add('kitty', { onlyIfAvailable: true });
|
|
282
|
+
}
|
|
283
|
+
add('tmux');
|
|
284
|
+
return candidates;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
add(preferred);
|
|
288
|
+
if (preferred !== 'tmux') add('tmux');
|
|
289
|
+
return candidates;
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function inlineCockpit(repoRoot, deps = {}) {
|
|
293
|
+
const controlHandle = control.startCockpitControl({
|
|
294
|
+
repoPath: repoRoot,
|
|
295
|
+
stdin: deps.stdin,
|
|
296
|
+
stdout: deps.stdout || process.stdout,
|
|
297
|
+
readState: deps.readState,
|
|
298
|
+
readSettings: deps.readSettings,
|
|
299
|
+
setInterval: deps.setInterval,
|
|
300
|
+
clearInterval: deps.clearInterval,
|
|
301
|
+
});
|
|
302
|
+
return {
|
|
303
|
+
action: 'rendered',
|
|
304
|
+
backend: 'inline',
|
|
305
|
+
sessionName: DEFAULT_SESSION_NAME,
|
|
306
|
+
repoRoot,
|
|
307
|
+
control: controlHandle,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function openDefaultCockpit(deps = {}) {
|
|
312
|
+
const {
|
|
313
|
+
resolveRepoRoot,
|
|
314
|
+
env = process.env,
|
|
315
|
+
} = deps;
|
|
316
|
+
if (typeof resolveRepoRoot !== 'function') {
|
|
317
|
+
throw new Error('openDefaultCockpit requires resolveRepoRoot');
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const target = deps.target || process.cwd();
|
|
321
|
+
const stdout = deps.stdout || process.stdout;
|
|
322
|
+
const options = {
|
|
323
|
+
sessionName: DEFAULT_SESSION_NAME,
|
|
324
|
+
backend: env.GUARDEX_COCKPIT_BACKEND || DEFAULT_INTERACTIVE_BACKEND,
|
|
325
|
+
attach: false,
|
|
326
|
+
target,
|
|
327
|
+
};
|
|
328
|
+
const repoRoot = resolveRepoRoot(target);
|
|
329
|
+
const controlCommand = cockpitControlCommand(repoRoot);
|
|
330
|
+
const terminalBackendOptions = terminalBackendOptionsFromDeps(deps);
|
|
331
|
+
const failures = [];
|
|
332
|
+
const autoHostPermitted = shouldAutoHost({}, { env, stdout });
|
|
333
|
+
|
|
334
|
+
for (const backend of defaultCockpitBackends(options.backend, terminalBackendOptions, { autoHostPermitted })) {
|
|
335
|
+
try {
|
|
336
|
+
return openWithBackend(backend, options, repoRoot, controlCommand, deps);
|
|
337
|
+
} catch (error) {
|
|
338
|
+
failures.push({
|
|
339
|
+
backend: backend.name,
|
|
340
|
+
message: error && error.message ? error.message : String(error),
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const result = inlineCockpit(repoRoot, deps);
|
|
346
|
+
result.failures = failures;
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
function render(repoPath = process.cwd()) {
|
|
351
|
+
return renderCockpit(readCockpitState(repoPath));
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
function startCockpit(options = {}) {
|
|
355
|
+
const repoPath = options.repoPath || process.cwd();
|
|
356
|
+
const refreshMs = Number.isFinite(options.refreshMs) && options.refreshMs > 0
|
|
357
|
+
? options.refreshMs
|
|
358
|
+
: 2000;
|
|
359
|
+
|
|
360
|
+
const paint = () => {
|
|
361
|
+
process.stdout.write('\x1Bc');
|
|
362
|
+
process.stdout.write(render(repoPath));
|
|
363
|
+
};
|
|
364
|
+
|
|
365
|
+
paint();
|
|
366
|
+
return setInterval(paint, refreshMs);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function openCockpit(rawArgs = [], deps = {}) {
|
|
370
|
+
const {
|
|
371
|
+
resolveRepoRoot,
|
|
372
|
+
toolName = 'gitguardex',
|
|
373
|
+
stdout = process.stdout,
|
|
374
|
+
} = deps;
|
|
375
|
+
if (typeof resolveRepoRoot !== 'function') {
|
|
376
|
+
throw new Error('openCockpit requires resolveRepoRoot');
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (rawArgs[0] === 'control') {
|
|
380
|
+
const controlOptions = parseCockpitControlArgs(rawArgs.slice(1));
|
|
381
|
+
return control.startCockpitControl({
|
|
382
|
+
repoPath: resolveRepoRoot(controlOptions.target),
|
|
383
|
+
refreshMs: controlOptions.refreshMs,
|
|
384
|
+
stdin: deps.stdin,
|
|
385
|
+
stdout,
|
|
386
|
+
readState: deps.readState,
|
|
387
|
+
readSettings: deps.readSettings,
|
|
388
|
+
setInterval: deps.setInterval,
|
|
389
|
+
clearInterval: deps.clearInterval,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const options = parseCockpitArgs(rawArgs);
|
|
394
|
+
const repoRoot = resolveRepoRoot(options.target);
|
|
395
|
+
const controlCommand = cockpitControlCommand(repoRoot);
|
|
396
|
+
const terminalBackendOptions = terminalBackendOptionsFromDeps(deps);
|
|
397
|
+
const backend = selectTerminalBackend(options.backend, terminalBackendOptions);
|
|
398
|
+
|
|
399
|
+
return openWithBackend(backend, options, repoRoot, controlCommand, { ...deps, stdout, toolName });
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
if (require.main === module) {
|
|
403
|
+
startCockpit({
|
|
404
|
+
repoPath: process.argv[2] || process.cwd(),
|
|
405
|
+
refreshMs: Number.parseInt(process.env.GUARDEX_COCKPIT_REFRESH_MS || '2000', 10),
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
module.exports = {
|
|
410
|
+
DEFAULT_SESSION_NAME,
|
|
411
|
+
DEFAULT_BACKEND,
|
|
412
|
+
DEFAULT_INTERACTIVE_BACKEND,
|
|
413
|
+
cockpitControlCommand,
|
|
414
|
+
defaultCockpitBackends,
|
|
415
|
+
parseCockpitArgs,
|
|
416
|
+
parseCockpitControlArgs,
|
|
417
|
+
openDefaultCockpit,
|
|
418
|
+
openCockpit,
|
|
419
|
+
openKittyCockpit,
|
|
420
|
+
render,
|
|
421
|
+
startCockpit,
|
|
422
|
+
...control,
|
|
423
|
+
...actions,
|
|
424
|
+
control,
|
|
425
|
+
actions,
|
|
426
|
+
};
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { PANE_MENU_ACTION_IDS } = require('./pane-menu');
|
|
4
|
+
|
|
5
|
+
const DEFAULT_ACTION_ROWS = Object.freeze(['new-agent', 'terminal', 'settings', 'shortcuts']);
|
|
6
|
+
const VALID_MODES = new Set(['main', 'menu', 'settings', 'shortcuts', 'new-agent', 'terminal', 'prompt']);
|
|
7
|
+
|
|
8
|
+
function action(type, payload = {}) {
|
|
9
|
+
return { type, payload };
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const NAVIGATION_BINDINGS = {
|
|
13
|
+
j: action('next'),
|
|
14
|
+
down: action('next'),
|
|
15
|
+
k: action('previous'),
|
|
16
|
+
up: action('previous'),
|
|
17
|
+
enter: action('view-selected'),
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const MAIN_BINDINGS = {
|
|
21
|
+
n: action('new-agent'),
|
|
22
|
+
t: action('terminal'),
|
|
23
|
+
m: action('menu'),
|
|
24
|
+
'alt-shift-m': action('menu'),
|
|
25
|
+
s: action('settings'),
|
|
26
|
+
'?': action('shortcuts'),
|
|
27
|
+
x: action(PANE_MENU_ACTION_IDS.CLOSE),
|
|
28
|
+
b: action(PANE_MENU_ACTION_IDS.CREATE_CHILD_WORKTREE),
|
|
29
|
+
f: action(PANE_MENU_ACTION_IDS.BROWSE_FILES),
|
|
30
|
+
h: action(PANE_MENU_ACTION_IDS.HIDE_PANE),
|
|
31
|
+
P: action(PANE_MENU_ACTION_IDS.PROJECT_FOCUS),
|
|
32
|
+
a: action(PANE_MENU_ACTION_IDS.ADD_AGENT),
|
|
33
|
+
A: action(PANE_MENU_ACTION_IDS.ADD_TERMINAL),
|
|
34
|
+
r: action(PANE_MENU_ACTION_IDS.REOPEN_CLOSED_WORKTREE),
|
|
35
|
+
D: action('doctor'),
|
|
36
|
+
d: action('diff'),
|
|
37
|
+
l: action('locks'),
|
|
38
|
+
y: action('sync'),
|
|
39
|
+
F: action('finish'),
|
|
40
|
+
c: action('cleanup-sessions'),
|
|
41
|
+
q: action('quit'),
|
|
42
|
+
...NAVIGATION_BINDINGS,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const BASE_BINDINGS = {
|
|
46
|
+
main: MAIN_BINDINGS,
|
|
47
|
+
menu: {
|
|
48
|
+
...NAVIGATION_BINDINGS,
|
|
49
|
+
esc: action('close-menu'),
|
|
50
|
+
q: action('quit'),
|
|
51
|
+
},
|
|
52
|
+
settings: {
|
|
53
|
+
...NAVIGATION_BINDINGS,
|
|
54
|
+
esc: action('close-settings'),
|
|
55
|
+
q: action('quit'),
|
|
56
|
+
},
|
|
57
|
+
shortcuts: {
|
|
58
|
+
esc: action('close-popup'),
|
|
59
|
+
q: action('quit'),
|
|
60
|
+
},
|
|
61
|
+
'new-agent': {
|
|
62
|
+
enter: action('agent:start'),
|
|
63
|
+
esc: action('close-popup'),
|
|
64
|
+
q: action('quit'),
|
|
65
|
+
},
|
|
66
|
+
terminal: {
|
|
67
|
+
enter: action('terminal:open'),
|
|
68
|
+
esc: action('close-popup'),
|
|
69
|
+
q: action('quit'),
|
|
70
|
+
},
|
|
71
|
+
prompt: {},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function cloneAction(binding) {
|
|
75
|
+
return action(binding.type, { ...binding.payload });
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function cloneBindings(bindings) {
|
|
79
|
+
return Object.fromEntries(
|
|
80
|
+
Object.entries(bindings).map(([mode, modeBindings]) => [
|
|
81
|
+
mode,
|
|
82
|
+
Object.fromEntries(
|
|
83
|
+
Object.entries(modeBindings).map(([key, binding]) => [key, cloneAction(binding)]),
|
|
84
|
+
),
|
|
85
|
+
]),
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultKeybindings() {
|
|
90
|
+
return cloneBindings(BASE_BINDINGS);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeMode(context = {}) {
|
|
94
|
+
return VALID_MODES.has(context.mode) ? context.mode : 'main';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function normalizeKey(key) {
|
|
98
|
+
if (key && typeof key === 'object') {
|
|
99
|
+
if ((key.meta || key.alt) && key.shift && String(key.name || key.key || '').toLowerCase() === 'm') {
|
|
100
|
+
return 'alt-shift-m';
|
|
101
|
+
}
|
|
102
|
+
return normalizeKey(key.name || key.sequence || key.key || '');
|
|
103
|
+
}
|
|
104
|
+
if (key === '\r' || key === '\n') return 'enter';
|
|
105
|
+
if (key === '\x1bM' || key === '\x1bm') return 'alt-shift-m';
|
|
106
|
+
if (key === '\x1b') return 'esc';
|
|
107
|
+
if (typeof key !== 'string') return '';
|
|
108
|
+
|
|
109
|
+
const normalized = key.trim();
|
|
110
|
+
if (normalized === '\x1bM' || normalized === '\x1bm') return 'alt-shift-m';
|
|
111
|
+
if (/^alt(?:\+|-)?shift(?:\+|-)?m$/i.test(normalized)) return 'alt-shift-m';
|
|
112
|
+
if (normalized.length === 1) return normalized;
|
|
113
|
+
|
|
114
|
+
const namedKey = normalized.toLowerCase();
|
|
115
|
+
if (namedKey === 'arrowdown') return 'down';
|
|
116
|
+
if (namedKey === 'arrowup') return 'up';
|
|
117
|
+
if (namedKey === 'return') return 'enter';
|
|
118
|
+
if (namedKey === 'escape') return 'esc';
|
|
119
|
+
return namedKey;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function resolveKeyAction(key, context = {}) {
|
|
123
|
+
const mode = normalizeMode(context);
|
|
124
|
+
const normalizedKey = normalizeKey(key);
|
|
125
|
+
const keybindings = context.keybindings || BASE_BINDINGS;
|
|
126
|
+
const binding = keybindings[mode] && keybindings[mode][normalizedKey];
|
|
127
|
+
|
|
128
|
+
if (!binding) {
|
|
129
|
+
return action('noop', { key: normalizedKey, mode });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return action(binding.type, {
|
|
133
|
+
...binding.payload,
|
|
134
|
+
key: normalizedKey,
|
|
135
|
+
mode,
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function number(value, fallback) {
|
|
140
|
+
const parsed = Number(value);
|
|
141
|
+
return Number.isFinite(parsed) ? parsed : fallback;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function wrapIndex(index, length) {
|
|
145
|
+
if (length <= 0) return 0;
|
|
146
|
+
const next = Number.isInteger(index) ? index : 0;
|
|
147
|
+
return ((next % length) + length) % length;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function actionRows(state = {}) {
|
|
151
|
+
if (!Array.isArray(state.actionRows) || state.actionRows.length === 0) {
|
|
152
|
+
return [...DEFAULT_ACTION_ROWS];
|
|
153
|
+
}
|
|
154
|
+
return state.actionRows.map((row) => String(row || '').trim()).filter(Boolean);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function moveSelection(state = {}, direction) {
|
|
158
|
+
const sessions = Array.isArray(state.sessions) ? state.sessions : [];
|
|
159
|
+
if (sessions.length > 0) {
|
|
160
|
+
return {
|
|
161
|
+
...state,
|
|
162
|
+
selectedScope: 'lane',
|
|
163
|
+
selectedIndex: wrapIndex(number(state.selectedIndex, 0) + direction, sessions.length),
|
|
164
|
+
selectedSessionId: '',
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const rows = actionRows(state);
|
|
169
|
+
return {
|
|
170
|
+
...state,
|
|
171
|
+
actionRows: rows,
|
|
172
|
+
selectedScope: 'action',
|
|
173
|
+
selectedIndex: 0,
|
|
174
|
+
actionIndex: wrapIndex(number(state.actionIndex, 0) + direction, rows.length),
|
|
175
|
+
selectedSessionId: '',
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function closeMode(state = {}) {
|
|
180
|
+
return {
|
|
181
|
+
...state,
|
|
182
|
+
mode: 'main',
|
|
183
|
+
lastIntent: null,
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function applyCockpitKey(state = {}, key) {
|
|
188
|
+
const current = {
|
|
189
|
+
...state,
|
|
190
|
+
mode: normalizeMode(state),
|
|
191
|
+
};
|
|
192
|
+
const resolved = resolveKeyAction(key, current);
|
|
193
|
+
|
|
194
|
+
switch (resolved.type) {
|
|
195
|
+
case 'next':
|
|
196
|
+
return moveSelection(current, 1);
|
|
197
|
+
case 'previous':
|
|
198
|
+
return moveSelection(current, -1);
|
|
199
|
+
case 'new-agent':
|
|
200
|
+
return { ...current, mode: 'new-agent', lastIntent: null };
|
|
201
|
+
case 'terminal':
|
|
202
|
+
return { ...current, mode: 'terminal', lastIntent: null };
|
|
203
|
+
case 'shortcuts':
|
|
204
|
+
return { ...current, mode: 'shortcuts', lastIntent: null };
|
|
205
|
+
case 'settings':
|
|
206
|
+
return { ...current, mode: 'settings', lastIntent: null };
|
|
207
|
+
case 'menu':
|
|
208
|
+
return { ...current, mode: 'menu', lastIntent: null };
|
|
209
|
+
case 'close-menu':
|
|
210
|
+
case 'close-settings':
|
|
211
|
+
case 'close-popup':
|
|
212
|
+
return closeMode(current);
|
|
213
|
+
case 'quit':
|
|
214
|
+
return { ...current, shouldExit: true };
|
|
215
|
+
default:
|
|
216
|
+
return current;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
applyCockpitKey,
|
|
222
|
+
defaultKeybindings,
|
|
223
|
+
resolveKeyAction,
|
|
224
|
+
};
|