prjct-cli 0.49.0 → 0.51.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 +38 -0
- package/core/commands/analysis.ts +199 -83
- package/core/services/diff-generator.ts +356 -0
- package/core/services/sync-service.ts +6 -0
- package/core/utils/output.ts +113 -5
- package/dist/bin/prjct.mjs +707 -323
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,43 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.51.0] - 2026-01-30
|
|
4
|
+
|
|
5
|
+
### Features
|
|
6
|
+
|
|
7
|
+
- Context diff preview before sync applies - PRJ-125 (#77)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [0.51.0] - 2026-01-30
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- **Context Diff Preview** (PRJ-125)
|
|
15
|
+
- See what changes sync will make before they're applied
|
|
16
|
+
- Interactive confirmation: apply, cancel, or show full diff
|
|
17
|
+
- Preserved sections clearly marked in preview
|
|
18
|
+
- Token count delta displayed
|
|
19
|
+
- `--preview` flag for dry-run only
|
|
20
|
+
- `--yes` flag to skip confirmation
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
## [0.50.0] - 2026-01-30
|
|
24
|
+
|
|
25
|
+
### Features
|
|
26
|
+
|
|
27
|
+
- Unified output system with new methods - PRJ-130 (#76)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## [0.50.0] - 2026-01-30
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- **Unified output system with new methods** (PRJ-130)
|
|
35
|
+
- Added `ICONS` constant for centralized icon definitions
|
|
36
|
+
- New methods: `info()`, `debug()`, `success()`, `list()`, `table()`, `box()`
|
|
37
|
+
- `debug()` only shows output when `DEBUG=1`
|
|
38
|
+
- Refactored existing methods to use `ICONS` for consistency
|
|
39
|
+
|
|
40
|
+
|
|
3
41
|
## [0.49.0] - 2026-01-30
|
|
4
42
|
|
|
5
43
|
### Features
|
|
@@ -2,15 +2,19 @@
|
|
|
2
2
|
* Analysis Commands: analyze, sync, and related helpers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import fs from 'node:fs/promises'
|
|
5
6
|
import path from 'node:path'
|
|
7
|
+
import prompts from 'prompts'
|
|
6
8
|
import { generateContext } from '../context/generator'
|
|
7
9
|
import analyzer from '../domain/analyzer'
|
|
8
10
|
import commandInstaller from '../infrastructure/command-installer'
|
|
9
11
|
import { formatCost } from '../schemas/metrics'
|
|
10
12
|
import { syncService } from '../services'
|
|
13
|
+
import { formatDiffPreview, formatFullDiff, generateSyncDiff } from '../services/diff-generator'
|
|
11
14
|
import { metricsStorage } from '../storage/metrics-storage'
|
|
12
15
|
import type { AnalyzeOptions, CommandResult, ProjectContext } from '../types'
|
|
13
16
|
import { showNextSteps } from '../utils/next-steps'
|
|
17
|
+
import out from '../utils/output'
|
|
14
18
|
import {
|
|
15
19
|
configManager,
|
|
16
20
|
contextBuilder,
|
|
@@ -195,7 +199,7 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
195
199
|
}
|
|
196
200
|
|
|
197
201
|
/**
|
|
198
|
-
* /p:sync - Comprehensive project sync
|
|
202
|
+
* /p:sync - Comprehensive project sync with diff preview
|
|
199
203
|
*
|
|
200
204
|
* Uses syncService to do ALL operations in one TypeScript execution:
|
|
201
205
|
* - Git analysis
|
|
@@ -205,120 +209,232 @@ export class AnalysisCommands extends PrjctCommandsBase {
|
|
|
205
209
|
* - Skill configuration
|
|
206
210
|
* - State updates
|
|
207
211
|
*
|
|
212
|
+
* Options:
|
|
213
|
+
* - --preview: Show what would change without applying
|
|
214
|
+
* - --yes: Skip confirmation prompt
|
|
215
|
+
*
|
|
208
216
|
* This eliminates the need for Claude to make 50+ individual tool calls.
|
|
217
|
+
*
|
|
218
|
+
* @see PRJ-125
|
|
209
219
|
*/
|
|
210
220
|
async sync(
|
|
211
221
|
projectPath: string = process.cwd(),
|
|
212
|
-
options: { aiTools?: string[] } = {}
|
|
222
|
+
options: { aiTools?: string[]; preview?: boolean; yes?: boolean } = {}
|
|
213
223
|
): Promise<CommandResult> {
|
|
214
224
|
try {
|
|
215
225
|
const initResult = await this.ensureProjectInit(projectPath)
|
|
216
226
|
if (!initResult.success) return initResult
|
|
217
227
|
|
|
218
|
-
const
|
|
219
|
-
|
|
228
|
+
const projectId = await configManager.getProjectId(projectPath)
|
|
229
|
+
if (!projectId) {
|
|
230
|
+
out.failWithHint('NO_PROJECT_ID')
|
|
231
|
+
return { success: false, error: 'No project ID found' }
|
|
232
|
+
}
|
|
220
233
|
|
|
221
|
-
|
|
222
|
-
const
|
|
234
|
+
const globalPath = pathManager.getGlobalProjectPath(projectId)
|
|
235
|
+
const startTime = Date.now()
|
|
223
236
|
|
|
224
|
-
if
|
|
225
|
-
|
|
226
|
-
|
|
237
|
+
// Generate diff preview if we have existing context
|
|
238
|
+
const claudeMdPath = path.join(globalPath, 'context', 'CLAUDE.md')
|
|
239
|
+
let existingContent: string | null = null
|
|
240
|
+
try {
|
|
241
|
+
existingContent = await fs.readFile(claudeMdPath, 'utf-8')
|
|
242
|
+
} catch {
|
|
243
|
+
// No existing file - first sync
|
|
227
244
|
}
|
|
228
245
|
|
|
229
|
-
//
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
console.log(`📝 Updated ${pathManager.getDisplayPath(globalConfigResult.path!)}`)
|
|
233
|
-
}
|
|
246
|
+
// For preview mode or when we have existing content, show diff first
|
|
247
|
+
if (existingContent && !options.yes) {
|
|
248
|
+
out.spin('Analyzing changes...')
|
|
234
249
|
|
|
235
|
-
|
|
236
|
-
|
|
250
|
+
// Do a dry-run sync to see what would change
|
|
251
|
+
const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
|
|
237
252
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
console.log(`└── Stack: ${result.stats.ecosystem}\n`)
|
|
253
|
+
if (!result.success) {
|
|
254
|
+
out.fail(result.error || 'Sync failed')
|
|
255
|
+
return { success: false, error: result.error }
|
|
256
|
+
}
|
|
243
257
|
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
258
|
+
// Read the newly generated CLAUDE.md
|
|
259
|
+
let newContent: string
|
|
260
|
+
try {
|
|
261
|
+
newContent = await fs.readFile(claudeMdPath, 'utf-8')
|
|
262
|
+
} catch {
|
|
263
|
+
newContent = ''
|
|
264
|
+
}
|
|
248
265
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
console.log('')
|
|
266
|
+
// Generate diff
|
|
267
|
+
const diff = generateSyncDiff(existingContent, newContent)
|
|
268
|
+
|
|
269
|
+
out.stop()
|
|
254
270
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
console.log(`🤖 AI Tools Context (${successTools.length})`)
|
|
259
|
-
for (const tool of result.aiTools) {
|
|
260
|
-
const status = tool.success ? '✓' : '✗'
|
|
261
|
-
console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
|
|
271
|
+
if (!diff.hasChanges) {
|
|
272
|
+
out.done('No changes detected (context is up to date)')
|
|
273
|
+
return { success: true, message: 'No changes' }
|
|
262
274
|
}
|
|
263
|
-
console.log('')
|
|
264
|
-
}
|
|
265
275
|
|
|
266
|
-
|
|
267
|
-
|
|
276
|
+
// Show diff preview
|
|
277
|
+
console.log(formatDiffPreview(diff))
|
|
278
|
+
|
|
279
|
+
// Preview-only mode - don't apply
|
|
280
|
+
if (options.preview) {
|
|
281
|
+
return {
|
|
282
|
+
success: true,
|
|
283
|
+
isPreview: true,
|
|
284
|
+
diff,
|
|
285
|
+
message: 'Preview complete (no changes applied)',
|
|
286
|
+
}
|
|
287
|
+
}
|
|
268
288
|
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
289
|
+
// Interactive confirmation
|
|
290
|
+
const response = await prompts({
|
|
291
|
+
type: 'select',
|
|
292
|
+
name: 'action',
|
|
293
|
+
message: 'Apply these changes?',
|
|
294
|
+
choices: [
|
|
295
|
+
{ title: 'Yes, apply changes', value: 'apply' },
|
|
296
|
+
{ title: 'No, cancel', value: 'cancel' },
|
|
297
|
+
{ title: 'Show full diff', value: 'diff' },
|
|
298
|
+
],
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
if (response.action === 'cancel' || !response.action) {
|
|
302
|
+
out.warn('Sync cancelled')
|
|
303
|
+
return { success: false, message: 'Cancelled by user' }
|
|
304
|
+
}
|
|
272
305
|
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
306
|
+
if (response.action === 'diff') {
|
|
307
|
+
console.log(`\n${formatFullDiff(diff)}`)
|
|
308
|
+
const confirm = await prompts({
|
|
309
|
+
type: 'confirm',
|
|
310
|
+
name: 'apply',
|
|
311
|
+
message: 'Apply these changes?',
|
|
312
|
+
initial: true,
|
|
313
|
+
})
|
|
314
|
+
if (!confirm.apply) {
|
|
315
|
+
out.warn('Sync cancelled')
|
|
316
|
+
return { success: false, message: 'Cancelled by user' }
|
|
317
|
+
}
|
|
277
318
|
}
|
|
278
|
-
|
|
319
|
+
|
|
320
|
+
// Changes already applied from dry-run, just show success
|
|
321
|
+
out.done('Changes applied')
|
|
322
|
+
return this.showSyncResult(result, startTime)
|
|
279
323
|
}
|
|
280
324
|
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
325
|
+
// First sync or --yes flag - proceed directly
|
|
326
|
+
out.spin('Syncing project...')
|
|
327
|
+
|
|
328
|
+
// Use syncService to do EVERYTHING in one call
|
|
329
|
+
const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
|
|
330
|
+
|
|
331
|
+
if (!result.success) {
|
|
332
|
+
out.fail(result.error || 'Sync failed')
|
|
333
|
+
return { success: false, error: result.error }
|
|
285
334
|
}
|
|
286
335
|
|
|
287
|
-
|
|
336
|
+
out.stop()
|
|
337
|
+
return this.showSyncResult(result, startTime)
|
|
338
|
+
} catch (error) {
|
|
339
|
+
out.fail((error as Error).message)
|
|
340
|
+
return { success: false, error: (error as Error).message }
|
|
341
|
+
}
|
|
342
|
+
}
|
|
288
343
|
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
344
|
+
/**
|
|
345
|
+
* Display sync results (extracted to avoid duplication)
|
|
346
|
+
*/
|
|
347
|
+
private async showSyncResult(
|
|
348
|
+
result: Awaited<ReturnType<typeof syncService.sync>>,
|
|
349
|
+
startTime: number
|
|
350
|
+
): Promise<CommandResult> {
|
|
351
|
+
// Update global config
|
|
352
|
+
const globalConfigResult = await commandInstaller.installGlobalConfig()
|
|
353
|
+
if (globalConfigResult.success) {
|
|
354
|
+
console.log(`📝 Updated ${pathManager.getDisplayPath(globalConfigResult.path!)}`)
|
|
355
|
+
}
|
|
294
356
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
357
|
+
// Format output
|
|
358
|
+
console.log(`🔄 Project synced to prjct v${result.cliVersion}\n`)
|
|
359
|
+
|
|
360
|
+
console.log('📊 Project Stats')
|
|
361
|
+
console.log(`├── Files: ~${result.stats.fileCount}`)
|
|
362
|
+
console.log(`├── Commits: ${result.git.commits}`)
|
|
363
|
+
console.log(`├── Version: ${result.stats.version}`)
|
|
364
|
+
console.log(`└── Stack: ${result.stats.ecosystem}\n`)
|
|
365
|
+
|
|
366
|
+
console.log('🌿 Git Status')
|
|
367
|
+
console.log(`├── Branch: ${result.git.branch}`)
|
|
368
|
+
console.log(`├── Uncommitted: ${result.git.hasChanges ? 'Yes' : 'Clean'}`)
|
|
369
|
+
console.log(`└── Recent: ${result.git.weeklyCommits} commits this week\n`)
|
|
370
|
+
|
|
371
|
+
console.log('📁 Context Updated')
|
|
372
|
+
for (const file of result.contextFiles) {
|
|
373
|
+
console.log(`├── ${file}`)
|
|
374
|
+
}
|
|
375
|
+
console.log('')
|
|
376
|
+
|
|
377
|
+
// Show AI Tools generated (multi-agent output)
|
|
378
|
+
if (result.aiTools && result.aiTools.length > 0) {
|
|
379
|
+
const successTools = result.aiTools.filter((t) => t.success)
|
|
380
|
+
console.log(`🤖 AI Tools Context (${successTools.length})`)
|
|
381
|
+
for (const tool of result.aiTools) {
|
|
382
|
+
const status = tool.success ? '✓' : '✗'
|
|
383
|
+
console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
|
|
384
|
+
}
|
|
307
385
|
console.log('')
|
|
386
|
+
}
|
|
308
387
|
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
388
|
+
const workflowAgents = result.agents.filter((a) => a.type === 'workflow').map((a) => a.name)
|
|
389
|
+
const domainAgents = result.agents.filter((a) => a.type === 'domain').map((a) => a.name)
|
|
390
|
+
|
|
391
|
+
console.log(`🤖 Agents Regenerated (${result.agents.length})`)
|
|
392
|
+
console.log(`├── Workflow: ${workflowAgents.join(', ')}`)
|
|
393
|
+
console.log(`└── Domain: ${domainAgents.join(', ') || 'none'}\n`)
|
|
394
|
+
|
|
395
|
+
if (result.skills.length > 0) {
|
|
396
|
+
console.log('📦 Skills Configured')
|
|
397
|
+
for (const skill of result.skills) {
|
|
398
|
+
console.log(`├── ${skill.agent}.md → ${skill.skill}`)
|
|
318
399
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
400
|
+
console.log('')
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
if (result.git.hasChanges) {
|
|
404
|
+
console.log('⚠️ You have uncommitted changes\n')
|
|
405
|
+
} else {
|
|
406
|
+
console.log('✨ Repository is clean!\n')
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
showNextSteps('sync')
|
|
410
|
+
|
|
411
|
+
// Summary metrics
|
|
412
|
+
const elapsed = Date.now() - startTime
|
|
413
|
+
const contextFilesCount =
|
|
414
|
+
result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
|
|
415
|
+
const agentCount = result.agents.length
|
|
416
|
+
|
|
417
|
+
console.log('─'.repeat(45))
|
|
418
|
+
console.log('📊 Sync Summary')
|
|
419
|
+
console.log(
|
|
420
|
+
` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`
|
|
421
|
+
)
|
|
422
|
+
console.log(` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`)
|
|
423
|
+
console.log(
|
|
424
|
+
` Agents: ${agentCount} (${result.agents.filter((a) => a.type === 'domain').length} domain)`
|
|
425
|
+
)
|
|
426
|
+
console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
|
|
427
|
+
console.log('')
|
|
428
|
+
|
|
429
|
+
return {
|
|
430
|
+
success: true,
|
|
431
|
+
data: result,
|
|
432
|
+
metrics: {
|
|
433
|
+
elapsed,
|
|
434
|
+
contextFilesCount,
|
|
435
|
+
agentCount,
|
|
436
|
+
fileCount: result.stats.fileCount,
|
|
437
|
+
},
|
|
322
438
|
}
|
|
323
439
|
}
|
|
324
440
|
|