prjct-cli 0.46.0 → 0.48.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.
@@ -9,6 +9,11 @@
9
9
 
10
10
  import fs from 'node:fs/promises'
11
11
  import path from 'node:path'
12
+ import {
13
+ hasPreservedSections,
14
+ mergePreservedSections,
15
+ validatePreserveBlocks,
16
+ } from '../utils/preserve-sections'
12
17
  import type { StackDetection } from './stack-detector'
13
18
 
14
19
  // ============================================================================
@@ -65,21 +70,68 @@ export class AgentGenerator {
65
70
  }
66
71
 
67
72
  /**
68
- * Remove existing agent files
73
+ * Cache of existing agent content (for preserving user sections)
74
+ */
75
+ private existingAgents: Map<string, string> = new Map()
76
+
77
+ /**
78
+ * Read existing agents and cache their content for preservation
79
+ * Then remove the files (they'll be regenerated with preserved sections)
69
80
  */
70
81
  private async purgeOldAgents(): Promise<void> {
82
+ this.existingAgents.clear()
83
+
71
84
  try {
72
85
  const files = await fs.readdir(this.agentsPath)
86
+ const mdFiles = files.filter((file) => file.endsWith('.md'))
87
+
88
+ // Read all existing agent files BEFORE deleting
73
89
  await Promise.all(
74
- files
75
- .filter((file) => file.endsWith('.md'))
76
- .map((file) => fs.unlink(path.join(this.agentsPath, file)))
90
+ mdFiles.map(async (file) => {
91
+ const filePath = path.join(this.agentsPath, file)
92
+ try {
93
+ const content = await fs.readFile(filePath, 'utf-8')
94
+ // Only cache if it has user-preserved sections
95
+ if (hasPreservedSections(content)) {
96
+ this.existingAgents.set(file, content)
97
+ }
98
+ } catch {
99
+ // File read failed, skip
100
+ }
101
+ })
77
102
  )
103
+
104
+ // Now delete the files
105
+ await Promise.all(mdFiles.map((file) => fs.unlink(path.join(this.agentsPath, file))))
78
106
  } catch {
79
107
  // Directory might not exist yet
80
108
  }
81
109
  }
82
110
 
111
+ /**
112
+ * Write agent file, preserving user sections from previous version
113
+ */
114
+ private async writeAgentWithPreservation(filename: string, content: string): Promise<void> {
115
+ const existingContent = this.existingAgents.get(filename)
116
+
117
+ let finalContent = content
118
+ if (existingContent) {
119
+ // Validate existing preserved blocks
120
+ const validation = validatePreserveBlocks(existingContent)
121
+ if (!validation.valid) {
122
+ console.warn(`⚠️ Agent ${filename} has invalid preserve blocks:`)
123
+ for (const error of validation.errors) {
124
+ console.warn(` ${error}`)
125
+ }
126
+ }
127
+
128
+ // Merge preserved sections from old content
129
+ finalContent = mergePreservedSections(content, existingContent)
130
+ }
131
+
132
+ await fs.writeFile(path.join(this.agentsPath, filename), finalContent, 'utf-8')
133
+ }
134
+
83
135
  /**
84
136
  * Generate workflow agents (always included)
85
137
  */
@@ -143,7 +195,7 @@ export class AgentGenerator {
143
195
  content = this.generateMinimalWorkflowAgent(name)
144
196
  }
145
197
 
146
- await fs.writeFile(path.join(this.agentsPath, `${name}.md`), content, 'utf-8')
198
+ await this.writeAgentWithPreservation(`${name}.md`, content)
147
199
  }
148
200
 
149
201
  /**
@@ -169,7 +221,7 @@ export class AgentGenerator {
169
221
  content = this.generateMinimalDomainAgent(name, stats, stack)
170
222
  }
171
223
 
172
- await fs.writeFile(path.join(this.agentsPath, `${name}.md`), content, 'utf-8')
224
+ await this.writeAgentWithPreservation(`${name}.md`, content)
173
225
  }
174
226
 
175
227
  /**
@@ -12,7 +12,7 @@
12
12
  import fs from 'node:fs/promises'
13
13
  import path from 'node:path'
14
14
  import dateHelper from '../utils/date-helper'
15
- import { mergePreservedSections } from '../utils/preserve-sections'
15
+ import { mergePreservedSections, validatePreserveBlocks } from '../utils/preserve-sections'
16
16
 
17
17
  // ============================================================================
18
18
  // TYPES
@@ -65,6 +65,35 @@ export class ContextFileGenerator {
65
65
  this.config = config
66
66
  }
67
67
 
68
+ /**
69
+ * Write file with preserved sections from existing content
70
+ * This ensures user customizations survive regeneration
71
+ */
72
+ private async writeWithPreservation(filePath: string, content: string): Promise<void> {
73
+ let finalContent = content
74
+
75
+ try {
76
+ const existingContent = await fs.readFile(filePath, 'utf-8')
77
+
78
+ // Validate existing preserved blocks
79
+ const validation = validatePreserveBlocks(existingContent)
80
+ if (!validation.valid) {
81
+ const filename = path.basename(filePath)
82
+ console.warn(`⚠️ ${filename} has invalid preserve blocks:`)
83
+ for (const error of validation.errors) {
84
+ console.warn(` ${error}`)
85
+ }
86
+ }
87
+
88
+ // Merge preserved sections from existing content
89
+ finalContent = mergePreservedSections(content, existingContent)
90
+ } catch {
91
+ // File doesn't exist yet - use generated content as-is
92
+ }
93
+
94
+ await fs.writeFile(filePath, finalContent, 'utf-8')
95
+ }
96
+
68
97
  /**
69
98
  * Generate all context files in parallel
70
99
  */
@@ -181,17 +210,8 @@ Load from \`~/.prjct-cli/projects/${this.config.projectId}/agents/\`:
181
210
  **Domain**: ${domainAgents.join(', ') || 'none'}
182
211
  `
183
212
 
184
- // Preserve user customizations from existing file
185
213
  const claudePath = path.join(contextPath, 'CLAUDE.md')
186
- let finalContent = content
187
- try {
188
- const existingContent = await fs.readFile(claudePath, 'utf-8')
189
- finalContent = mergePreservedSections(content, existingContent)
190
- } catch {
191
- // File doesn't exist yet - use generated content as-is
192
- }
193
-
194
- await fs.writeFile(claudePath, finalContent, 'utf-8')
214
+ await this.writeWithPreservation(claudePath, content)
195
215
  }
196
216
 
197
217
  /**
@@ -222,7 +242,7 @@ _No active task_
222
242
  Use \`p. task "description"\` to start working.
223
243
  `
224
244
 
225
- await fs.writeFile(path.join(contextPath, 'now.md'), content, 'utf-8')
245
+ await this.writeWithPreservation(path.join(contextPath, 'now.md'), content)
226
246
  }
227
247
 
228
248
  /**
@@ -248,7 +268,7 @@ ${
248
268
  }
249
269
  `
250
270
 
251
- await fs.writeFile(path.join(contextPath, 'next.md'), content, 'utf-8')
271
+ await this.writeWithPreservation(path.join(contextPath, 'next.md'), content)
252
272
  }
253
273
 
254
274
  /**
@@ -272,7 +292,7 @@ ${
272
292
  }
273
293
  `
274
294
 
275
- await fs.writeFile(path.join(contextPath, 'ideas.md'), content, 'utf-8')
295
+ await this.writeWithPreservation(path.join(contextPath, 'ideas.md'), content)
276
296
  }
277
297
 
278
298
  /**
@@ -303,7 +323,7 @@ ${
303
323
  **Total shipped:** ${shipped.shipped.length}
304
324
  `
305
325
 
306
- await fs.writeFile(path.join(contextPath, 'shipped.md'), content, 'utf-8')
326
+ await this.writeWithPreservation(path.join(contextPath, 'shipped.md'), content)
307
327
  }
308
328
  }
309
329
 
@@ -0,0 +1,320 @@
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 { VERSION } from './version'
12
+
13
+ // ANSI colors
14
+ const CYAN = '\x1b[36m'
15
+ const DIM = '\x1b[2m'
16
+ const BOLD = '\x1b[1m'
17
+ const RESET = '\x1b[0m'
18
+ const GREEN = '\x1b[32m'
19
+ const YELLOW = '\x1b[33m'
20
+
21
+ /**
22
+ * Terminal commands that run directly in the shell
23
+ */
24
+ const TERMINAL_COMMANDS = [
25
+ {
26
+ name: 'start',
27
+ description: 'First-time setup wizard',
28
+ example: 'prjct start',
29
+ },
30
+ {
31
+ name: 'init',
32
+ description: 'Initialize project in current directory',
33
+ example: 'prjct init',
34
+ },
35
+ {
36
+ name: 'sync',
37
+ description: 'Sync project state and update context files',
38
+ example: 'prjct sync',
39
+ },
40
+ {
41
+ name: 'watch',
42
+ description: 'Auto-sync on file changes',
43
+ example: 'prjct watch',
44
+ options: ['--verbose', '--debounce=<ms>', '--interval=<sec>'],
45
+ },
46
+ {
47
+ name: 'doctor',
48
+ description: 'Check system health and dependencies',
49
+ example: 'prjct doctor',
50
+ },
51
+ {
52
+ name: 'serve',
53
+ description: 'Start web dashboard server',
54
+ example: 'prjct serve [port]',
55
+ },
56
+ {
57
+ name: 'context',
58
+ description: 'Smart context filtering tools for AI',
59
+ example: 'prjct context files "add auth"',
60
+ subcommands: ['files', 'signatures', 'imports', 'recent', 'summary'],
61
+ },
62
+ {
63
+ name: 'linear',
64
+ description: 'Linear issue tracker CLI',
65
+ example: 'prjct linear list',
66
+ subcommands: ['list', 'get', 'create', 'update'],
67
+ },
68
+ {
69
+ name: 'uninstall',
70
+ description: 'Complete system removal of prjct',
71
+ example: 'prjct uninstall --backup',
72
+ options: ['--force', '--backup', '--dry-run', '--keep-package'],
73
+ },
74
+ ]
75
+
76
+ /**
77
+ * Global CLI flags
78
+ */
79
+ const GLOBAL_FLAGS = [
80
+ { flag: '-q, --quiet', description: 'Suppress all output (errors to stderr only)' },
81
+ { flag: '-v, --version', description: 'Show version and provider status' },
82
+ { flag: '-h, --help', description: 'Show this help message' },
83
+ ]
84
+
85
+ /**
86
+ * Format the main help output
87
+ */
88
+ export function formatMainHelp(): string {
89
+ const lines: string[] = []
90
+
91
+ // Header
92
+ lines.push('')
93
+ lines.push(`${CYAN}${BOLD}prjct${RESET} v${VERSION} - Context layer for AI coding agents`)
94
+ lines.push(`${DIM}Works with Claude Code, Gemini CLI, Cursor, Windsurf, and more.${RESET}`)
95
+ lines.push('')
96
+
97
+ // Quick Start
98
+ lines.push(`${BOLD}QUICK START${RESET}`)
99
+ lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
100
+ lines.push(` ${GREEN}1.${RESET} prjct start ${DIM}# Configure AI providers${RESET}`)
101
+ lines.push(` ${GREEN}2.${RESET} cd my-project && prjct init`)
102
+ lines.push(` ${GREEN}3.${RESET} Open in Claude Code / Gemini CLI / Cursor`)
103
+ lines.push(` ${GREEN}4.${RESET} p. sync ${DIM}# Analyze project${RESET}`)
104
+ lines.push('')
105
+
106
+ // Terminal Commands
107
+ lines.push(`${BOLD}TERMINAL COMMANDS${RESET}`)
108
+ lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
109
+ for (const cmd of TERMINAL_COMMANDS) {
110
+ const name = `prjct ${cmd.name}`.padEnd(22)
111
+ lines.push(` ${name} ${cmd.description}`)
112
+ }
113
+ lines.push('')
114
+
115
+ // AI Agent Commands
116
+ lines.push(`${BOLD}AI AGENT COMMANDS${RESET} ${DIM}(inside Claude/Gemini/Cursor)${RESET}`)
117
+ lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
118
+ lines.push(` ${'Command'.padEnd(22)} Description`)
119
+ lines.push(` ${DIM}${'─'.repeat(56)}${RESET}`)
120
+
121
+ // Core commands
122
+ const coreCommands = COMMANDS.filter((c) => c.group === 'core' && c.usage?.claude)
123
+ for (const cmd of coreCommands.slice(0, 10)) {
124
+ const usage = `p. ${cmd.name}`.padEnd(22)
125
+ lines.push(` ${usage} ${cmd.description}`)
126
+ }
127
+ lines.push(` ${DIM}... and ${coreCommands.length - 10} more (run 'prjct help commands')${RESET}`)
128
+ lines.push('')
129
+
130
+ // Global Flags
131
+ lines.push(`${BOLD}FLAGS${RESET}`)
132
+ lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
133
+ for (const flag of GLOBAL_FLAGS) {
134
+ lines.push(` ${flag.flag.padEnd(22)} ${flag.description}`)
135
+ }
136
+ lines.push('')
137
+
138
+ // More Info
139
+ lines.push(`${BOLD}MORE INFO${RESET}`)
140
+ lines.push(`${DIM}${'─'.repeat(60)}${RESET}`)
141
+ lines.push(` Documentation: ${CYAN}https://prjct.app${RESET}`)
142
+ lines.push(` GitHub: ${CYAN}https://github.com/jlopezlira/prjct-cli${RESET}`)
143
+ lines.push(` Per-command: prjct help <command>`)
144
+ lines.push('')
145
+
146
+ return lines.join('\n')
147
+ }
148
+
149
+ /**
150
+ * Format help for a specific terminal command
151
+ */
152
+ export function formatTerminalCommandHelp(commandName: string): string | null {
153
+ const cmd = TERMINAL_COMMANDS.find((c) => c.name === commandName)
154
+ if (!cmd) return null
155
+
156
+ const lines: string[] = []
157
+
158
+ lines.push('')
159
+ lines.push(`${CYAN}${BOLD}prjct ${cmd.name}${RESET} - ${cmd.description}`)
160
+ lines.push('')
161
+
162
+ lines.push(`${BOLD}USAGE${RESET}`)
163
+ lines.push(` ${cmd.example}`)
164
+ lines.push('')
165
+
166
+ if (cmd.options) {
167
+ lines.push(`${BOLD}OPTIONS${RESET}`)
168
+ for (const opt of cmd.options) {
169
+ lines.push(` ${opt}`)
170
+ }
171
+ lines.push('')
172
+ }
173
+
174
+ if (cmd.subcommands) {
175
+ lines.push(`${BOLD}SUBCOMMANDS${RESET}`)
176
+ for (const sub of cmd.subcommands) {
177
+ lines.push(` ${sub}`)
178
+ }
179
+ lines.push('')
180
+ }
181
+
182
+ return lines.join('\n')
183
+ }
184
+
185
+ /**
186
+ * Format help for an AI agent command
187
+ */
188
+ export function formatAgentCommandHelp(commandName: string): string | null {
189
+ const cmd = COMMANDS.find((c) => c.name === commandName)
190
+ if (!cmd) return null
191
+
192
+ const lines: string[] = []
193
+
194
+ lines.push('')
195
+ lines.push(`${CYAN}${BOLD}p. ${cmd.name}${RESET} - ${cmd.description}`)
196
+ lines.push('')
197
+
198
+ lines.push(`${BOLD}USAGE${RESET}`)
199
+ if (cmd.usage?.claude) {
200
+ lines.push(` Claude/Gemini: ${cmd.usage.claude.replace('/p:', 'p. ')}`)
201
+ }
202
+ if (cmd.usage?.terminal) {
203
+ lines.push(` Terminal: ${cmd.usage.terminal}`)
204
+ }
205
+ lines.push('')
206
+
207
+ if (cmd.params) {
208
+ lines.push(`${BOLD}PARAMETERS${RESET}`)
209
+ lines.push(` ${cmd.params}`)
210
+ lines.push('')
211
+ }
212
+
213
+ if (cmd.features && cmd.features.length > 0) {
214
+ lines.push(`${BOLD}FEATURES${RESET}`)
215
+ for (const feature of cmd.features) {
216
+ lines.push(` • ${feature}`)
217
+ }
218
+ lines.push('')
219
+ }
220
+
221
+ if (cmd.blockingRules) {
222
+ lines.push(`${BOLD}REQUIREMENTS${RESET}`)
223
+ lines.push(` ${YELLOW}⚠${RESET} ${cmd.blockingRules.check}`)
224
+ lines.push('')
225
+ }
226
+
227
+ // Category info
228
+ const category = CATEGORIES[cmd.group]
229
+ if (category) {
230
+ lines.push(`${DIM}Category: ${category.title}${RESET}`)
231
+ if (cmd.isOptional) {
232
+ lines.push(`${DIM}This is an optional command.${RESET}`)
233
+ }
234
+ lines.push('')
235
+ }
236
+
237
+ return lines.join('\n')
238
+ }
239
+
240
+ /**
241
+ * Format help for a specific command (auto-detect type)
242
+ */
243
+ export function formatCommandHelp(commandName: string): string {
244
+ // Try terminal command first
245
+ const terminalHelp = formatTerminalCommandHelp(commandName)
246
+ if (terminalHelp) return terminalHelp
247
+
248
+ // Try agent command
249
+ const agentHelp = formatAgentCommandHelp(commandName)
250
+ if (agentHelp) return agentHelp
251
+
252
+ // Command not found
253
+ return `
254
+ ${YELLOW}Command '${commandName}' not found.${RESET}
255
+
256
+ Run 'prjct help' to see all available commands.
257
+ `
258
+ }
259
+
260
+ /**
261
+ * Format list of all commands grouped by category
262
+ */
263
+ export function formatCommandList(): string {
264
+ const lines: string[] = []
265
+
266
+ lines.push('')
267
+ lines.push(`${CYAN}${BOLD}All Commands${RESET}`)
268
+ lines.push('')
269
+
270
+ // Group by category
271
+ const categories = Object.entries(CATEGORIES).sort((a, b) => a[1].order - b[1].order)
272
+
273
+ for (const [categoryKey, category] of categories) {
274
+ const categoryCommands = COMMANDS.filter((c) => c.group === categoryKey)
275
+ if (categoryCommands.length === 0) continue
276
+
277
+ lines.push(
278
+ `${BOLD}${category.title}${RESET} ${DIM}(${categoryCommands.length} commands)${RESET}`
279
+ )
280
+ lines.push(`${DIM}${category.description}${RESET}`)
281
+ lines.push('')
282
+
283
+ for (const cmd of categoryCommands) {
284
+ const name = `p. ${cmd.name}`.padEnd(18)
285
+ const desc =
286
+ cmd.description.length > 45 ? `${cmd.description.slice(0, 42)}...` : cmd.description
287
+ lines.push(` ${name} ${desc}`)
288
+ }
289
+ lines.push('')
290
+ }
291
+
292
+ lines.push(`${DIM}Run 'prjct help <command>' for detailed help on a specific command.${RESET}`)
293
+ lines.push('')
294
+
295
+ return lines.join('\n')
296
+ }
297
+
298
+ /**
299
+ * Get help output based on topic
300
+ */
301
+ export function getHelp(topic?: string): string {
302
+ if (!topic) {
303
+ return formatMainHelp()
304
+ }
305
+
306
+ if (topic === 'commands' || topic === 'all') {
307
+ return formatCommandList()
308
+ }
309
+
310
+ return formatCommandHelp(topic)
311
+ }
312
+
313
+ export default {
314
+ formatMainHelp,
315
+ formatCommandHelp,
316
+ formatCommandList,
317
+ formatTerminalCommandHelp,
318
+ formatAgentCommandHelp,
319
+ getHelp,
320
+ }
@@ -117,7 +117,9 @@ export class MarkdownBuilder {
117
117
  * Add multiple list items
118
118
  */
119
119
  list(items: string[], options?: { checked?: boolean }): this {
120
- items.forEach((item) => this.li(item, options))
120
+ for (const item of items) {
121
+ this.li(item, options)
122
+ }
121
123
  return this
122
124
  }
123
125
 
@@ -125,7 +127,9 @@ export class MarkdownBuilder {
125
127
  * Add multiple numbered list items
126
128
  */
127
129
  orderedList(items: string[]): this {
128
- items.forEach((item, i) => this.oli(item, i + 1))
130
+ for (let i = 0; i < items.length; i++) {
131
+ this.oli(items[i], i + 1)
132
+ }
129
133
  return this
130
134
  }
131
135
 
@@ -225,7 +229,9 @@ export class MarkdownBuilder {
225
229
  * Iterate and build for each item
226
230
  */
227
231
  each<T>(items: T[], builder: (md: MarkdownBuilder, item: T, index: number) => void): this {
228
- items.forEach((item, i) => builder(this, item, i))
232
+ for (let i = 0; i < items.length; i++) {
233
+ builder(this, items[i], i)
234
+ }
229
235
  return this
230
236
  }
231
237
 
@@ -128,7 +128,7 @@ export function mergePreservedSections(newContent: string, oldContent: string):
128
128
  merged += '\n\n'
129
129
  }
130
130
 
131
- return merged.trimEnd() + '\n'
131
+ return `${merged.trimEnd()}\n`
132
132
  }
133
133
 
134
134
  /**
@@ -3,12 +3,6 @@ import type { DetectedProjectCommands } from '../types'
3
3
  import * as fileHelper from './file-helper'
4
4
 
5
5
  type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
6
- type DetectedStack = 'js' | 'python' | 'go' | 'rust' | 'dotnet' | 'java' | 'unknown'
7
-
8
- interface DetectedCommand {
9
- command: string
10
- tool: string
11
- }
12
6
 
13
7
  interface PackageJson {
14
8
  scripts?: Record<string, string>