prjct-cli 1.1.0 → 1.2.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,101 @@
1
1
  # Changelog
2
2
 
3
+ ## [1.2.0] - 2026-02-06
4
+
5
+ ### Features
6
+
7
+ - git hooks integration for auto-sync (PRJ-128) (#112)
8
+
9
+
10
+ ## [1.2.0] - 2026-02-05
11
+
12
+ ### Added
13
+
14
+ - **Git hooks integration (PRJ-128)**: New `prjct hooks` command for auto-syncing context on commit and branch checkout
15
+
16
+ ### Implementation Details
17
+
18
+ New `prjct hooks` CLI subcommand with three operations:
19
+ - `prjct hooks install` — auto-detects hook manager (lefthook > husky > direct `.git/hooks/`) and installs post-commit + post-checkout hooks
20
+ - `prjct hooks uninstall` — cleanly removes only prjct hooks, preserving existing hooks
21
+ - `prjct hooks status` — shows active hooks, strategy, and available managers
22
+
23
+ Hook scripts feature:
24
+ - **Rate limiting** — 30-second lockfile prevents over-syncing on rapid commits
25
+ - **Background execution** — hooks run `prjct sync` in background, never blocking git
26
+ - **Branch-only checkout** — post-checkout only fires on branch switch, not file checkout
27
+ - **Cross-platform** — handles macOS/Linux differences in `stat` and `md5` commands
28
+
29
+ Supports three installation strategies:
30
+ - **Lefthook** — adds `prjct-sync-*` commands to existing `lefthook.yml`
31
+ - **Husky** — appends to existing `.husky/` hook scripts
32
+ - **Direct** — writes to `.git/hooks/` as fallback
33
+
34
+ Hook configuration saved to `project.json` for persistence across sessions.
35
+
36
+ ### Learnings
37
+
38
+ - Strategy pattern works well for hook manager abstraction (detect → select → install)
39
+ - `stat -f%m` (macOS) vs `stat -c%Y` (Linux) for file modification time
40
+ - Lefthook section merging needs careful regex to avoid duplicates
41
+ - `$3` parameter in post-checkout distinguishes branch checkout (1) from file checkout (0)
42
+
43
+ ### Test Plan
44
+
45
+ #### For QA
46
+ 1. Run `prjct hooks status` — verify shows "Not installed" with available managers
47
+ 2. Run `prjct hooks install` — verify detects manager and installs hooks
48
+ 3. Run `prjct hooks status` — verify shows "Active"
49
+ 4. Make a git commit — verify sync runs in background
50
+ 5. Switch branches — verify post-checkout triggers sync
51
+ 6. Run `prjct hooks uninstall` — verify clean removal
52
+ 7. Run `bun run build && bun run typecheck` — zero errors
53
+
54
+ #### For Users
55
+ **What changed:** New `prjct hooks` command for automatic context syncing
56
+ **How to use:** Run `prjct hooks install` in any prjct project
57
+ **Breaking changes:** None
58
+
59
+ ## [1.1.1] - 2026-02-06
60
+
61
+ ### Bug Fixes
62
+
63
+ - visual grouping with boxes and tables for structured output (PRJ-134) (#110)
64
+
65
+ ## [1.1.1] - 2026-02-05
66
+
67
+ ### Improved
68
+
69
+ - **Visual grouping for structured output (PRJ-134)**: Added `out.section()` and integrated `out.box()`, `out.table()`, `out.list()` into sync, doctor, and status commands
70
+
71
+ ### Implementation Details
72
+
73
+ Added `out.section(title)` method to the unified output system (`core/utils/output.ts`) — bold title with dim underline, chainable, quiet-mode aware.
74
+
75
+ Refactored three commands to use unified output helpers instead of raw `console.log`:
76
+ - **Sync** (`analysis.ts`): Summary metrics in `out.box()`, generated items via `out.section()` + `out.list()`
77
+ - **Doctor** (`doctor-service.ts`): Section headers via `out.section()`, recommendations via `out.list()`, summary via `out.done()`/`out.warn()`/`out.fail()`
78
+ - **Status** (`staleness-checker.ts`): Key-value details wrapped in box-drawing characters
79
+
80
+ ### Learnings
81
+
82
+ - `staleness-checker.formatStatus()` returns a string (not direct output), so `out.box()` can't be used directly — used inline box-drawing chars instead
83
+ - Doctor service had its own icon logic for check results that was worth preserving alongside the new section headers
84
+ - Unified output helpers reduce code while maintaining the same visual style
85
+
86
+ ### Test Plan
87
+
88
+ #### For QA
89
+ 1. Run `prjct sync` — verify boxed "Sync Summary" with metrics, "Generated" section header with underline, `✓` bullet items
90
+ 2. Run `prjct doctor` — verify bold+underline section headers for "System Tools", "Project Status", "Recommendations"
91
+ 3. Run `prjct status` — verify key-value details in box-drawing characters
92
+ 4. Run with `--quiet` flag — verify no visual output is printed
93
+
94
+ #### For Users
95
+ **What changed:** CLI output now uses visual grouping (boxes, section headers, structured lists) for better scannability
96
+ **How to use:** No changes needed — output is automatically improved
97
+ **Breaking changes:** None
98
+
3
99
  ## [1.1.0] - 2026-02-05
4
100
 
5
101
  ### Features
package/bin/prjct.ts CHANGED
@@ -108,6 +108,12 @@ if (args[0] === 'start' || args[0] === 'setup') {
108
108
  console.log(JSON.stringify(result, null, 2))
109
109
  process.exitCode = result.tool === 'error' ? 1 : 0
110
110
  }
111
+ } else if (args[0] === 'hooks') {
112
+ // Git hooks management
113
+ const { hooksService } = await import('../core/services/hooks-service')
114
+ const subcommand = args[1] || 'status'
115
+ const exitCode = await hooksService.run(process.cwd(), subcommand)
116
+ process.exitCode = exitCode
111
117
  } else if (args[0] === 'doctor') {
112
118
  // Health check command
113
119
  const { doctorService } = await import('../core/services/doctor-service')
@@ -478,67 +478,55 @@ export class AnalysisCommands extends PrjctCommandsBase {
478
478
  // ═══════════════════════════════════════════════════════════════════════
479
479
  // SUCCESS LINE - Immediate confirmation with timing
480
480
  // ═══════════════════════════════════════════════════════════════════════
481
- console.log(`✅ Synced ${result.stats.name || 'project'} (${(elapsed / 1000).toFixed(1)}s)\n`)
481
+ out.done(`Synced ${result.stats.name || 'project'} (${(elapsed / 1000).toFixed(1)}s)`)
482
+ console.log('')
482
483
 
483
484
  // ═══════════════════════════════════════════════════════════════════════
484
- // KEY METRICS - Single scannable line
485
+ // SUMMARY BOX - Key metrics grouped visually
485
486
  // ═══════════════════════════════════════════════════════════════════════
486
- // Only show compression rate if meaningful (> 10%)
487
487
  const compressionPct = result.syncMetrics?.compressionRate
488
488
  ? Math.round(result.syncMetrics.compressionRate * 100)
489
489
  : 0
490
- const metricsLine = [
491
- `${result.stats.fileCount} files → ${contextFilesCount} context`,
492
- `${agentCount} agents`,
493
- compressionPct > 10 ? `${compressionPct}% reduction` : null,
494
- ]
495
- .filter(Boolean)
496
- .join(' | ')
497
- console.log(metricsLine)
498
-
499
- // Stack and branch info
500
490
  const framework = result.stats.frameworks.length > 0 ? ` (${result.stats.frameworks[0]})` : ''
501
- console.log(`Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}\n`)
491
+ const boxLines = [
492
+ `${result.stats.fileCount} files → ${contextFilesCount} context | ${agentCount} agents${compressionPct > 10 ? ` | ${compressionPct}% reduction` : ''}`,
493
+ `Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}`,
494
+ ]
495
+ out.box('Sync Summary', boxLines.join('\n'))
502
496
 
503
497
  // ═══════════════════════════════════════════════════════════════════════
504
498
  // CHANGES SECTION - What was generated/updated
505
499
  // ═══════════════════════════════════════════════════════════════════════
506
- console.log('Generated:')
507
-
508
- // Context files (condensed)
500
+ const generatedItems: string[] = []
509
501
  if (result.contextFiles.length > 0) {
510
- console.log(` ✓ ${result.contextFiles.length} context files`)
502
+ generatedItems.push(`${result.contextFiles.length} context files`)
511
503
  }
512
-
513
- // AI tools
514
504
  const successTools = result.aiTools?.filter((t) => t.success) || []
515
505
  if (successTools.length > 0) {
516
- const toolNames = successTools.map((t) => t.toolId).join(', ')
517
- console.log(` ✓ AI tools: ${toolNames}`)
506
+ generatedItems.push(`AI tools: ${successTools.map((t) => t.toolId).join(', ')}`)
518
507
  }
519
-
520
- // Agents (show count with breakdown)
521
508
  if (agentCount > 0) {
522
509
  const agentSummary =
523
510
  domainAgentCount > 0
524
511
  ? `${agentCount} agents (${domainAgentCount} domain)`
525
512
  : `${agentCount} agents`
526
- console.log(` ✓ ${agentSummary}`)
513
+ generatedItems.push(agentSummary)
527
514
  }
528
-
529
- // Skills
530
515
  if (result.skills.length > 0) {
531
516
  const skillWord = result.skills.length === 1 ? 'skill' : 'skills'
532
- console.log(` ✓ ${result.skills.length} ${skillWord}`)
517
+ generatedItems.push(`${result.skills.length} ${skillWord}`)
533
518
  }
534
519
 
520
+ out.section('Generated')
521
+ out.list(generatedItems, { bullet: '✓' })
535
522
  console.log('')
536
523
 
537
524
  // ═══════════════════════════════════════════════════════════════════════
538
525
  // STATUS INDICATOR - Repository state
539
526
  // ═══════════════════════════════════════════════════════════════════════
540
527
  if (result.git.hasChanges) {
541
- console.log('⚠️ Uncommitted changes detected\n')
528
+ out.warn('Uncommitted changes detected')
529
+ console.log('')
542
530
  }
543
531
 
544
532
  // ═══════════════════════════════════════════════════════════════════════
@@ -207,6 +207,7 @@ export class PlanningCommands extends PrjctCommandsBase {
207
207
  console.log(' Quick start:')
208
208
  console.log(' prjct sync Update context after changes')
209
209
  console.log(' prjct task Start working on a task')
210
+ console.log(' prjct hooks Auto-sync on commit/checkout')
210
211
  console.log('')
211
212
 
212
213
  if (wizardResult) {
package/core/index.ts CHANGED
@@ -306,6 +306,7 @@ TERMINAL COMMANDS (this CLI)
306
306
  prjct setup Reconfigure installations
307
307
  prjct sync Sync project state
308
308
  prjct watch Auto-sync on file changes (Ctrl+C to stop)
309
+ prjct hooks Manage git hooks for auto-sync
309
310
  prjct doctor Check system health and dependencies
310
311
 
311
312
  EXAMPLES
@@ -15,6 +15,7 @@ import path from 'node:path'
15
15
  import chalk from 'chalk'
16
16
  import configManager from '../infrastructure/config-manager'
17
17
  import pathManager from '../infrastructure/path-manager'
18
+ import out from '../utils/output'
18
19
  import { VERSION } from '../utils/version'
19
20
 
20
21
  // ============================================================================
@@ -367,32 +368,28 @@ class DoctorService {
367
368
  // ==========================================================================
368
369
 
369
370
  private printHeader(): void {
370
- console.log('')
371
- console.log(chalk.bold(`prjct doctor v${VERSION}`))
372
- console.log(chalk.dim('─'.repeat(40)))
371
+ out.section(`prjct doctor v${VERSION}`)
373
372
  }
374
373
 
375
374
  private printSection(title: string, checks: CheckResult[]): void {
376
- console.log('')
377
- console.log(chalk.bold(title))
375
+ out.section(title)
378
376
 
379
- for (const check of checks) {
377
+ const items = checks.map((check) => {
380
378
  const icon = this.getStatusIcon(check.status, check.optional)
381
379
  const name = check.name.padEnd(14)
382
380
  const detail = check.version || check.message || ''
383
381
  const optionalTag = check.optional && check.status === 'error' ? chalk.dim(' (optional)') : ''
382
+ return `${icon} ${name} ${chalk.dim(detail)}${optionalTag}`
383
+ })
384
384
 
385
- console.log(` ${icon} ${name} ${chalk.dim(detail)}${optionalTag}`)
385
+ for (const item of items) {
386
+ console.log(` ${item}`)
386
387
  }
387
388
  }
388
389
 
389
390
  private printRecommendations(recommendations: string[]): void {
390
- console.log('')
391
- console.log(chalk.bold('Recommendations'))
392
-
393
- for (const rec of recommendations) {
394
- console.log(` ${chalk.yellow('•')} ${rec}`)
395
- }
391
+ out.section('Recommendations')
392
+ out.list(recommendations, { bullet: chalk.yellow('') })
396
393
  }
397
394
 
398
395
  private printSummary(result: DoctorResult): void {
@@ -400,11 +397,11 @@ class DoctorService {
400
397
  console.log(chalk.dim('─'.repeat(40)))
401
398
 
402
399
  if (result.hasErrors) {
403
- console.log(chalk.red('Some required checks failed'))
400
+ out.fail('Some required checks failed')
404
401
  } else if (result.hasWarnings) {
405
- console.log(chalk.yellow('All required checks passed (some warnings)'))
402
+ out.warn('All required checks passed (some warnings)')
406
403
  } else {
407
- console.log(chalk.green('All checks passed'))
404
+ out.done('All checks passed')
408
405
  }
409
406
 
410
407
  console.log('')