prjct-cli 0.45.5 → 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.
@@ -12,6 +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
16
 
16
17
  // ============================================================================
17
18
  // TYPES
@@ -180,7 +181,17 @@ Load from \`~/.prjct-cli/projects/${this.config.projectId}/agents/\`:
180
181
  **Domain**: ${domainAgents.join(', ') || 'none'}
181
182
  `
182
183
 
183
- await fs.writeFile(path.join(contextPath, 'CLAUDE.md'), content, 'utf-8')
184
+ // Preserve user customizations from existing file
185
+ 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')
184
195
  }
185
196
 
186
197
  /**
@@ -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,218 @@
1
+ /**
2
+ * Preserve Sections Utility
3
+ *
4
+ * Extracts and preserves user-customized sections during file regeneration.
5
+ * Users can mark sections with preserve markers to survive sync.
6
+ *
7
+ * Usage in CLAUDE.md or other context files:
8
+ * ```markdown
9
+ * <!-- prjct:preserve -->
10
+ * # My Custom Rules
11
+ * - Always use tabs
12
+ * - Prefer functional patterns
13
+ * <!-- /prjct:preserve -->
14
+ * ```
15
+ *
16
+ * @see PRJ-115
17
+ * @module utils/preserve-sections
18
+ */
19
+
20
+ export interface PreservedSection {
21
+ id: string
22
+ content: string
23
+ startIndex: number
24
+ endIndex: number
25
+ }
26
+
27
+ // Markers for preserved sections
28
+ const PRESERVE_START = '<!-- prjct:preserve -->'
29
+ const PRESERVE_END = '<!-- /prjct:preserve -->'
30
+ const PRESERVE_END_PATTERN = '<!-- /prjct:preserve -->'
31
+
32
+ // Named section markers (optional identifier) - create fresh regex each time
33
+ // Uses [\w-]+ to allow hyphens in section names (e.g., "custom-rules")
34
+ function createPreserveStartRegex(): RegExp {
35
+ return /<!-- prjct:preserve(?::([\w-]+))? -->/g
36
+ }
37
+
38
+ /**
39
+ * Extract all preserved sections from content
40
+ */
41
+ export function extractPreservedSections(content: string): PreservedSection[] {
42
+ const sections: PreservedSection[] = []
43
+ const regex = createPreserveStartRegex()
44
+
45
+ let match: RegExpExecArray | null
46
+ let sectionIndex = 0
47
+
48
+ while ((match = regex.exec(content)) !== null) {
49
+ const startIndex = match.index
50
+ const startTag = match[0]
51
+ const sectionId = match[1] || `section-${sectionIndex++}`
52
+
53
+ // Find the closing tag
54
+ const endTagStart = content.indexOf(PRESERVE_END_PATTERN, startIndex + startTag.length)
55
+
56
+ if (endTagStart === -1) {
57
+ // No closing tag found - skip this section
58
+ continue
59
+ }
60
+
61
+ const endIndex = endTagStart + PRESERVE_END_PATTERN.length
62
+
63
+ // Extract the content between markers (including markers)
64
+ const fullContent = content.substring(startIndex, endIndex)
65
+
66
+ sections.push({
67
+ id: sectionId,
68
+ content: fullContent,
69
+ startIndex,
70
+ endIndex,
71
+ })
72
+ }
73
+
74
+ return sections
75
+ }
76
+
77
+ /**
78
+ * Extract the inner content of preserved sections (without markers)
79
+ */
80
+ export function extractPreservedContent(content: string): string[] {
81
+ const sections = extractPreservedSections(content)
82
+
83
+ return sections.map((section) => {
84
+ // Remove the markers to get just the inner content
85
+ let inner = section.content
86
+ inner = inner.replace(createPreserveStartRegex(), '').replace(PRESERVE_END_PATTERN, '')
87
+ return inner.trim()
88
+ })
89
+ }
90
+
91
+ /**
92
+ * Check if content has any preserved sections
93
+ */
94
+ export function hasPreservedSections(content: string): boolean {
95
+ return content.includes(PRESERVE_START) || createPreserveStartRegex().test(content)
96
+ }
97
+
98
+ /**
99
+ * Merge preserved sections from old content into new content
100
+ *
101
+ * Strategy:
102
+ * 1. Extract preserved sections from old content
103
+ * 2. Append them to the end of new content
104
+ * 3. Ensure proper spacing
105
+ *
106
+ * @param newContent - Freshly generated content
107
+ * @param oldContent - Previous content with user customizations
108
+ * @returns Merged content with preserved sections
109
+ */
110
+ export function mergePreservedSections(newContent: string, oldContent: string): string {
111
+ const preservedSections = extractPreservedSections(oldContent)
112
+
113
+ if (preservedSections.length === 0) {
114
+ return newContent
115
+ }
116
+
117
+ // Build the merged content
118
+ let merged = newContent.trimEnd()
119
+
120
+ // Add separator before preserved sections
121
+ merged += '\n\n---\n\n'
122
+ merged += '## Your Customizations\n\n'
123
+ merged += '_The sections below are preserved during sync. Edit freely._\n\n'
124
+
125
+ // Append each preserved section
126
+ for (const section of preservedSections) {
127
+ merged += section.content
128
+ merged += '\n\n'
129
+ }
130
+
131
+ return merged.trimEnd() + '\n'
132
+ }
133
+
134
+ /**
135
+ * Create an empty preserve block for users to customize
136
+ */
137
+ export function createEmptyPreserveBlock(title?: string): string {
138
+ const header = title ? `\n# ${title}\n` : '\n'
139
+ return `${PRESERVE_START}${header}<!-- Add your custom instructions here -->\n${PRESERVE_END}`
140
+ }
141
+
142
+ /**
143
+ * Wrap content in preserve markers
144
+ */
145
+ export function wrapInPreserveMarkers(content: string, id?: string): string {
146
+ const startTag = id ? `<!-- prjct:preserve:${id} -->` : PRESERVE_START
147
+ return `${startTag}\n${content}\n${PRESERVE_END}`
148
+ }
149
+
150
+ /**
151
+ * Remove all preserved sections from content
152
+ * (Useful for getting the auto-generated portion only)
153
+ */
154
+ export function stripPreservedSections(content: string): string {
155
+ const sections = extractPreservedSections(content)
156
+
157
+ if (sections.length === 0) {
158
+ return content
159
+ }
160
+
161
+ // Remove sections in reverse order to preserve indices
162
+ let result = content
163
+ for (let i = sections.length - 1; i >= 0; i--) {
164
+ const section = sections[i]
165
+ result = result.substring(0, section.startIndex) + result.substring(section.endIndex)
166
+ }
167
+
168
+ // Clean up any resulting double newlines
169
+ result = result.replace(/\n{3,}/g, '\n\n')
170
+
171
+ return result.trim()
172
+ }
173
+
174
+ /**
175
+ * Validate that all preserve blocks are properly closed
176
+ */
177
+ export function validatePreserveBlocks(content: string): {
178
+ valid: boolean
179
+ errors: string[]
180
+ } {
181
+ const errors: string[] = []
182
+
183
+ const startMatches = content.match(/<!-- prjct:preserve(?::\w+)? -->/g) || []
184
+ const endMatches = content.match(/<!-- \/prjct:preserve -->/g) || []
185
+
186
+ if (startMatches.length !== endMatches.length) {
187
+ errors.push(
188
+ `Mismatched preserve markers: ${startMatches.length} opening, ${endMatches.length} closing`
189
+ )
190
+ }
191
+
192
+ // Check for nested blocks (not supported)
193
+ let depth = 0
194
+ let maxDepth = 0
195
+ const lines = content.split('\n')
196
+
197
+ for (let i = 0; i < lines.length; i++) {
198
+ const line = lines[i]
199
+ if (/<!-- prjct:preserve(?::\w+)? -->/.test(line)) {
200
+ depth++
201
+ maxDepth = Math.max(maxDepth, depth)
202
+ }
203
+ if (line.includes(PRESERVE_END_PATTERN)) {
204
+ depth--
205
+ }
206
+ if (depth > 1) {
207
+ errors.push(`Nested preserve blocks detected at line ${i + 1} (not supported)`)
208
+ }
209
+ if (depth < 0) {
210
+ errors.push(`Unexpected closing marker at line ${i + 1}`)
211
+ }
212
+ }
213
+
214
+ return {
215
+ valid: errors.length === 0,
216
+ errors,
217
+ }
218
+ }