@imdeadpool/guardex 7.0.41 → 7.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (118) hide show
  1. package/README.md +94 -13
  2. package/package.json +3 -1
  3. package/skills/gitguardex/SKILL.md +13 -0
  4. package/skills/guardex-merge-skills-to-dev/SKILL.md +59 -0
  5. package/skills/gx-act/SKILL.md +82 -0
  6. package/src/agents/cleanup-sessions.js +126 -0
  7. package/src/agents/finish.js +172 -0
  8. package/src/agents/inspect.js +202 -0
  9. package/src/agents/launch.js +249 -0
  10. package/src/agents/registry.js +133 -0
  11. package/src/agents/selection-panel.js +571 -0
  12. package/src/agents/sessions.js +151 -0
  13. package/src/agents/start.js +591 -0
  14. package/src/agents/status.js +146 -0
  15. package/src/agents/terminal.js +152 -0
  16. package/src/budget/index.js +344 -0
  17. package/src/ci-init/index.js +265 -0
  18. package/src/cli/args.js +357 -3
  19. package/src/cli/commands/agents.js +364 -0
  20. package/src/cli/commands/bootstrap.js +92 -0
  21. package/src/cli/commands/branch.js +127 -0
  22. package/src/cli/commands/claude.js +674 -0
  23. package/src/cli/commands/doctor.js +268 -0
  24. package/src/cli/commands/finish.js +26 -0
  25. package/src/cli/commands/mcp.js +122 -0
  26. package/src/cli/commands/misc.js +304 -0
  27. package/src/cli/commands/pr.js +439 -0
  28. package/src/cli/commands/prompt.js +92 -0
  29. package/src/cli/commands/release.js +305 -0
  30. package/src/cli/commands/report.js +244 -0
  31. package/src/cli/commands/review.js +32 -0
  32. package/src/cli/commands/setup.js +242 -0
  33. package/src/cli/commands/status.js +338 -0
  34. package/src/cli/commands/watch.js +234 -0
  35. package/src/cli/main.js +85 -3613
  36. package/src/cli/shared/repo-env.js +161 -0
  37. package/src/cli/shared/sandbox.js +417 -0
  38. package/src/cli/shared/scaffolding.js +535 -0
  39. package/src/cli/shared/toolchain-shims.js +420 -0
  40. package/src/cockpit/action-runner.js +3 -0
  41. package/src/cockpit/actions.js +80 -0
  42. package/src/cockpit/control.js +1121 -0
  43. package/src/cockpit/index.js +426 -0
  44. package/src/cockpit/kitty-layout.js +549 -0
  45. package/src/cockpit/kitty-tree.js +144 -0
  46. package/src/cockpit/logs-reader.js +182 -0
  47. package/src/cockpit/menu.js +204 -0
  48. package/src/cockpit/pane-actions.js +597 -0
  49. package/src/cockpit/pane-menu.js +387 -0
  50. package/src/cockpit/projects-finder.js +178 -0
  51. package/src/cockpit/render.js +215 -0
  52. package/src/cockpit/settings-render.js +128 -0
  53. package/src/cockpit/settings.js +124 -0
  54. package/src/cockpit/shortcuts.js +24 -0
  55. package/src/cockpit/sidebar.js +311 -0
  56. package/src/cockpit/state.js +72 -0
  57. package/src/cockpit/theme.js +128 -0
  58. package/src/cockpit/welcome.js +266 -0
  59. package/src/context.js +304 -43
  60. package/src/core/runtime.js +6 -1
  61. package/src/doctor/index.js +45 -15
  62. package/src/finish/index.js +186 -7
  63. package/src/finish/preflight.js +177 -0
  64. package/src/finish/review-gate.js +182 -0
  65. package/src/git/index.js +511 -4
  66. package/src/hooks/index.js +0 -64
  67. package/src/kitty/command.js +101 -0
  68. package/src/kitty/runtime.js +250 -0
  69. package/src/mcp/collect.js +370 -0
  70. package/src/mcp/server.js +157 -0
  71. package/src/output/index.js +68 -2
  72. package/src/pr-review.js +264 -0
  73. package/src/pr.js +381 -0
  74. package/src/sandbox/index.js +13 -2
  75. package/src/scaffold/agent-worktree-prep.js +213 -0
  76. package/src/scaffold/index.js +127 -10
  77. package/src/speckit/index.js +226 -0
  78. package/src/submodule/index.js +288 -0
  79. package/src/terminal/index.js +45 -0
  80. package/src/terminal/kitty.js +622 -0
  81. package/src/terminal/tmux.js +125 -0
  82. package/src/tmux/command.js +27 -0
  83. package/src/tmux/session.js +89 -0
  84. package/src/toolchain/index.js +20 -0
  85. package/templates/AGENTS.monorepo-apps.md +26 -0
  86. package/templates/AGENTS.multiagent-safety.md +63 -323
  87. package/templates/AGENTS.multiagent-safety.min.md +11 -0
  88. package/templates/codex/skills/gitguardex/SKILL.md +2 -0
  89. package/templates/codex/skills/gx-act/SKILL.md +82 -0
  90. package/templates/githooks/pre-commit +44 -20
  91. package/templates/github/workflows/README.md +87 -0
  92. package/templates/github/workflows/ci-full.yml +55 -0
  93. package/templates/github/workflows/ci.yml +56 -0
  94. package/templates/github/workflows/cr.yml +20 -1
  95. package/templates/scripts/agent-branch-finish.sh +519 -23
  96. package/templates/scripts/agent-branch-merge.sh +4 -1
  97. package/templates/scripts/agent-branch-start.sh +176 -24
  98. package/templates/scripts/agent-preflight.sh +115 -0
  99. package/templates/scripts/agent-worktree-prune.sh +96 -5
  100. package/templates/scripts/codex-agent.sh +41 -97
  101. package/templates/scripts/openspec/init-plan-workspace.sh +43 -0
  102. package/templates/scripts/review-bot-watch.sh +31 -2
  103. package/templates/scripts/agent-session-state.js +0 -171
  104. package/templates/scripts/install-vscode-active-agents-extension.js +0 -135
  105. package/templates/vscode/guardex-active-agents/README.md +0 -34
  106. package/templates/vscode/guardex-active-agents/extension.js +0 -3782
  107. package/templates/vscode/guardex-active-agents/fileicons/gitguardex-fileicons.json +0 -54
  108. package/templates/vscode/guardex-active-agents/fileicons/icons/agent.svg +0 -5
  109. package/templates/vscode/guardex-active-agents/fileicons/icons/branch.svg +0 -7
  110. package/templates/vscode/guardex-active-agents/fileicons/icons/config.svg +0 -4
  111. package/templates/vscode/guardex-active-agents/fileicons/icons/hook.svg +0 -4
  112. package/templates/vscode/guardex-active-agents/fileicons/icons/openspec.svg +0 -5
  113. package/templates/vscode/guardex-active-agents/fileicons/icons/plan.svg +0 -4
  114. package/templates/vscode/guardex-active-agents/fileicons/icons/spec.svg +0 -5
  115. package/templates/vscode/guardex-active-agents/icon.png +0 -0
  116. package/templates/vscode/guardex-active-agents/media/active-agents-hivemind.svg +0 -14
  117. package/templates/vscode/guardex-active-agents/package.json +0 -169
  118. package/templates/vscode/guardex-active-agents/session-schema.js +0 -1348
@@ -0,0 +1,549 @@
1
+ 'use strict';
2
+
3
+ const { readCockpitSettings } = require('./settings');
4
+ const { readCockpitState } = require('./state');
5
+ const kittyRuntime = require('../kitty/runtime');
6
+ const kittyTerminal = require('../terminal/kitty');
7
+
8
+ const DEFAULT_SESSION_NAME = 'guardex';
9
+ const DEFAULT_COLUMNS = 120;
10
+ const DEFAULT_KITTY_BIN = 'kitty';
11
+ const DEFAULT_WELCOME_COMMAND = 'gx';
12
+
13
+ function text(value, fallback = '') {
14
+ if (typeof value === 'string') return value.trim() || fallback;
15
+ if (value === null || value === undefined) return fallback;
16
+ return String(value).trim() || fallback;
17
+ }
18
+
19
+ function requireText(value, name) {
20
+ const normalized = text(value);
21
+ if (!normalized) {
22
+ throw new TypeError(`${name} must be a non-empty string`);
23
+ }
24
+ return normalized;
25
+ }
26
+
27
+ function firstText(...values) {
28
+ for (const value of values) {
29
+ const normalized = text(value);
30
+ if (normalized) return normalized;
31
+ }
32
+ return '';
33
+ }
34
+
35
+ function positiveInteger(value, fallback) {
36
+ const parsed = Number.parseInt(String(value), 10);
37
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
38
+ }
39
+
40
+ function shellQuote(value) {
41
+ return `'${String(value).replace(/'/g, "'\\''")}'`;
42
+ }
43
+
44
+ function commandShape(args, kittyBin = DEFAULT_KITTY_BIN) {
45
+ return {
46
+ cmd: text(kittyBin, DEFAULT_KITTY_BIN),
47
+ args,
48
+ };
49
+ }
50
+
51
+ function appendShellCommand(args, command) {
52
+ const normalized = text(command);
53
+ if (normalized) {
54
+ args.push('--', 'sh', '-lc', normalized);
55
+ }
56
+ return args;
57
+ }
58
+
59
+ function launchCommand(window, kittyBin) {
60
+ const args = [
61
+ '@',
62
+ 'launch',
63
+ '--type=window',
64
+ ];
65
+ if (window.location) {
66
+ args.push(`--location=${window.location}`);
67
+ }
68
+ args.push(
69
+ '--cwd',
70
+ window.cwd,
71
+ '--title',
72
+ window.title,
73
+ );
74
+ appendShellCommand(args, window.command);
75
+ return commandShape(args, kittyBin);
76
+ }
77
+
78
+ function focusCommand(window, kittyBin) {
79
+ return commandShape(['@', 'focus-window', '--match', window.match], kittyBin);
80
+ }
81
+
82
+ function matchTitle(title) {
83
+ return `title:${title}`;
84
+ }
85
+
86
+ function agentId(agent, index) {
87
+ return firstText(
88
+ agent.id,
89
+ agent.sessionId,
90
+ agent.agentId,
91
+ agent.branch,
92
+ `agent-${index + 1}`,
93
+ );
94
+ }
95
+
96
+ function agentLabel(agent, index) {
97
+ const explicitTitle = text(agent.title);
98
+ if (explicitTitle) return explicitTitle;
99
+ const id = agentId(agent, index);
100
+ const label = firstText(
101
+ agent.label,
102
+ agent.agentName,
103
+ agent.agent,
104
+ agent.name,
105
+ );
106
+ if (label && id && label !== id) return `${label} ${id}`;
107
+ return firstText(
108
+ label,
109
+ id,
110
+ `agent-${index + 1}`,
111
+ );
112
+ }
113
+
114
+ function agentTitle(agent, index) {
115
+ return `${String(index + 1).padStart(2, '0')}: ${agentLabel(agent, index)}`;
116
+ }
117
+
118
+ function objectValue(value) {
119
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
120
+ }
121
+
122
+ function canonicalSessions(state = {}) {
123
+ const source = objectValue(state);
124
+ const agentsStatus = objectValue(source.agentsStatus);
125
+ if (Array.isArray(agentsStatus.sessions)) return agentsStatus.sessions;
126
+ if (Array.isArray(source.sessions)) return source.sessions;
127
+ if (Array.isArray(source.agents)) return source.agents;
128
+ return [];
129
+ }
130
+
131
+ function isInactiveSession(session = {}) {
132
+ const status = firstText(session.status, session.activity).toLowerCase();
133
+ return new Set([
134
+ 'closed',
135
+ 'complete',
136
+ 'completed',
137
+ 'dead',
138
+ 'done',
139
+ 'exited',
140
+ 'finished',
141
+ 'merged',
142
+ 'stopped',
143
+ ]).has(status);
144
+ }
145
+
146
+ function repoRootFrom(state = {}, settings = {}) {
147
+ const agentsStatus = objectValue(state.agentsStatus);
148
+ return requireText(
149
+ firstText(settings.repoRoot, settings.repoPath, state.repoRoot, state.repoPath, agentsStatus.repoRoot),
150
+ 'repoRoot',
151
+ );
152
+ }
153
+
154
+ function laneShellCommand(session = {}) {
155
+ const label = firstText(session.branch, session.id, session.sessionId, 'agent lane');
156
+ return `printf '%s\\n' ${shellQuote(`GitGuardex cockpit lane: ${label}`)}; exec \${SHELL:-bash}`;
157
+ }
158
+
159
+ function normalizeCockpitSession(session, index, repoRoot, total) {
160
+ const source = objectValue(session);
161
+ const worktree = firstText(source.worktreePath, source.worktree, source.path, source.cwd);
162
+ const missingWorktree = !worktree || source.worktreeExists === false;
163
+ const cwd = requireText(missingWorktree ? repoRoot : worktree, `sessions[${index}].cwd`);
164
+ const title = agentTitle({
165
+ ...source,
166
+ agentName: firstText(source.agentName, source.agent),
167
+ }, index);
168
+
169
+ return {
170
+ id: agentId(source, index),
171
+ index,
172
+ total,
173
+ role: 'agent',
174
+ title,
175
+ cwd,
176
+ worktree,
177
+ worktreeExists: !missingWorktree,
178
+ missingWorktree,
179
+ branch: text(source.branch),
180
+ status: text(source.status),
181
+ activity: text(source.activity),
182
+ task: text(source.task),
183
+ metadata: objectValue(source.metadata),
184
+ launchCommand: text(source.launchCommand),
185
+ command: laneShellCommand(source),
186
+ match: matchTitle(title),
187
+ location: index === 0 ? 'vsplit' : 'hsplit',
188
+ };
189
+ }
190
+
191
+ function shouldShowDetailsPane(settings = {}) {
192
+ return Boolean(
193
+ settings.showDetailsPane ||
194
+ settings.detailsPane ||
195
+ settings.showLogPane ||
196
+ settings.logPane ||
197
+ settings.bottomPane,
198
+ );
199
+ }
200
+
201
+ function detailsPaneCommand(repoRoot, settings = {}) {
202
+ return text(settings.detailsCommand || settings.logCommand, `gx agents status --target ${shellQuote(repoRoot)}`);
203
+ }
204
+
205
+ function normalizeAgent(agent, index, repoRoot, total) {
206
+ const source = agent && typeof agent === 'object' ? agent : {};
207
+ const cwd = requireText(
208
+ firstText(source.cwd, source.worktree, source.worktreePath, source.path, repoRoot),
209
+ `agents[${index}].cwd`,
210
+ );
211
+ const title = agentTitle(source, index);
212
+ return {
213
+ id: agentId(source, index),
214
+ index,
215
+ total,
216
+ title,
217
+ cwd,
218
+ worktree: firstText(source.worktree, source.worktreePath, source.path, source.cwd),
219
+ command: firstText(source.command, source.launchCommand, source.shellCommand, 'exec ${SHELL:-bash}'),
220
+ branch: text(source.branch),
221
+ match: matchTitle(title),
222
+ };
223
+ }
224
+
225
+ function buildKittyCockpitPlan(state = {}, settings = {}) {
226
+ const normalizedState = objectValue(state);
227
+ const normalizedSettings = objectValue(settings);
228
+ const repoRoot = repoRootFrom(normalizedState, normalizedSettings);
229
+ const sessionName = text(normalizedSettings.sessionName, DEFAULT_SESSION_NAME);
230
+ const columns = positiveInteger(normalizedSettings.columns, DEFAULT_COLUMNS);
231
+ const kittyBin = text(normalizedSettings.kittyBin, DEFAULT_KITTY_BIN);
232
+ const controlCommand = text(
233
+ normalizedSettings.controlCommand,
234
+ `gx cockpit control --target ${shellQuote(repoRoot)}`,
235
+ );
236
+ const activeSessions = canonicalSessions(normalizedState).filter((session) => !isInactiveSession(session));
237
+ const agentWindows = activeSessions.map((session, index) => (
238
+ normalizeCockpitSession(session, index, repoRoot, activeSessions.length)
239
+ ));
240
+ const hasAgents = agentWindows.length > 0;
241
+ const controlTitle = text(
242
+ normalizedSettings.controlTitle,
243
+ hasAgents ? `${sessionName}: control` : `${sessionName}: welcome`,
244
+ );
245
+ const controlWindow = {
246
+ id: 'control',
247
+ role: 'control',
248
+ title: controlTitle,
249
+ cwd: repoRoot,
250
+ command: controlCommand,
251
+ match: matchTitle(controlTitle),
252
+ persistent: true,
253
+ welcome: !hasAgents,
254
+ };
255
+
256
+ const detailWindow = hasAgents && shouldShowDetailsPane(normalizedSettings)
257
+ ? {
258
+ id: 'details',
259
+ role: 'details',
260
+ title: `${sessionName}: details`,
261
+ cwd: repoRoot,
262
+ command: detailsPaneCommand(repoRoot, normalizedSettings),
263
+ match: matchTitle(`${sessionName}: details`),
264
+ location: 'hsplit',
265
+ }
266
+ : null;
267
+
268
+ const steps = [
269
+ {
270
+ id: 'launch-control',
271
+ role: 'control',
272
+ action: 'launch',
273
+ window: controlWindow,
274
+ command: launchCommand(controlWindow, kittyBin),
275
+ },
276
+ ...agentWindows.map((window) => ({
277
+ id: `launch-agent-${window.index + 1}`,
278
+ role: 'agent',
279
+ action: 'launch',
280
+ agentId: window.id,
281
+ window,
282
+ command: launchCommand(window, kittyBin),
283
+ })),
284
+ ];
285
+
286
+ if (detailWindow) {
287
+ steps.push({
288
+ id: 'launch-details',
289
+ role: 'details',
290
+ action: 'launch',
291
+ window: detailWindow,
292
+ command: launchCommand(detailWindow, kittyBin),
293
+ });
294
+ }
295
+
296
+ if (normalizedSettings.focusControl !== false) {
297
+ steps.push({
298
+ id: 'focus-control',
299
+ role: 'control',
300
+ action: 'focus',
301
+ window: controlWindow,
302
+ command: focusCommand(controlWindow, kittyBin),
303
+ });
304
+ }
305
+
306
+ const panes = [controlWindow, ...agentWindows, ...(detailWindow ? [detailWindow] : [])];
307
+
308
+ return {
309
+ schemaVersion: 1,
310
+ backend: 'kitty',
311
+ dryRun: Boolean(normalizedSettings.dryRun),
312
+ sessionName,
313
+ repoRoot,
314
+ columns,
315
+ welcome: !hasAgents,
316
+ controlPaneCommand: controlWindow.command,
317
+ agentPaneCommands: agentWindows.map((window) => ({
318
+ id: window.id,
319
+ title: window.title,
320
+ cwd: window.cwd,
321
+ worktree: window.worktree,
322
+ command: window.command,
323
+ missingWorktree: window.missingWorktree,
324
+ })),
325
+ titles: panes.map((pane) => pane.title),
326
+ workingDirectories: panes.map((pane) => ({
327
+ id: pane.id,
328
+ role: pane.role,
329
+ cwd: pane.cwd,
330
+ worktree: pane.worktree || '',
331
+ })),
332
+ layout: {
333
+ control: controlWindow,
334
+ agentArea: {
335
+ id: 'agent-area',
336
+ role: 'agent-area',
337
+ title: `${sessionName}: agents`,
338
+ cwd: repoRoot,
339
+ panes: agentWindows.length,
340
+ },
341
+ agents: agentWindows,
342
+ details: detailWindow,
343
+ },
344
+ steps,
345
+ commands: steps.map((step) => step.command),
346
+ };
347
+ }
348
+
349
+ function createKittyCockpitPlan(options = {}) {
350
+ const repoRoot = requireText(options.repoRoot, 'repoRoot');
351
+ const sessionName = text(options.sessionName, DEFAULT_SESSION_NAME);
352
+ const agents = Array.isArray(options.agents) ? options.agents : [];
353
+ const columns = positiveInteger(options.columns, DEFAULT_COLUMNS);
354
+ const kittyBin = text(options.kittyBin, DEFAULT_KITTY_BIN);
355
+ const controlCommand = text(
356
+ options.controlCommand,
357
+ `gx cockpit control --target ${shellQuote(repoRoot)}`,
358
+ );
359
+ const welcomeCommand = text(options.welcomeCommand, DEFAULT_WELCOME_COMMAND);
360
+
361
+ const controlTitle = `${sessionName}: control`;
362
+ const agentAreaTitle = `${sessionName}: agents`;
363
+ const controlWindow = {
364
+ id: 'control',
365
+ role: 'control',
366
+ title: controlTitle,
367
+ cwd: repoRoot,
368
+ command: controlCommand,
369
+ match: matchTitle(controlTitle),
370
+ persistent: true,
371
+ };
372
+ const agentAreaWindow = {
373
+ id: 'agent-area',
374
+ role: 'agent-area',
375
+ title: agentAreaTitle,
376
+ cwd: repoRoot,
377
+ command: welcomeCommand,
378
+ match: matchTitle(agentAreaTitle),
379
+ location: 'vsplit',
380
+ };
381
+ const agentWindows = agents.map((agent, index) => ({
382
+ ...normalizeAgent(agent, index, repoRoot, agents.length),
383
+ role: 'agent',
384
+ location: 'vsplit',
385
+ }));
386
+
387
+ const steps = [
388
+ {
389
+ id: 'launch-control',
390
+ role: 'control',
391
+ action: 'launch',
392
+ window: controlWindow,
393
+ command: launchCommand(controlWindow, kittyBin),
394
+ },
395
+ {
396
+ id: 'launch-agent-area',
397
+ role: 'agent-area',
398
+ action: 'launch',
399
+ window: agentAreaWindow,
400
+ command: launchCommand(agentAreaWindow, kittyBin),
401
+ },
402
+ ...agentWindows.map((window) => ({
403
+ id: `launch-agent-${window.index + 1}`,
404
+ role: 'agent',
405
+ action: 'launch',
406
+ agentId: window.id,
407
+ window,
408
+ command: launchCommand(window, kittyBin),
409
+ })),
410
+ ];
411
+
412
+ if (options.focusControl !== false) {
413
+ steps.push({
414
+ id: 'focus-control',
415
+ role: 'control',
416
+ action: 'focus',
417
+ window: controlWindow,
418
+ command: focusCommand(controlWindow, kittyBin),
419
+ });
420
+ }
421
+
422
+ return {
423
+ schemaVersion: 1,
424
+ backend: 'kitty',
425
+ dryRun: Boolean(options.dryRun),
426
+ sessionName,
427
+ repoRoot,
428
+ columns,
429
+ layout: {
430
+ control: controlWindow,
431
+ agentArea: agentAreaWindow,
432
+ agents: agentWindows,
433
+ },
434
+ steps,
435
+ commands: steps.map((step) => step.command),
436
+ };
437
+ }
438
+
439
+ function shouldBootstrapHost(options = {}) {
440
+ if (options.bootstrap === true) return true;
441
+ if (options.bootstrap === false) return false;
442
+ if (options.host === true) return true;
443
+ if (options.host === false) return false;
444
+ const env = options.env && typeof options.env === 'object' ? options.env : process.env;
445
+ if (firstText(env.KITTY_LISTEN_ON)) return false;
446
+ return Boolean(options.bootstrapWhenHostless);
447
+ }
448
+
449
+ function injectRemoteControlIntoPlan(plan, socket) {
450
+ if (!socket || !plan || !Array.isArray(plan.commands)) return plan;
451
+ const inject = (args) => kittyTerminal.injectRemoteControl(args, socket);
452
+ const updatedCommands = plan.commands.map((command) => {
453
+ if (!command || !Array.isArray(command.args)) return command;
454
+ return { ...command, args: inject(command.args) };
455
+ });
456
+ const updatedSteps = Array.isArray(plan.steps)
457
+ ? plan.steps.map((step) => {
458
+ if (!step || !step.command || !Array.isArray(step.command.args)) return step;
459
+ return {
460
+ ...step,
461
+ command: { ...step.command, args: inject(step.command.args) },
462
+ };
463
+ })
464
+ : plan.steps;
465
+ return {
466
+ ...plan,
467
+ host: { socket },
468
+ commands: updatedCommands,
469
+ steps: updatedSteps,
470
+ };
471
+ }
472
+
473
+ function bootstrapHostIfRequested(options, repoRoot) {
474
+ if (!shouldBootstrapHost(options)) return null;
475
+ const sessionName = text(options.sessionName, DEFAULT_SESSION_NAME);
476
+ const dryRun = Boolean(options.dryRun);
477
+ const backend = options.backend && typeof options.backend.bootstrapHost === 'function'
478
+ ? options.backend
479
+ : kittyTerminal.createKittyBackend({
480
+ kittyBin: options.kittyBin,
481
+ env: options.env,
482
+ runtime: options.hostRuntime,
483
+ runner: options.hostRunner,
484
+ dryRun,
485
+ });
486
+ return backend.bootstrapHost({
487
+ repoRoot,
488
+ socket: options.socket,
489
+ socketPrefix: options.socketPrefix,
490
+ title: text(options.controlTitle, `${sessionName}: cockpit`),
491
+ fs: options.fs,
492
+ spawn: options.spawn,
493
+ timeoutMs: options.hostTimeoutMs,
494
+ intervalMs: options.hostIntervalMs,
495
+ sleep: options.sleep,
496
+ });
497
+ }
498
+
499
+ function openKittyCockpit(options = {}) {
500
+ const repoRoot = requireText(
501
+ firstText(options.repoRoot, options.repoPath, options.target, process.cwd()),
502
+ 'repoRoot',
503
+ );
504
+ const readState = typeof options.readState === 'function' ? options.readState : readCockpitState;
505
+ const readSettings = typeof options.readSettings === 'function' ? options.readSettings : readCockpitSettings;
506
+ const state = options.state || readState(repoRoot);
507
+ const settings = {
508
+ ...readSettings(repoRoot),
509
+ ...(options.settings || {}),
510
+ repoRoot,
511
+ sessionName: options.sessionName,
512
+ controlCommand: options.controlCommand,
513
+ controlTitle: options.controlTitle,
514
+ columns: options.columns,
515
+ kittyBin: options.kittyBin,
516
+ dryRun: options.dryRun,
517
+ focusControl: options.focusControl,
518
+ };
519
+ const host = bootstrapHostIfRequested(options, repoRoot);
520
+ const socket = host && host.socket ? host.socket : '';
521
+ const basePlan = buildKittyCockpitPlan(state, settings);
522
+ const plan = socket ? injectRemoteControlIntoPlan(basePlan, socket) : basePlan;
523
+ const execution = kittyRuntime.openKittyCockpit({
524
+ plan,
525
+ dryRun: plan.dryRun,
526
+ runner: options.runner,
527
+ env: options.env,
528
+ timeout: options.timeout,
529
+ });
530
+
531
+ return {
532
+ action: 'created',
533
+ backend: 'kitty',
534
+ sessionName: plan.sessionName,
535
+ repoRoot: plan.repoRoot,
536
+ dryRun: plan.dryRun,
537
+ host: host || null,
538
+ plan,
539
+ execution,
540
+ };
541
+ }
542
+
543
+ module.exports = {
544
+ buildKittyCockpitPlan,
545
+ DEFAULT_COLUMNS,
546
+ DEFAULT_SESSION_NAME,
547
+ createKittyCockpitPlan,
548
+ openKittyCockpit,
549
+ };
@@ -0,0 +1,144 @@
1
+ 'use strict';
2
+
3
+ const cp = require('node:child_process');
4
+ const os = require('node:os');
5
+ const path = require('node:path');
6
+
7
+ const DEFAULT_BIN = 'kitty';
8
+ const DEFAULT_TIMEOUT_MS = 1500;
9
+
10
+ function text(value, fallback = '') {
11
+ if (typeof value === 'string') return value.trim() || fallback;
12
+ if (value === null || value === undefined) return fallback;
13
+ return String(value).trim() || fallback;
14
+ }
15
+
16
+ function defaultRunner(cmd, args, options = {}) {
17
+ return cp.spawnSync(cmd, args, {
18
+ cwd: options.cwd,
19
+ env: options.env ? { ...process.env, ...options.env } : process.env,
20
+ encoding: 'utf8',
21
+ stdio: 'pipe',
22
+ timeout: options.timeout || DEFAULT_TIMEOUT_MS,
23
+ });
24
+ }
25
+
26
+ function buildLsArgs(socket) {
27
+ const sock = text(socket);
28
+ const args = ['@'];
29
+ if (sock) args.push(`--to=${sock}`);
30
+ args.push('ls');
31
+ return args;
32
+ }
33
+
34
+ function classifyWindow(window = {}) {
35
+ const title = String(window.title || '').toLowerCase();
36
+ const cmdline = Array.isArray(window.cmdline) ? window.cmdline.join(' ').toLowerCase() : '';
37
+ if (/^gx cockpit/.test(title) || /gx cockpit/.test(cmdline)) return 'control';
38
+ if (title.startsWith('agent ') || /agent\//.test(cmdline)) return 'agent';
39
+ if (title === 'terminal' || cmdline.endsWith('bash') || cmdline.endsWith('zsh') || cmdline.endsWith('sh')) return 'shell';
40
+ if (/codex|claude|gemini|cursor|opencode/.test(title) || /codex|claude|gemini|cursor|opencode/.test(cmdline)) return 'agent';
41
+ return 'shell';
42
+ }
43
+
44
+ function flattenOsWindow(osWindow = {}) {
45
+ const tabs = Array.isArray(osWindow.tabs) ? osWindow.tabs : [];
46
+ const windows = [];
47
+ for (const tab of tabs) {
48
+ const tabWindows = Array.isArray(tab.windows) ? tab.windows : [];
49
+ for (const window of tabWindows) {
50
+ windows.push({
51
+ id: Number.isFinite(window.id) ? window.id : null,
52
+ title: text(window.title),
53
+ cwd: text(window.cwd),
54
+ cmdline: Array.isArray(window.cmdline) ? window.cmdline : [],
55
+ pid: Number.isFinite(window.pid) ? window.pid : null,
56
+ isFocused: Boolean(window.is_focused || window.focused),
57
+ isActive: Boolean(window.is_active || window.active),
58
+ kind: classifyWindow(window),
59
+ tabId: Number.isFinite(tab.id) ? tab.id : null,
60
+ tabTitle: text(tab.title),
61
+ });
62
+ }
63
+ }
64
+ return windows;
65
+ }
66
+
67
+ function pickOsWindow(payload, options = {}) {
68
+ if (!Array.isArray(payload) || payload.length === 0) return null;
69
+ const targetId = Number.parseInt(options.osWindowId, 10);
70
+ if (Number.isFinite(targetId)) {
71
+ return payload.find((entry) => entry && entry.id === targetId) || payload[0];
72
+ }
73
+ return payload.find((entry) => entry && (entry.is_focused || entry.focused)) || payload[0];
74
+ }
75
+
76
+ function buildSessionLabel(options = {}) {
77
+ if (text(options.sessionLabel)) return text(options.sessionLabel);
78
+ const env = options.env || process.env;
79
+ const fromEnv = text(env.GUARDEX_SESSION_LABEL);
80
+ if (fromEnv) return fromEnv;
81
+ const repoRoot = text(options.repoRoot);
82
+ if (repoRoot) return path.basename(repoRoot);
83
+ return 'session';
84
+ }
85
+
86
+ function userLabel(options = {}) {
87
+ const env = options.env || process.env;
88
+ return text(env.USER) || text(env.LOGNAME) || (typeof os.userInfo === 'function' ? text(os.userInfo().username) : '') || 'user';
89
+ }
90
+
91
+ function emptyTree(options = {}) {
92
+ return {
93
+ user: userLabel(options),
94
+ sessionLabel: buildSessionLabel(options),
95
+ osWindowId: null,
96
+ windows: [],
97
+ error: '',
98
+ };
99
+ }
100
+
101
+ function readKittyTree(options = {}) {
102
+ const env = options.env || process.env;
103
+ const socket = text(options.socket || env.KITTY_LISTEN_ON);
104
+ if (!socket) {
105
+ return { ...emptyTree(options), error: 'no KITTY_LISTEN_ON socket' };
106
+ }
107
+ const bin = text(options.bin || env.GUARDEX_KITTY_BIN, DEFAULT_BIN);
108
+ const runner = typeof options.runner === 'function' ? options.runner : defaultRunner;
109
+ const result = runner(bin, buildLsArgs(socket), { env, timeout: options.timeoutMs });
110
+ if (!result || result.error || result.status !== 0) {
111
+ const msg = result && (result.stderr || result.error || '').toString().trim();
112
+ return { ...emptyTree(options), error: msg || 'kitty @ ls failed' };
113
+ }
114
+ let payload;
115
+ try {
116
+ payload = JSON.parse(String(result.stdout || ''));
117
+ } catch (error) {
118
+ return { ...emptyTree(options), error: `parse error: ${error.message}` };
119
+ }
120
+ const osWindow = pickOsWindow(payload, options);
121
+ if (!osWindow) {
122
+ return { ...emptyTree(options), error: 'no os-window in kitty tree' };
123
+ }
124
+ return {
125
+ user: userLabel(options),
126
+ sessionLabel: buildSessionLabel(options),
127
+ osWindowId: Number.isFinite(osWindow.id) ? osWindow.id : null,
128
+ windows: flattenOsWindow(osWindow),
129
+ error: '',
130
+ };
131
+ }
132
+
133
+ module.exports = {
134
+ DEFAULT_BIN,
135
+ DEFAULT_TIMEOUT_MS,
136
+ buildLsArgs,
137
+ classifyWindow,
138
+ emptyTree,
139
+ flattenOsWindow,
140
+ pickOsWindow,
141
+ readKittyTree,
142
+ userLabel,
143
+ buildSessionLabel,
144
+ };