@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,597 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const cp = require('node:child_process');
|
|
4
|
+
const path = require('node:path');
|
|
5
|
+
const { readCockpitSettings } = require('./settings');
|
|
6
|
+
const { PANE_MENU_ACTION_IDS } = require('./pane-menu');
|
|
7
|
+
|
|
8
|
+
const ACTION_ALIASES = new Map([
|
|
9
|
+
['finish-pr', 'finish'],
|
|
10
|
+
['finish / pr', 'finish'],
|
|
11
|
+
[PANE_MENU_ACTION_IDS.MERGE, 'finish'],
|
|
12
|
+
[PANE_MENU_ACTION_IDS.BROWSE_FILES, 'files'],
|
|
13
|
+
['browse files', 'files'],
|
|
14
|
+
['project focus', 'project-focus'],
|
|
15
|
+
['reopen', 'reopen-closed-worktree'],
|
|
16
|
+
['reopen closed worktree', 'reopen-closed-worktree'],
|
|
17
|
+
['copy path', 'copy-path'],
|
|
18
|
+
['open in editor', 'open-editor'],
|
|
19
|
+
['open-editor', 'open-editor'],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
const CLIPBOARD_COMMANDS = [
|
|
23
|
+
{ cmd: 'wl-copy', args: [] },
|
|
24
|
+
{ cmd: 'termux-clipboard-set', args: [] },
|
|
25
|
+
{ cmd: 'pbcopy', args: [], input: true },
|
|
26
|
+
{ cmd: 'xclip', args: ['-selection', 'clipboard'], input: true },
|
|
27
|
+
{ cmd: 'xsel', args: ['--clipboard', '--input'], input: true },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
function defaultRunCommand(cmd, args = [], options = {}) {
|
|
31
|
+
return cp.spawnSync(cmd, args, {
|
|
32
|
+
cwd: options.cwd,
|
|
33
|
+
env: options.env ? { ...process.env, ...options.env } : process.env,
|
|
34
|
+
encoding: 'utf8',
|
|
35
|
+
input: options.input,
|
|
36
|
+
stdio: 'pipe',
|
|
37
|
+
timeout: options.timeout,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function firstString(...values) {
|
|
42
|
+
for (const value of values) {
|
|
43
|
+
if (typeof value === 'string' && value.trim().length > 0) {
|
|
44
|
+
return value.trim();
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return '';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function selectedSession(context = {}) {
|
|
51
|
+
return context.session || context.selectedSession || context.lane || {};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function selectedPane(context = {}) {
|
|
55
|
+
const session = selectedSession(context);
|
|
56
|
+
return context.pane || context.selectedPane || session.pane || session.tmux || {};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeAction(action) {
|
|
60
|
+
const raw = typeof action === 'string'
|
|
61
|
+
? action
|
|
62
|
+
: firstString(action && action.id, action && action.action, action && action.type, action && action.label);
|
|
63
|
+
const normalized = String(raw || '').trim().toLowerCase();
|
|
64
|
+
return ACTION_ALIASES.get(normalized) || normalized;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function normalizeResult(result) {
|
|
68
|
+
const payload = result && typeof result === 'object' ? result : {};
|
|
69
|
+
const status = Number.isInteger(payload.status) ? payload.status : 0;
|
|
70
|
+
const ok = Object.prototype.hasOwnProperty.call(payload, 'ok')
|
|
71
|
+
? Boolean(payload.ok)
|
|
72
|
+
: !payload.error && status === 0;
|
|
73
|
+
return {
|
|
74
|
+
ok,
|
|
75
|
+
stdout: String(payload.stdout || ''),
|
|
76
|
+
stderr: payload.error ? String(payload.error.message || payload.error) : String(payload.stderr || ''),
|
|
77
|
+
message: typeof payload.message === 'string' ? payload.message : '',
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function shellQuote(value) {
|
|
82
|
+
const text = String(value);
|
|
83
|
+
if (/^[A-Za-z0-9_/:=.,@%+-]+$/.test(text)) return text;
|
|
84
|
+
return `'${text.replace(/'/g, "'\\''")}'`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function renderCommand(cmd, args = []) {
|
|
88
|
+
return [cmd, ...args].map(shellQuote).join(' ');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function resultShape({ ok, message, command = '', stdout = '', stderr = '' }) {
|
|
92
|
+
return {
|
|
93
|
+
ok: Boolean(ok),
|
|
94
|
+
message: String(message || ''),
|
|
95
|
+
command: String(command || ''),
|
|
96
|
+
stdout: String(stdout || ''),
|
|
97
|
+
stderr: String(stderr || ''),
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeOperationResult(result, fallbackMessage) {
|
|
102
|
+
if (typeof result === 'string') {
|
|
103
|
+
return resultShape({ ok: true, message: result });
|
|
104
|
+
}
|
|
105
|
+
const payload = normalizeResult(result);
|
|
106
|
+
return resultShape({
|
|
107
|
+
ok: payload.ok,
|
|
108
|
+
message: payload.message || fallbackMessage,
|
|
109
|
+
command: typeof result?.command === 'string' ? result.command : '',
|
|
110
|
+
stdout: payload.stdout,
|
|
111
|
+
stderr: payload.stderr,
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function statusMessage(label, detail = '') {
|
|
116
|
+
const suffix = detail ? ` ${detail}` : '';
|
|
117
|
+
return resultShape({
|
|
118
|
+
ok: false,
|
|
119
|
+
message: `${label} is not implemented in cockpit yet.${suffix}`,
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function resolveBranch(context = {}) {
|
|
124
|
+
const session = selectedSession(context);
|
|
125
|
+
return firstString(
|
|
126
|
+
context.branch,
|
|
127
|
+
session.branch,
|
|
128
|
+
session.lane && session.lane.branch,
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function resolveWorktreePath(context = {}) {
|
|
133
|
+
const session = selectedSession(context);
|
|
134
|
+
return firstString(
|
|
135
|
+
context.worktreePath,
|
|
136
|
+
context.path,
|
|
137
|
+
session.worktreePath,
|
|
138
|
+
session.worktree && session.worktree.path,
|
|
139
|
+
session.path,
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function resolvePaneId(context = {}) {
|
|
144
|
+
const session = selectedSession(context);
|
|
145
|
+
const pane = selectedPane(context);
|
|
146
|
+
return firstString(
|
|
147
|
+
context.paneId,
|
|
148
|
+
context.tmuxPaneId,
|
|
149
|
+
context.tmuxTarget,
|
|
150
|
+
pane.paneId,
|
|
151
|
+
pane.id,
|
|
152
|
+
pane.tmuxPaneId,
|
|
153
|
+
pane.tmuxTarget,
|
|
154
|
+
session.paneId,
|
|
155
|
+
session.tmuxPaneId,
|
|
156
|
+
session.tmuxTarget,
|
|
157
|
+
session.tmux && session.tmux.paneId,
|
|
158
|
+
session.pane && session.pane.id,
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function resolveRepoRoot(context = {}) {
|
|
163
|
+
return path.resolve(firstString(context.repoRoot, context.repoPath, context.target, process.cwd()));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
function terminalBackend(context = {}) {
|
|
167
|
+
return context.terminalBackend || context.backend || context.runtime?.terminalBackend || {};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function runtimeHooks(context = {}) {
|
|
171
|
+
return context.runtime && typeof context.runtime === 'object' ? context.runtime : {};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function operationContext(actionId, context = {}) {
|
|
175
|
+
const session = selectedSession(context);
|
|
176
|
+
const pane = selectedPane(context);
|
|
177
|
+
return {
|
|
178
|
+
actionId,
|
|
179
|
+
pane,
|
|
180
|
+
paneId: resolvePaneId(context),
|
|
181
|
+
session,
|
|
182
|
+
branch: resolveBranch(context),
|
|
183
|
+
worktreePath: resolveWorktreePath(context),
|
|
184
|
+
repoRoot: resolveRepoRoot(context),
|
|
185
|
+
runtime: runtimeHooks(context),
|
|
186
|
+
env: context.env || process.env,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function callHook(label, fn, payload, fallbackMessage) {
|
|
191
|
+
try {
|
|
192
|
+
return normalizeOperationResult(fn(payload), fallbackMessage);
|
|
193
|
+
} catch (error) {
|
|
194
|
+
return resultShape({
|
|
195
|
+
ok: false,
|
|
196
|
+
message: `${label} failed: ${error.message || error}`,
|
|
197
|
+
stderr: String(error.stack || error.message || error),
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function runCommand(context, cmd, args = [], options = {}) {
|
|
203
|
+
const runner = typeof context.runCommand === 'function' ? context.runCommand : defaultRunCommand;
|
|
204
|
+
const rendered = renderCommand(cmd, args);
|
|
205
|
+
const payload = normalizeResult(runner(cmd, args, options));
|
|
206
|
+
const detail = payload.ok ? payload.stdout : payload.stderr || payload.stdout;
|
|
207
|
+
return resultShape({
|
|
208
|
+
ok: payload.ok,
|
|
209
|
+
message: payload.ok ? 'Command completed.' : `Command failed: ${detail.trim() || rendered}`,
|
|
210
|
+
command: rendered,
|
|
211
|
+
stdout: payload.stdout,
|
|
212
|
+
stderr: payload.stderr,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function commandExists(context, cmd) {
|
|
217
|
+
if (typeof context.commandExists === 'function') {
|
|
218
|
+
return Boolean(context.commandExists(cmd));
|
|
219
|
+
}
|
|
220
|
+
const runner = typeof context.runCommand === 'function' ? context.runCommand : defaultRunCommand;
|
|
221
|
+
const result = normalizeResult(runner('which', [cmd], { cwd: resolveRepoRoot(context) }));
|
|
222
|
+
return result.ok && result.stdout.trim().length > 0;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function requireBranch(context, actionName) {
|
|
226
|
+
const branch = resolveBranch(context);
|
|
227
|
+
if (branch) return { branch };
|
|
228
|
+
return resultShape({
|
|
229
|
+
ok: false,
|
|
230
|
+
message: `${actionName} requires a selected lane branch.`,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function requireWorktreePath(context, actionName) {
|
|
235
|
+
const worktreePath = resolveWorktreePath(context);
|
|
236
|
+
if (worktreePath) return { worktreePath };
|
|
237
|
+
return resultShape({
|
|
238
|
+
ok: false,
|
|
239
|
+
message: `${actionName} requires a selected lane worktree path.`,
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function runGxAgentsInspect(subcommand, context) {
|
|
244
|
+
const required = requireBranch(context, subcommand);
|
|
245
|
+
if (!required.branch) return required;
|
|
246
|
+
return runCommand(
|
|
247
|
+
context,
|
|
248
|
+
context.gxCommand || 'gx',
|
|
249
|
+
['agents', subcommand, '--target', resolveRepoRoot(context), '--branch', required.branch],
|
|
250
|
+
{ cwd: resolveRepoRoot(context) },
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function runView(context) {
|
|
255
|
+
const backend = terminalBackend(context);
|
|
256
|
+
const payload = operationContext(PANE_MENU_ACTION_IDS.VIEW, context);
|
|
257
|
+
if (typeof backend.focusPane === 'function') {
|
|
258
|
+
return callHook('View', backend.focusPane.bind(backend), payload, 'Focused pane.');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
const paneId = payload.paneId;
|
|
262
|
+
if (paneId) {
|
|
263
|
+
return runCommand(context, context.tmuxCommand || 'tmux', ['select-pane', '-t', paneId], {
|
|
264
|
+
cwd: payload.repoRoot,
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return runGxAgentsInspect('files', context);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function runHidePane(context) {
|
|
272
|
+
const backend = terminalBackend(context);
|
|
273
|
+
const payload = operationContext(PANE_MENU_ACTION_IDS.HIDE_PANE, context);
|
|
274
|
+
if (typeof backend.hidePane === 'function') {
|
|
275
|
+
return callHook('Hide Pane', backend.hidePane.bind(backend), payload, 'Hid pane.');
|
|
276
|
+
}
|
|
277
|
+
if (typeof backend.isolatePane === 'function') {
|
|
278
|
+
return callHook('Hide Pane', backend.isolatePane.bind(backend), payload, 'Isolated pane.');
|
|
279
|
+
}
|
|
280
|
+
return statusMessage('Hide Pane', 'The selected terminal backend does not support hide/isolate. No pane/worktree state was changed.');
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function runSync(context) {
|
|
284
|
+
const required = requireWorktreePath(context, 'Sync');
|
|
285
|
+
if (!required.worktreePath) return required;
|
|
286
|
+
const args = ['sync', '--target', required.worktreePath];
|
|
287
|
+
const base = firstString(context.base, context.baseBranch, selectedSession(context).base);
|
|
288
|
+
if (base) args.push('--base', base);
|
|
289
|
+
return runCommand(context, context.gxCommand || 'gx', args, { cwd: required.worktreePath });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
function runFinish(context) {
|
|
293
|
+
const required = requireBranch(context, 'Finish');
|
|
294
|
+
if (!required.branch) return required;
|
|
295
|
+
return runCommand(
|
|
296
|
+
context,
|
|
297
|
+
context.gxCommand || 'gx',
|
|
298
|
+
[
|
|
299
|
+
'agents',
|
|
300
|
+
'finish',
|
|
301
|
+
'--target',
|
|
302
|
+
resolveRepoRoot(context),
|
|
303
|
+
'--branch',
|
|
304
|
+
required.branch,
|
|
305
|
+
'--via-pr',
|
|
306
|
+
'--wait-for-merge',
|
|
307
|
+
'--cleanup',
|
|
308
|
+
],
|
|
309
|
+
{ cwd: resolveRepoRoot(context) },
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function runCreatePr(context) {
|
|
314
|
+
const hooks = runtimeHooks(context);
|
|
315
|
+
const safety = context.safety && typeof context.safety === 'object' ? context.safety : {};
|
|
316
|
+
const createPr = context.createPullRequest || hooks.createPullRequest || safety.createPullRequest;
|
|
317
|
+
if (typeof createPr === 'function') {
|
|
318
|
+
return callHook('Create GitHub PR', createPr, operationContext(PANE_MENU_ACTION_IDS.CREATE_PR, context), 'Started guarded PR creation.');
|
|
319
|
+
}
|
|
320
|
+
return statusMessage('Create GitHub PR', 'Use the guarded PR-only finish flow until PR-only creation is wired.');
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function runClose(context) {
|
|
324
|
+
const backend = terminalBackend(context);
|
|
325
|
+
const payload = operationContext(PANE_MENU_ACTION_IDS.CLOSE, context);
|
|
326
|
+
if (typeof backend.closePane === 'function') {
|
|
327
|
+
return callHook('Close', backend.closePane.bind(backend), payload, 'Closed pane.');
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const paneId = payload.paneId;
|
|
331
|
+
if (!paneId) {
|
|
332
|
+
return resultShape({
|
|
333
|
+
ok: false,
|
|
334
|
+
message: 'Close requires an associated tmux pane; branch, worktree, and session metadata were left untouched.',
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
const result = runCommand(context, context.tmuxCommand || 'tmux', ['kill-pane', '-t', paneId], {
|
|
338
|
+
cwd: payload.repoRoot,
|
|
339
|
+
});
|
|
340
|
+
return {
|
|
341
|
+
...result,
|
|
342
|
+
message: result.ok
|
|
343
|
+
? 'Closed associated tmux pane only; branch, worktree, and session metadata were left untouched.'
|
|
344
|
+
: result.message,
|
|
345
|
+
};
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function resolveClipboardCommand(context) {
|
|
349
|
+
if (context.clipboardCommand && typeof context.clipboardCommand === 'object') {
|
|
350
|
+
return {
|
|
351
|
+
cmd: context.clipboardCommand.cmd,
|
|
352
|
+
args: Array.isArray(context.clipboardCommand.args) ? context.clipboardCommand.args : [],
|
|
353
|
+
input: Boolean(context.clipboardCommand.input),
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
if (typeof context.clipboardCommand === 'string' && context.clipboardCommand.trim()) {
|
|
357
|
+
return { cmd: context.clipboardCommand.trim(), args: [], input: true };
|
|
358
|
+
}
|
|
359
|
+
return CLIPBOARD_COMMANDS.find((candidate) => commandExists(context, candidate.cmd)) || null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function runCopyPath(context) {
|
|
363
|
+
const required = requireWorktreePath(context, 'Copy Path');
|
|
364
|
+
if (!required.worktreePath) return required;
|
|
365
|
+
const clipboard = resolveClipboardCommand(context);
|
|
366
|
+
if (!clipboard) {
|
|
367
|
+
return resultShape({
|
|
368
|
+
ok: true,
|
|
369
|
+
message: 'No clipboard utility found; printed worktree path.',
|
|
370
|
+
command: renderCommand('printf', ['%s\\n', required.worktreePath]),
|
|
371
|
+
stdout: `${required.worktreePath}\n`,
|
|
372
|
+
});
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
const args = clipboard.input ? clipboard.args : [...clipboard.args, required.worktreePath];
|
|
376
|
+
const result = runCommand(context, clipboard.cmd, args, {
|
|
377
|
+
cwd: required.worktreePath,
|
|
378
|
+
input: clipboard.input ? required.worktreePath : undefined,
|
|
379
|
+
});
|
|
380
|
+
return {
|
|
381
|
+
...result,
|
|
382
|
+
message: result.ok ? 'Copied worktree path.' : result.message,
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
function splitCommand(rawCommand) {
|
|
387
|
+
const parts = [];
|
|
388
|
+
let current = '';
|
|
389
|
+
let quote = '';
|
|
390
|
+
let escaped = false;
|
|
391
|
+
|
|
392
|
+
for (const char of String(rawCommand || '')) {
|
|
393
|
+
if (escaped) {
|
|
394
|
+
current += char;
|
|
395
|
+
escaped = false;
|
|
396
|
+
continue;
|
|
397
|
+
}
|
|
398
|
+
if (char === '\\') {
|
|
399
|
+
escaped = true;
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
if (quote) {
|
|
403
|
+
if (char === quote) {
|
|
404
|
+
quote = '';
|
|
405
|
+
} else {
|
|
406
|
+
current += char;
|
|
407
|
+
}
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (char === '"' || char === "'") {
|
|
411
|
+
quote = char;
|
|
412
|
+
continue;
|
|
413
|
+
}
|
|
414
|
+
if (/\s/.test(char)) {
|
|
415
|
+
if (current) {
|
|
416
|
+
parts.push(current);
|
|
417
|
+
current = '';
|
|
418
|
+
}
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
current += char;
|
|
422
|
+
}
|
|
423
|
+
if (current) parts.push(current);
|
|
424
|
+
return parts;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
function resolveEditorParts(context, worktreePath) {
|
|
428
|
+
const settings = context.settings && typeof context.settings === 'object'
|
|
429
|
+
? context.settings
|
|
430
|
+
: readCockpitSettings(resolveRepoRoot(context));
|
|
431
|
+
const configured = firstString(settings.editorCommand);
|
|
432
|
+
if (configured) return splitCommand(configured);
|
|
433
|
+
if (commandExists(context, 'code')) return ['code'];
|
|
434
|
+
|
|
435
|
+
return {
|
|
436
|
+
printOnly: true,
|
|
437
|
+
parts: ['code', worktreePath],
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function runOpenEditor(context) {
|
|
442
|
+
const required = requireWorktreePath(context, 'Open in Editor');
|
|
443
|
+
if (!required.worktreePath) return required;
|
|
444
|
+
const resolved = resolveEditorParts(context, required.worktreePath);
|
|
445
|
+
if (resolved.printOnly) {
|
|
446
|
+
return resultShape({
|
|
447
|
+
ok: true,
|
|
448
|
+
message: 'No editor command configured and code was not found; printed editor command.',
|
|
449
|
+
command: renderCommand(resolved.parts[0], resolved.parts.slice(1)),
|
|
450
|
+
stdout: `${renderCommand(resolved.parts[0], resolved.parts.slice(1))}\n`,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
const [cmd, ...args] = resolved;
|
|
455
|
+
const result = runCommand(context, cmd, [...args, required.worktreePath], {
|
|
456
|
+
cwd: required.worktreePath,
|
|
457
|
+
});
|
|
458
|
+
return {
|
|
459
|
+
...result,
|
|
460
|
+
message: result.ok ? 'Opened worktree in editor.' : result.message,
|
|
461
|
+
};
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
function runCreateChildWorktree(context) {
|
|
465
|
+
const required = requireWorktreePath(context, 'Create Child Worktree');
|
|
466
|
+
if (!required.worktreePath) return required;
|
|
467
|
+
const hooks = runtimeHooks(context);
|
|
468
|
+
const createChild = context.createChildWorktree || hooks.createChildWorktree;
|
|
469
|
+
if (typeof createChild === 'function') {
|
|
470
|
+
return callHook('Create Child Worktree', createChild, operationContext(PANE_MENU_ACTION_IDS.CREATE_CHILD_WORKTREE, context), 'Created child worktree through safe workflow.');
|
|
471
|
+
}
|
|
472
|
+
return statusMessage('Create Child Worktree', 'No safe child-worktree workflow is available. No child branch or worktree was created.');
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function runAddTerminal(context) {
|
|
476
|
+
const backend = terminalBackend(context);
|
|
477
|
+
const payload = operationContext(PANE_MENU_ACTION_IDS.ADD_TERMINAL, context);
|
|
478
|
+
if (typeof backend.launchTerminalPane === 'function') {
|
|
479
|
+
return callHook('Add Terminal to Worktree', backend.launchTerminalPane.bind(backend), payload, 'Launched terminal pane.');
|
|
480
|
+
}
|
|
481
|
+
return statusMessage('Add Terminal to Worktree', 'The selected terminal backend does not support pane launch. No terminal pane was created.');
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function resolveStartAgentLane(context) {
|
|
485
|
+
const hooks = runtimeHooks(context);
|
|
486
|
+
if (typeof context.startAgentLane === 'function') {
|
|
487
|
+
return { startAgentLane: context.startAgentLane, injected: true };
|
|
488
|
+
}
|
|
489
|
+
if (typeof hooks.startAgentLane === 'function') {
|
|
490
|
+
return { startAgentLane: hooks.startAgentLane, injected: true };
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
return null;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function runAddAgent(context) {
|
|
497
|
+
const required = requireWorktreePath(context, 'Add Agent to Worktree');
|
|
498
|
+
if (!required.worktreePath) return required;
|
|
499
|
+
const hooks = runtimeHooks(context);
|
|
500
|
+
const resolved = resolveStartAgentLane(context);
|
|
501
|
+
if (!resolved) {
|
|
502
|
+
return statusMessage('Add Agent to Worktree', 'No safe agent launch workflow is available. No agent branch or worktree was created.');
|
|
503
|
+
}
|
|
504
|
+
const payload = operationContext(PANE_MENU_ACTION_IDS.ADD_AGENT, context);
|
|
505
|
+
const request = {
|
|
506
|
+
repoRoot: payload.repoRoot,
|
|
507
|
+
worktreePath: payload.worktreePath,
|
|
508
|
+
task: firstString(context.task, `agent for ${payload.branch || payload.worktreePath}`),
|
|
509
|
+
agent: firstString(context.agent, context.defaultAgent, hooks.defaultAgent, 'codex'),
|
|
510
|
+
base: firstString(context.base, payload.branch, context.baseBranch),
|
|
511
|
+
claims: Array.isArray(context.claims) ? context.claims : [],
|
|
512
|
+
metadata: {
|
|
513
|
+
parentBranch: payload.branch,
|
|
514
|
+
parentWorktreePath: payload.worktreePath,
|
|
515
|
+
source: 'cockpit-pane-menu',
|
|
516
|
+
},
|
|
517
|
+
};
|
|
518
|
+
return callHook('Add Agent to Worktree', () => resolved.startAgentLane(request, payload), 'Started safe agent launch workflow.');
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
const PANE_ACTION_HANDLERS = Object.freeze({
|
|
522
|
+
view: runView,
|
|
523
|
+
'hide-pane': runHidePane,
|
|
524
|
+
files: (context) => runGxAgentsInspect('files', context),
|
|
525
|
+
diff: (context) => runGxAgentsInspect('diff', context),
|
|
526
|
+
locks: (context) => runGxAgentsInspect('locks', context),
|
|
527
|
+
sync: runSync,
|
|
528
|
+
finish: runFinish,
|
|
529
|
+
'create-pr': runCreatePr,
|
|
530
|
+
'project-focus': () => statusMessage('Project Focus', 'Project visibility state was left unchanged.'),
|
|
531
|
+
close: runClose,
|
|
532
|
+
rename: () => statusMessage('Rename', 'Pane metadata was left unchanged.'),
|
|
533
|
+
'copy-path': runCopyPath,
|
|
534
|
+
'open-editor': runOpenEditor,
|
|
535
|
+
'toggle-autopilot': () => statusMessage('Toggle Autopilot', 'Autopilot settings were left unchanged.'),
|
|
536
|
+
'create-child-worktree': runCreateChildWorktree,
|
|
537
|
+
'add-terminal': runAddTerminal,
|
|
538
|
+
'add-agent': runAddAgent,
|
|
539
|
+
'reopen-closed-worktree': () => statusMessage('Reopen Closed Worktree', 'No closed worktree was restored.'),
|
|
540
|
+
'terminal:open': runAddTerminal,
|
|
541
|
+
'agent:start': runAddAgent,
|
|
542
|
+
});
|
|
543
|
+
|
|
544
|
+
const COCKPIT_INTENT_ALIASES = Object.freeze({
|
|
545
|
+
'terminal:open': 'add-terminal',
|
|
546
|
+
'agent:start': 'add-agent',
|
|
547
|
+
});
|
|
548
|
+
|
|
549
|
+
function dispatchCockpitIntent(intent, context = {}) {
|
|
550
|
+
if (!intent || typeof intent !== 'object' || !intent.type) {
|
|
551
|
+
return resultShape({ ok: false, message: 'No cockpit intent to dispatch.' });
|
|
552
|
+
}
|
|
553
|
+
const aliased = COCKPIT_INTENT_ALIASES[intent.type] || intent.type;
|
|
554
|
+
const merged = {
|
|
555
|
+
...context,
|
|
556
|
+
...intent,
|
|
557
|
+
sessionId: intent.sessionId || context.sessionId,
|
|
558
|
+
branch: intent.branch || context.branch,
|
|
559
|
+
worktreePath: intent.worktreePath || context.worktreePath,
|
|
560
|
+
task: intent.task || context.task,
|
|
561
|
+
agent: intent.agent || context.agent,
|
|
562
|
+
base: intent.base || context.base,
|
|
563
|
+
};
|
|
564
|
+
return dispatchPaneAction(aliased, merged);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function dispatchPaneAction(action, context = {}) {
|
|
568
|
+
const normalized = normalizeAction(action);
|
|
569
|
+
const handler = PANE_ACTION_HANDLERS[normalized];
|
|
570
|
+
if (!handler) {
|
|
571
|
+
return resultShape({
|
|
572
|
+
ok: false,
|
|
573
|
+
message: `Unknown cockpit action: ${normalized || '(empty)'}`,
|
|
574
|
+
});
|
|
575
|
+
}
|
|
576
|
+
try {
|
|
577
|
+
return handler(context);
|
|
578
|
+
} catch (error) {
|
|
579
|
+
return resultShape({
|
|
580
|
+
ok: false,
|
|
581
|
+
message: `Cockpit action failed: ${error.message || error}`,
|
|
582
|
+
stderr: String(error.stack || error.message || error),
|
|
583
|
+
});
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
module.exports = {
|
|
588
|
+
COCKPIT_INTENT_ALIASES,
|
|
589
|
+
PANE_ACTION_HANDLERS,
|
|
590
|
+
dispatchCockpitIntent,
|
|
591
|
+
dispatchPaneAction,
|
|
592
|
+
normalizeAction,
|
|
593
|
+
operationContext,
|
|
594
|
+
renderCommand,
|
|
595
|
+
runCockpitAction: dispatchPaneAction,
|
|
596
|
+
splitCommand,
|
|
597
|
+
};
|