prjct-cli 0.45.5 → 0.46.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
CHANGED
|
@@ -1,5 +1,23 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.46.0] - 2026-01-30
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- preserve user customizations during sync - PRJ-115 (#70)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.46.0] - 2026-01-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Preserve user customizations during sync** (PRJ-115)
|
|
15
|
+
- Wrap custom content in `<!-- prjct:preserve -->` markers
|
|
16
|
+
- Content survives `p. sync` regeneration
|
|
17
|
+
- Supports named sections: `<!-- prjct:preserve:my-rules -->`
|
|
18
|
+
- Works with CLAUDE.md, .cursorrules, and all context files
|
|
19
|
+
|
|
20
|
+
|
|
3
21
|
## [0.45.5] - 2026-01-30
|
|
4
22
|
|
|
5
23
|
### Bug Fixes
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test'
|
|
2
|
+
import {
|
|
3
|
+
extractPreservedContent,
|
|
4
|
+
extractPreservedSections,
|
|
5
|
+
hasPreservedSections,
|
|
6
|
+
mergePreservedSections,
|
|
7
|
+
stripPreservedSections,
|
|
8
|
+
validatePreserveBlocks,
|
|
9
|
+
wrapInPreserveMarkers,
|
|
10
|
+
} from '../../utils/preserve-sections'
|
|
11
|
+
|
|
12
|
+
describe('preserve-sections', () => {
|
|
13
|
+
describe('extractPreservedSections', () => {
|
|
14
|
+
it('should extract a single preserved section', () => {
|
|
15
|
+
const content = `# Header
|
|
16
|
+
|
|
17
|
+
<!-- prjct:preserve -->
|
|
18
|
+
My custom content
|
|
19
|
+
<!-- /prjct:preserve -->
|
|
20
|
+
|
|
21
|
+
# Footer`
|
|
22
|
+
|
|
23
|
+
const sections = extractPreservedSections(content)
|
|
24
|
+
expect(sections).toHaveLength(1)
|
|
25
|
+
expect(sections[0].content).toContain('My custom content')
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
it('should extract multiple preserved sections', () => {
|
|
29
|
+
const content = `# Header
|
|
30
|
+
|
|
31
|
+
<!-- prjct:preserve -->
|
|
32
|
+
First section
|
|
33
|
+
<!-- /prjct:preserve -->
|
|
34
|
+
|
|
35
|
+
Some content
|
|
36
|
+
|
|
37
|
+
<!-- prjct:preserve -->
|
|
38
|
+
Second section
|
|
39
|
+
<!-- /prjct:preserve -->`
|
|
40
|
+
|
|
41
|
+
const sections = extractPreservedSections(content)
|
|
42
|
+
expect(sections).toHaveLength(2)
|
|
43
|
+
expect(sections[0].content).toContain('First section')
|
|
44
|
+
expect(sections[1].content).toContain('Second section')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
it('should handle named sections', () => {
|
|
48
|
+
const content = `<!-- prjct:preserve:custom-rules -->
|
|
49
|
+
My rules
|
|
50
|
+
<!-- /prjct:preserve -->`
|
|
51
|
+
|
|
52
|
+
const sections = extractPreservedSections(content)
|
|
53
|
+
expect(sections).toHaveLength(1)
|
|
54
|
+
expect(sections[0].id).toBe('custom-rules')
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should return empty array when no preserved sections', () => {
|
|
58
|
+
const content = '# Just normal content'
|
|
59
|
+
const sections = extractPreservedSections(content)
|
|
60
|
+
expect(sections).toHaveLength(0)
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('should ignore unclosed preserve blocks', () => {
|
|
64
|
+
const content = `<!-- prjct:preserve -->
|
|
65
|
+
No closing tag here`
|
|
66
|
+
|
|
67
|
+
const sections = extractPreservedSections(content)
|
|
68
|
+
expect(sections).toHaveLength(0)
|
|
69
|
+
})
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
describe('extractPreservedContent', () => {
|
|
73
|
+
it('should extract inner content without markers', () => {
|
|
74
|
+
const content = `<!-- prjct:preserve -->
|
|
75
|
+
My content
|
|
76
|
+
<!-- /prjct:preserve -->`
|
|
77
|
+
|
|
78
|
+
const inner = extractPreservedContent(content)
|
|
79
|
+
expect(inner).toHaveLength(1)
|
|
80
|
+
expect(inner[0]).toBe('My content')
|
|
81
|
+
})
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
describe('hasPreservedSections', () => {
|
|
85
|
+
it('should return true when preserved sections exist', () => {
|
|
86
|
+
const content = '<!-- prjct:preserve -->content<!-- /prjct:preserve -->'
|
|
87
|
+
expect(hasPreservedSections(content)).toBe(true)
|
|
88
|
+
})
|
|
89
|
+
|
|
90
|
+
it('should return false when no preserved sections', () => {
|
|
91
|
+
const content = '# Normal markdown'
|
|
92
|
+
expect(hasPreservedSections(content)).toBe(false)
|
|
93
|
+
})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
describe('mergePreservedSections', () => {
|
|
97
|
+
it('should append preserved sections to new content', () => {
|
|
98
|
+
const oldContent = `# Old Header
|
|
99
|
+
|
|
100
|
+
<!-- prjct:preserve -->
|
|
101
|
+
# My Rules
|
|
102
|
+
- Use tabs
|
|
103
|
+
<!-- /prjct:preserve -->`
|
|
104
|
+
|
|
105
|
+
const newContent = `# New Generated Content
|
|
106
|
+
|
|
107
|
+
This is fresh.`
|
|
108
|
+
|
|
109
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
110
|
+
|
|
111
|
+
expect(merged).toContain('# New Generated Content')
|
|
112
|
+
expect(merged).toContain('My Rules')
|
|
113
|
+
expect(merged).toContain('Use tabs')
|
|
114
|
+
expect(merged).toContain('prjct:preserve')
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('should return new content unchanged when no preserved sections', () => {
|
|
118
|
+
const oldContent = '# Old content without preserve markers'
|
|
119
|
+
const newContent = '# New content'
|
|
120
|
+
|
|
121
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
122
|
+
expect(merged).toBe(newContent)
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
it('should preserve multiple sections', () => {
|
|
126
|
+
const oldContent = `<!-- prjct:preserve -->
|
|
127
|
+
Section 1
|
|
128
|
+
<!-- /prjct:preserve -->
|
|
129
|
+
|
|
130
|
+
<!-- prjct:preserve -->
|
|
131
|
+
Section 2
|
|
132
|
+
<!-- /prjct:preserve -->`
|
|
133
|
+
|
|
134
|
+
const newContent = '# New'
|
|
135
|
+
|
|
136
|
+
const merged = mergePreservedSections(newContent, oldContent)
|
|
137
|
+
expect(merged).toContain('Section 1')
|
|
138
|
+
expect(merged).toContain('Section 2')
|
|
139
|
+
})
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
describe('wrapInPreserveMarkers', () => {
|
|
143
|
+
it('should wrap content with default markers', () => {
|
|
144
|
+
const content = 'My content'
|
|
145
|
+
const wrapped = wrapInPreserveMarkers(content)
|
|
146
|
+
|
|
147
|
+
expect(wrapped).toContain('<!-- prjct:preserve -->')
|
|
148
|
+
expect(wrapped).toContain('My content')
|
|
149
|
+
expect(wrapped).toContain('<!-- /prjct:preserve -->')
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
it('should wrap content with named markers', () => {
|
|
153
|
+
const content = 'My content'
|
|
154
|
+
const wrapped = wrapInPreserveMarkers(content, 'custom')
|
|
155
|
+
|
|
156
|
+
expect(wrapped).toContain('<!-- prjct:preserve:custom -->')
|
|
157
|
+
})
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
describe('stripPreservedSections', () => {
|
|
161
|
+
it('should remove preserved sections from content', () => {
|
|
162
|
+
const content = `# Header
|
|
163
|
+
|
|
164
|
+
<!-- prjct:preserve -->
|
|
165
|
+
Custom stuff
|
|
166
|
+
<!-- /prjct:preserve -->
|
|
167
|
+
|
|
168
|
+
# Footer`
|
|
169
|
+
|
|
170
|
+
const stripped = stripPreservedSections(content)
|
|
171
|
+
expect(stripped).toContain('# Header')
|
|
172
|
+
expect(stripped).toContain('# Footer')
|
|
173
|
+
expect(stripped).not.toContain('Custom stuff')
|
|
174
|
+
expect(stripped).not.toContain('prjct:preserve')
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
it('should return content unchanged when no preserved sections', () => {
|
|
178
|
+
const content = '# Normal content'
|
|
179
|
+
const stripped = stripPreservedSections(content)
|
|
180
|
+
expect(stripped).toBe(content)
|
|
181
|
+
})
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
describe('validatePreserveBlocks', () => {
|
|
185
|
+
it('should validate correct blocks', () => {
|
|
186
|
+
const content = `<!-- prjct:preserve -->
|
|
187
|
+
Content
|
|
188
|
+
<!-- /prjct:preserve -->`
|
|
189
|
+
|
|
190
|
+
const result = validatePreserveBlocks(content)
|
|
191
|
+
expect(result.valid).toBe(true)
|
|
192
|
+
expect(result.errors).toHaveLength(0)
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should detect mismatched markers', () => {
|
|
196
|
+
const content = `<!-- prjct:preserve -->
|
|
197
|
+
Content without closing`
|
|
198
|
+
|
|
199
|
+
const result = validatePreserveBlocks(content)
|
|
200
|
+
expect(result.valid).toBe(false)
|
|
201
|
+
expect(result.errors.length).toBeGreaterThan(0)
|
|
202
|
+
})
|
|
203
|
+
|
|
204
|
+
it('should detect nested blocks', () => {
|
|
205
|
+
const content = `<!-- prjct:preserve -->
|
|
206
|
+
<!-- prjct:preserve -->
|
|
207
|
+
Nested
|
|
208
|
+
<!-- /prjct:preserve -->
|
|
209
|
+
<!-- /prjct:preserve -->`
|
|
210
|
+
|
|
211
|
+
const result = validatePreserveBlocks(content)
|
|
212
|
+
expect(result.valid).toBe(false)
|
|
213
|
+
expect(result.errors.some((e) => e.includes('Nested'))).toBe(true)
|
|
214
|
+
})
|
|
215
|
+
})
|
|
216
|
+
})
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import fs from 'node:fs/promises'
|
|
9
9
|
import path from 'node:path'
|
|
10
|
+
import { mergePreservedSections } from '../utils/preserve-sections'
|
|
10
11
|
import { getFormatter, type ProjectContext } from './formatters'
|
|
11
12
|
import { AI_TOOLS, type AIToolConfig, DEFAULT_AI_TOOLS, getAIToolConfig } from './registry'
|
|
12
13
|
|
|
@@ -71,7 +72,7 @@ async function generateForTool(
|
|
|
71
72
|
|
|
72
73
|
try {
|
|
73
74
|
// Generate content
|
|
74
|
-
|
|
75
|
+
let content = formatter(context, config)
|
|
75
76
|
|
|
76
77
|
// Determine output path
|
|
77
78
|
let outputPath: string
|
|
@@ -84,6 +85,14 @@ async function generateForTool(
|
|
|
84
85
|
// Ensure directory exists
|
|
85
86
|
await fs.mkdir(path.dirname(outputPath), { recursive: true })
|
|
86
87
|
|
|
88
|
+
// Read existing file to preserve user customizations
|
|
89
|
+
try {
|
|
90
|
+
const existingContent = await fs.readFile(outputPath, 'utf-8')
|
|
91
|
+
content = mergePreservedSections(content, existingContent)
|
|
92
|
+
} catch {
|
|
93
|
+
// File doesn't exist yet - use generated content as-is
|
|
94
|
+
}
|
|
95
|
+
|
|
87
96
|
// Write file
|
|
88
97
|
await fs.writeFile(outputPath, content, 'utf-8')
|
|
89
98
|
|
|
@@ -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,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
|
+
}
|
package/dist/bin/prjct.mjs
CHANGED
|
@@ -17217,6 +17217,60 @@ var init_formatters = __esm({
|
|
|
17217
17217
|
}
|
|
17218
17218
|
});
|
|
17219
17219
|
|
|
17220
|
+
// core/utils/preserve-sections.ts
|
|
17221
|
+
function createPreserveStartRegex() {
|
|
17222
|
+
return /<!-- prjct:preserve(?::([\w-]+))? -->/g;
|
|
17223
|
+
}
|
|
17224
|
+
function extractPreservedSections(content) {
|
|
17225
|
+
const sections = [];
|
|
17226
|
+
const regex = createPreserveStartRegex();
|
|
17227
|
+
let match;
|
|
17228
|
+
let sectionIndex = 0;
|
|
17229
|
+
while ((match = regex.exec(content)) !== null) {
|
|
17230
|
+
const startIndex = match.index;
|
|
17231
|
+
const startTag = match[0];
|
|
17232
|
+
const sectionId = match[1] || `section-${sectionIndex++}`;
|
|
17233
|
+
const endTagStart = content.indexOf(PRESERVE_END_PATTERN, startIndex + startTag.length);
|
|
17234
|
+
if (endTagStart === -1) {
|
|
17235
|
+
continue;
|
|
17236
|
+
}
|
|
17237
|
+
const endIndex = endTagStart + PRESERVE_END_PATTERN.length;
|
|
17238
|
+
const fullContent = content.substring(startIndex, endIndex);
|
|
17239
|
+
sections.push({
|
|
17240
|
+
id: sectionId,
|
|
17241
|
+
content: fullContent,
|
|
17242
|
+
startIndex,
|
|
17243
|
+
endIndex
|
|
17244
|
+
});
|
|
17245
|
+
}
|
|
17246
|
+
return sections;
|
|
17247
|
+
}
|
|
17248
|
+
function mergePreservedSections(newContent, oldContent) {
|
|
17249
|
+
const preservedSections = extractPreservedSections(oldContent);
|
|
17250
|
+
if (preservedSections.length === 0) {
|
|
17251
|
+
return newContent;
|
|
17252
|
+
}
|
|
17253
|
+
let merged = newContent.trimEnd();
|
|
17254
|
+
merged += "\n\n---\n\n";
|
|
17255
|
+
merged += "## Your Customizations\n\n";
|
|
17256
|
+
merged += "_The sections below are preserved during sync. Edit freely._\n\n";
|
|
17257
|
+
for (const section of preservedSections) {
|
|
17258
|
+
merged += section.content;
|
|
17259
|
+
merged += "\n\n";
|
|
17260
|
+
}
|
|
17261
|
+
return merged.trimEnd() + "\n";
|
|
17262
|
+
}
|
|
17263
|
+
var PRESERVE_END_PATTERN;
|
|
17264
|
+
var init_preserve_sections = __esm({
|
|
17265
|
+
"core/utils/preserve-sections.ts"() {
|
|
17266
|
+
"use strict";
|
|
17267
|
+
PRESERVE_END_PATTERN = "<!-- /prjct:preserve -->";
|
|
17268
|
+
__name(createPreserveStartRegex, "createPreserveStartRegex");
|
|
17269
|
+
__name(extractPreservedSections, "extractPreservedSections");
|
|
17270
|
+
__name(mergePreservedSections, "mergePreservedSections");
|
|
17271
|
+
}
|
|
17272
|
+
});
|
|
17273
|
+
|
|
17220
17274
|
// core/ai-tools/registry.ts
|
|
17221
17275
|
import { execSync as execSync4 } from "node:child_process";
|
|
17222
17276
|
import fs34 from "node:fs";
|
|
@@ -17356,7 +17410,7 @@ async function generateForTool(context2, config, globalPath, repoPath) {
|
|
|
17356
17410
|
};
|
|
17357
17411
|
}
|
|
17358
17412
|
try {
|
|
17359
|
-
|
|
17413
|
+
let content = formatter(context2, config);
|
|
17360
17414
|
let outputPath;
|
|
17361
17415
|
if (config.outputPath === "repo") {
|
|
17362
17416
|
outputPath = path38.join(repoPath, config.outputFile);
|
|
@@ -17364,6 +17418,11 @@ async function generateForTool(context2, config, globalPath, repoPath) {
|
|
|
17364
17418
|
outputPath = path38.join(globalPath, "context", config.outputFile);
|
|
17365
17419
|
}
|
|
17366
17420
|
await fs35.mkdir(path38.dirname(outputPath), { recursive: true });
|
|
17421
|
+
try {
|
|
17422
|
+
const existingContent = await fs35.readFile(outputPath, "utf-8");
|
|
17423
|
+
content = mergePreservedSections(content, existingContent);
|
|
17424
|
+
} catch {
|
|
17425
|
+
}
|
|
17367
17426
|
await fs35.writeFile(outputPath, content, "utf-8");
|
|
17368
17427
|
return {
|
|
17369
17428
|
toolId: config.id,
|
|
@@ -17384,6 +17443,7 @@ async function generateForTool(context2, config, globalPath, repoPath) {
|
|
|
17384
17443
|
var init_generator2 = __esm({
|
|
17385
17444
|
"core/ai-tools/generator.ts"() {
|
|
17386
17445
|
"use strict";
|
|
17446
|
+
init_preserve_sections();
|
|
17387
17447
|
init_formatters();
|
|
17388
17448
|
init_registry();
|
|
17389
17449
|
__name(generateAIToolContexts, "generateAIToolContexts");
|
|
@@ -17409,6 +17469,7 @@ var init_context_generator = __esm({
|
|
|
17409
17469
|
"core/services/context-generator.ts"() {
|
|
17410
17470
|
"use strict";
|
|
17411
17471
|
init_date_helper();
|
|
17472
|
+
init_preserve_sections();
|
|
17412
17473
|
ContextFileGenerator = class {
|
|
17413
17474
|
static {
|
|
17414
17475
|
__name(this, "ContextFileGenerator");
|
|
@@ -17515,7 +17576,14 @@ Load from \`~/.prjct-cli/projects/${this.config.projectId}/agents/\`:
|
|
|
17515
17576
|
**Workflow**: ${workflowAgents.join(", ")}
|
|
17516
17577
|
**Domain**: ${domainAgents.join(", ") || "none"}
|
|
17517
17578
|
`;
|
|
17518
|
-
|
|
17579
|
+
const claudePath = path39.join(contextPath, "CLAUDE.md");
|
|
17580
|
+
let finalContent = content;
|
|
17581
|
+
try {
|
|
17582
|
+
const existingContent = await fs36.readFile(claudePath, "utf-8");
|
|
17583
|
+
finalContent = mergePreservedSections(content, existingContent);
|
|
17584
|
+
} catch {
|
|
17585
|
+
}
|
|
17586
|
+
await fs36.writeFile(claudePath, finalContent, "utf-8");
|
|
17519
17587
|
}
|
|
17520
17588
|
/**
|
|
17521
17589
|
* Generate now.md - current task status
|
|
@@ -23330,7 +23398,7 @@ var require_package = __commonJS({
|
|
|
23330
23398
|
"package.json"(exports, module) {
|
|
23331
23399
|
module.exports = {
|
|
23332
23400
|
name: "prjct-cli",
|
|
23333
|
-
version: "0.
|
|
23401
|
+
version: "0.46.0",
|
|
23334
23402
|
description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
|
|
23335
23403
|
main: "core/index.ts",
|
|
23336
23404
|
bin: {
|