prjct-cli 0.46.0 → 0.47.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.
- package/CHANGELOG.md +42 -0
- package/bin/prjct.ts +3 -47
- package/core/agentic/command-executor.ts +8 -1
- package/core/commands/command-data.ts +16 -0
- package/core/commands/commands.ts +7 -0
- package/core/commands/register.ts +1 -0
- package/core/commands/setup.ts +4 -4
- package/core/commands/shipping.ts +26 -3
- package/core/commands/workflow.ts +105 -2
- package/core/utils/help.ts +321 -0
- package/core/utils/subtask-table.ts +234 -0
- package/core/workflow/index.ts +1 -0
- package/core/workflow/workflow-preferences.ts +312 -0
- package/dist/bin/prjct.mjs +4394 -3828
- package/package.json +1 -1
- package/templates/commands/workflow.md +150 -0
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Help System - Structured help output for prjct CLI
|
|
3
|
+
*
|
|
4
|
+
* Provides consistent, well-formatted help text for all commands.
|
|
5
|
+
*
|
|
6
|
+
* @see PRJ-133
|
|
7
|
+
* @module utils/help
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { CATEGORIES, COMMANDS } from '../commands/command-data'
|
|
11
|
+
import type { CommandMeta } from '../types'
|
|
12
|
+
import { VERSION } from './version'
|
|
13
|
+
|
|
14
|
+
// ANSI colors
|
|
15
|
+
const CYAN = '\x1b[36m'
|
|
16
|
+
const DIM = '\x1b[2m'
|
|
17
|
+
const BOLD = '\x1b[1m'
|
|
18
|
+
const RESET = '\x1b[0m'
|
|
19
|
+
const GREEN = '\x1b[32m'
|
|
20
|
+
const YELLOW = '\x1b[33m'
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Terminal commands that run directly in the shell
|
|
24
|
+
*/
|
|
25
|
+
const TERMINAL_COMMANDS = [
|
|
26
|
+
{
|
|
27
|
+
name: 'start',
|
|
28
|
+
description: 'First-time setup wizard',
|
|
29
|
+
example: 'prjct start',
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
name: 'init',
|
|
33
|
+
description: 'Initialize project in current directory',
|
|
34
|
+
example: 'prjct init',
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
name: 'sync',
|
|
38
|
+
description: 'Sync project state and update context files',
|
|
39
|
+
example: 'prjct sync',
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: 'watch',
|
|
43
|
+
description: 'Auto-sync on file changes',
|
|
44
|
+
example: 'prjct watch',
|
|
45
|
+
options: ['--verbose', '--debounce=<ms>', '--interval=<sec>'],
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
name: 'doctor',
|
|
49
|
+
description: 'Check system health and dependencies',
|
|
50
|
+
example: 'prjct doctor',
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: 'serve',
|
|
54
|
+
description: 'Start web dashboard server',
|
|
55
|
+
example: 'prjct serve [port]',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
name: 'context',
|
|
59
|
+
description: 'Smart context filtering tools for AI',
|
|
60
|
+
example: 'prjct context files "add auth"',
|
|
61
|
+
subcommands: ['files', 'signatures', 'imports', 'recent', 'summary'],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: 'linear',
|
|
65
|
+
description: 'Linear issue tracker CLI',
|
|
66
|
+
example: 'prjct linear list',
|
|
67
|
+
subcommands: ['list', 'get', 'create', 'update'],
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'uninstall',
|
|
71
|
+
description: 'Complete system removal of prjct',
|
|
72
|
+
example: 'prjct uninstall --backup',
|
|
73
|
+
options: ['--force', '--backup', '--dry-run', '--keep-package'],
|
|
74
|
+
},
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Global CLI flags
|
|
79
|
+
*/
|
|
80
|
+
const GLOBAL_FLAGS = [
|
|
81
|
+
{ flag: '-q, --quiet', description: 'Suppress all output (errors to stderr only)' },
|
|
82
|
+
{ flag: '-v, --version', description: 'Show version and provider status' },
|
|
83
|
+
{ flag: '-h, --help', description: 'Show this help message' },
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Format the main help output
|
|
88
|
+
*/
|
|
89
|
+
export function formatMainHelp(): string {
|
|
90
|
+
const lines: string[] = []
|
|
91
|
+
|
|
92
|
+
// Header
|
|
93
|
+
lines.push('')
|
|
94
|
+
lines.push(`${CYAN}${BOLD}prjct${RESET} v${VERSION} - Context layer for AI coding agents`)
|
|
95
|
+
lines.push(`${DIM}Works with Claude Code, Gemini CLI, Cursor, Windsurf, and more.${RESET}`)
|
|
96
|
+
lines.push('')
|
|
97
|
+
|
|
98
|
+
// Quick Start
|
|
99
|
+
lines.push(`${BOLD}QUICK START${RESET}`)
|
|
100
|
+
lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
|
|
101
|
+
lines.push(` ${GREEN}1.${RESET} prjct start ${DIM}# Configure AI providers${RESET}`)
|
|
102
|
+
lines.push(` ${GREEN}2.${RESET} cd my-project && prjct init`)
|
|
103
|
+
lines.push(` ${GREEN}3.${RESET} Open in Claude Code / Gemini CLI / Cursor`)
|
|
104
|
+
lines.push(` ${GREEN}4.${RESET} p. sync ${DIM}# Analyze project${RESET}`)
|
|
105
|
+
lines.push('')
|
|
106
|
+
|
|
107
|
+
// Terminal Commands
|
|
108
|
+
lines.push(`${BOLD}TERMINAL COMMANDS${RESET}`)
|
|
109
|
+
lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
|
|
110
|
+
for (const cmd of TERMINAL_COMMANDS) {
|
|
111
|
+
const name = `prjct ${cmd.name}`.padEnd(22)
|
|
112
|
+
lines.push(` ${name} ${cmd.description}`)
|
|
113
|
+
}
|
|
114
|
+
lines.push('')
|
|
115
|
+
|
|
116
|
+
// AI Agent Commands
|
|
117
|
+
lines.push(`${BOLD}AI AGENT COMMANDS${RESET} ${DIM}(inside Claude/Gemini/Cursor)${RESET}`)
|
|
118
|
+
lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
|
|
119
|
+
lines.push(` ${'Command'.padEnd(22)} Description`)
|
|
120
|
+
lines.push(` ${DIM}${'─'.repeat(56)}${RESET}`)
|
|
121
|
+
|
|
122
|
+
// Core commands
|
|
123
|
+
const coreCommands = COMMANDS.filter((c) => c.group === 'core' && c.usage?.claude)
|
|
124
|
+
for (const cmd of coreCommands.slice(0, 10)) {
|
|
125
|
+
const usage = `p. ${cmd.name}`.padEnd(22)
|
|
126
|
+
lines.push(` ${usage} ${cmd.description}`)
|
|
127
|
+
}
|
|
128
|
+
lines.push(` ${DIM}... and ${coreCommands.length - 10} more (run 'prjct help commands')${RESET}`)
|
|
129
|
+
lines.push('')
|
|
130
|
+
|
|
131
|
+
// Global Flags
|
|
132
|
+
lines.push(`${BOLD}FLAGS${RESET}`)
|
|
133
|
+
lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
|
|
134
|
+
for (const flag of GLOBAL_FLAGS) {
|
|
135
|
+
lines.push(` ${flag.flag.padEnd(22)} ${flag.description}`)
|
|
136
|
+
}
|
|
137
|
+
lines.push('')
|
|
138
|
+
|
|
139
|
+
// More Info
|
|
140
|
+
lines.push(`${BOLD}MORE INFO${RESET}`)
|
|
141
|
+
lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
|
|
142
|
+
lines.push(` Documentation: ${CYAN}https://prjct.app${RESET}`)
|
|
143
|
+
lines.push(` GitHub: ${CYAN}https://github.com/jlopezlira/prjct-cli${RESET}`)
|
|
144
|
+
lines.push(` Per-command: prjct help <command>`)
|
|
145
|
+
lines.push('')
|
|
146
|
+
|
|
147
|
+
return lines.join('\n')
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Format help for a specific terminal command
|
|
152
|
+
*/
|
|
153
|
+
export function formatTerminalCommandHelp(commandName: string): string | null {
|
|
154
|
+
const cmd = TERMINAL_COMMANDS.find((c) => c.name === commandName)
|
|
155
|
+
if (!cmd) return null
|
|
156
|
+
|
|
157
|
+
const lines: string[] = []
|
|
158
|
+
|
|
159
|
+
lines.push('')
|
|
160
|
+
lines.push(`${CYAN}${BOLD}prjct ${cmd.name}${RESET} - ${cmd.description}`)
|
|
161
|
+
lines.push('')
|
|
162
|
+
|
|
163
|
+
lines.push(`${BOLD}USAGE${RESET}`)
|
|
164
|
+
lines.push(` ${cmd.example}`)
|
|
165
|
+
lines.push('')
|
|
166
|
+
|
|
167
|
+
if (cmd.options) {
|
|
168
|
+
lines.push(`${BOLD}OPTIONS${RESET}`)
|
|
169
|
+
for (const opt of cmd.options) {
|
|
170
|
+
lines.push(` ${opt}`)
|
|
171
|
+
}
|
|
172
|
+
lines.push('')
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
if (cmd.subcommands) {
|
|
176
|
+
lines.push(`${BOLD}SUBCOMMANDS${RESET}`)
|
|
177
|
+
for (const sub of cmd.subcommands) {
|
|
178
|
+
lines.push(` ${sub}`)
|
|
179
|
+
}
|
|
180
|
+
lines.push('')
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return lines.join('\n')
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Format help for an AI agent command
|
|
188
|
+
*/
|
|
189
|
+
export function formatAgentCommandHelp(commandName: string): string | null {
|
|
190
|
+
const cmd = COMMANDS.find((c) => c.name === commandName)
|
|
191
|
+
if (!cmd) return null
|
|
192
|
+
|
|
193
|
+
const lines: string[] = []
|
|
194
|
+
|
|
195
|
+
lines.push('')
|
|
196
|
+
lines.push(`${CYAN}${BOLD}p. ${cmd.name}${RESET} - ${cmd.description}`)
|
|
197
|
+
lines.push('')
|
|
198
|
+
|
|
199
|
+
lines.push(`${BOLD}USAGE${RESET}`)
|
|
200
|
+
if (cmd.usage?.claude) {
|
|
201
|
+
lines.push(` Claude/Gemini: ${cmd.usage.claude.replace('/p:', 'p. ')}`)
|
|
202
|
+
}
|
|
203
|
+
if (cmd.usage?.terminal) {
|
|
204
|
+
lines.push(` Terminal: ${cmd.usage.terminal}`)
|
|
205
|
+
}
|
|
206
|
+
lines.push('')
|
|
207
|
+
|
|
208
|
+
if (cmd.params) {
|
|
209
|
+
lines.push(`${BOLD}PARAMETERS${RESET}`)
|
|
210
|
+
lines.push(` ${cmd.params}`)
|
|
211
|
+
lines.push('')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (cmd.features && cmd.features.length > 0) {
|
|
215
|
+
lines.push(`${BOLD}FEATURES${RESET}`)
|
|
216
|
+
for (const feature of cmd.features) {
|
|
217
|
+
lines.push(` • ${feature}`)
|
|
218
|
+
}
|
|
219
|
+
lines.push('')
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
if (cmd.blockingRules) {
|
|
223
|
+
lines.push(`${BOLD}REQUIREMENTS${RESET}`)
|
|
224
|
+
lines.push(` ${YELLOW}⚠${RESET} ${cmd.blockingRules.check}`)
|
|
225
|
+
lines.push('')
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Category info
|
|
229
|
+
const category = CATEGORIES[cmd.group]
|
|
230
|
+
if (category) {
|
|
231
|
+
lines.push(`${DIM}Category: ${category.title}${RESET}`)
|
|
232
|
+
if (cmd.isOptional) {
|
|
233
|
+
lines.push(`${DIM}This is an optional command.${RESET}`)
|
|
234
|
+
}
|
|
235
|
+
lines.push('')
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
return lines.join('\n')
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Format help for a specific command (auto-detect type)
|
|
243
|
+
*/
|
|
244
|
+
export function formatCommandHelp(commandName: string): string {
|
|
245
|
+
// Try terminal command first
|
|
246
|
+
const terminalHelp = formatTerminalCommandHelp(commandName)
|
|
247
|
+
if (terminalHelp) return terminalHelp
|
|
248
|
+
|
|
249
|
+
// Try agent command
|
|
250
|
+
const agentHelp = formatAgentCommandHelp(commandName)
|
|
251
|
+
if (agentHelp) return agentHelp
|
|
252
|
+
|
|
253
|
+
// Command not found
|
|
254
|
+
return `
|
|
255
|
+
${YELLOW}Command '${commandName}' not found.${RESET}
|
|
256
|
+
|
|
257
|
+
Run 'prjct help' to see all available commands.
|
|
258
|
+
`
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Format list of all commands grouped by category
|
|
263
|
+
*/
|
|
264
|
+
export function formatCommandList(): string {
|
|
265
|
+
const lines: string[] = []
|
|
266
|
+
|
|
267
|
+
lines.push('')
|
|
268
|
+
lines.push(`${CYAN}${BOLD}All Commands${RESET}`)
|
|
269
|
+
lines.push('')
|
|
270
|
+
|
|
271
|
+
// Group by category
|
|
272
|
+
const categories = Object.entries(CATEGORIES).sort((a, b) => a[1].order - b[1].order)
|
|
273
|
+
|
|
274
|
+
for (const [categoryKey, category] of categories) {
|
|
275
|
+
const categoryCommands = COMMANDS.filter((c) => c.group === categoryKey)
|
|
276
|
+
if (categoryCommands.length === 0) continue
|
|
277
|
+
|
|
278
|
+
lines.push(
|
|
279
|
+
`${BOLD}${category.title}${RESET} ${DIM}(${categoryCommands.length} commands)${RESET}`
|
|
280
|
+
)
|
|
281
|
+
lines.push(`${DIM}${category.description}${RESET}`)
|
|
282
|
+
lines.push('')
|
|
283
|
+
|
|
284
|
+
for (const cmd of categoryCommands) {
|
|
285
|
+
const name = `p. ${cmd.name}`.padEnd(18)
|
|
286
|
+
const desc =
|
|
287
|
+
cmd.description.length > 45 ? `${cmd.description.slice(0, 42)}...` : cmd.description
|
|
288
|
+
lines.push(` ${name} ${desc}`)
|
|
289
|
+
}
|
|
290
|
+
lines.push('')
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
lines.push(`${DIM}Run 'prjct help <command>' for detailed help on a specific command.${RESET}`)
|
|
294
|
+
lines.push('')
|
|
295
|
+
|
|
296
|
+
return lines.join('\n')
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Get help output based on topic
|
|
301
|
+
*/
|
|
302
|
+
export function getHelp(topic?: string): string {
|
|
303
|
+
if (!topic) {
|
|
304
|
+
return formatMainHelp()
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (topic === 'commands' || topic === 'all') {
|
|
308
|
+
return formatCommandList()
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
return formatCommandHelp(topic)
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
export default {
|
|
315
|
+
formatMainHelp,
|
|
316
|
+
formatCommandHelp,
|
|
317
|
+
formatCommandList,
|
|
318
|
+
formatTerminalCommandHelp,
|
|
319
|
+
formatAgentCommandHelp,
|
|
320
|
+
getHelp,
|
|
321
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subtask Progress Display
|
|
3
|
+
*
|
|
4
|
+
* Clean, minimal visual display of subtask progress.
|
|
5
|
+
* No tables - just a clean list with animated status.
|
|
6
|
+
*
|
|
7
|
+
* @see PRJ-138
|
|
8
|
+
* @module utils/subtask-table
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
// ANSI codes
|
|
12
|
+
const RESET = '\x1b[0m'
|
|
13
|
+
const BOLD = '\x1b[1m'
|
|
14
|
+
const DIM = '\x1b[2m'
|
|
15
|
+
const GREEN = '\x1b[32m'
|
|
16
|
+
const YELLOW = '\x1b[33m'
|
|
17
|
+
const WHITE = '\x1b[37m'
|
|
18
|
+
const GRAY = '\x1b[90m'
|
|
19
|
+
|
|
20
|
+
// Color palette for domains (cycle through these)
|
|
21
|
+
const DOMAIN_COLOR_PALETTE = [
|
|
22
|
+
'\x1b[36m', // Cyan
|
|
23
|
+
'\x1b[35m', // Magenta
|
|
24
|
+
'\x1b[33m', // Yellow
|
|
25
|
+
'\x1b[34m', // Blue
|
|
26
|
+
'\x1b[32m', // Green
|
|
27
|
+
'\x1b[91m', // Light Red
|
|
28
|
+
'\x1b[95m', // Light Magenta
|
|
29
|
+
'\x1b[96m', // Light Cyan
|
|
30
|
+
]
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Get consistent color for a domain name using hash
|
|
34
|
+
* Same domain name always returns same color
|
|
35
|
+
*/
|
|
36
|
+
function getDomainColor(domain: string): string {
|
|
37
|
+
let hash = 0
|
|
38
|
+
for (const char of domain) {
|
|
39
|
+
hash = (hash << 5) - hash + char.charCodeAt(0)
|
|
40
|
+
hash = hash & hash
|
|
41
|
+
}
|
|
42
|
+
const index = Math.abs(hash) % DOMAIN_COLOR_PALETTE.length
|
|
43
|
+
return DOMAIN_COLOR_PALETTE[index]
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Hide/show cursor
|
|
47
|
+
const HIDE_CURSOR = '\x1b[?25l'
|
|
48
|
+
const SHOW_CURSOR = '\x1b[?25h'
|
|
49
|
+
|
|
50
|
+
// Spinner frames (dots animation)
|
|
51
|
+
const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
|
|
52
|
+
|
|
53
|
+
export type SubtaskStatus = 'pending' | 'in_progress' | 'completed' | 'failed' | 'blocked'
|
|
54
|
+
|
|
55
|
+
export interface SubtaskDisplay {
|
|
56
|
+
id: string
|
|
57
|
+
domain: string
|
|
58
|
+
description: string
|
|
59
|
+
status: SubtaskStatus
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format a single subtask line
|
|
64
|
+
*/
|
|
65
|
+
function formatSubtaskLine(
|
|
66
|
+
index: number,
|
|
67
|
+
subtask: SubtaskDisplay,
|
|
68
|
+
spinnerFrame: string = '▶'
|
|
69
|
+
): string {
|
|
70
|
+
const num = `${DIM}${String(index + 1).padStart(2)}${RESET}`
|
|
71
|
+
const domainColor = getDomainColor(subtask.domain)
|
|
72
|
+
const domain = `${domainColor}${subtask.domain.padEnd(10)}${RESET}`
|
|
73
|
+
const desc =
|
|
74
|
+
subtask.description.length > 32
|
|
75
|
+
? subtask.description.slice(0, 29) + '...'
|
|
76
|
+
: subtask.description.padEnd(32)
|
|
77
|
+
|
|
78
|
+
let status: string
|
|
79
|
+
switch (subtask.status) {
|
|
80
|
+
case 'completed':
|
|
81
|
+
status = `${GREEN}✓ Complete${RESET}`
|
|
82
|
+
break
|
|
83
|
+
case 'in_progress':
|
|
84
|
+
status = `${YELLOW}${spinnerFrame} Working...${RESET}`
|
|
85
|
+
break
|
|
86
|
+
case 'pending':
|
|
87
|
+
status = `${GRAY}○ Pending${RESET}`
|
|
88
|
+
break
|
|
89
|
+
case 'failed':
|
|
90
|
+
status = `\x1b[31m✗ Failed${RESET}`
|
|
91
|
+
break
|
|
92
|
+
case 'blocked':
|
|
93
|
+
status = `${GRAY}⊘ Blocked${RESET}`
|
|
94
|
+
break
|
|
95
|
+
default:
|
|
96
|
+
status = `${GRAY}○ ${subtask.status}${RESET}`
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return ` ${num} ${domain} ${desc} ${status}`
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Render static subtask progress (no animation)
|
|
104
|
+
*/
|
|
105
|
+
export function renderSubtaskProgress(subtasks: SubtaskDisplay[]): string {
|
|
106
|
+
if (subtasks.length === 0) return ''
|
|
107
|
+
|
|
108
|
+
const lines: string[] = []
|
|
109
|
+
|
|
110
|
+
lines.push('')
|
|
111
|
+
lines.push(` ${BOLD}${WHITE}SUBTASK PROGRESS${RESET}`)
|
|
112
|
+
lines.push(` ${DIM}${'─'.repeat(58)}${RESET}`)
|
|
113
|
+
|
|
114
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
115
|
+
lines.push(formatSubtaskLine(i, subtasks[i]))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push('')
|
|
119
|
+
|
|
120
|
+
return lines.join('\n')
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Print static subtask progress
|
|
125
|
+
*/
|
|
126
|
+
export function printSubtaskProgress(subtasks: SubtaskDisplay[]): void {
|
|
127
|
+
console.log(renderSubtaskProgress(subtasks))
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Animated subtask progress with spinner
|
|
132
|
+
* Returns a controller to update/stop the animation
|
|
133
|
+
*/
|
|
134
|
+
export function createSubtaskAnimation(subtasks: SubtaskDisplay[]) {
|
|
135
|
+
let frameIndex = 0
|
|
136
|
+
let intervalId: ReturnType<typeof setInterval> | null = null
|
|
137
|
+
let lastOutput = ''
|
|
138
|
+
|
|
139
|
+
const render = () => {
|
|
140
|
+
const spinnerFrame = SPINNER_FRAMES[frameIndex % SPINNER_FRAMES.length]
|
|
141
|
+
const lines: string[] = []
|
|
142
|
+
|
|
143
|
+
lines.push('')
|
|
144
|
+
lines.push(` ${BOLD}${WHITE}SUBTASK PROGRESS${RESET}`)
|
|
145
|
+
lines.push(` ${DIM}${'─'.repeat(58)}${RESET}`)
|
|
146
|
+
|
|
147
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
148
|
+
lines.push(formatSubtaskLine(i, subtasks[i], spinnerFrame))
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
lines.push('')
|
|
152
|
+
|
|
153
|
+
return lines.join('\n')
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const clear = () => {
|
|
157
|
+
if (lastOutput) {
|
|
158
|
+
const lineCount = lastOutput.split('\n').length
|
|
159
|
+
// Move up and clear each line
|
|
160
|
+
process.stdout.write(`\x1b[${lineCount}A`)
|
|
161
|
+
for (let i = 0; i < lineCount; i++) {
|
|
162
|
+
process.stdout.write('\x1b[2K\n')
|
|
163
|
+
}
|
|
164
|
+
process.stdout.write(`\x1b[${lineCount}A`)
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const draw = () => {
|
|
169
|
+
clear()
|
|
170
|
+
lastOutput = render()
|
|
171
|
+
process.stdout.write(lastOutput)
|
|
172
|
+
frameIndex++
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
/**
|
|
177
|
+
* Start the animation
|
|
178
|
+
*/
|
|
179
|
+
start: () => {
|
|
180
|
+
process.stdout.write(HIDE_CURSOR)
|
|
181
|
+
lastOutput = render()
|
|
182
|
+
process.stdout.write(lastOutput)
|
|
183
|
+
intervalId = setInterval(draw, 80)
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Update subtask status
|
|
188
|
+
*/
|
|
189
|
+
update: (index: number, status: SubtaskStatus) => {
|
|
190
|
+
if (index >= 0 && index < subtasks.length) {
|
|
191
|
+
subtasks[index].status = status
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Stop animation and show final state
|
|
197
|
+
*/
|
|
198
|
+
stop: () => {
|
|
199
|
+
if (intervalId) {
|
|
200
|
+
clearInterval(intervalId)
|
|
201
|
+
intervalId = null
|
|
202
|
+
}
|
|
203
|
+
clear()
|
|
204
|
+
// Print final state with static icons
|
|
205
|
+
const finalLines: string[] = []
|
|
206
|
+
finalLines.push('')
|
|
207
|
+
finalLines.push(` ${BOLD}${WHITE}SUBTASK PROGRESS${RESET}`)
|
|
208
|
+
finalLines.push(` ${DIM}${'─'.repeat(58)}${RESET}`)
|
|
209
|
+
for (let i = 0; i < subtasks.length; i++) {
|
|
210
|
+
finalLines.push(formatSubtaskLine(i, subtasks[i], '▶'))
|
|
211
|
+
}
|
|
212
|
+
finalLines.push('')
|
|
213
|
+
process.stdout.write(finalLines.join('\n'))
|
|
214
|
+
process.stdout.write(SHOW_CURSOR)
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Get current subtasks state
|
|
219
|
+
*/
|
|
220
|
+
getSubtasks: () => [...subtasks],
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Simple progress line
|
|
226
|
+
* Output: "Progress: 2/4 subtasks complete"
|
|
227
|
+
*/
|
|
228
|
+
export function renderProgressLine(completed: number, total: number): string {
|
|
229
|
+
return ` ${DIM}Progress:${RESET} ${completed}/${total} subtasks complete`
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// Legacy exports for backwards compatibility
|
|
233
|
+
export const renderSubtaskTable = renderSubtaskProgress
|
|
234
|
+
export const printSubtaskTable = printSubtaskProgress
|
package/core/workflow/index.ts
CHANGED