prjct-cli 0.4.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.
Files changed (71) hide show
  1. package/CHANGELOG.md +312 -0
  2. package/CLAUDE.md +300 -0
  3. package/LICENSE +21 -0
  4. package/README.md +424 -0
  5. package/bin/prjct +214 -0
  6. package/core/agent-detector.js +249 -0
  7. package/core/agents/claude-agent.js +250 -0
  8. package/core/agents/codex-agent.js +256 -0
  9. package/core/agents/terminal-agent.js +465 -0
  10. package/core/analyzer.js +596 -0
  11. package/core/animations-simple.js +240 -0
  12. package/core/animations.js +277 -0
  13. package/core/author-detector.js +218 -0
  14. package/core/capability-installer.js +190 -0
  15. package/core/command-installer.js +775 -0
  16. package/core/commands.js +2050 -0
  17. package/core/config-manager.js +335 -0
  18. package/core/migrator.js +784 -0
  19. package/core/path-manager.js +324 -0
  20. package/core/project-capabilities.js +144 -0
  21. package/core/session-manager.js +439 -0
  22. package/core/version.js +107 -0
  23. package/core/workflow-engine.js +213 -0
  24. package/core/workflow-prompts.js +192 -0
  25. package/core/workflow-rules.js +147 -0
  26. package/package.json +80 -0
  27. package/scripts/install.sh +433 -0
  28. package/scripts/verify-installation.sh +158 -0
  29. package/templates/agents/AGENTS.md +164 -0
  30. package/templates/commands/analyze.md +125 -0
  31. package/templates/commands/cleanup.md +102 -0
  32. package/templates/commands/context.md +105 -0
  33. package/templates/commands/design.md +113 -0
  34. package/templates/commands/done.md +44 -0
  35. package/templates/commands/fix.md +87 -0
  36. package/templates/commands/git.md +79 -0
  37. package/templates/commands/help.md +72 -0
  38. package/templates/commands/idea.md +50 -0
  39. package/templates/commands/init.md +237 -0
  40. package/templates/commands/next.md +74 -0
  41. package/templates/commands/now.md +35 -0
  42. package/templates/commands/progress.md +92 -0
  43. package/templates/commands/recap.md +86 -0
  44. package/templates/commands/roadmap.md +107 -0
  45. package/templates/commands/ship.md +41 -0
  46. package/templates/commands/stuck.md +48 -0
  47. package/templates/commands/task.md +97 -0
  48. package/templates/commands/test.md +94 -0
  49. package/templates/commands/workflow.md +224 -0
  50. package/templates/examples/natural-language-examples.md +320 -0
  51. package/templates/mcp-config.json +8 -0
  52. package/templates/workflows/analyze.md +159 -0
  53. package/templates/workflows/cleanup.md +73 -0
  54. package/templates/workflows/context.md +72 -0
  55. package/templates/workflows/design.md +88 -0
  56. package/templates/workflows/done.md +20 -0
  57. package/templates/workflows/fix.md +201 -0
  58. package/templates/workflows/git.md +192 -0
  59. package/templates/workflows/help.md +13 -0
  60. package/templates/workflows/idea.md +22 -0
  61. package/templates/workflows/init.md +80 -0
  62. package/templates/workflows/natural-language-handler.md +183 -0
  63. package/templates/workflows/next.md +44 -0
  64. package/templates/workflows/now.md +19 -0
  65. package/templates/workflows/progress.md +113 -0
  66. package/templates/workflows/recap.md +66 -0
  67. package/templates/workflows/roadmap.md +95 -0
  68. package/templates/workflows/ship.md +18 -0
  69. package/templates/workflows/stuck.md +25 -0
  70. package/templates/workflows/task.md +109 -0
  71. package/templates/workflows/test.md +243 -0
@@ -0,0 +1,596 @@
1
+ const fs = require('fs').promises
2
+ const path = require('path')
3
+ const { promisify } = require('util')
4
+ const { exec: execCallback } = require('child_process')
5
+ const exec = promisify(execCallback)
6
+
7
+ /**
8
+ * CodebaseAnalyzer - Analyzes existing code to sync with .prjct/ state
9
+ *
10
+ * Detects:
11
+ * - Implemented commands in bin/prjct and core/commands.js
12
+ * - Completed features from git history and file structure
13
+ * - Project structure and technologies
14
+ *
15
+ * Syncs with:
16
+ * - next.md (marks completed tasks)
17
+ * - shipped.md (adds detected features)
18
+ * - analysis/repo-summary.md (creates detailed report)
19
+ */
20
+ class CodebaseAnalyzer {
21
+ constructor() {
22
+ this.projectPath = null
23
+ this.analysis = null
24
+ }
25
+
26
+ /**
27
+ * Main analysis entry point
28
+ */
29
+ async analyzeProject(projectPath = process.cwd()) {
30
+ this.projectPath = projectPath
31
+
32
+ this.analysis = {
33
+ commands: await this.detectImplementedCommands(),
34
+ features: await this.detectCompletedFeatures(),
35
+ structure: await this.detectProjectStructure(),
36
+ gitHistory: await this.scanGitHistory(),
37
+ technologies: await this.detectTechnologies(),
38
+ }
39
+
40
+ return this.analysis
41
+ }
42
+
43
+ /**
44
+ * Detect implemented commands in bin/prjct
45
+ */
46
+ async detectImplementedCommands() {
47
+ const commands = []
48
+
49
+ try {
50
+ const binPath = path.join(this.projectPath, 'bin', 'prjct')
51
+ const content = await fs.readFile(binPath, 'utf-8')
52
+
53
+ const caseMatches = content.matchAll(/case\s+'([^']+)':/g)
54
+ for (const match of caseMatches) {
55
+ commands.push(match[1])
56
+ }
57
+
58
+ const commandsPath = path.join(this.projectPath, 'core', 'commands.js')
59
+ if (await this.fileExists(commandsPath)) {
60
+ const commandsContent = await fs.readFile(commandsPath, 'utf-8')
61
+ const methodMatches = commandsContent.matchAll(/async\s+(\w+)\s*\(/g)
62
+
63
+ for (const match of methodMatches) {
64
+ const methodName = match[1]
65
+ if (!methodName.startsWith('_') &&
66
+ methodName !== 'constructor' &&
67
+ methodName !== 'initializeAgent' &&
68
+ !commands.includes(methodName)) {
69
+ commands.push(methodName)
70
+ }
71
+ }
72
+ }
73
+ } catch (error) {
74
+ console.error('[analyzer] Error detecting commands:', error.message)
75
+ }
76
+
77
+ return commands
78
+ }
79
+
80
+ /**
81
+ * Detect completed features from various sources
82
+ */
83
+ async detectCompletedFeatures() {
84
+ const features = []
85
+
86
+ const gitFeatures = await this.extractFeaturesFromGit()
87
+ features.push(...gitFeatures)
88
+
89
+ const packageFeatures = await this.extractFeaturesFromPackageJson()
90
+ features.push(...packageFeatures)
91
+
92
+ const structureFeatures = await this.extractFeaturesFromStructure()
93
+ features.push(...structureFeatures)
94
+
95
+ return [...new Set(features)]
96
+ }
97
+
98
+ /**
99
+ * Extract features from git commit history
100
+ */
101
+ async extractFeaturesFromGit() {
102
+ const features = []
103
+
104
+ try {
105
+ const { stdout } = await exec(
106
+ 'git log --all --pretty=format:"%s" --grep="^feat:" --grep="^ship:" --grep="^feature:" -i',
107
+ { cwd: this.projectPath },
108
+ )
109
+
110
+ if (stdout) {
111
+ const commits = stdout.split('\n')
112
+ for (const commit of commits) {
113
+ const match = commit.match(/^(?:feat|ship|feature):\s*(.+)/i)
114
+ if (match) {
115
+ features.push(match[1].trim())
116
+ }
117
+ }
118
+ }
119
+ } catch (error) {
120
+ }
121
+
122
+ return features
123
+ }
124
+
125
+ /**
126
+ * Extract features from package.json dependencies
127
+ */
128
+ async extractFeaturesFromPackageJson() {
129
+ const features = []
130
+
131
+ try {
132
+ const packagePath = path.join(this.projectPath, 'package.json')
133
+ const content = await fs.readFile(packagePath, 'utf-8')
134
+ const pkg = JSON.parse(content)
135
+
136
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
137
+
138
+ const featureMap = {
139
+ express: 'REST API server',
140
+ next: 'Next.js application',
141
+ react: 'React frontend',
142
+ vue: 'Vue application',
143
+ typescript: 'TypeScript support',
144
+ jest: 'Testing framework',
145
+ prisma: 'Database ORM',
146
+ mongoose: 'MongoDB integration',
147
+ stripe: 'Payment processing',
148
+ passport: 'Authentication system',
149
+ }
150
+
151
+ for (const [dep, feature] of Object.entries(featureMap)) {
152
+ if (deps[dep]) {
153
+ features.push(feature)
154
+ }
155
+ }
156
+ } catch (error) {
157
+ }
158
+
159
+ return features
160
+ }
161
+
162
+ /**
163
+ * Extract features from directory structure
164
+ */
165
+ async extractFeaturesFromStructure() {
166
+ const features = []
167
+
168
+ try {
169
+ const entries = await fs.readdir(this.projectPath, { withFileTypes: true })
170
+
171
+ const featureDirs = {
172
+ auth: 'Authentication system',
173
+ api: 'API endpoints',
174
+ admin: 'Admin panel',
175
+ dashboard: 'Dashboard interface',
176
+ payment: 'Payment integration',
177
+ notifications: 'Notification system',
178
+ chat: 'Chat functionality',
179
+ search: 'Search feature',
180
+ analytics: 'Analytics tracking',
181
+ }
182
+
183
+ for (const entry of entries) {
184
+ if (entry.isDirectory()) {
185
+ const dirName = entry.name.toLowerCase()
186
+ if (featureDirs[dirName]) {
187
+ features.push(featureDirs[dirName])
188
+ }
189
+ }
190
+ }
191
+ } catch (error) {
192
+ }
193
+
194
+ return features
195
+ }
196
+
197
+ /**
198
+ * Detect project structure and organization
199
+ */
200
+ async detectProjectStructure() {
201
+ const structure = {
202
+ hasTests: false,
203
+ hasDocs: false,
204
+ hasCI: false,
205
+ fileCount: 0,
206
+ directories: [],
207
+ }
208
+
209
+ try {
210
+ const entries = await fs.readdir(this.projectPath, { withFileTypes: true })
211
+
212
+ for (const entry of entries) {
213
+ if (entry.isDirectory()) {
214
+ structure.directories.push(entry.name)
215
+
216
+ if (entry.name === 'test' || entry.name === 'tests' || entry.name === '__tests__') {
217
+ structure.hasTests = true
218
+ }
219
+ if (entry.name === 'docs' || entry.name === 'documentation') {
220
+ structure.hasDocs = true
221
+ }
222
+ }
223
+ }
224
+
225
+ const ciFiles = ['.github/workflows', '.gitlab-ci.yml', 'jenkins', '.circleci']
226
+ for (const ciFile of ciFiles) {
227
+ if (await this.fileExists(path.join(this.projectPath, ciFile))) {
228
+ structure.hasCI = true
229
+ break
230
+ }
231
+ }
232
+
233
+ structure.fileCount = await this.countFiles(this.projectPath)
234
+ } catch (error) {
235
+ }
236
+
237
+ return structure
238
+ }
239
+
240
+ /**
241
+ * Scan git history for insights
242
+ */
243
+ async scanGitHistory() {
244
+ const history = {
245
+ totalCommits: 0,
246
+ contributors: [],
247
+ firstCommit: null,
248
+ lastCommit: null,
249
+ hasGit: false,
250
+ }
251
+
252
+ try {
253
+ await exec('git rev-parse --git-dir', { cwd: this.projectPath })
254
+ history.hasGit = true
255
+
256
+ const { stdout: countOut } = await exec('git rev-list --count HEAD', { cwd: this.projectPath })
257
+ history.totalCommits = parseInt(countOut.trim()) || 0
258
+
259
+ const { stdout: contributorsOut } = await exec(
260
+ 'git log --format="%an" | sort -u',
261
+ { cwd: this.projectPath },
262
+ )
263
+ history.contributors = contributorsOut.trim().split('\n').filter(Boolean)
264
+
265
+ const { stdout: firstOut } = await exec(
266
+ 'git log --reverse --format="%ai" --max-count=1',
267
+ { cwd: this.projectPath },
268
+ )
269
+ history.firstCommit = firstOut.trim()
270
+
271
+ const { stdout: lastOut } = await exec(
272
+ 'git log --format="%ai" --max-count=1',
273
+ { cwd: this.projectPath },
274
+ )
275
+ history.lastCommit = lastOut.trim()
276
+ } catch (error) {
277
+ history.hasGit = false
278
+ }
279
+
280
+ return history
281
+ }
282
+
283
+ /**
284
+ * Detect technologies used in the project
285
+ */
286
+ async detectTechnologies() {
287
+ const technologies = []
288
+
289
+ try {
290
+ const packagePath = path.join(this.projectPath, 'package.json')
291
+ if (await this.fileExists(packagePath)) {
292
+ const content = await fs.readFile(packagePath, 'utf-8')
293
+ const pkg = JSON.parse(content)
294
+
295
+ technologies.push('Node.js')
296
+
297
+ const deps = { ...pkg.dependencies, ...pkg.devDependencies }
298
+
299
+ if (deps.typescript) technologies.push('TypeScript')
300
+ if (deps.react) technologies.push('React')
301
+ if (deps.next) technologies.push('Next.js')
302
+ if (deps.vue) technologies.push('Vue.js')
303
+ if (deps.express) technologies.push('Express')
304
+ if (deps.fastify) technologies.push('Fastify')
305
+ }
306
+
307
+ const entries = await fs.readdir(this.projectPath)
308
+
309
+ if (entries.some(f => f.endsWith('.py'))) technologies.push('Python')
310
+ if (entries.some(f => f.endsWith('.go'))) technologies.push('Go')
311
+ if (entries.some(f => f.endsWith('.rs'))) technologies.push('Rust')
312
+ if (entries.some(f => f.endsWith('.rb'))) technologies.push('Ruby')
313
+ if (entries.some(f => f.endsWith('.java'))) technologies.push('Java')
314
+ } catch (error) {
315
+ }
316
+
317
+ return [...new Set(technologies)]
318
+ }
319
+
320
+ /**
321
+ * Sync analysis results with .prjct/ files
322
+ */
323
+ async syncWithPrjctFiles(globalProjectPath) {
324
+ const syncResults = {
325
+ nextMdUpdated: false,
326
+ shippedMdUpdated: false,
327
+ tasksMarkedComplete: 0,
328
+ featuresAdded: 0,
329
+ }
330
+
331
+ try {
332
+ syncResults.tasksMarkedComplete = await this.updateNextMd(globalProjectPath)
333
+ if (syncResults.tasksMarkedComplete > 0) {
334
+ syncResults.nextMdUpdated = true
335
+ }
336
+
337
+ syncResults.featuresAdded = await this.updateShippedMd(globalProjectPath)
338
+ if (syncResults.featuresAdded > 0) {
339
+ syncResults.shippedMdUpdated = true
340
+ }
341
+
342
+ await this.createAnalysisReport(globalProjectPath)
343
+ } catch (error) {
344
+ console.error('[analyzer] Error syncing with .prjct files:', error.message)
345
+ }
346
+
347
+ return syncResults
348
+ }
349
+
350
+ /**
351
+ * Update next.md by marking completed tasks
352
+ */
353
+ async updateNextMd(globalProjectPath) {
354
+ let tasksMarkedComplete = 0
355
+
356
+ try {
357
+ const nextPath = path.join(globalProjectPath, 'core', 'next.md')
358
+
359
+ if (!(await this.fileExists(nextPath))) {
360
+ return 0
361
+ }
362
+
363
+ const content = await fs.readFile(nextPath, 'utf-8')
364
+ const lines = content.split('\n')
365
+ const implementedCommands = this.analysis.commands.map(c => c.toLowerCase())
366
+
367
+ let modified = false
368
+ const newLines = []
369
+
370
+ for (const line of lines) {
371
+ if (line.startsWith('- ') && !line.includes('✅')) {
372
+ const taskText = line.substring(2).toLowerCase()
373
+
374
+ const isImplemented = implementedCommands.some(cmd =>
375
+ taskText.includes(cmd) || taskText.includes(`/p:${cmd}`),
376
+ )
377
+
378
+ if (isImplemented) {
379
+ newLines.push(line.replace('- ', '- ✅ ') + ' _(auto-detected)_')
380
+ tasksMarkedComplete++
381
+ modified = true
382
+ } else {
383
+ newLines.push(line)
384
+ }
385
+ } else {
386
+ newLines.push(line)
387
+ }
388
+ }
389
+
390
+ if (modified) {
391
+ await fs.writeFile(nextPath, newLines.join('\n'), 'utf-8')
392
+ }
393
+ } catch (error) {
394
+ console.error('[analyzer] Error updating next.md:', error.message)
395
+ }
396
+
397
+ return tasksMarkedComplete
398
+ }
399
+
400
+ /**
401
+ * Update shipped.md with detected features
402
+ */
403
+ async updateShippedMd(globalProjectPath) {
404
+ let featuresAdded = 0
405
+
406
+ try {
407
+ const shippedPath = path.join(globalProjectPath, 'progress', 'shipped.md')
408
+
409
+ if (!(await this.fileExists(shippedPath))) {
410
+ return 0
411
+ }
412
+
413
+ let content = await fs.readFile(shippedPath, 'utf-8')
414
+
415
+ const now = new Date()
416
+ const week = this.getWeekNumber(now)
417
+ const year = now.getFullYear()
418
+ const weekHeader = `## Week ${week}, ${year}`
419
+
420
+ if (!content.includes(weekHeader)) {
421
+ content += `\n${weekHeader}\n`
422
+ }
423
+
424
+ for (const feature of this.analysis.features) {
425
+ if (!content.includes(feature)) {
426
+ const entry = `- ✅ **${feature}** _(auto-detected on ${now.toLocaleDateString()})_\n`
427
+ const insertIndex = content.indexOf(weekHeader) + weekHeader.length + 1
428
+ content = content.slice(0, insertIndex) + entry + content.slice(insertIndex)
429
+ featuresAdded++
430
+ }
431
+ }
432
+
433
+ if (featuresAdded > 0) {
434
+ await fs.writeFile(shippedPath, content, 'utf-8')
435
+ }
436
+ } catch (error) {
437
+ console.error('[analyzer] Error updating shipped.md:', error.message)
438
+ }
439
+
440
+ return featuresAdded
441
+ }
442
+
443
+ /**
444
+ * Create detailed analysis report
445
+ */
446
+ async createAnalysisReport(globalProjectPath) {
447
+ try {
448
+ const analysisDir = path.join(globalProjectPath, 'analysis')
449
+ await fs.mkdir(analysisDir, { recursive: true })
450
+
451
+ const reportPath = path.join(analysisDir, 'repo-summary.md')
452
+ const report = this.generateAnalysisReport()
453
+
454
+ await fs.writeFile(reportPath, report, 'utf-8')
455
+ } catch (error) {
456
+ console.error('[analyzer] Error creating analysis report:', error.message)
457
+ }
458
+ }
459
+
460
+ /**
461
+ * Generate formatted analysis report
462
+ */
463
+ generateAnalysisReport() {
464
+ const { commands, features, structure, gitHistory, technologies } = this.analysis
465
+
466
+ return `# Project Analysis Report
467
+
468
+ **Generated:** ${new Date().toLocaleString()}
469
+
470
+ ## 📊 Overview
471
+
472
+ - **Technologies:** ${technologies.join(', ') || 'Not detected'}
473
+ - **Commands Implemented:** ${commands.length}
474
+ - **Features Detected:** ${features.length}
475
+ - **Total Files:** ~${structure.fileCount}
476
+
477
+ ## 🛠️ Implemented Commands
478
+
479
+ ${commands.map(cmd => `- \`/p:${cmd}\``).join('\n') || '_(none detected)_'}
480
+
481
+ ## ✨ Completed Features
482
+
483
+ ${features.map(f => `- ${f}`).join('\n') || '_(none detected)_'}
484
+
485
+ ## 🏗️ Project Structure
486
+
487
+ - **Has Tests:** ${structure.hasTests ? '✅' : '❌'}
488
+ - **Has Documentation:** ${structure.hasDocs ? '✅' : '❌'}
489
+ - **Has CI/CD:** ${structure.hasCI ? '✅' : '❌'}
490
+ - **Directories:** ${structure.directories.join(', ')}
491
+
492
+ ## 📜 Git History
493
+
494
+ ${gitHistory.hasGit
495
+ ? `
496
+ - **Total Commits:** ${gitHistory.totalCommits}
497
+ - **Contributors:** ${gitHistory.contributors.join(', ')}
498
+ - **First Commit:** ${gitHistory.firstCommit}
499
+ - **Last Commit:** ${gitHistory.lastCommit}
500
+ `
501
+ : '_Not a git repository_'}
502
+
503
+ ## 💡 Recommendations
504
+
505
+ ${this.generateRecommendations()}
506
+
507
+ ---
508
+ _This report was auto-generated by prjct analyze_
509
+ `
510
+ }
511
+
512
+ /**
513
+ * Generate recommendations based on analysis
514
+ */
515
+ generateRecommendations() {
516
+ const recommendations = []
517
+ const { structure, gitHistory } = this.analysis
518
+
519
+ if (!structure.hasTests) {
520
+ recommendations.push('- Consider adding tests to improve code quality')
521
+ }
522
+
523
+ if (!structure.hasDocs) {
524
+ recommendations.push('- Add documentation to help onboard new contributors')
525
+ }
526
+
527
+ if (!structure.hasCI) {
528
+ recommendations.push('- Set up CI/CD for automated testing and deployment')
529
+ }
530
+
531
+ if (gitHistory.hasGit && gitHistory.totalCommits < 10) {
532
+ recommendations.push('- Early stage project - focus on core features first')
533
+ }
534
+
535
+ if (recommendations.length === 0) {
536
+ return '- Project is well-structured! Keep up the good work.'
537
+ }
538
+
539
+ return recommendations.join('\n')
540
+ }
541
+
542
+ /**
543
+ * Helper: Check if file exists
544
+ */
545
+ async fileExists(filePath) {
546
+ try {
547
+ await fs.access(filePath)
548
+ return true
549
+ } catch {
550
+ return false
551
+ }
552
+ }
553
+
554
+ /**
555
+ * Helper: Count files recursively (with limit for performance)
556
+ */
557
+ async countFiles(dirPath, maxDepth = 3, currentDepth = 0) {
558
+ if (currentDepth > maxDepth) return 0
559
+
560
+ let count = 0
561
+
562
+ try {
563
+ const entries = await fs.readdir(dirPath, { withFileTypes: true })
564
+
565
+ for (const entry of entries) {
566
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') {
567
+ continue
568
+ }
569
+
570
+ if (entry.isFile()) {
571
+ count++
572
+ } else if (entry.isDirectory()) {
573
+ count += await this.countFiles(
574
+ path.join(dirPath, entry.name),
575
+ maxDepth,
576
+ currentDepth + 1,
577
+ )
578
+ }
579
+ }
580
+ } catch {
581
+ }
582
+
583
+ return count
584
+ }
585
+
586
+ /**
587
+ * Helper: Get week number
588
+ */
589
+ getWeekNumber(date) {
590
+ const firstDayOfYear = new Date(date.getFullYear(), 0, 1)
591
+ const pastDaysOfYear = (date - firstDayOfYear) / 86400000
592
+ return Math.ceil((pastDaysOfYear + firstDayOfYear.getDay() + 1) / 7)
593
+ }
594
+ }
595
+
596
+ module.exports = new CodebaseAnalyzer()