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.
@@ -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
@@ -4,3 +4,4 @@
4
4
  */
5
5
 
6
6
  export * from './state-machine'
7
+ export * from './workflow-preferences'