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.
- package/CHANGELOG.md +60 -0
- package/bin/prjct.ts +3 -47
- package/core/__tests__/utils/preserve-sections.test.ts +216 -0
- package/core/agentic/command-executor.ts +8 -1
- package/core/ai-tools/generator.ts +10 -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/services/context-generator.ts +12 -1
- package/core/utils/help.ts +321 -0
- package/core/utils/preserve-sections.ts +218 -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 +4464 -3830
- package/package.json +1 -1
- package/templates/commands/workflow.md +150 -0
|
@@ -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
|
-
|
|
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
|
+
}
|