prjct-cli 0.60.0 → 0.60.2

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,78 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.60.2] - 2026-02-05
4
+
5
+ ### Performance
6
+
7
+ - parallelize agent/skill loading with Promise.all (PRJ-110) (#101)
8
+
9
+
10
+ ## [0.60.2] - 2026-02-05
11
+
12
+ ### Performance
13
+
14
+ - **Parallel agent/skill loading (PRJ-110)**: Agent and skill loading now uses `Promise.all` for parallel I/O
15
+
16
+ ### Implementation Details
17
+
18
+ Refactored `loadAgents()` and `loadSkills()` in `core/agentic/orchestrator-executor.ts` to use `Promise.all` with map instead of sequential for loops. Also parallelized `loadAllAgents()` in `core/domain/agent-loader.ts`. Pattern: collect items → map to async promises → Promise.all → filter nulls with type guard.
19
+
20
+ ### Learnings
21
+
22
+ - Use `Promise.all(items.map(async (item) => ...))` for parallel async operations
23
+ - Return null for failed items, then filter - can't push to array in parallel
24
+ - Collect unique items first (deduplication), then parallelize reads
25
+
26
+ ### Test Plan
27
+
28
+ #### For QA
29
+ 1. Run `prjct sync --yes` - verify agents load successfully
30
+ 2. Run `p. task "test"` - verify orchestrator works
31
+ 3. Check no errors in agent/skill loading output
32
+
33
+ #### For Users
34
+ - Agent and skill loading is now faster (parallel I/O)
35
+ - No changes needed - improvement is automatic
36
+
37
+
38
+ ## [0.60.1] - 2026-02-05
39
+
40
+ ### Bug Fixes
41
+
42
+ - improve sync output with summary-first format (PRJ-100) (#100)
43
+
44
+
45
+ ## [0.60.1] - 2026-02-05
46
+
47
+ ### Improved
48
+
49
+ - **Sync output UX (PRJ-100)**: Summary-first format with key metrics prominent, ~50% less output
50
+
51
+ ### Implementation Details
52
+
53
+ Refactored `showSyncResult()` in `core/commands/analysis.ts` to show success line with timing immediately, followed by single-line metrics (files → context | agents | reduction). Removed redundant bottom summary section. Fixed compressionRate calculation (was decimal, now percentage). Added conditional display for low-value metrics (only show reduction if >10%). Fixed pluralization ("1 skill" not "1 skills").
54
+
55
+ ### Learnings
56
+
57
+ - `syncMetrics.compressionRate` is a decimal (0-1), not percentage - multiply by 100
58
+ - Summary-first output pattern improves scannability
59
+ - Conditional metric display reduces noise for low-value data
60
+
61
+ ### Test Plan
62
+
63
+ #### For QA
64
+ 1. Run `prjct sync --yes` on any project
65
+ 2. Verify output shows success line first with timing: `✅ Synced {project} ({time}s)`
66
+ 3. Verify single-line metrics: `{files} files → {context} context | {agents} agents`
67
+ 4. Verify compression rate only shows if > 10%
68
+ 5. Verify pluralization is correct ("1 skill" not "1 skills")
69
+
70
+ #### For Users
71
+ - Sync output is now more scannable - key metrics appear first instead of buried at bottom
72
+ - Run `p. sync` as usual - new format is automatic
73
+ - No breaking changes
74
+
75
+
3
76
  ## [0.60.0] - 2026-02-05
4
77
 
5
78
  ### Features
@@ -341,13 +341,15 @@ export class OrchestratorExecutor {
341
341
  *
342
342
  * Reads agent markdown files from {globalPath}/agents/
343
343
  * and extracts their content and skills from frontmatter.
344
+ *
345
+ * Uses parallel file reads for performance (PRJ-110).
344
346
  */
345
347
  async loadAgents(domains: string[], projectId: string): Promise<LoadedAgent[]> {
346
348
  const globalPath = pathManager.getGlobalProjectPath(projectId)
347
349
  const agentsDir = path.join(globalPath, 'agents')
348
- const agents: LoadedAgent[] = []
349
350
 
350
- for (const domain of domains) {
351
+ // Load all domain agents in parallel
352
+ const agentPromises = domains.map(async (domain): Promise<LoadedAgent | null> => {
351
353
  // Try exact match first, then variations
352
354
  const possibleNames = [`${domain}.md`, `${domain}-agent.md`, `prjct-${domain}.md`]
353
355
 
@@ -357,21 +359,22 @@ export class OrchestratorExecutor {
357
359
  const content = await fs.readFile(filePath, 'utf-8')
358
360
  const { frontmatter, body } = this.parseAgentFile(content)
359
361
 
360
- agents.push({
362
+ return {
361
363
  name: fileName.replace('.md', ''),
362
364
  domain,
363
365
  content: body,
364
366
  skills: frontmatter.skills || [],
365
367
  filePath,
366
- })
367
-
368
- // Found one, no need to try other variations
369
- break
370
- } catch {}
368
+ }
369
+ } catch {
370
+ // Try next variation
371
+ }
371
372
  }
372
- }
373
+ return null
374
+ })
373
375
 
374
- return agents
376
+ const results = await Promise.all(agentPromises)
377
+ return results.filter((agent): agent is LoadedAgent => agent !== null)
375
378
  }
376
379
 
377
380
  /**
@@ -400,51 +403,46 @@ export class OrchestratorExecutor {
400
403
  * Load skills from agent frontmatter
401
404
  *
402
405
  * Skills are stored in ~/.claude/skills/{name}.md
406
+ *
407
+ * Uses parallel file reads for performance (PRJ-110).
403
408
  */
404
409
  async loadSkills(agents: LoadedAgent[]): Promise<LoadedSkill[]> {
405
410
  const skillsDir = path.join(os.homedir(), '.claude', 'skills')
406
- const skills: LoadedSkill[] = []
407
- const loadedSkillNames = new Set<string>()
408
411
 
412
+ // Collect unique skill names from all agents
413
+ const uniqueSkillNames = new Set<string>()
409
414
  for (const agent of agents) {
410
415
  for (const skillName of agent.skills) {
411
- // Skip if already loaded
412
- if (loadedSkillNames.has(skillName)) continue
416
+ uniqueSkillNames.add(skillName)
417
+ }
418
+ }
413
419
 
420
+ // Load all skills in parallel
421
+ const skillPromises = Array.from(uniqueSkillNames).map(
422
+ async (skillName): Promise<LoadedSkill | null> => {
414
423
  // Check both patterns: flat file and subdirectory (ecosystem standard)
415
424
  const flatPath = path.join(skillsDir, `${skillName}.md`)
416
425
  const subdirPath = path.join(skillsDir, skillName, 'SKILL.md')
417
426
 
418
- let content: string | null = null
419
- let resolvedPath = flatPath
420
-
421
427
  // Prefer subdirectory format (ecosystem standard)
422
428
  try {
423
- content = await fs.readFile(subdirPath, 'utf-8')
424
- resolvedPath = subdirPath
429
+ const content = await fs.readFile(subdirPath, 'utf-8')
430
+ return { name: skillName, content, filePath: subdirPath }
425
431
  } catch {
426
432
  // Fall back to flat file
427
433
  try {
428
- content = await fs.readFile(flatPath, 'utf-8')
429
- resolvedPath = flatPath
434
+ const content = await fs.readFile(flatPath, 'utf-8')
435
+ return { name: skillName, content, filePath: flatPath }
430
436
  } catch {
431
437
  // Skill not found - not an error, just skip
432
- console.warn(`Skill not found: ${skillName}`)
438
+ return null
433
439
  }
434
440
  }
435
-
436
- if (content) {
437
- skills.push({
438
- name: skillName,
439
- content,
440
- filePath: resolvedPath,
441
- })
442
- loadedSkillNames.add(skillName)
443
- }
444
441
  }
445
- }
442
+ )
446
443
 
447
- return skills
444
+ const results = await Promise.all(skillPromises)
445
+ return results.filter((skill): skill is LoadedSkill => skill !== null)
448
446
  }
449
447
 
450
448
  /**
@@ -455,89 +455,97 @@ export class AnalysisCommands extends PrjctCommandsBase {
455
455
 
456
456
  /**
457
457
  * Display sync results (extracted to avoid duplication)
458
+ *
459
+ * UX Design (PRJ-100):
460
+ * - Summary first: success + key metrics on first lines
461
+ * - Scannable: single-line metrics, minimal vertical space
462
+ * - Changes focused: show what changed, not everything that exists
463
+ * - Next steps prominent: clear call to action at bottom
458
464
  */
459
465
  private async showSyncResult(
460
466
  result: Awaited<ReturnType<typeof syncService.sync>>,
461
467
  startTime: number
462
468
  ): Promise<CommandResult> {
463
- // Update global config
464
- const globalConfigResult = await commandInstaller.installGlobalConfig()
465
- if (globalConfigResult.success) {
466
- console.log(`📝 Updated ${pathManager.getDisplayPath(globalConfigResult.path!)}`)
469
+ const elapsed = Date.now() - startTime
470
+ const contextFilesCount =
471
+ result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
472
+ const agentCount = result.agents.length
473
+ const domainAgentCount = result.agents.filter((a) => a.type === 'domain').length
474
+
475
+ // Update global config (silent - don't clutter output)
476
+ await commandInstaller.installGlobalConfig()
477
+
478
+ // ═══════════════════════════════════════════════════════════════════════
479
+ // SUCCESS LINE - Immediate confirmation with timing
480
+ // ═══════════════════════════════════════════════════════════════════════
481
+ console.log(`✅ Synced ${result.stats.name || 'project'} (${(elapsed / 1000).toFixed(1)}s)\n`)
482
+
483
+ // ═══════════════════════════════════════════════════════════════════════
484
+ // KEY METRICS - Single scannable line
485
+ // ═══════════════════════════════════════════════════════════════════════
486
+ // Only show compression rate if meaningful (> 10%)
487
+ const compressionPct = result.syncMetrics?.compressionRate
488
+ ? Math.round(result.syncMetrics.compressionRate * 100)
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
+ const framework = result.stats.frameworks.length > 0 ? ` (${result.stats.frameworks[0]})` : ''
501
+ console.log(`Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}\n`)
502
+
503
+ // ═══════════════════════════════════════════════════════════════════════
504
+ // CHANGES SECTION - What was generated/updated
505
+ // ═══════════════════════════════════════════════════════════════════════
506
+ console.log('Generated:')
507
+
508
+ // Context files (condensed)
509
+ if (result.contextFiles.length > 0) {
510
+ console.log(` ✓ ${result.contextFiles.length} context files`)
467
511
  }
468
512
 
469
- // Format output
470
- console.log(`🔄 Project synced to prjct v${result.cliVersion}\n`)
471
-
472
- console.log('📊 Project Stats')
473
- console.log(`├── Files: ~${result.stats.fileCount}`)
474
- console.log(`├── Commits: ${result.git.commits}`)
475
- console.log(`├── Version: ${result.stats.version}`)
476
- console.log(`└── Stack: ${result.stats.ecosystem}\n`)
477
-
478
- console.log('🌿 Git Status')
479
- console.log(`├── Branch: ${result.git.branch}`)
480
- console.log(`├── Uncommitted: ${result.git.hasChanges ? 'Yes' : 'Clean'}`)
481
- console.log(`└── Recent: ${result.git.weeklyCommits} commits this week\n`)
482
-
483
- console.log('📁 Context Updated')
484
- for (const file of result.contextFiles) {
485
- console.log(`├── ${file}`)
513
+ // AI tools
514
+ const successTools = result.aiTools?.filter((t) => t.success) || []
515
+ if (successTools.length > 0) {
516
+ const toolNames = successTools.map((t) => t.toolId).join(', ')
517
+ console.log(` ✓ AI tools: ${toolNames}`)
486
518
  }
487
- console.log('')
488
519
 
489
- // Show AI Tools generated (multi-agent output)
490
- if (result.aiTools && result.aiTools.length > 0) {
491
- const successTools = result.aiTools.filter((t) => t.success)
492
- console.log(`🤖 AI Tools Context (${successTools.length})`)
493
- for (const tool of result.aiTools) {
494
- const status = tool.success ? '✓' : '✗'
495
- console.log(`├── ${status} ${tool.outputFile} (${tool.toolId})`)
496
- }
497
- console.log('')
520
+ // Agents (show count with breakdown)
521
+ if (agentCount > 0) {
522
+ const agentSummary =
523
+ domainAgentCount > 0
524
+ ? `${agentCount} agents (${domainAgentCount} domain)`
525
+ : `${agentCount} agents`
526
+ console.log(` ✓ ${agentSummary}`)
498
527
  }
499
528
 
500
- const workflowAgents = result.agents.filter((a) => a.type === 'workflow').map((a) => a.name)
501
- const domainAgents = result.agents.filter((a) => a.type === 'domain').map((a) => a.name)
502
-
503
- console.log(`🤖 Agents Regenerated (${result.agents.length})`)
504
- console.log(`├── Workflow: ${workflowAgents.join(', ')}`)
505
- console.log(`└── Domain: ${domainAgents.join(', ') || 'none'}\n`)
506
-
529
+ // Skills
507
530
  if (result.skills.length > 0) {
508
- console.log('📦 Skills Configured')
509
- for (const skill of result.skills) {
510
- console.log(`├── ${skill.agent}.md → ${skill.skill}`)
511
- }
512
- console.log('')
531
+ const skillWord = result.skills.length === 1 ? 'skill' : 'skills'
532
+ console.log(` ✓ ${result.skills.length} ${skillWord}`)
513
533
  }
514
534
 
535
+ console.log('')
536
+
537
+ // ═══════════════════════════════════════════════════════════════════════
538
+ // STATUS INDICATOR - Repository state
539
+ // ═══════════════════════════════════════════════════════════════════════
515
540
  if (result.git.hasChanges) {
516
- console.log('⚠️ You have uncommitted changes\n')
517
- } else {
518
- console.log('✨ Repository is clean!\n')
541
+ console.log('⚠️ Uncommitted changes detected\n')
519
542
  }
520
543
 
544
+ // ═══════════════════════════════════════════════════════════════════════
545
+ // NEXT STEPS - Clear call to action
546
+ // ═══════════════════════════════════════════════════════════════════════
521
547
  showNextSteps('sync')
522
548
 
523
- // Summary metrics
524
- const elapsed = Date.now() - startTime
525
- const contextFilesCount =
526
- result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0)
527
- const agentCount = result.agents.length
528
-
529
- console.log('─'.repeat(45))
530
- console.log('📊 Sync Summary')
531
- console.log(
532
- ` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(', ') || 'no frameworks'})`
533
- )
534
- console.log(` Files: ${result.stats.fileCount} analyzed → ${contextFilesCount} context files`)
535
- console.log(
536
- ` Agents: ${agentCount} (${result.agents.filter((a) => a.type === 'domain').length} domain)`
537
- )
538
- console.log(` Time: ${(elapsed / 1000).toFixed(1)}s`)
539
- console.log('')
540
-
541
549
  return {
542
550
  success: true,
543
551
  data: result,
@@ -94,22 +94,22 @@ class AgentLoader {
94
94
 
95
95
  /**
96
96
  * Load all agents for the project
97
+ *
98
+ * Uses parallel file reads for performance (PRJ-110).
97
99
  */
98
100
  async loadAllAgents(): Promise<Agent[]> {
99
101
  try {
100
102
  const files = await fs.readdir(this.agentsDir)
101
103
  const agentFiles = files.filter((f) => f.endsWith('.md') && !f.startsWith('.'))
102
104
 
103
- const agents: Agent[] = []
104
- for (const file of agentFiles) {
105
+ // Load all agents in parallel
106
+ const agentPromises = agentFiles.map((file) => {
105
107
  const agentName = file.replace('.md', '')
106
- const agent = await this.loadAgent(agentName)
107
- if (agent) {
108
- agents.push(agent)
109
- }
110
- }
108
+ return this.loadAgent(agentName)
109
+ })
111
110
 
112
- return agents
111
+ const results = await Promise.all(agentPromises)
112
+ return results.filter((agent): agent is Agent => agent !== null)
113
113
  } catch (error) {
114
114
  if (isNotFoundError(error)) {
115
115
  return [] // Agents directory doesn't exist yet
@@ -11788,31 +11788,33 @@ var init_orchestrator_executor = __esm({
11788
11788
  *
11789
11789
  * Reads agent markdown files from {globalPath}/agents/
11790
11790
  * and extracts their content and skills from frontmatter.
11791
+ *
11792
+ * Uses parallel file reads for performance (PRJ-110).
11791
11793
  */
11792
11794
  async loadAgents(domains, projectId) {
11793
11795
  const globalPath = path_manager_default.getGlobalProjectPath(projectId);
11794
11796
  const agentsDir = path23.join(globalPath, "agents");
11795
- const agents = [];
11796
- for (const domain of domains) {
11797
+ const agentPromises = domains.map(async (domain) => {
11797
11798
  const possibleNames = [`${domain}.md`, `${domain}-agent.md`, `prjct-${domain}.md`];
11798
11799
  for (const fileName of possibleNames) {
11799
11800
  const filePath = path23.join(agentsDir, fileName);
11800
11801
  try {
11801
11802
  const content = await fs24.readFile(filePath, "utf-8");
11802
11803
  const { frontmatter, body } = this.parseAgentFile(content);
11803
- agents.push({
11804
+ return {
11804
11805
  name: fileName.replace(".md", ""),
11805
11806
  domain,
11806
11807
  content: body,
11807
11808
  skills: frontmatter.skills || [],
11808
11809
  filePath
11809
- });
11810
- break;
11810
+ };
11811
11811
  } catch {
11812
11812
  }
11813
11813
  }
11814
- }
11815
- return agents;
11814
+ return null;
11815
+ });
11816
+ const results = await Promise.all(agentPromises);
11817
+ return results.filter((agent) => agent !== null);
11816
11818
  }
11817
11819
  /**
11818
11820
  * Parse agent markdown file to extract frontmatter and body
@@ -11832,40 +11834,36 @@ var init_orchestrator_executor = __esm({
11832
11834
  * Load skills from agent frontmatter
11833
11835
  *
11834
11836
  * Skills are stored in ~/.claude/skills/{name}.md
11837
+ *
11838
+ * Uses parallel file reads for performance (PRJ-110).
11835
11839
  */
11836
11840
  async loadSkills(agents) {
11837
11841
  const skillsDir = path23.join(os8.homedir(), ".claude", "skills");
11838
- const skills = [];
11839
- const loadedSkillNames = /* @__PURE__ */ new Set();
11842
+ const uniqueSkillNames = /* @__PURE__ */ new Set();
11840
11843
  for (const agent of agents) {
11841
11844
  for (const skillName of agent.skills) {
11842
- if (loadedSkillNames.has(skillName)) continue;
11845
+ uniqueSkillNames.add(skillName);
11846
+ }
11847
+ }
11848
+ const skillPromises = Array.from(uniqueSkillNames).map(
11849
+ async (skillName) => {
11843
11850
  const flatPath = path23.join(skillsDir, `${skillName}.md`);
11844
11851
  const subdirPath = path23.join(skillsDir, skillName, "SKILL.md");
11845
- let content = null;
11846
- let resolvedPath = flatPath;
11847
11852
  try {
11848
- content = await fs24.readFile(subdirPath, "utf-8");
11849
- resolvedPath = subdirPath;
11853
+ const content = await fs24.readFile(subdirPath, "utf-8");
11854
+ return { name: skillName, content, filePath: subdirPath };
11850
11855
  } catch {
11851
11856
  try {
11852
- content = await fs24.readFile(flatPath, "utf-8");
11853
- resolvedPath = flatPath;
11857
+ const content = await fs24.readFile(flatPath, "utf-8");
11858
+ return { name: skillName, content, filePath: flatPath };
11854
11859
  } catch {
11855
- console.warn(`Skill not found: ${skillName}`);
11860
+ return null;
11856
11861
  }
11857
11862
  }
11858
- if (content) {
11859
- skills.push({
11860
- name: skillName,
11861
- content,
11862
- filePath: resolvedPath
11863
- });
11864
- loadedSkillNames.add(skillName);
11865
- }
11866
11863
  }
11867
- }
11868
- return skills;
11864
+ );
11865
+ const results = await Promise.all(skillPromises);
11866
+ return results.filter((skill) => skill !== null);
11869
11867
  }
11870
11868
  /**
11871
11869
  * Determine if task should be fragmented into subtasks
@@ -18093,72 +18091,53 @@ ${formatFullDiff(diff)}`);
18093
18091
  }
18094
18092
  /**
18095
18093
  * Display sync results (extracted to avoid duplication)
18094
+ *
18095
+ * UX Design (PRJ-100):
18096
+ * - Summary first: success + key metrics on first lines
18097
+ * - Scannable: single-line metrics, minimal vertical space
18098
+ * - Changes focused: show what changed, not everything that exists
18099
+ * - Next steps prominent: clear call to action at bottom
18096
18100
  */
18097
18101
  async showSyncResult(result, startTime) {
18098
- const globalConfigResult = await command_installer_default.installGlobalConfig();
18099
- if (globalConfigResult.success) {
18100
- console.log(`\u{1F4DD} Updated ${path_manager_default.getDisplayPath(globalConfigResult.path)}`);
18101
- }
18102
- console.log(`\u{1F504} Project synced to prjct v${result.cliVersion}
18103
- `);
18104
- console.log("\u{1F4CA} Project Stats");
18105
- console.log(`\u251C\u2500\u2500 Files: ~${result.stats.fileCount}`);
18106
- console.log(`\u251C\u2500\u2500 Commits: ${result.git.commits}`);
18107
- console.log(`\u251C\u2500\u2500 Version: ${result.stats.version}`);
18108
- console.log(`\u2514\u2500\u2500 Stack: ${result.stats.ecosystem}
18102
+ const elapsed = Date.now() - startTime;
18103
+ const contextFilesCount = result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0);
18104
+ const agentCount = result.agents.length;
18105
+ const domainAgentCount = result.agents.filter((a) => a.type === "domain").length;
18106
+ await command_installer_default.installGlobalConfig();
18107
+ console.log(`\u2705 Synced ${result.stats.name || "project"} (${(elapsed / 1e3).toFixed(1)}s)
18109
18108
  `);
18110
- console.log("\u{1F33F} Git Status");
18111
- console.log(`\u251C\u2500\u2500 Branch: ${result.git.branch}`);
18112
- console.log(`\u251C\u2500\u2500 Uncommitted: ${result.git.hasChanges ? "Yes" : "Clean"}`);
18113
- console.log(`\u2514\u2500\u2500 Recent: ${result.git.weeklyCommits} commits this week
18109
+ const compressionPct = result.syncMetrics?.compressionRate ? Math.round(result.syncMetrics.compressionRate * 100) : 0;
18110
+ const metricsLine = [
18111
+ `${result.stats.fileCount} files \u2192 ${contextFilesCount} context`,
18112
+ `${agentCount} agents`,
18113
+ compressionPct > 10 ? `${compressionPct}% reduction` : null
18114
+ ].filter(Boolean).join(" | ");
18115
+ console.log(metricsLine);
18116
+ const framework = result.stats.frameworks.length > 0 ? ` (${result.stats.frameworks[0]})` : "";
18117
+ console.log(`Stack: ${result.stats.ecosystem}${framework} | Branch: ${result.git.branch}
18114
18118
  `);
18115
- console.log("\u{1F4C1} Context Updated");
18116
- for (const file of result.contextFiles) {
18117
- console.log(`\u251C\u2500\u2500 ${file}`);
18119
+ console.log("Generated:");
18120
+ if (result.contextFiles.length > 0) {
18121
+ console.log(` \u2713 ${result.contextFiles.length} context files`);
18118
18122
  }
18119
- console.log("");
18120
- if (result.aiTools && result.aiTools.length > 0) {
18121
- const successTools = result.aiTools.filter((t) => t.success);
18122
- console.log(`\u{1F916} AI Tools Context (${successTools.length})`);
18123
- for (const tool of result.aiTools) {
18124
- const status = tool.success ? "\u2713" : "\u2717";
18125
- console.log(`\u251C\u2500\u2500 ${status} ${tool.outputFile} (${tool.toolId})`);
18126
- }
18127
- console.log("");
18123
+ const successTools = result.aiTools?.filter((t) => t.success) || [];
18124
+ if (successTools.length > 0) {
18125
+ const toolNames = successTools.map((t) => t.toolId).join(", ");
18126
+ console.log(` \u2713 AI tools: ${toolNames}`);
18127
+ }
18128
+ if (agentCount > 0) {
18129
+ const agentSummary = domainAgentCount > 0 ? `${agentCount} agents (${domainAgentCount} domain)` : `${agentCount} agents`;
18130
+ console.log(` \u2713 ${agentSummary}`);
18128
18131
  }
18129
- const workflowAgents = result.agents.filter((a) => a.type === "workflow").map((a) => a.name);
18130
- const domainAgents = result.agents.filter((a) => a.type === "domain").map((a) => a.name);
18131
- console.log(`\u{1F916} Agents Regenerated (${result.agents.length})`);
18132
- console.log(`\u251C\u2500\u2500 Workflow: ${workflowAgents.join(", ")}`);
18133
- console.log(`\u2514\u2500\u2500 Domain: ${domainAgents.join(", ") || "none"}
18134
- `);
18135
18132
  if (result.skills.length > 0) {
18136
- console.log("\u{1F4E6} Skills Configured");
18137
- for (const skill of result.skills) {
18138
- console.log(`\u251C\u2500\u2500 ${skill.agent}.md \u2192 ${skill.skill}`);
18139
- }
18140
- console.log("");
18133
+ const skillWord = result.skills.length === 1 ? "skill" : "skills";
18134
+ console.log(` \u2713 ${result.skills.length} ${skillWord}`);
18141
18135
  }
18136
+ console.log("");
18142
18137
  if (result.git.hasChanges) {
18143
- console.log("\u26A0\uFE0F You have uncommitted changes\n");
18144
- } else {
18145
- console.log("\u2728 Repository is clean!\n");
18138
+ console.log("\u26A0\uFE0F Uncommitted changes detected\n");
18146
18139
  }
18147
18140
  showNextSteps("sync");
18148
- const elapsed = Date.now() - startTime;
18149
- const contextFilesCount = result.contextFiles.length + (result.aiTools?.filter((t) => t.success).length || 0);
18150
- const agentCount = result.agents.length;
18151
- console.log("\u2500".repeat(45));
18152
- console.log("\u{1F4CA} Sync Summary");
18153
- console.log(
18154
- ` Stack: ${result.stats.ecosystem} (${result.stats.frameworks.join(", ") || "no frameworks"})`
18155
- );
18156
- console.log(` Files: ${result.stats.fileCount} analyzed \u2192 ${contextFilesCount} context files`);
18157
- console.log(
18158
- ` Agents: ${agentCount} (${result.agents.filter((a) => a.type === "domain").length} domain)`
18159
- );
18160
- console.log(` Time: ${(elapsed / 1e3).toFixed(1)}s`);
18161
- console.log("");
18162
18141
  return {
18163
18142
  success: true,
18164
18143
  data: result,
@@ -26431,7 +26410,7 @@ var require_package = __commonJS({
26431
26410
  "package.json"(exports, module) {
26432
26411
  module.exports = {
26433
26412
  name: "prjct-cli",
26434
- version: "0.60.0",
26413
+ version: "0.60.2",
26435
26414
  description: "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
26436
26415
  main: "core/index.ts",
26437
26416
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "prjct-cli",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "Context layer for AI agents. Project context for Claude Code, Gemini CLI, and more.",
5
5
  "main": "core/index.ts",
6
6
  "bin": {