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 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 startTime = Date.now()
219
- console.log('🔄 Syncing project...\n')
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
- // Use syncService to do EVERYTHING in one call
222
- const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
234
+ const globalPath = pathManager.getGlobalProjectPath(projectId)
235
+ const startTime = Date.now()
223
236
 
224
- if (!result.success) {
225
- console.error('❌ Sync failed:', result.error)
226
- return { success: false, error: result.error }
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
- // Update global config
230
- const globalConfigResult = await commandInstaller.installGlobalConfig()
231
- if (globalConfigResult.success) {
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
- // Format output
236
- console.log(`🔄 Project synced to prjct v${result.cliVersion}\n`)
250
+ // Do a dry-run sync to see what would change
251
+ const result = await syncService.sync(projectPath, { aiTools: options.aiTools })
237
252
 
238
- console.log('📊 Project Stats')
239
- console.log(`├── Files: ~${result.stats.fileCount}`)
240
- console.log(`├── Commits: ${result.git.commits}`)
241
- console.log(`├── Version: ${result.stats.version}`)
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
- console.log('🌿 Git Status')
245
- console.log(`├── Branch: ${result.git.branch}`)
246
- console.log(`├── Uncommitted: ${result.git.hasChanges ? 'Yes' : 'Clean'}`)
247
- console.log(`└── Recent: ${result.git.weeklyCommits} commits this week\n`)
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
- console.log('📁 Context Updated')
250
- for (const file of result.contextFiles) {
251
- console.log(`├── ${file}`)
252
- }
253
- console.log('')
266
+ // Generate diff
267
+ const diff = generateSyncDiff(existingContent, newContent)
268
+
269
+ out.stop()
254
270
 
255
- // Show AI Tools generated (multi-agent output)
256
- if (result.aiTools && result.aiTools.length > 0) {
257
- const successTools = result.aiTools.filter((t) => t.success)
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
- const workflowAgents = result.agents.filter((a) => a.type === 'workflow').map((a) => a.name)
267
- const domainAgents = result.agents.filter((a) => a.type === 'domain').map((a) => a.name)
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
- console.log(`🤖 Agents Regenerated (${result.agents.length})`)
270
- console.log(`├── Workflow: ${workflowAgents.join(', ')}`)
271
- console.log(`└── Domain: ${domainAgents.join(', ') || 'none'}\n`)
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
- if (result.skills.length > 0) {
274
- console.log('📦 Skills Configured')
275
- for (const skill of result.skills) {
276
- console.log(`├── ${skill.agent}.md → ${skill.skill}`)
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
- console.log('')
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
- if (result.git.hasChanges) {
282
- console.log('⚠️ You have uncommitted changes\n')
283
- } else {
284
- console.log('✨ Repository is clean!\n')
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
- showNextSteps('sync')
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
- // Summary metrics
290
- const elapsed = Date.now() - startTime
291
- const contextFilesCount =
292
- result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
293
- const agentCount = result.agents.length
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
- console.log('─'.repeat(45))
296
- console.log(`📊 Sync Summary`)
297
- console.log(
298
- ` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`
299
- )
300
- console.log(
301
- ` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`
302
- )
303
- console.log(
304
- ` Agents: ${agentCount} (${result.agents.filter((a) => a.type === 'domain').length} domain)`
305
- )
306
- console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
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
- return {
310
- success: true,
311
- data: result,
312
- metrics: {
313
- elapsed,
314
- contextFilesCount,
315
- agentCount,
316
- fileCount: result.stats.fileCount,
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
- } catch (error) {
320
- console.error('❌ Error:', (error as Error).message)
321
- return { success: false, error: (error as Error).message }
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