@proletariat/cli 0.3.24 → 0.3.26
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 +3 -3
- package/dist/commands/action/index.js +2 -2
- package/dist/commands/action/update.js +3 -3
- package/dist/commands/agent/auth.js +1 -1
- package/dist/commands/agent/cleanup.js +6 -6
- package/dist/commands/agent/discover.js +1 -1
- package/dist/commands/agent/remove.js +4 -4
- package/dist/commands/autocomplete/setup.d.ts +2 -2
- package/dist/commands/autocomplete/setup.js +5 -5
- package/dist/commands/branch/create.js +31 -30
- package/dist/commands/category/create.js +4 -5
- package/dist/commands/category/delete.js +2 -3
- package/dist/commands/category/rename.js +2 -3
- package/dist/commands/claude.d.ts +2 -8
- package/dist/commands/claude.js +26 -26
- package/dist/commands/commit.d.ts +2 -8
- package/dist/commands/commit.js +4 -26
- package/dist/commands/config/index.d.ts +2 -10
- package/dist/commands/config/index.js +8 -34
- package/dist/commands/docker/index.d.ts +2 -2
- package/dist/commands/docker/index.js +8 -8
- package/dist/commands/epic/activate.js +9 -17
- package/dist/commands/epic/archive.js +13 -24
- package/dist/commands/epic/create.js +7 -6
- package/dist/commands/epic/delete.js +4 -5
- 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/feedback/view.d.ts +15 -0
- package/dist/commands/feedback/view.js +109 -0
- package/dist/commands/gh/index.js +4 -0
- package/dist/commands/link/index.js +2 -2
- package/dist/commands/pmo/init.d.ts +2 -2
- package/dist/commands/pmo/init.js +7 -7
- package/dist/commands/project/spec.js +6 -6
- 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/health.d.ts +29 -0
- package/dist/commands/session/health.js +495 -0
- package/dist/commands/session/index.js +4 -0
- package/dist/commands/session/list.d.ts +0 -8
- package/dist/commands/session/list.js +130 -81
- package/dist/commands/spec/create.js +1 -1
- package/dist/commands/spec/edit.js +64 -35
- package/dist/commands/staff/add.d.ts +2 -2
- package/dist/commands/staff/add.js +15 -14
- package/dist/commands/staff/index.js +2 -2
- package/dist/commands/staff/remove.js +4 -4
- package/dist/commands/status/index.js +6 -7
- package/dist/commands/support/book.d.ts +10 -0
- package/dist/commands/support/book.js +54 -0
- package/dist/commands/support/discord.d.ts +10 -0
- 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/template/apply.js +10 -11
- package/dist/commands/template/create.js +18 -17
- package/dist/commands/template/index.d.ts +2 -2
- package/dist/commands/template/index.js +6 -6
- package/dist/commands/template/save.js +8 -7
- package/dist/commands/template/update.js +6 -7
- package/dist/commands/terminal/title.d.ts +2 -26
- package/dist/commands/terminal/title.js +4 -33
- package/dist/commands/theme/index.d.ts +2 -2
- package/dist/commands/theme/index.js +19 -18
- package/dist/commands/theme/set.d.ts +2 -2
- package/dist/commands/theme/set.js +5 -5
- package/dist/commands/ticket/create.js +52 -26
- package/dist/commands/ticket/delete.js +15 -13
- package/dist/commands/ticket/edit.js +59 -20
- package/dist/commands/ticket/epic.js +12 -10
- package/dist/commands/ticket/move.d.ts +7 -0
- package/dist/commands/ticket/move.js +132 -0
- package/dist/commands/ticket/project.js +11 -9
- package/dist/commands/ticket/reassign.js +23 -19
- package/dist/commands/ticket/spec.js +7 -5
- package/dist/commands/ticket/update.js +55 -53
- package/dist/commands/whoami.js +1 -0
- package/dist/commands/work/ready.js +7 -7
- package/dist/commands/work/revise.js +13 -11
- package/dist/commands/work/spawn.d.ts +1 -0
- package/dist/commands/work/spawn.js +225 -64
- package/dist/commands/work/start.d.ts +1 -0
- package/dist/commands/work/start.js +301 -173
- package/dist/hooks/init.js +4 -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/multiline-input.d.ts +63 -0
- package/dist/lib/multiline-input.js +360 -0
- package/dist/lib/pr/index.d.ts +4 -0
- package/dist/lib/pr/index.js +32 -14
- package/dist/lib/prompt-command.d.ts +3 -0
- package/dist/lib/prompt-json.d.ts +77 -6
- package/dist/lib/prompt-json.js +46 -0
- package/dist/lib/repos/git.d.ts +7 -0
- package/dist/lib/repos/git.js +20 -0
- package/oclif.manifest.json +2913 -2246
- package/package.json +1 -1
|
@@ -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
|
}
|
|
@@ -103,7 +103,7 @@ export default class SpecCreate extends PMOCommand {
|
|
|
103
103
|
});
|
|
104
104
|
resolver.addPrompt({
|
|
105
105
|
flagName: 'problem',
|
|
106
|
-
type: '
|
|
106
|
+
type: 'multiline',
|
|
107
107
|
message: 'Problem statement (optional):',
|
|
108
108
|
when: (ctx) => ctx.flags.title !== undefined,
|
|
109
109
|
});
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { Flags, Args } from '@oclif/core';
|
|
2
|
-
import inquirer from 'inquirer';
|
|
3
2
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
4
3
|
import { styles } from '../../lib/styles.js';
|
|
5
4
|
import { shouldOutputJson, outputErrorAsJson, createMetadata, } from '../../lib/prompt-json.js';
|
|
6
5
|
import { FlagResolver } from '../../lib/flags/index.js';
|
|
6
|
+
import { multiLineInput } from '../../lib/multiline-input.js';
|
|
7
7
|
export default class SpecEdit extends PMOCommand {
|
|
8
8
|
static description = 'Edit an existing spec';
|
|
9
9
|
static examples = [
|
|
@@ -114,6 +114,25 @@ export default class SpecEdit extends PMOCommand {
|
|
|
114
114
|
const hasFlags = flags.title || flags.status || flags.type || flags.problem ||
|
|
115
115
|
flags.solution || flags.decisions;
|
|
116
116
|
if (flags.interactive || !hasFlags) {
|
|
117
|
+
// In JSON mode without flags, output a form prompt instead of interactive prompts
|
|
118
|
+
if (jsonMode) {
|
|
119
|
+
const { outputPromptAsJson, buildFormPromptConfig } = await import('../../lib/prompt-json.js');
|
|
120
|
+
const formConfig = buildFormPromptConfig([
|
|
121
|
+
{ type: 'input', name: 'title', message: 'Title:', default: spec.title },
|
|
122
|
+
{ type: 'list', name: 'status', message: 'Status:', choices: statusChoices, default: spec.status },
|
|
123
|
+
{ type: 'list', name: 'type', message: 'Type:', choices: typeChoices, default: spec.type || '' },
|
|
124
|
+
{ type: 'multiline', name: 'problem', message: 'Problem statement:', default: spec.problem || '' },
|
|
125
|
+
{ type: 'multiline', name: 'solution', message: 'Solution:', default: spec.solution || '' },
|
|
126
|
+
{ type: 'multiline', name: 'decisions', message: 'Design decisions:', default: spec.decisions || '' },
|
|
127
|
+
]);
|
|
128
|
+
formConfig.context = {
|
|
129
|
+
hint: `Edit spec with: prlt spec edit ${specId} --title "..." --problem "..." --json`,
|
|
130
|
+
specId,
|
|
131
|
+
currentValues: { title: spec.title, status: spec.status, type: spec.type, problem: spec.problem, solution: spec.solution, decisions: spec.decisions },
|
|
132
|
+
};
|
|
133
|
+
outputPromptAsJson(formConfig, createMetadata('spec edit', flags));
|
|
134
|
+
return; // outputPromptAsJson exits, but TypeScript doesn't know
|
|
135
|
+
}
|
|
117
136
|
// Interactive mode - prompt for editable fields
|
|
118
137
|
updates = await this.promptForEdits(spec, typeChoices, statusChoices);
|
|
119
138
|
}
|
|
@@ -162,7 +181,8 @@ export default class SpecEdit extends PMOCommand {
|
|
|
162
181
|
this.log(styles.muted(`View spec: prlt spec view ${updatedSpec.id}`));
|
|
163
182
|
}
|
|
164
183
|
async promptForEdits(spec, typeChoices, statusChoices) {
|
|
165
|
-
|
|
184
|
+
// First prompt for title, status, and type
|
|
185
|
+
const basicAnswers = await this.prompt([
|
|
166
186
|
{
|
|
167
187
|
type: 'input',
|
|
168
188
|
name: 'title',
|
|
@@ -184,48 +204,57 @@ export default class SpecEdit extends PMOCommand {
|
|
|
184
204
|
choices: typeChoices,
|
|
185
205
|
default: spec.type || '',
|
|
186
206
|
},
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
207
|
+
], null);
|
|
208
|
+
// Prompt for problem statement using multiline input
|
|
209
|
+
const problemResult = await multiLineInput({
|
|
210
|
+
message: 'Problem statement:',
|
|
211
|
+
default: spec.problem || '',
|
|
212
|
+
hint: 'Describe the problem this spec addresses. Ctrl+D to finish, Ctrl+C to cancel',
|
|
213
|
+
});
|
|
214
|
+
if (problemResult.cancelled) {
|
|
215
|
+
throw new Error('Edit cancelled');
|
|
216
|
+
}
|
|
217
|
+
// Prompt for solution using multiline input
|
|
218
|
+
const solutionResult = await multiLineInput({
|
|
219
|
+
message: 'Solution:',
|
|
220
|
+
default: spec.solution || '',
|
|
221
|
+
hint: 'Describe the proposed solution. Ctrl+D to finish, Ctrl+C to cancel',
|
|
222
|
+
});
|
|
223
|
+
if (solutionResult.cancelled) {
|
|
224
|
+
throw new Error('Edit cancelled');
|
|
225
|
+
}
|
|
226
|
+
// Prompt for decisions using multiline input
|
|
227
|
+
const decisionsResult = await multiLineInput({
|
|
228
|
+
message: 'Design decisions:',
|
|
229
|
+
default: spec.decisions || '',
|
|
230
|
+
hint: 'Document key design decisions. Ctrl+D to finish, Ctrl+C to cancel',
|
|
231
|
+
});
|
|
232
|
+
if (decisionsResult.cancelled) {
|
|
233
|
+
throw new Error('Edit cancelled');
|
|
234
|
+
}
|
|
209
235
|
// Build updates object with only changed fields
|
|
210
236
|
const updates = {};
|
|
211
|
-
if (
|
|
212
|
-
updates.title =
|
|
237
|
+
if (basicAnswers.title !== spec.title) {
|
|
238
|
+
updates.title = basicAnswers.title;
|
|
213
239
|
}
|
|
214
|
-
if (
|
|
215
|
-
updates.status =
|
|
240
|
+
if (basicAnswers.status !== spec.status) {
|
|
241
|
+
updates.status = basicAnswers.status;
|
|
216
242
|
}
|
|
217
|
-
const newType =
|
|
243
|
+
const newType = basicAnswers.type === '' ? undefined : basicAnswers.type;
|
|
218
244
|
if (newType !== spec.type) {
|
|
219
245
|
updates.type = newType;
|
|
220
246
|
}
|
|
221
|
-
if (
|
|
222
|
-
|
|
247
|
+
if (problemResult.value !== (spec.problem || '')) {
|
|
248
|
+
// Preserve empty string to allow clearing the field
|
|
249
|
+
updates.problem = problemResult.value;
|
|
223
250
|
}
|
|
224
|
-
if (
|
|
225
|
-
|
|
251
|
+
if (solutionResult.value !== (spec.solution || '')) {
|
|
252
|
+
// Preserve empty string to allow clearing the field
|
|
253
|
+
updates.solution = solutionResult.value;
|
|
226
254
|
}
|
|
227
|
-
if (
|
|
228
|
-
|
|
255
|
+
if (decisionsResult.value !== (spec.decisions || '')) {
|
|
256
|
+
// Preserve empty string to allow clearing the field
|
|
257
|
+
updates.decisions = decisionsResult.value;
|
|
229
258
|
}
|
|
230
259
|
return updates;
|
|
231
260
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export default class Add extends
|
|
1
|
+
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
2
|
+
export default class Add extends PromptCommand {
|
|
3
3
|
static description: string;
|
|
4
4
|
static examples: string[];
|
|
5
5
|
static args: {
|
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { Args, Flags } from '@oclif/core';
|
|
2
2
|
import chalk from 'chalk';
|
|
3
3
|
import inquirer from 'inquirer';
|
|
4
|
+
import { PromptCommand } from '../../lib/prompt-command.js';
|
|
4
5
|
import { getWorkspaceInfo, validateAgentNames, addAgentsToWorkspace } from '../../lib/agents/commands.js';
|
|
5
6
|
import { ensureBuiltinThemes, BUILTIN_THEMES, isValidAgentName, normalizeAgentName } from '../../lib/themes.js';
|
|
6
7
|
import { getTheme, getThemes, getAvailableThemeNames, getActiveTheme } from '../../lib/database/index.js';
|
|
7
8
|
import { shouldOutputJson, outputPromptAsJson, outputErrorAsJson, createMetadata, buildPromptConfig, } from '../../lib/prompt-json.js';
|
|
8
|
-
export default class Add extends
|
|
9
|
+
export default class Add extends PromptCommand {
|
|
9
10
|
static description = 'Add new agents to the workspace';
|
|
10
11
|
static examples = [
|
|
11
12
|
'<%= config.bin %> <%= command.id %> zeus',
|
|
@@ -82,13 +83,13 @@ export default class Add extends Command {
|
|
|
82
83
|
return;
|
|
83
84
|
}
|
|
84
85
|
// Interactive selection from theme
|
|
85
|
-
const { selected } = await
|
|
86
|
+
const { selected } = await this.prompt([{
|
|
86
87
|
type: 'checkbox',
|
|
87
88
|
name: 'selected',
|
|
88
89
|
message: selectMessage,
|
|
89
90
|
choices: nameChoices,
|
|
90
91
|
validate: (input) => input.length > 0 || 'Please select at least one name'
|
|
91
|
-
}]);
|
|
92
|
+
}], null);
|
|
92
93
|
agentNames = selected;
|
|
93
94
|
}
|
|
94
95
|
// Interactive mode: show names from workspace's active theme
|
|
@@ -127,19 +128,19 @@ export default class Add extends Command {
|
|
|
127
128
|
new inquirer.Separator(),
|
|
128
129
|
{ name: chalk.blue(nameChoices[nameChoices.length - 1].name), value: nameChoices[nameChoices.length - 1].value }
|
|
129
130
|
];
|
|
130
|
-
const { selected } = await
|
|
131
|
+
const { selected } = await this.prompt([{
|
|
131
132
|
type: 'checkbox',
|
|
132
133
|
name: 'selected',
|
|
133
134
|
message: selectMessage,
|
|
134
135
|
choices,
|
|
135
136
|
pageSize: 20,
|
|
136
137
|
validate: (input) => input.length > 0 || 'Please select at least one name'
|
|
137
|
-
}]);
|
|
138
|
+
}], null);
|
|
138
139
|
// Check if custom was selected
|
|
139
140
|
const hasCustom = selected.includes('__custom__');
|
|
140
141
|
const themedSelections = selected.filter((s) => s !== '__custom__');
|
|
141
142
|
if (hasCustom) {
|
|
142
|
-
const { customNames } = await
|
|
143
|
+
const { customNames } = await this.prompt([{
|
|
143
144
|
type: 'input',
|
|
144
145
|
name: 'customNames',
|
|
145
146
|
message: 'Enter custom agent names (space-separated):',
|
|
@@ -148,7 +149,7 @@ export default class Add extends Command {
|
|
|
148
149
|
return 'Please enter at least one name';
|
|
149
150
|
return true;
|
|
150
151
|
}
|
|
151
|
-
}]);
|
|
152
|
+
}], null);
|
|
152
153
|
const rawNames = customNames.trim().split(/\s+/);
|
|
153
154
|
const normalizedCustom = rawNames.map((n) => normalizeAgentName(n)).filter((n) => n && isValidAgentName(n));
|
|
154
155
|
agentNames.push(...normalizedCustom);
|
|
@@ -187,14 +188,14 @@ export default class Add extends Command {
|
|
|
187
188
|
new inquirer.Separator(),
|
|
188
189
|
{ name: chalk.blue(themeChoices[themeChoices.length - 1].name), value: themeChoices[themeChoices.length - 1].value }
|
|
189
190
|
];
|
|
190
|
-
const { selectedTheme } = await
|
|
191
|
+
const { selectedTheme } = await this.prompt([{
|
|
191
192
|
type: 'list',
|
|
192
193
|
name: 'selectedTheme',
|
|
193
194
|
message: selectMessage,
|
|
194
195
|
choices: interactiveChoices
|
|
195
|
-
}]);
|
|
196
|
+
}], null);
|
|
196
197
|
if (selectedTheme === '__custom__') {
|
|
197
|
-
const { customNames } = await
|
|
198
|
+
const { customNames } = await this.prompt([{
|
|
198
199
|
type: 'input',
|
|
199
200
|
name: 'customNames',
|
|
200
201
|
message: 'Enter custom agent names (space-separated):',
|
|
@@ -203,7 +204,7 @@ export default class Add extends Command {
|
|
|
203
204
|
return 'Please enter at least one name';
|
|
204
205
|
return true;
|
|
205
206
|
}
|
|
206
|
-
}]);
|
|
207
|
+
}], null);
|
|
207
208
|
const rawNames = customNames.trim().split(/\s+/);
|
|
208
209
|
const normalizedCustom = rawNames.map((n) => ({
|
|
209
210
|
original: n,
|
|
@@ -222,14 +223,14 @@ export default class Add extends Command {
|
|
|
222
223
|
themeId = selectedTheme;
|
|
223
224
|
const theme = getTheme(workspaceInfo.path, selectedTheme);
|
|
224
225
|
const availableNames = getAvailableThemeNames(workspaceInfo.path, selectedTheme);
|
|
225
|
-
const { selected } = await
|
|
226
|
+
const { selected } = await this.prompt([{
|
|
226
227
|
type: 'checkbox',
|
|
227
228
|
name: 'selected',
|
|
228
229
|
message: `Select agents from ${theme?.display_name}:`,
|
|
229
230
|
choices: availableNames.map(name => ({ name, value: name })),
|
|
230
231
|
pageSize: 20,
|
|
231
232
|
validate: (input) => input.length > 0 || 'Please select at least one name'
|
|
232
|
-
}]);
|
|
233
|
+
}], null);
|
|
233
234
|
agentNames = selected;
|
|
234
235
|
}
|
|
235
236
|
}
|
|
@@ -46,7 +46,7 @@ export default class Staff extends PMOCommand {
|
|
|
46
46
|
}
|
|
47
47
|
this.log(colors.primary('Staff Agents'));
|
|
48
48
|
this.log(colors.textMuted('Persistent agents with dedicated worktrees.\n'));
|
|
49
|
-
const { action } = await
|
|
49
|
+
const { action } = await this.prompt([{
|
|
50
50
|
type: 'list',
|
|
51
51
|
name: 'action',
|
|
52
52
|
message,
|
|
@@ -57,7 +57,7 @@ export default class Staff extends PMOCommand {
|
|
|
57
57
|
new inquirer.Separator(),
|
|
58
58
|
{ name: '❌ ' + menuChoices[3].name, value: menuChoices[3].value },
|
|
59
59
|
]
|
|
60
|
-
}]);
|
|
60
|
+
}], null);
|
|
61
61
|
if (action === 'cancel') {
|
|
62
62
|
this.log(colors.textMuted('Operation cancelled.'));
|
|
63
63
|
return;
|
|
@@ -71,7 +71,7 @@ export default class Remove extends PMOCommand {
|
|
|
71
71
|
outputPromptAsJson(buildPromptConfig('list', 'name', selectMessage, agentChoices), createMetadata('staff remove', flags));
|
|
72
72
|
return;
|
|
73
73
|
}
|
|
74
|
-
const { selected } = await
|
|
74
|
+
const { selected } = await this.prompt([
|
|
75
75
|
{
|
|
76
76
|
type: 'list',
|
|
77
77
|
name: 'selected',
|
|
@@ -82,7 +82,7 @@ export default class Remove extends PMOCommand {
|
|
|
82
82
|
{ name: '❌ ' + agentChoices[agentChoices.length - 1].name, value: agentChoices[agentChoices.length - 1].value }
|
|
83
83
|
]
|
|
84
84
|
}
|
|
85
|
-
]);
|
|
85
|
+
], null);
|
|
86
86
|
if (selected === 'cancel') {
|
|
87
87
|
this.log(colors.textMuted('Operation cancelled.'));
|
|
88
88
|
return;
|
|
@@ -109,7 +109,7 @@ export default class Remove extends PMOCommand {
|
|
|
109
109
|
outputPromptAsJson(buildPromptConfig('list', 'confirmed', confirmMessage, confirmChoices), createMetadata('staff remove', flags));
|
|
110
110
|
return;
|
|
111
111
|
}
|
|
112
|
-
const { confirm } = await
|
|
112
|
+
const { confirm } = await this.prompt([
|
|
113
113
|
{
|
|
114
114
|
type: 'list',
|
|
115
115
|
name: 'confirm',
|
|
@@ -120,7 +120,7 @@ export default class Remove extends PMOCommand {
|
|
|
120
120
|
],
|
|
121
121
|
default: 0 // Default to "No, cancel"
|
|
122
122
|
}
|
|
123
|
-
]);
|
|
123
|
+
], null);
|
|
124
124
|
if (!confirm) {
|
|
125
125
|
this.log(colors.textMuted('Removal cancelled.'));
|
|
126
126
|
return;
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import inquirer from 'inquirer';
|
|
2
1
|
import { PMOCommand, pmoBaseFlags } from '../../lib/pmo/index.js';
|
|
3
2
|
import { FlagResolver, shouldOutputJson } from '../../lib/flags/index.js';
|
|
4
3
|
export default class Status extends PMOCommand {
|
|
@@ -62,34 +61,34 @@ export default class Status extends PMOCommand {
|
|
|
62
61
|
case 'update': {
|
|
63
62
|
// First list statuses, then prompt for selection
|
|
64
63
|
await this.config.runCommand('status:list', []);
|
|
65
|
-
const { statusId } = await
|
|
64
|
+
const { statusId } = await this.prompt([{
|
|
66
65
|
type: 'input',
|
|
67
66
|
name: 'statusId',
|
|
68
67
|
message: 'Status ID to update:',
|
|
69
68
|
validate: (input) => input.length > 0 || 'Status ID is required',
|
|
70
|
-
}]);
|
|
69
|
+
}], null);
|
|
71
70
|
await this.config.runCommand('status:update', [statusId]);
|
|
72
71
|
break;
|
|
73
72
|
}
|
|
74
73
|
case 'move': {
|
|
75
74
|
await this.config.runCommand('status:list', []);
|
|
76
|
-
const { statusId } = await
|
|
75
|
+
const { statusId } = await this.prompt([{
|
|
77
76
|
type: 'input',
|
|
78
77
|
name: 'statusId',
|
|
79
78
|
message: 'Status ID to move:',
|
|
80
79
|
validate: (input) => input.length > 0 || 'Status ID is required',
|
|
81
|
-
}]);
|
|
80
|
+
}], null);
|
|
82
81
|
await this.config.runCommand('status:move', [statusId]);
|
|
83
82
|
break;
|
|
84
83
|
}
|
|
85
84
|
case 'delete': {
|
|
86
85
|
await this.config.runCommand('status:list', []);
|
|
87
|
-
const { statusId } = await
|
|
86
|
+
const { statusId } = await this.prompt([{
|
|
88
87
|
type: 'input',
|
|
89
88
|
name: 'statusId',
|
|
90
89
|
message: 'Status ID to delete:',
|
|
91
90
|
validate: (input) => input.length > 0 || 'Status ID is required',
|
|
92
|
-
}]);
|
|
91
|
+
}], null);
|
|
93
92
|
await this.config.runCommand('status:delete', [statusId]);
|
|
94
93
|
break;
|
|
95
94
|
}
|