@proletariat/cli 0.3.23 → 0.3.25
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/dist/commands/action/create.js +4 -4
- package/dist/commands/action/update.js +3 -3
- package/dist/commands/agent/{temp/cleanup.d.ts → cleanup.d.ts} +1 -1
- package/dist/commands/agent/{temp/cleanup.js → cleanup.js} +4 -4
- package/dist/commands/agent/index.js +8 -8
- package/dist/commands/branch/create.js +2 -2
- package/dist/commands/epic/activate.js +9 -17
- package/dist/commands/epic/archive.js +13 -24
- package/dist/commands/epic/create.d.ts +1 -0
- package/dist/commands/epic/create.js +46 -8
- package/dist/commands/epic/index.js +2 -2
- package/dist/commands/epic/move.js +28 -47
- package/dist/commands/epic/progress.js +10 -14
- package/dist/commands/epic/project.js +42 -59
- package/dist/commands/epic/reorder.js +25 -30
- package/dist/commands/epic/spec.d.ts +1 -0
- package/dist/commands/epic/spec.js +39 -40
- package/dist/commands/epic/ticket.d.ts +2 -0
- package/dist/commands/epic/ticket.js +63 -37
- package/dist/commands/feedback/index.d.ts +10 -0
- package/dist/commands/feedback/index.js +60 -0
- package/dist/commands/feedback/list.d.ts +12 -0
- package/dist/commands/feedback/list.js +126 -0
- package/dist/commands/feedback/submit.d.ts +16 -0
- package/dist/commands/feedback/submit.js +220 -0
- package/dist/commands/{template/phase/delete.d.ts → feedback/view.d.ts} +7 -5
- package/dist/commands/feedback/view.js +109 -0
- package/dist/commands/gh/index.js +4 -0
- package/dist/commands/{epic/link/remove.d.ts → link/create.d.ts} +6 -7
- package/dist/commands/link/create.js +141 -0
- package/dist/commands/{epic/link/relates.d.ts → link/index.d.ts} +4 -5
- package/dist/commands/link/index.js +87 -0
- package/dist/commands/{epic/link/duplicates.d.ts → link/list.d.ts} +7 -4
- package/dist/commands/link/list.js +182 -0
- package/dist/commands/{spec/link → link}/remove.d.ts +4 -5
- package/dist/commands/link/remove.js +120 -0
- package/dist/commands/mcp-server.d.ts +22 -0
- package/dist/commands/mcp-server.js +98 -0
- package/dist/commands/phase/create.js +1 -1
- package/dist/commands/project/create.d.ts +1 -0
- package/dist/commands/project/create.js +38 -4
- package/dist/commands/repo/create.d.ts +38 -0
- package/dist/commands/repo/create.js +283 -0
- package/dist/commands/repo/index.js +7 -0
- package/dist/commands/roadmap/add-project.js +9 -22
- package/dist/commands/roadmap/create.d.ts +0 -1
- package/dist/commands/roadmap/create.js +46 -40
- package/dist/commands/roadmap/delete.js +10 -24
- package/dist/commands/roadmap/generate.d.ts +1 -0
- package/dist/commands/roadmap/generate.js +21 -22
- package/dist/commands/roadmap/remove-project.js +14 -34
- package/dist/commands/roadmap/reorder.js +19 -26
- package/dist/commands/roadmap/update.js +27 -26
- package/dist/commands/roadmap/view.js +5 -12
- package/dist/commands/session/attach.d.ts +1 -8
- package/dist/commands/session/attach.js +93 -59
- package/dist/commands/session/list.d.ts +0 -8
- package/dist/commands/session/list.js +130 -81
- package/dist/commands/spec/create.d.ts +1 -0
- package/dist/commands/spec/create.js +44 -3
- package/dist/commands/spec/edit.js +63 -33
- package/dist/commands/spec/index.js +2 -2
- package/dist/commands/{agent/staff → staff}/add.js +10 -10
- package/dist/commands/{agent/staff → staff}/index.d.ts +1 -1
- package/dist/commands/{agent/staff → staff}/index.js +7 -7
- package/dist/commands/{agent/staff → staff}/list.js +3 -3
- package/dist/commands/{agent/staff → staff}/remove.d.ts +1 -1
- package/dist/commands/{agent/staff → staff}/remove.js +8 -8
- package/dist/commands/{template/phase/index.d.ts → support/book.d.ts} +2 -2
- package/dist/commands/support/book.js +54 -0
- package/dist/commands/{template/ticket/index.d.ts → support/discord.d.ts} +2 -2
- package/dist/commands/support/discord.js +54 -0
- package/dist/commands/support/docs.d.ts +10 -0
- package/dist/commands/support/docs.js +54 -0
- package/dist/commands/support/index.d.ts +19 -0
- package/dist/commands/support/index.js +81 -0
- package/dist/commands/support/issues.d.ts +11 -0
- package/dist/commands/support/issues.js +77 -0
- package/dist/commands/support/logs.d.ts +18 -0
- package/dist/commands/support/logs.js +247 -0
- package/dist/commands/{ticket/template → template}/apply.d.ts +8 -6
- package/dist/commands/template/apply.js +262 -0
- package/dist/commands/{ticket/template → template}/create.d.ts +5 -6
- package/dist/commands/template/create.js +238 -0
- package/dist/commands/template/index.js +48 -36
- package/dist/commands/{ticket/template → template}/save.d.ts +2 -2
- package/dist/commands/template/save.js +104 -0
- package/dist/commands/{phase/template → template}/update.d.ts +2 -2
- package/dist/commands/template/update.js +99 -0
- package/dist/commands/{agent/themes → theme}/add-names.d.ts +1 -1
- package/dist/commands/{agent/themes → theme}/add-names.js +6 -6
- package/dist/commands/{agent/themes → theme}/create.d.ts +1 -1
- package/dist/commands/{agent/themes → theme}/create.js +5 -5
- package/dist/commands/{agent/themes → theme}/index.d.ts +1 -1
- package/dist/commands/{agent/themes → theme}/index.js +10 -10
- package/dist/commands/{agent/themes → theme}/list.d.ts +1 -1
- package/dist/commands/{agent/themes → theme}/list.js +5 -5
- package/dist/commands/{agent/themes → theme}/set.d.ts +1 -1
- package/dist/commands/{agent/themes → theme}/set.js +7 -7
- package/dist/commands/ticket/create.d.ts +1 -0
- package/dist/commands/ticket/create.js +75 -15
- package/dist/commands/ticket/edit.js +44 -13
- package/dist/commands/ticket/index.js +6 -6
- package/dist/commands/ticket/move.d.ts +7 -0
- package/dist/commands/ticket/move.js +132 -0
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +72 -8
- package/dist/commands/work/start.js +6 -0
- package/dist/lib/execution/runners.js +21 -17
- package/dist/lib/execution/session-utils.d.ts +60 -0
- package/dist/lib/execution/session-utils.js +162 -0
- package/dist/lib/execution/spawner.d.ts +2 -0
- package/dist/lib/execution/spawner.js +42 -0
- package/dist/lib/flags/resolver.d.ts +2 -2
- package/dist/lib/flags/resolver.js +15 -0
- package/dist/lib/init/index.js +18 -0
- package/dist/lib/mcp/helpers.d.ts +43 -0
- package/dist/lib/mcp/helpers.js +57 -0
- package/dist/lib/mcp/index.d.ts +6 -0
- package/dist/lib/mcp/index.js +6 -0
- package/dist/lib/mcp/tools/action.d.ts +6 -0
- package/dist/lib/mcp/tools/action.js +88 -0
- package/dist/lib/mcp/tools/board.d.ts +6 -0
- package/dist/lib/mcp/tools/board.js +139 -0
- package/dist/lib/mcp/tools/category.d.ts +6 -0
- package/dist/lib/mcp/tools/category.js +84 -0
- package/dist/lib/mcp/tools/cli-passthrough.d.ts +15 -0
- package/dist/lib/mcp/tools/cli-passthrough.js +333 -0
- package/dist/lib/mcp/tools/epic.d.ts +6 -0
- package/dist/lib/mcp/tools/epic.js +178 -0
- package/dist/lib/mcp/tools/index.d.ts +18 -0
- package/dist/lib/mcp/tools/index.js +19 -0
- package/dist/lib/mcp/tools/phase.d.ts +6 -0
- package/dist/lib/mcp/tools/phase.js +131 -0
- package/dist/lib/mcp/tools/project.d.ts +6 -0
- package/dist/lib/mcp/tools/project.js +196 -0
- package/dist/lib/mcp/tools/roadmap.d.ts +6 -0
- package/dist/lib/mcp/tools/roadmap.js +123 -0
- package/dist/lib/mcp/tools/spec.d.ts +6 -0
- package/dist/lib/mcp/tools/spec.js +196 -0
- package/dist/lib/mcp/tools/status.d.ts +6 -0
- package/dist/lib/mcp/tools/status.js +109 -0
- package/dist/lib/mcp/tools/template.d.ts +6 -0
- package/dist/lib/mcp/tools/template.js +107 -0
- package/dist/lib/mcp/tools/ticket.d.ts +6 -0
- package/dist/lib/mcp/tools/ticket.js +393 -0
- package/dist/lib/mcp/tools/view.d.ts +6 -0
- package/dist/lib/mcp/tools/view.js +76 -0
- package/dist/lib/mcp/tools/work.d.ts +6 -0
- package/dist/lib/mcp/tools/work.js +132 -0
- package/dist/lib/mcp/tools/workflow.d.ts +6 -0
- package/dist/lib/mcp/tools/workflow.js +95 -0
- package/dist/lib/mcp/types.d.ts +17 -0
- package/dist/lib/mcp/types.js +4 -0
- package/dist/lib/multiline-input.d.ts +63 -0
- package/dist/lib/multiline-input.js +360 -0
- package/dist/lib/prompt-json.d.ts +57 -6
- package/dist/lib/prompt-json.js +45 -0
- package/dist/lib/repos/git.d.ts +7 -0
- package/dist/lib/repos/git.js +20 -0
- package/oclif.manifest.json +3690 -4995
- package/package.json +6 -4
- package/dist/commands/agent/temp/index.d.ts +0 -14
- package/dist/commands/agent/temp/index.js +0 -85
- package/dist/commands/agent/temp/list.d.ts +0 -7
- package/dist/commands/agent/temp/list.js +0 -108
- package/dist/commands/epic/link/block.d.ts +0 -14
- package/dist/commands/epic/link/block.js +0 -81
- package/dist/commands/epic/link/duplicates.js +0 -68
- package/dist/commands/epic/link/index.d.ts +0 -19
- package/dist/commands/epic/link/index.js +0 -272
- package/dist/commands/epic/link/relates.js +0 -68
- package/dist/commands/epic/link/remove.js +0 -93
- package/dist/commands/phase/template/apply.d.ts +0 -17
- package/dist/commands/phase/template/apply.js +0 -108
- package/dist/commands/phase/template/create.d.ts +0 -17
- package/dist/commands/phase/template/create.js +0 -104
- package/dist/commands/phase/template/delete.d.ts +0 -17
- package/dist/commands/phase/template/delete.js +0 -100
- package/dist/commands/phase/template/index.d.ts +0 -15
- package/dist/commands/phase/template/index.js +0 -130
- package/dist/commands/phase/template/list.d.ts +0 -16
- package/dist/commands/phase/template/list.js +0 -97
- package/dist/commands/phase/template/update.js +0 -89
- package/dist/commands/spec/link/depends.d.ts +0 -14
- package/dist/commands/spec/link/depends.js +0 -64
- package/dist/commands/spec/link/duplicates.d.ts +0 -14
- package/dist/commands/spec/link/duplicates.js +0 -63
- package/dist/commands/spec/link/index.d.ts +0 -19
- package/dist/commands/spec/link/index.js +0 -207
- package/dist/commands/spec/link/relates.d.ts +0 -14
- package/dist/commands/spec/link/relates.js +0 -63
- package/dist/commands/spec/link/remove.js +0 -96
- package/dist/commands/template/phase/apply.d.ts +0 -14
- package/dist/commands/template/phase/apply.js +0 -43
- package/dist/commands/template/phase/create.d.ts +0 -13
- package/dist/commands/template/phase/create.js +0 -38
- package/dist/commands/template/phase/delete.js +0 -36
- package/dist/commands/template/phase/index.js +0 -63
- package/dist/commands/template/phase/list.d.ts +0 -11
- package/dist/commands/template/phase/list.js +0 -36
- package/dist/commands/template/phase/update.d.ts +0 -14
- package/dist/commands/template/phase/update.js +0 -43
- package/dist/commands/template/ticket/apply.d.ts +0 -17
- package/dist/commands/template/ticket/apply.js +0 -60
- package/dist/commands/template/ticket/create.d.ts +0 -20
- package/dist/commands/template/ticket/create.js +0 -89
- package/dist/commands/template/ticket/delete.d.ts +0 -13
- package/dist/commands/template/ticket/delete.js +0 -38
- package/dist/commands/template/ticket/index.js +0 -63
- package/dist/commands/template/ticket/list.d.ts +0 -11
- package/dist/commands/template/ticket/list.js +0 -36
- package/dist/commands/template/ticket/save.d.ts +0 -15
- package/dist/commands/template/ticket/save.js +0 -46
- package/dist/commands/ticket/link/block.d.ts +0 -14
- package/dist/commands/ticket/link/block.js +0 -96
- package/dist/commands/ticket/link/duplicates.d.ts +0 -14
- package/dist/commands/ticket/link/duplicates.js +0 -95
- package/dist/commands/ticket/link/index.d.ts +0 -19
- package/dist/commands/ticket/link/index.js +0 -256
- package/dist/commands/ticket/link/relates.d.ts +0 -14
- package/dist/commands/ticket/link/relates.js +0 -95
- package/dist/commands/ticket/link/remove.d.ts +0 -16
- package/dist/commands/ticket/link/remove.js +0 -132
- package/dist/commands/ticket/template/apply.js +0 -252
- package/dist/commands/ticket/template/create.js +0 -386
- package/dist/commands/ticket/template/delete.d.ts +0 -17
- package/dist/commands/ticket/template/delete.js +0 -94
- package/dist/commands/ticket/template/index.d.ts +0 -15
- package/dist/commands/ticket/template/index.js +0 -120
- package/dist/commands/ticket/template/list.d.ts +0 -16
- package/dist/commands/ticket/template/list.js +0 -112
- package/dist/commands/ticket/template/save.js +0 -163
- /package/dist/commands/{agent/staff → staff}/add.d.ts +0 -0
- /package/dist/commands/{agent/staff → staff}/list.d.ts +0 -0
|
@@ -19,16 +19,9 @@ export default class SessionAttach extends PMOCommand {
|
|
|
19
19
|
/**
|
|
20
20
|
* Get verified sessions from DB that have actual tmux processes
|
|
21
21
|
* DB-driven approach: Start with executions, verify tmux sessions exist
|
|
22
|
+
* Also discovers orphan sessions matching prlt naming pattern but not in DB
|
|
22
23
|
*/
|
|
23
24
|
private getVerifiedSessions;
|
|
24
|
-
/**
|
|
25
|
-
* Get list of host tmux session names
|
|
26
|
-
*/
|
|
27
|
-
private getHostTmuxSessionNames;
|
|
28
|
-
/**
|
|
29
|
-
* Get map of containerId -> tmux session names
|
|
30
|
-
*/
|
|
31
|
-
private getContainerTmuxSessionMap;
|
|
32
25
|
/**
|
|
33
26
|
* Attach to session in current terminal
|
|
34
27
|
*/
|
|
@@ -7,6 +7,7 @@ import Database from 'better-sqlite3';
|
|
|
7
7
|
import { styles } from '../../lib/styles.js';
|
|
8
8
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
9
9
|
import { ExecutionStorage } from '../../lib/execution/index.js';
|
|
10
|
+
import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
|
|
10
11
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
11
12
|
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
12
13
|
export default class SessionAttach extends PMOCommand {
|
|
@@ -122,6 +123,7 @@ export default class SessionAttach extends PMOCommand {
|
|
|
122
123
|
/**
|
|
123
124
|
* Get verified sessions from DB that have actual tmux processes
|
|
124
125
|
* DB-driven approach: Start with executions, verify tmux sessions exist
|
|
126
|
+
* Also discovers orphan sessions matching prlt naming pattern but not in DB
|
|
125
127
|
*/
|
|
126
128
|
getVerifiedSessions() {
|
|
127
129
|
const sessions = [];
|
|
@@ -134,87 +136,119 @@ export default class SessionAttach extends PMOCommand {
|
|
|
134
136
|
executionStorage = new ExecutionStorage(db);
|
|
135
137
|
}
|
|
136
138
|
catch {
|
|
137
|
-
|
|
139
|
+
// Not in workspace, but we can still discover tmux sessions
|
|
138
140
|
}
|
|
139
141
|
try {
|
|
140
|
-
// Get
|
|
141
|
-
const
|
|
142
|
+
// Get actual tmux sessions for verification
|
|
143
|
+
const hostTmuxSessions = getHostTmuxSessionNames();
|
|
144
|
+
const containerTmuxSessions = getContainerTmuxSessionMap();
|
|
145
|
+
// Flatten all container sessions for orphan detection
|
|
146
|
+
const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
|
|
147
|
+
// Track which tmux sessions we've matched to DB records
|
|
148
|
+
const matchedHostSessions = new Set();
|
|
149
|
+
const matchedContainerSessions = new Set();
|
|
150
|
+
// Get active executions from DB (if available)
|
|
151
|
+
const activeExecutions = executionStorage ? [
|
|
142
152
|
...(executionStorage.listExecutions({ status: 'running' }) || []),
|
|
143
153
|
...(executionStorage.listExecutions({ status: 'starting' }) || []),
|
|
144
|
-
];
|
|
145
|
-
// Get actual tmux sessions for verification
|
|
146
|
-
const hostTmuxSessions = this.getHostTmuxSessionNames();
|
|
147
|
-
const containerTmuxSessions = this.getContainerTmuxSessionMap();
|
|
154
|
+
] : [];
|
|
148
155
|
for (const exec of activeExecutions) {
|
|
149
|
-
if (!exec.sessionId)
|
|
150
|
-
continue;
|
|
151
156
|
const isContainer = exec.environment === 'devcontainer';
|
|
152
157
|
let exists = false;
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
158
|
+
let containerId;
|
|
159
|
+
let actualSessionId = exec.sessionId;
|
|
160
|
+
// If sessionId is NULL, try to find session by naming convention
|
|
161
|
+
if (!exec.sessionId) {
|
|
162
|
+
if (isContainer && exec.containerId) {
|
|
163
|
+
const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
|
|
164
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
|
|
165
|
+
if (match) {
|
|
166
|
+
actualSessionId = match;
|
|
167
|
+
exists = true;
|
|
168
|
+
containerId = exec.containerId;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
else {
|
|
172
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
|
|
173
|
+
if (match) {
|
|
174
|
+
actualSessionId = match;
|
|
175
|
+
exists = true;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// If still no match, skip this execution
|
|
179
|
+
if (!actualSessionId) {
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
156
182
|
}
|
|
157
183
|
else {
|
|
158
|
-
|
|
184
|
+
// sessionId is set, verify it exists
|
|
185
|
+
if (isContainer && exec.containerId) {
|
|
186
|
+
const containerSessions = containerTmuxSessions.get(exec.containerId);
|
|
187
|
+
exists = containerSessions?.includes(exec.sessionId) ?? false;
|
|
188
|
+
containerId = exec.containerId;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
exists = hostTmuxSessions.includes(exec.sessionId);
|
|
192
|
+
}
|
|
159
193
|
}
|
|
160
|
-
//
|
|
161
|
-
if (exists) {
|
|
194
|
+
// Track matched sessions
|
|
195
|
+
if (exists && actualSessionId) {
|
|
196
|
+
if (isContainer && containerId) {
|
|
197
|
+
matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
matchedHostSessions.add(actualSessionId);
|
|
201
|
+
}
|
|
162
202
|
sessions.push({
|
|
163
|
-
name:
|
|
164
|
-
sessionId:
|
|
203
|
+
name: actualSessionId,
|
|
204
|
+
sessionId: actualSessionId,
|
|
165
205
|
type: isContainer ? 'container' : 'host',
|
|
166
|
-
containerId
|
|
206
|
+
containerId,
|
|
167
207
|
ticketId: exec.ticketId,
|
|
168
208
|
agentName: exec.agentName,
|
|
209
|
+
source: 'db',
|
|
169
210
|
});
|
|
170
211
|
}
|
|
171
212
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
return output.split('\n');
|
|
188
|
-
}
|
|
189
|
-
catch {
|
|
190
|
-
return [];
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Get map of containerId -> tmux session names
|
|
195
|
-
*/
|
|
196
|
-
getContainerTmuxSessionMap() {
|
|
197
|
-
const sessionMap = new Map();
|
|
198
|
-
try {
|
|
199
|
-
const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
200
|
-
if (!containersOutput)
|
|
201
|
-
return sessionMap;
|
|
202
|
-
for (const containerId of containersOutput.split('\n')) {
|
|
203
|
-
try {
|
|
204
|
-
const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
205
|
-
if (tmuxOutput) {
|
|
206
|
-
sessionMap.set(containerId, tmuxOutput.split('\n'));
|
|
207
|
-
}
|
|
213
|
+
// Discover orphan sessions: tmux sessions matching prlt pattern but not in DB
|
|
214
|
+
// Host sessions
|
|
215
|
+
for (const sessionName of hostTmuxSessions) {
|
|
216
|
+
if (matchedHostSessions.has(sessionName))
|
|
217
|
+
continue;
|
|
218
|
+
const parsed = parseSessionName(sessionName);
|
|
219
|
+
if (parsed) {
|
|
220
|
+
sessions.push({
|
|
221
|
+
name: sessionName,
|
|
222
|
+
sessionId: sessionName,
|
|
223
|
+
type: 'host',
|
|
224
|
+
ticketId: parsed.ticketId,
|
|
225
|
+
agentName: parsed.agentName,
|
|
226
|
+
source: 'discovered',
|
|
227
|
+
});
|
|
208
228
|
}
|
|
209
|
-
|
|
210
|
-
|
|
229
|
+
}
|
|
230
|
+
// Container sessions
|
|
231
|
+
for (const { sessionName, containerId } of allContainerSessions) {
|
|
232
|
+
if (matchedContainerSessions.has(`${containerId}:${sessionName}`))
|
|
233
|
+
continue;
|
|
234
|
+
const parsed = parseSessionName(sessionName);
|
|
235
|
+
if (parsed) {
|
|
236
|
+
sessions.push({
|
|
237
|
+
name: sessionName,
|
|
238
|
+
sessionId: sessionName,
|
|
239
|
+
type: 'container',
|
|
240
|
+
containerId,
|
|
241
|
+
ticketId: parsed.ticketId,
|
|
242
|
+
agentName: parsed.agentName,
|
|
243
|
+
source: 'discovered',
|
|
244
|
+
});
|
|
211
245
|
}
|
|
212
246
|
}
|
|
213
247
|
}
|
|
214
|
-
|
|
215
|
-
|
|
248
|
+
finally {
|
|
249
|
+
db?.close();
|
|
216
250
|
}
|
|
217
|
-
return
|
|
251
|
+
return sessions;
|
|
218
252
|
}
|
|
219
253
|
/**
|
|
220
254
|
* Attach to session in current terminal
|
|
@@ -11,12 +11,4 @@ export default class SessionList extends PMOCommand {
|
|
|
11
11
|
promptIfMultiple: boolean;
|
|
12
12
|
};
|
|
13
13
|
execute(): Promise<void>;
|
|
14
|
-
/**
|
|
15
|
-
* Get list of host tmux session names
|
|
16
|
-
*/
|
|
17
|
-
private getHostTmuxSessionNames;
|
|
18
|
-
/**
|
|
19
|
-
* Get map of containerId -> tmux session names
|
|
20
|
-
*/
|
|
21
|
-
private getContainerTmuxSessionMap;
|
|
22
14
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { Flags } from '@oclif/core';
|
|
2
|
-
import { execSync } from 'node:child_process';
|
|
3
2
|
import * as path from 'node:path';
|
|
4
3
|
import Database from 'better-sqlite3';
|
|
5
4
|
import { styles } from '../../lib/styles.js';
|
|
6
5
|
import { getWorkspaceInfo } from '../../lib/agents/commands.js';
|
|
7
6
|
import { ExecutionStorage } from '../../lib/execution/index.js';
|
|
7
|
+
import { parseSessionName, getHostTmuxSessionNames, getContainerTmuxSessionMap, flattenContainerSessions, findSessionForExecution, } from '../../lib/execution/session-utils.js';
|
|
8
8
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
9
9
|
export default class SessionList extends PMOCommand {
|
|
10
10
|
static description = 'List active tmux sessions (host and container)';
|
|
@@ -28,6 +28,7 @@ export default class SessionList extends PMOCommand {
|
|
|
28
28
|
// Get workspace info for execution records
|
|
29
29
|
let executionStorage = null;
|
|
30
30
|
let db = null;
|
|
31
|
+
let hasWorkspace = true;
|
|
31
32
|
try {
|
|
32
33
|
const workspaceInfo = getWorkspaceInfo();
|
|
33
34
|
const dbPath = path.join(workspaceInfo.path, '.proletariat', 'workspace.db');
|
|
@@ -35,47 +36,120 @@ export default class SessionList extends PMOCommand {
|
|
|
35
36
|
executionStorage = new ExecutionStorage(db);
|
|
36
37
|
}
|
|
37
38
|
catch {
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
this.log('');
|
|
41
|
-
return;
|
|
39
|
+
// Not in a workspace, but we can still discover tmux sessions
|
|
40
|
+
hasWorkspace = false;
|
|
42
41
|
}
|
|
43
42
|
try {
|
|
44
43
|
// DB-driven approach: Start with executions, verify tmux sessions exist
|
|
45
|
-
const runningExecutions = executionStorage
|
|
46
|
-
const startingExecutions = executionStorage
|
|
44
|
+
const runningExecutions = executionStorage?.listExecutions({ status: 'running' }) || [];
|
|
45
|
+
const startingExecutions = executionStorage?.listExecutions({ status: 'starting' }) || [];
|
|
47
46
|
const activeExecutions = [...runningExecutions, ...startingExecutions];
|
|
48
47
|
// Get list of actual tmux sessions for verification
|
|
49
|
-
const hostTmuxSessions =
|
|
50
|
-
const containerTmuxSessions =
|
|
48
|
+
const hostTmuxSessions = getHostTmuxSessionNames();
|
|
49
|
+
const containerTmuxSessions = getContainerTmuxSessionMap();
|
|
50
|
+
// Flatten all container sessions for orphan detection
|
|
51
|
+
const allContainerSessions = flattenContainerSessions(containerTmuxSessions);
|
|
52
|
+
// Track which tmux sessions we've matched to DB records
|
|
53
|
+
const matchedHostSessions = new Set();
|
|
54
|
+
const matchedContainerSessions = new Set();
|
|
51
55
|
// Build verified session list from DB records
|
|
52
56
|
const sessions = [];
|
|
53
57
|
for (const exec of activeExecutions) {
|
|
54
|
-
if (!exec.sessionId)
|
|
55
|
-
continue; // Skip executions without sessionId
|
|
56
58
|
const isContainer = exec.environment === 'devcontainer';
|
|
57
59
|
let exists = false;
|
|
58
60
|
let containerId;
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
61
|
+
let actualSessionId = exec.sessionId;
|
|
62
|
+
// If sessionId is NULL, try to find session by naming convention
|
|
63
|
+
if (!exec.sessionId) {
|
|
64
|
+
if (isContainer && exec.containerId) {
|
|
65
|
+
const containerSessions = containerTmuxSessions.get(exec.containerId) || [];
|
|
66
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, containerSessions);
|
|
67
|
+
if (match) {
|
|
68
|
+
actualSessionId = match;
|
|
69
|
+
exists = true;
|
|
70
|
+
containerId = exec.containerId;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
const match = findSessionForExecution(exec.ticketId, exec.agentName, hostTmuxSessions);
|
|
75
|
+
if (match) {
|
|
76
|
+
actualSessionId = match;
|
|
77
|
+
exists = true;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
// If still no match, skip this execution (truly has no session)
|
|
81
|
+
if (!actualSessionId) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
64
84
|
}
|
|
65
85
|
else {
|
|
66
|
-
//
|
|
67
|
-
|
|
86
|
+
// sessionId is set, verify it exists
|
|
87
|
+
if (isContainer && exec.containerId) {
|
|
88
|
+
const containerSessions = containerTmuxSessions.get(exec.containerId);
|
|
89
|
+
exists = containerSessions?.includes(exec.sessionId) ?? false;
|
|
90
|
+
containerId = exec.containerId;
|
|
91
|
+
}
|
|
92
|
+
else {
|
|
93
|
+
exists = hostTmuxSessions.includes(exec.sessionId);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
// Track matched sessions to detect orphans later
|
|
97
|
+
if (exists && actualSessionId) {
|
|
98
|
+
if (isContainer && containerId) {
|
|
99
|
+
matchedContainerSessions.add(`${containerId}:${actualSessionId}`);
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
matchedHostSessions.add(actualSessionId);
|
|
103
|
+
}
|
|
68
104
|
}
|
|
69
105
|
// Only include if session exists, unless --all flag
|
|
70
|
-
|
|
106
|
+
// Note: actualSessionId is guaranteed non-null here due to continue above
|
|
107
|
+
if ((exists || flags.all) && actualSessionId) {
|
|
71
108
|
sessions.push({
|
|
72
|
-
sessionId:
|
|
109
|
+
sessionId: actualSessionId,
|
|
73
110
|
ticketId: exec.ticketId,
|
|
74
111
|
agentName: exec.agentName,
|
|
75
112
|
status: exists ? exec.status : 'stale',
|
|
76
113
|
environment: isContainer ? 'container' : 'host',
|
|
77
114
|
containerId,
|
|
78
115
|
exists,
|
|
116
|
+
source: 'db',
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
// Discover orphan sessions: tmux sessions matching prlt pattern but not in DB
|
|
121
|
+
// Host sessions
|
|
122
|
+
for (const sessionName of hostTmuxSessions) {
|
|
123
|
+
if (matchedHostSessions.has(sessionName))
|
|
124
|
+
continue;
|
|
125
|
+
const parsed = parseSessionName(sessionName);
|
|
126
|
+
if (parsed) {
|
|
127
|
+
sessions.push({
|
|
128
|
+
sessionId: sessionName,
|
|
129
|
+
ticketId: parsed.ticketId,
|
|
130
|
+
agentName: parsed.agentName,
|
|
131
|
+
status: 'orphan',
|
|
132
|
+
environment: 'host',
|
|
133
|
+
exists: true,
|
|
134
|
+
source: 'discovered',
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Container sessions
|
|
139
|
+
for (const { sessionName, containerId } of allContainerSessions) {
|
|
140
|
+
if (matchedContainerSessions.has(`${containerId}:${sessionName}`))
|
|
141
|
+
continue;
|
|
142
|
+
const parsed = parseSessionName(sessionName);
|
|
143
|
+
if (parsed) {
|
|
144
|
+
sessions.push({
|
|
145
|
+
sessionId: sessionName,
|
|
146
|
+
ticketId: parsed.ticketId,
|
|
147
|
+
agentName: parsed.agentName,
|
|
148
|
+
status: 'orphan',
|
|
149
|
+
environment: 'container',
|
|
150
|
+
containerId,
|
|
151
|
+
exists: true,
|
|
152
|
+
source: 'discovered',
|
|
79
153
|
});
|
|
80
154
|
}
|
|
81
155
|
}
|
|
@@ -84,23 +158,30 @@ export default class SessionList extends PMOCommand {
|
|
|
84
158
|
this.log(styles.header('🖥️ Active Sessions'));
|
|
85
159
|
this.log('═'.repeat(90));
|
|
86
160
|
this.log(styles.muted(' ' +
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
161
|
+
'Session'.padEnd(34) +
|
|
162
|
+
'Ticket'.padEnd(12) +
|
|
163
|
+
'Agent'.padEnd(18) +
|
|
164
|
+
'Type'.padEnd(15) +
|
|
91
165
|
'Status'));
|
|
92
|
-
this.log(' ' + '─'.repeat(
|
|
166
|
+
this.log(' ' + '─'.repeat(88));
|
|
93
167
|
for (const session of sessions) {
|
|
94
168
|
const typeIcon = session.environment === 'container' ? '🐳 container' : '💻 host';
|
|
95
169
|
const statusColor = session.status === 'running' ? styles.success :
|
|
96
170
|
session.status === 'starting' ? styles.warning :
|
|
97
|
-
session.status === 'stale' ? styles.error :
|
|
171
|
+
session.status === 'stale' ? styles.error :
|
|
172
|
+
session.status === 'orphan' ? styles.warning : styles.muted;
|
|
173
|
+
// For orphan sessions, append source indicator
|
|
174
|
+
const statusText = session.source === 'discovered' ? `${session.status}*` : session.status;
|
|
175
|
+
// Truncate long session names to fit column
|
|
176
|
+
const displaySession = session.sessionId.length > 32
|
|
177
|
+
? session.sessionId.substring(0, 29) + '...'
|
|
178
|
+
: session.sessionId;
|
|
98
179
|
this.log(' ' +
|
|
99
|
-
padEnd(
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
padEnd(
|
|
103
|
-
statusColor(
|
|
180
|
+
displaySession.padEnd(34) +
|
|
181
|
+
session.ticketId.padEnd(12) +
|
|
182
|
+
session.agentName.padEnd(18) +
|
|
183
|
+
typeIcon.padEnd(15) +
|
|
184
|
+
statusColor(statusText));
|
|
104
185
|
}
|
|
105
186
|
this.log('');
|
|
106
187
|
this.log('═'.repeat(90));
|
|
@@ -118,12 +199,27 @@ export default class SessionList extends PMOCommand {
|
|
|
118
199
|
this.log(styles.muted(' Run `prlt work stop <work-id>` to clean up.'));
|
|
119
200
|
this.log('');
|
|
120
201
|
}
|
|
202
|
+
// Show orphan sessions note
|
|
203
|
+
const orphanSessions = sessions.filter(s => s.source === 'discovered');
|
|
204
|
+
if (orphanSessions.length > 0) {
|
|
205
|
+
this.log(styles.muted(`\n📋 ${orphanSessions.length} session(s) discovered from tmux (marked with *).`));
|
|
206
|
+
this.log(styles.muted(' These sessions match the prlt naming pattern but are not tracked in the database.'));
|
|
207
|
+
this.log('');
|
|
208
|
+
}
|
|
121
209
|
}
|
|
122
210
|
else {
|
|
123
211
|
this.log('');
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
212
|
+
if (!hasWorkspace) {
|
|
213
|
+
this.log(styles.muted('Not in a workspace and no prlt-pattern tmux sessions found.'));
|
|
214
|
+
this.log('');
|
|
215
|
+
this.log(styles.muted('Run from a proletariat HQ directory to see tracked sessions,'));
|
|
216
|
+
this.log(styles.muted('or start work with: prlt work start <ticket-id>'));
|
|
217
|
+
}
|
|
218
|
+
else {
|
|
219
|
+
this.log(styles.muted('No active sessions found.'));
|
|
220
|
+
this.log('');
|
|
221
|
+
this.log(styles.muted('Start work with: prlt work start <ticket-id>'));
|
|
222
|
+
}
|
|
127
223
|
this.log('');
|
|
128
224
|
}
|
|
129
225
|
}
|
|
@@ -131,51 +227,4 @@ export default class SessionList extends PMOCommand {
|
|
|
131
227
|
db?.close();
|
|
132
228
|
}
|
|
133
229
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Get list of host tmux session names
|
|
136
|
-
*/
|
|
137
|
-
getHostTmuxSessionNames() {
|
|
138
|
-
try {
|
|
139
|
-
execSync('which tmux', { stdio: 'pipe' });
|
|
140
|
-
const output = execSync('tmux list-sessions -F "#{session_name}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
141
|
-
if (!output)
|
|
142
|
-
return [];
|
|
143
|
-
return output.split('\n');
|
|
144
|
-
}
|
|
145
|
-
catch {
|
|
146
|
-
return [];
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
/**
|
|
150
|
-
* Get map of containerId -> tmux session names
|
|
151
|
-
*/
|
|
152
|
-
getContainerTmuxSessionMap() {
|
|
153
|
-
const sessionMap = new Map();
|
|
154
|
-
try {
|
|
155
|
-
const containersOutput = execSync('docker ps --filter "label=devcontainer.local_folder" --format "{{.ID}}"', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
156
|
-
if (!containersOutput)
|
|
157
|
-
return sessionMap;
|
|
158
|
-
for (const containerId of containersOutput.split('\n')) {
|
|
159
|
-
try {
|
|
160
|
-
const tmuxOutput = execSync(`docker exec ${containerId} tmux list-sessions -F "#{session_name}" 2>/dev/null`, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
161
|
-
if (tmuxOutput) {
|
|
162
|
-
sessionMap.set(containerId, tmuxOutput.split('\n'));
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
catch {
|
|
166
|
-
// Container has no tmux sessions
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
catch {
|
|
171
|
-
// Docker not available
|
|
172
|
-
}
|
|
173
|
-
return sessionMap;
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
// =============================================================================
|
|
177
|
-
// Helper Functions
|
|
178
|
-
// =============================================================================
|
|
179
|
-
function padEnd(str, length) {
|
|
180
|
-
return str.padEnd(length);
|
|
181
230
|
}
|
|
@@ -11,6 +11,7 @@ export default class SpecCreate extends PMOCommand {
|
|
|
11
11
|
type: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
12
12
|
problem: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
13
13
|
interactive: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
|
+
'dry-run': import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
14
15
|
json: import("@oclif/core/interfaces").BooleanFlag<boolean>;
|
|
15
16
|
project: import("@oclif/core/interfaces").OptionFlag<string | undefined, import("@oclif/core/interfaces").CustomOptions>;
|
|
16
17
|
};
|
|
@@ -2,7 +2,7 @@ import { Flags, Args } from '@oclif/core';
|
|
|
2
2
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
3
|
import { styles } from '../../lib/styles.js';
|
|
4
4
|
import { slugify } from '../../lib/pmo/utils.js';
|
|
5
|
-
import { shouldOutputJson } from '../../lib/prompt-json.js';
|
|
5
|
+
import { shouldOutputJson, outputDryRunSuccessAsJson, outputDryRunErrorsAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
6
|
import { FlagResolver } from '../../lib/flags/index.js';
|
|
7
7
|
export default class SpecCreate extends PMOCommand {
|
|
8
8
|
static description = 'Create a new spec';
|
|
@@ -10,6 +10,7 @@ export default class SpecCreate extends PMOCommand {
|
|
|
10
10
|
'<%= config.bin %> <%= command.id %> "User Authentication"',
|
|
11
11
|
'<%= config.bin %> <%= command.id %> --title "API Design" --type product',
|
|
12
12
|
'<%= config.bin %> <%= command.id %> -i # Interactive mode',
|
|
13
|
+
'<%= config.bin %> <%= command.id %> --title "Test" --dry-run --json # Validate without creating',
|
|
13
14
|
];
|
|
14
15
|
static args = {
|
|
15
16
|
title: Args.string({
|
|
@@ -21,7 +22,7 @@ export default class SpecCreate extends PMOCommand {
|
|
|
21
22
|
...pmoBaseFlags,
|
|
22
23
|
title: Flags.string({
|
|
23
24
|
char: 't',
|
|
24
|
-
description: 'Spec title',
|
|
25
|
+
description: 'Spec title [required for non-interactive]',
|
|
25
26
|
}),
|
|
26
27
|
status: Flags.string({
|
|
27
28
|
char: 's',
|
|
@@ -41,6 +42,10 @@ export default class SpecCreate extends PMOCommand {
|
|
|
41
42
|
description: 'Interactive mode',
|
|
42
43
|
default: false,
|
|
43
44
|
}),
|
|
45
|
+
'dry-run': Flags.boolean({
|
|
46
|
+
description: 'Validate inputs without creating spec (use with --json for structured output)',
|
|
47
|
+
default: false,
|
|
48
|
+
}),
|
|
44
49
|
};
|
|
45
50
|
async execute() {
|
|
46
51
|
const { args, flags } = await this.parse(SpecCreate);
|
|
@@ -98,7 +103,7 @@ export default class SpecCreate extends PMOCommand {
|
|
|
98
103
|
});
|
|
99
104
|
resolver.addPrompt({
|
|
100
105
|
flagName: 'problem',
|
|
101
|
-
type: '
|
|
106
|
+
type: 'multiline',
|
|
102
107
|
message: 'Problem statement (optional):',
|
|
103
108
|
when: (ctx) => ctx.flags.title !== undefined,
|
|
104
109
|
});
|
|
@@ -108,6 +113,42 @@ export default class SpecCreate extends PMOCommand {
|
|
|
108
113
|
const specType = resolved.type === '' ? undefined : resolved.type;
|
|
109
114
|
// Generate ID from title
|
|
110
115
|
const specId = slugify(resolved.title);
|
|
116
|
+
// Check if spec already exists
|
|
117
|
+
const existing = await this.storage.getSpec(specId);
|
|
118
|
+
if (existing) {
|
|
119
|
+
if (flags['dry-run']) {
|
|
120
|
+
if (jsonMode) {
|
|
121
|
+
outputDryRunErrorsAsJson([{ field: 'id', error: `Spec "${specId}" already exists` }], createMetadata('spec create', flags));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
this.error(`Spec "${specId}" already exists.`);
|
|
125
|
+
}
|
|
126
|
+
// Handle dry-run: show what would be created without actually creating
|
|
127
|
+
if (flags['dry-run']) {
|
|
128
|
+
const wouldCreate = {
|
|
129
|
+
id: specId,
|
|
130
|
+
title: resolved.title,
|
|
131
|
+
status: resolved.status || 'draft',
|
|
132
|
+
...(specType && { type: specType }),
|
|
133
|
+
...(resolved.problem && { problem: resolved.problem }),
|
|
134
|
+
};
|
|
135
|
+
if (jsonMode) {
|
|
136
|
+
outputDryRunSuccessAsJson('spec', wouldCreate, createMetadata('spec create', flags));
|
|
137
|
+
}
|
|
138
|
+
// Human-readable dry-run output
|
|
139
|
+
this.log(styles.warning('\n[DRY RUN] Would create spec:'));
|
|
140
|
+
this.log(styles.muted(` ID: ${specId}`));
|
|
141
|
+
this.log(styles.muted(` Title: ${resolved.title}`));
|
|
142
|
+
this.log(styles.muted(` Status: ${resolved.status || 'draft'}`));
|
|
143
|
+
if (specType) {
|
|
144
|
+
this.log(styles.muted(` Type: ${specType}`));
|
|
145
|
+
}
|
|
146
|
+
if (resolved.problem) {
|
|
147
|
+
this.log(styles.muted(` Problem: ${resolved.problem}`));
|
|
148
|
+
}
|
|
149
|
+
this.log(styles.muted('\n(No spec was created)'));
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
111
152
|
// Create spec in database
|
|
112
153
|
const spec = await this.storage.createSpec({
|
|
113
154
|
id: specId,
|