prjct-cli 0.44.1 → 0.45.3

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 (207) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/bin/prjct.ts +131 -10
  3. package/core/__tests__/agentic/memory-system.test.ts +39 -26
  4. package/core/__tests__/agentic/plan-mode.test.ts +64 -46
  5. package/core/__tests__/agentic/prompt-builder.test.ts +14 -14
  6. package/core/__tests__/services/project-index.test.ts +353 -0
  7. package/core/__tests__/types/fs.test.ts +3 -3
  8. package/core/__tests__/utils/date-helper.test.ts +10 -10
  9. package/core/__tests__/utils/output.test.ts +9 -6
  10. package/core/__tests__/utils/project-commands.test.ts +5 -6
  11. package/core/agentic/agent-router.ts +9 -10
  12. package/core/agentic/chain-of-thought.ts +16 -4
  13. package/core/agentic/command-executor.ts +66 -40
  14. package/core/agentic/context-builder.ts +8 -5
  15. package/core/agentic/ground-truth.ts +15 -9
  16. package/core/agentic/index.ts +145 -152
  17. package/core/agentic/loop-detector.ts +40 -11
  18. package/core/agentic/memory-system.ts +98 -35
  19. package/core/agentic/orchestrator-executor.ts +135 -71
  20. package/core/agentic/plan-mode.ts +46 -16
  21. package/core/agentic/prompt-builder.ts +108 -42
  22. package/core/agentic/services.ts +10 -9
  23. package/core/agentic/skill-loader.ts +9 -15
  24. package/core/agentic/smart-context.ts +129 -79
  25. package/core/agentic/template-executor.ts +13 -12
  26. package/core/agentic/template-loader.ts +7 -4
  27. package/core/agentic/tool-registry.ts +16 -13
  28. package/core/agents/index.ts +1 -1
  29. package/core/agents/performance.ts +10 -27
  30. package/core/ai-tools/formatters.ts +8 -6
  31. package/core/ai-tools/generator.ts +4 -4
  32. package/core/ai-tools/index.ts +1 -1
  33. package/core/ai-tools/registry.ts +21 -11
  34. package/core/bus/bus.ts +23 -16
  35. package/core/bus/index.ts +2 -2
  36. package/core/cli/linear.ts +3 -5
  37. package/core/cli/start.ts +28 -25
  38. package/core/commands/analysis.ts +287 -29
  39. package/core/commands/analytics.ts +52 -44
  40. package/core/commands/base.ts +15 -13
  41. package/core/commands/cleanup.ts +6 -13
  42. package/core/commands/command-data.ts +49 -8
  43. package/core/commands/commands.ts +60 -23
  44. package/core/commands/context.ts +4 -4
  45. package/core/commands/design.ts +3 -10
  46. package/core/commands/index.ts +5 -8
  47. package/core/commands/maintenance.ts +7 -4
  48. package/core/commands/planning.ts +179 -56
  49. package/core/commands/register.ts +14 -9
  50. package/core/commands/registry.ts +15 -14
  51. package/core/commands/setup.ts +26 -14
  52. package/core/commands/shipping.ts +11 -16
  53. package/core/commands/snapshots.ts +16 -32
  54. package/core/commands/uninstall.ts +541 -0
  55. package/core/commands/workflow.ts +24 -28
  56. package/core/constants/index.ts +10 -22
  57. package/core/context/generator.ts +82 -33
  58. package/core/context-tools/files-tool.ts +583 -0
  59. package/core/context-tools/imports-tool.ts +403 -0
  60. package/core/context-tools/index.ts +433 -0
  61. package/core/context-tools/recent-tool.ts +307 -0
  62. package/core/context-tools/signatures-tool.ts +501 -0
  63. package/core/context-tools/summary-tool.ts +307 -0
  64. package/core/context-tools/token-counter.ts +284 -0
  65. package/core/context-tools/types.ts +253 -0
  66. package/core/domain/agent-generator.ts +7 -5
  67. package/core/domain/agent-loader.ts +2 -2
  68. package/core/domain/analyzer.ts +19 -16
  69. package/core/domain/architecture-generator.ts +6 -3
  70. package/core/domain/context-estimator.ts +3 -4
  71. package/core/domain/snapshot-manager.ts +25 -22
  72. package/core/domain/task-stack.ts +24 -14
  73. package/core/errors.ts +1 -1
  74. package/core/events/events.ts +2 -4
  75. package/core/events/index.ts +1 -2
  76. package/core/index.ts +28 -12
  77. package/core/infrastructure/agent-detector.ts +3 -3
  78. package/core/infrastructure/ai-provider.ts +23 -20
  79. package/core/infrastructure/author-detector.ts +16 -10
  80. package/core/infrastructure/capability-installer.ts +2 -2
  81. package/core/infrastructure/claude-agent.ts +6 -6
  82. package/core/infrastructure/command-installer.ts +22 -17
  83. package/core/infrastructure/config-manager.ts +18 -14
  84. package/core/infrastructure/editors-config.ts +8 -4
  85. package/core/infrastructure/path-manager.ts +8 -6
  86. package/core/infrastructure/permission-manager.ts +20 -17
  87. package/core/infrastructure/setup.ts +42 -38
  88. package/core/infrastructure/update-checker.ts +5 -5
  89. package/core/integrations/issue-tracker/enricher.ts +8 -19
  90. package/core/integrations/issue-tracker/index.ts +2 -2
  91. package/core/integrations/issue-tracker/manager.ts +15 -15
  92. package/core/integrations/issue-tracker/types.ts +5 -22
  93. package/core/integrations/jira/client.ts +67 -59
  94. package/core/integrations/jira/index.ts +11 -14
  95. package/core/integrations/jira/mcp-adapter.ts +5 -10
  96. package/core/integrations/jira/service.ts +10 -10
  97. package/core/integrations/linear/client.ts +27 -18
  98. package/core/integrations/linear/index.ts +9 -12
  99. package/core/integrations/linear/service.ts +11 -11
  100. package/core/integrations/linear/sync.ts +8 -8
  101. package/core/outcomes/analyzer.ts +5 -18
  102. package/core/outcomes/index.ts +2 -2
  103. package/core/outcomes/recorder.ts +3 -3
  104. package/core/plugin/builtin/webhook.ts +19 -15
  105. package/core/plugin/hooks.ts +29 -21
  106. package/core/plugin/index.ts +7 -7
  107. package/core/plugin/loader.ts +19 -19
  108. package/core/plugin/registry.ts +12 -23
  109. package/core/schemas/agents.ts +1 -1
  110. package/core/schemas/analysis.ts +1 -1
  111. package/core/schemas/enriched-task.ts +62 -49
  112. package/core/schemas/ideas.ts +13 -13
  113. package/core/schemas/index.ts +17 -27
  114. package/core/schemas/issues.ts +40 -25
  115. package/core/schemas/metrics.ts +143 -0
  116. package/core/schemas/outcomes.ts +70 -62
  117. package/core/schemas/permissions.ts +15 -12
  118. package/core/schemas/prd.ts +27 -14
  119. package/core/schemas/project.ts +3 -3
  120. package/core/schemas/roadmap.ts +47 -34
  121. package/core/schemas/schemas.ts +3 -4
  122. package/core/schemas/shipped.ts +3 -3
  123. package/core/schemas/state.ts +43 -29
  124. package/core/server/index.ts +5 -6
  125. package/core/server/routes-extended.ts +68 -72
  126. package/core/server/routes.ts +3 -3
  127. package/core/server/server.ts +31 -26
  128. package/core/services/agent-generator.ts +237 -0
  129. package/core/services/agent-service.ts +2 -2
  130. package/core/services/breakdown-service.ts +2 -4
  131. package/core/services/context-generator.ts +299 -0
  132. package/core/services/context-selector.ts +420 -0
  133. package/core/services/doctor-service.ts +426 -0
  134. package/core/services/file-categorizer.ts +448 -0
  135. package/core/services/file-scorer.ts +270 -0
  136. package/core/services/git-analyzer.ts +267 -0
  137. package/core/services/index.ts +27 -10
  138. package/core/services/memory-service.ts +3 -4
  139. package/core/services/project-index.ts +911 -0
  140. package/core/services/project-service.ts +4 -4
  141. package/core/services/skill-installer.ts +14 -17
  142. package/core/services/skill-lock.ts +3 -3
  143. package/core/services/skill-service.ts +12 -6
  144. package/core/services/stack-detector.ts +245 -0
  145. package/core/services/sync-service.ts +170 -329
  146. package/core/services/watch-service.ts +294 -0
  147. package/core/session/compaction.ts +23 -31
  148. package/core/session/index.ts +11 -5
  149. package/core/session/log-migration.ts +3 -3
  150. package/core/session/metrics.ts +19 -14
  151. package/core/session/session-log-manager.ts +12 -17
  152. package/core/session/task-session-manager.ts +25 -25
  153. package/core/session/utils.ts +1 -1
  154. package/core/storage/ideas-storage.ts +41 -57
  155. package/core/storage/index-storage.ts +514 -0
  156. package/core/storage/index.ts +41 -13
  157. package/core/storage/metrics-storage.ts +320 -0
  158. package/core/storage/queue-storage.ts +35 -45
  159. package/core/storage/shipped-storage.ts +17 -20
  160. package/core/storage/state-storage.ts +50 -30
  161. package/core/storage/storage-manager.ts +6 -6
  162. package/core/storage/storage.ts +18 -15
  163. package/core/sync/auth-config.ts +3 -3
  164. package/core/sync/index.ts +13 -19
  165. package/core/sync/oauth-handler.ts +3 -3
  166. package/core/sync/sync-client.ts +4 -9
  167. package/core/sync/sync-manager.ts +12 -14
  168. package/core/types/commands.ts +42 -7
  169. package/core/types/index.ts +284 -302
  170. package/core/types/integrations.ts +3 -3
  171. package/core/types/storage.ts +49 -0
  172. package/core/types/utils.ts +3 -3
  173. package/core/utils/agent-stream.ts +3 -1
  174. package/core/utils/animations.ts +14 -11
  175. package/core/utils/branding.ts +7 -7
  176. package/core/utils/cache.ts +1 -3
  177. package/core/utils/collection-filters.ts +3 -15
  178. package/core/utils/date-helper.ts +2 -7
  179. package/core/utils/file-helper.ts +13 -8
  180. package/core/utils/jsonl-helper.ts +13 -10
  181. package/core/utils/keychain.ts +4 -8
  182. package/core/utils/logger.ts +1 -1
  183. package/core/utils/next-steps.ts +3 -3
  184. package/core/utils/output.ts +58 -11
  185. package/core/utils/project-commands.ts +6 -6
  186. package/core/utils/project-credentials.ts +5 -12
  187. package/core/utils/runtime.ts +2 -2
  188. package/core/utils/session-helper.ts +3 -4
  189. package/core/utils/version.ts +3 -3
  190. package/core/wizard/index.ts +13 -0
  191. package/core/wizard/onboarding.ts +633 -0
  192. package/core/workflow/state-machine.ts +7 -7
  193. package/dist/bin/prjct.mjs +18907 -13189
  194. package/dist/core/infrastructure/command-installer.js +96 -111
  195. package/dist/core/infrastructure/editors-config.js +6 -6
  196. package/dist/core/infrastructure/setup.js +256 -257
  197. package/dist/core/utils/version.js +9 -9
  198. package/package.json +11 -12
  199. package/scripts/build.js +3 -3
  200. package/scripts/postinstall.js +2 -2
  201. package/templates/mcp-config.json +6 -1
  202. package/templates/permissions/permissive.jsonc +1 -1
  203. package/templates/permissions/strict.jsonc +5 -9
  204. package/templates/global/docs/agents.md +0 -88
  205. package/templates/global/docs/architecture.md +0 -103
  206. package/templates/global/docs/commands.md +0 -96
  207. package/templates/global/docs/validation.md +0 -95
@@ -146,3 +146,52 @@ export interface IdeasJson {
146
146
  ideas: Idea[]
147
147
  lastUpdated: string
148
148
  }
149
+
150
+ // =============================================================================
151
+ // Metrics Storage Types
152
+ // =============================================================================
153
+
154
+ /**
155
+ * Daily stats for trend analysis
156
+ */
157
+ export interface DailyStats {
158
+ date: string // YYYY-MM-DD
159
+ tokensSaved: number // Tokens saved that day
160
+ syncs: number // Number of syncs
161
+ avgCompressionRate: number // Average compression rate (0-1)
162
+ totalDuration: number // Total sync time in ms
163
+ }
164
+
165
+ /**
166
+ * Agent usage tracking
167
+ */
168
+ export interface AgentUsage {
169
+ agentName: string // e.g., "backend", "frontend"
170
+ usageCount: number // Times invoked
171
+ tokensSaved: number // Tokens saved by this agent
172
+ }
173
+
174
+ /**
175
+ * Metrics collection for value dashboard
176
+ */
177
+ export interface MetricsJson {
178
+ // Token metrics
179
+ totalTokensSaved: number
180
+ avgCompressionRate: number // 0-1 (e.g., 0.63 = 63% reduction)
181
+
182
+ // Sync metrics
183
+ syncCount: number
184
+ watchTriggers: number // Auto-syncs from watch mode
185
+ avgSyncDuration: number // Average in ms
186
+ totalSyncDuration: number // Total in ms
187
+
188
+ // Agent usage
189
+ agentUsage: AgentUsage[]
190
+
191
+ // Time series for trends
192
+ dailyStats: DailyStats[]
193
+
194
+ // Metadata
195
+ firstSync: string // ISO8601 - when tracking started
196
+ lastUpdated: string // ISO8601
197
+ }
@@ -5,12 +5,12 @@
5
5
 
6
6
  // Re-export file system types
7
7
  export {
8
- NodeError,
9
- isNotFoundError,
10
- isPermissionError,
11
8
  isDirNotEmptyError,
12
9
  isFileExistsError,
13
10
  isNodeError,
11
+ isNotFoundError,
12
+ isPermissionError,
13
+ NodeError,
14
14
  } from './fs'
15
15
 
16
16
  export type AsyncFunction<T = unknown> = (...args: unknown[]) => Promise<T>
@@ -118,7 +118,9 @@ class AgentStream {
118
118
  complete(taskName: string, totalDuration?: number): void {
119
119
  if (this.quiet) return
120
120
 
121
- const durationStr = totalDuration ? ` ${chalk.dim(`[${this.formatDuration(totalDuration)}]`)}` : ''
121
+ const durationStr = totalDuration
122
+ ? ` ${chalk.dim(`[${this.formatDuration(totalDuration)}]`)}`
123
+ : ''
122
124
  console.log(chalk.green(`✅ ${taskName}${durationStr}`))
123
125
  }
124
126
 
@@ -132,15 +132,15 @@ function sleep(ms: number): Promise<void> {
132
132
 
133
133
  export async function animate(frameList: string[], duration = 100): Promise<void> {
134
134
  for (const frame of frameList) {
135
- process.stdout.write('\r' + frame)
135
+ process.stdout.write(`\r${frame}`)
136
136
  await sleep(duration)
137
137
  }
138
- process.stdout.write('\r' + ' '.repeat(30) + '\r')
138
+ process.stdout.write(`\r${' '.repeat(30)}\r`)
139
139
  }
140
140
 
141
141
  export async function typeWriter(text: string, delay = 30): Promise<void> {
142
142
  for (let i = 0; i <= text.length; i++) {
143
- process.stdout.write('\r' + text.slice(0, i) + (i < text.length ? '▋' : ''))
143
+ process.stdout.write(`\r${text.slice(0, i)}${i < text.length ? '▋' : ''}`)
144
144
  await sleep(delay)
145
145
  }
146
146
  process.stdout.write('\n')
@@ -154,8 +154,8 @@ export async function progressBar(duration = 1000, label = 'Processing'): Promis
154
154
  const percent = Math.round((i / steps) * 100)
155
155
  const filled = '▓'.repeat(i)
156
156
  const empty = '░'.repeat(steps - i)
157
- const bar = `${colors.dim(label)} [${colors.primary(filled)}${colors.dim(empty)}] ${colors.text(percent + '%')}`
158
- process.stdout.write('\r' + bar)
157
+ const bar = `${colors.dim(label)} [${colors.primary(filled)}${colors.dim(empty)}] ${colors.text(`${percent}%`)}`
158
+ process.stdout.write(`\r${bar}`)
159
159
  await sleep(stepDuration)
160
160
  }
161
161
  process.stdout.write('\n')
@@ -168,9 +168,9 @@ export async function sparkle(message: string): Promise<void> {
168
168
  for (let i = 0; i < 3; i++) {
169
169
  const spark = sparkles[Math.floor(Math.random() * sparkles.length)]
170
170
  output = `${spark} ${message} ${spark}`
171
- process.stdout.write('\r' + output)
171
+ process.stdout.write(`\r${output}`)
172
172
  await sleep(200)
173
- process.stdout.write('\r' + ' '.repeat(output.length))
173
+ process.stdout.write(`\r${' '.repeat(output.length)}`)
174
174
  await sleep(100)
175
175
  }
176
176
 
@@ -216,13 +216,17 @@ ${colors.dim('Added to your ideas backlog')}
216
216
  `
217
217
  }
218
218
 
219
- export function formatCleanup(filesRemoved: number, tasksArchived: number, spaceFeed: number): string {
219
+ export function formatCleanup(
220
+ filesRemoved: number,
221
+ tasksArchived: number,
222
+ spaceFeed: number
223
+ ): string {
220
224
  return `
221
225
  ${banners.cleanup}
222
226
 
223
227
  ${colors.text('🗑️ Files removed:')} ${colors.success.bold(String(filesRemoved))}
224
228
  ${colors.text('📦 Tasks archived:')} ${colors.success.bold(String(tasksArchived))}
225
- ${colors.text('💾 Space freed:')} ${colors.success.bold(spaceFeed + ' MB')}
229
+ ${colors.text('💾 Space freed:')} ${colors.success.bold(`${spaceFeed} MB`)}
226
230
 
227
231
  ${colors.celebrate('✨ Your project is clean and lean!')}
228
232
  `
@@ -261,6 +265,5 @@ export default {
261
265
  formatError,
262
266
  formatIdea,
263
267
  formatCleanup,
264
- formatRecap
268
+ formatRecap,
265
269
  }
266
-
@@ -6,8 +6,8 @@
6
6
  */
7
7
 
8
8
  import chalk from 'chalk'
9
+ import { getProviderBranding } from '../infrastructure/ai-provider'
9
10
  import type { AIProviderName } from '../types/provider'
10
- import { getProviderBranding, Providers } from '../infrastructure/ai-provider'
11
11
 
12
12
  const SPINNER_FRAMES = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏']
13
13
  const SPINNER_SPEED = 80
@@ -48,21 +48,21 @@ const branding: Branding = {
48
48
  // Spinner config
49
49
  spinner: {
50
50
  frames: SPINNER_FRAMES,
51
- speed: SPINNER_SPEED
51
+ speed: SPINNER_SPEED,
52
52
  },
53
53
 
54
54
  // CLI output (with chalk colors)
55
55
  cli: {
56
- header: () => chalk.cyan.bold('⚡') + ' ' + chalk.cyan('prjct'),
56
+ header: () => `${chalk.cyan.bold('⚡')} ${chalk.cyan('prjct')}`,
57
57
  footer: () => chalk.dim('⚡ prjct'),
58
58
  spin: (frame: number, msg?: string) =>
59
- chalk.cyan('⚡') + ' ' + chalk.cyan('prjct') + ' ' + chalk.cyan(SPINNER_FRAMES[frame % 10]) + ' ' + chalk.dim(msg || '')
59
+ `${chalk.cyan('⚡')} ${chalk.cyan('prjct')} ${chalk.cyan(SPINNER_FRAMES[frame % 10])} ${chalk.dim(msg || '')}`,
60
60
  },
61
61
 
62
62
  // Template (plain text)
63
63
  template: {
64
64
  header: '⚡ prjct',
65
- footer: '⚡ prjct'
65
+ footer: '⚡ prjct',
66
66
  },
67
67
 
68
68
  // Default Git commit footer (generic)
@@ -71,7 +71,7 @@ const branding: Branding = {
71
71
  // URLs
72
72
  urls: {
73
73
  website: 'https://prjct.app',
74
- docs: 'https://prjct.app/docs'
74
+ docs: 'https://prjct.app/docs',
75
75
  },
76
76
 
77
77
  // Provider-aware commit footer
@@ -82,7 +82,7 @@ const branding: Branding = {
82
82
  // Provider-aware signature
83
83
  getSignature: (provider: AIProviderName = 'claude') => {
84
84
  return getProviderBranding(provider).signature
85
- }
85
+ },
86
86
  }
87
87
 
88
88
  export default branding
@@ -91,9 +91,7 @@ export class TTLCache<T> {
91
91
  private evictOldEntries(): void {
92
92
  if (this.cache.size <= this.maxSize) return
93
93
 
94
- const entries = Array.from(this.cache.entries()).sort(
95
- (a, b) => a[1].timestamp - b[1].timestamp
96
- )
94
+ const entries = Array.from(this.cache.entries()).sort((a, b) => a[1].timestamp - b[1].timestamp)
97
95
 
98
96
  const toRemove = entries.slice(0, this.cache.size - this.maxSize)
99
97
  for (const [key] of toRemove) {
@@ -29,33 +29,21 @@ export const SECTION_ORDER: Record<TaskSection, number> = {
29
29
  /**
30
30
  * Filter items by a specific field value
31
31
  */
32
- export function filterByField<T, K extends keyof T>(
33
- items: T[],
34
- field: K,
35
- value: T[K]
36
- ): T[] {
32
+ export function filterByField<T, K extends keyof T>(items: T[], field: K, value: T[K]): T[] {
37
33
  return items.filter((item) => item[field] === value)
38
34
  }
39
35
 
40
36
  /**
41
37
  * Filter items by multiple field values (OR logic)
42
38
  */
43
- export function filterByFieldIn<T, K extends keyof T>(
44
- items: T[],
45
- field: K,
46
- values: T[K][]
47
- ): T[] {
39
+ export function filterByFieldIn<T, K extends keyof T>(items: T[], field: K, values: T[K][]): T[] {
48
40
  return items.filter((item) => values.includes(item[field]))
49
41
  }
50
42
 
51
43
  /**
52
44
  * Filter items excluding a specific field value
53
45
  */
54
- export function filterByFieldNot<T, K extends keyof T>(
55
- items: T[],
56
- field: K,
57
- value: T[K]
58
- ): T[] {
46
+ export function filterByFieldNot<T, K extends keyof T>(items: T[], field: K, value: T[K]): T[] {
59
47
  return items.filter((item) => item[field] !== value)
60
48
  }
61
49
 
@@ -97,11 +97,7 @@ export function getDateRange(fromDate: Date, toDate: Date): Date[] {
97
97
 
98
98
  while (current <= toDate) {
99
99
  dates.push(new Date(current))
100
- current = new Date(
101
- current.getFullYear(),
102
- current.getMonth(),
103
- current.getDate() + 1,
104
- )
100
+ current = new Date(current.getFullYear(), current.getMonth(), current.getDate() + 1)
105
101
  }
106
102
 
107
103
  return dates
@@ -186,6 +182,5 @@ export default {
186
182
  formatDuration,
187
183
  calculateDuration,
188
184
  getStartOfDay,
189
- getEndOfDay
185
+ getEndOfDay,
190
186
  }
191
-
@@ -1,6 +1,6 @@
1
- import fs from 'fs/promises'
2
- import path from 'path'
3
- import { type NodeError, isNotFoundError } from '../types/fs'
1
+ import fs from 'node:fs/promises'
2
+ import path from 'node:path'
3
+ import { isNotFoundError } from '../types/fs'
4
4
 
5
5
  /**
6
6
  * File Helper - Centralized file operations with error handling
@@ -20,7 +20,10 @@ interface ListFilesOptions {
20
20
  /**
21
21
  * Read JSON file and parse
22
22
  */
23
- export async function readJson<T = unknown>(filePath: string, defaultValue: T | null = null): Promise<T | null> {
23
+ export async function readJson<T = unknown>(
24
+ filePath: string,
25
+ defaultValue: T | null = null
26
+ ): Promise<T | null> {
24
27
  try {
25
28
  const content = await fs.readFile(filePath, 'utf-8')
26
29
  return JSON.parse(content) as T
@@ -87,7 +90,7 @@ export async function appendToFile(filePath: string, content: string): Promise<v
87
90
  export async function appendLine(filePath: string, line: string): Promise<void> {
88
91
  const dir = path.dirname(filePath)
89
92
  await fs.mkdir(dir, { recursive: true })
90
- await fs.appendFile(filePath, line + '\n', 'utf-8')
93
+ await fs.appendFile(filePath, `${line}\n`, 'utf-8')
91
94
  }
92
95
 
93
96
  /**
@@ -176,7 +179,10 @@ export async function deleteDir(dirPath: string): Promise<boolean> {
176
179
  /**
177
180
  * List files in directory
178
181
  */
179
- export async function listFiles(dirPath: string, options: ListFilesOptions = {}): Promise<string[]> {
182
+ export async function listFiles(
183
+ dirPath: string,
184
+ options: ListFilesOptions = {}
185
+ ): Promise<string[]> {
180
186
  try {
181
187
  const entries = await fs.readdir(dirPath, { withFileTypes: true })
182
188
  let files = entries
@@ -285,6 +291,5 @@ export default {
285
291
  readLines,
286
292
  writeLines,
287
293
  getFileExtension,
288
- getFileNameWithoutExtension
294
+ getFileNameWithoutExtension,
289
295
  }
290
-
@@ -1,7 +1,7 @@
1
- import fs from 'fs/promises'
2
- import fsSync from 'fs'
3
- import readline from 'readline'
4
- import path from 'path'
1
+ import fsSync from 'node:fs'
2
+ import fs from 'node:fs/promises'
3
+ import path from 'node:path'
4
+ import readline from 'node:readline'
5
5
  import { isNotFoundError } from '../types/fs'
6
6
 
7
7
  /**
@@ -46,7 +46,7 @@ export function parseJsonLines<T = Record<string, unknown>>(content: string): T[
46
46
  * Convert array of objects to JSONL string
47
47
  */
48
48
  export function stringifyJsonLines(objects: unknown[]): string {
49
- return objects.map((obj) => JSON.stringify(obj)).join('\n') + '\n'
49
+ return `${objects.map((obj) => JSON.stringify(obj)).join('\n')}\n`
50
50
  }
51
51
 
52
52
  /**
@@ -77,7 +77,7 @@ export async function writeJsonLines(filePath: string, objects: unknown[]): Prom
77
77
  * Uses append mode for efficiency (no full file read/write)
78
78
  */
79
79
  export async function appendJsonLine(filePath: string, object: unknown): Promise<void> {
80
- const line = JSON.stringify(object) + '\n'
80
+ const line = `${JSON.stringify(object)}\n`
81
81
  await fs.appendFile(filePath, line, 'utf-8')
82
82
  }
83
83
 
@@ -144,7 +144,9 @@ export async function getFirstJsonLines<T = Record<string, unknown>>(
144
144
  * Merge multiple JSONL files into one array
145
145
  * Useful for reading multiple sessions
146
146
  */
147
- export async function mergeJsonLines<T = Record<string, unknown>>(filePaths: string[]): Promise<T[]> {
147
+ export async function mergeJsonLines<T = Record<string, unknown>>(
148
+ filePaths: string[]
149
+ ): Promise<T[]> {
148
150
  const allEntries: T[] = []
149
151
 
150
152
  for (const filePath of filePaths) {
@@ -240,7 +242,9 @@ export async function rotateJsonLinesIfNeeded(filePath: string, maxSizeMB = 10):
240
242
  // Move file to archive
241
243
  await fs.rename(filePath, archivePath)
242
244
 
243
- console.log(`📦 Rotated ${path.basename(filePath)} (${sizeMB.toFixed(1)}MB) → ${path.basename(archivePath)}`)
245
+ console.log(
246
+ `📦 Rotated ${path.basename(filePath)} (${sizeMB.toFixed(1)}MB) → ${path.basename(archivePath)}`
247
+ )
244
248
 
245
249
  return true
246
250
  }
@@ -299,6 +303,5 @@ export default {
299
303
  getFileSizeMB,
300
304
  rotateJsonLinesIfNeeded,
301
305
  appendJsonLineWithRotation,
302
- checkFileSizeWarning
306
+ checkFileSizeWarning,
303
307
  }
304
-
@@ -5,8 +5,8 @@
5
5
  * Falls back to environment variables if keychain is not available.
6
6
  */
7
7
 
8
- import { exec } from 'child_process'
9
- import { promisify } from 'util'
8
+ import { exec } from 'node:child_process'
9
+ import { promisify } from 'node:util'
10
10
 
11
11
  const execAsync = promisify(exec)
12
12
 
@@ -30,9 +30,7 @@ export async function setCredential(key: CredentialKey, value: string): Promise<
30
30
  )
31
31
 
32
32
  // Add new entry
33
- await execAsync(
34
- `security add-generic-password -s "${SERVICE_NAME}" -a "${key}" -w "${value}"`
35
- )
33
+ await execAsync(`security add-generic-password -s "${SERVICE_NAME}" -a "${key}" -w "${value}"`)
36
34
 
37
35
  return true
38
36
  } catch (error) {
@@ -70,9 +68,7 @@ export async function deleteCredential(key: CredentialKey): Promise<boolean> {
70
68
  }
71
69
 
72
70
  try {
73
- await execAsync(
74
- `security delete-generic-password -s "${SERVICE_NAME}" -a "${key}" 2>/dev/null`
75
- )
71
+ await execAsync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${key}" 2>/dev/null`)
76
72
  return true
77
73
  } catch (_error) {
78
74
  // Not found in keychain - expected
@@ -79,7 +79,7 @@ const logger: Logger = {
79
79
  isEnabled: () => currentLevel >= 0,
80
80
 
81
81
  // Get current level name (pre-computed, no runtime lookup)
82
- level: () => currentLevelName
82
+ level: () => currentLevelName,
83
83
  }
84
84
 
85
85
  export default logger
@@ -6,7 +6,7 @@
6
6
  */
7
7
 
8
8
  import chalk from 'chalk'
9
- import { workflowStateMachine, type WorkflowState } from '../workflow/state-machine'
9
+ import { type WorkflowState, workflowStateMachine } from '../workflow/state-machine'
10
10
 
11
11
  interface NextStep {
12
12
  cmd: string
@@ -59,7 +59,7 @@ export function showNextSteps(command: string, options: { quiet?: boolean } = {}
59
59
 
60
60
  if (validCommands.length === 0) return
61
61
 
62
- const steps: NextStep[] = validCommands.map(cmd => ({
62
+ const steps: NextStep[] = validCommands.map((cmd) => ({
63
63
  cmd: `p. ${cmd}`,
64
64
  desc: CMD_DESCRIPTIONS[cmd] || cmd,
65
65
  }))
@@ -78,7 +78,7 @@ export function getNextSteps(command: string): NextStep[] {
78
78
  const resultingState = COMMAND_TO_STATE[command] || 'idle'
79
79
  const validCommands = workflowStateMachine.getValidCommands(resultingState)
80
80
 
81
- return validCommands.map(cmd => ({
81
+ return validCommands.map((cmd) => ({
82
82
  cmd: `p. ${cmd}`,
83
83
  desc: CMD_DESCRIPTIONS[cmd] || cmd,
84
84
  }))
@@ -2,27 +2,56 @@
2
2
  * Minimal Output System for prjct-cli
3
3
  * Spinner while working → Single line result
4
4
  * With prjct branding
5
+ *
6
+ * Supports --quiet mode for CI/CD and scripting
5
7
  */
6
8
 
7
9
  import chalk from 'chalk'
8
10
  import branding from './branding'
9
11
 
10
- const FRAMES = branding.spinner.frames
12
+ const _FRAMES = branding.spinner.frames
11
13
  const SPEED = branding.spinner.speed
12
14
 
13
15
  let interval: ReturnType<typeof setInterval> | null = null
14
16
  let frame = 0
15
17
 
18
+ // Quiet mode - suppress all stdout except errors
19
+ let quietMode = false
20
+
21
+ /**
22
+ * Enable quiet mode (no stdout, only stderr for errors)
23
+ */
24
+ export function setQuietMode(enabled: boolean): void {
25
+ quietMode = enabled
26
+ }
27
+
28
+ /**
29
+ * Check if quiet mode is enabled
30
+ */
31
+ export function isQuietMode(): boolean {
32
+ return quietMode
33
+ }
34
+
16
35
  const truncate = (s: string | undefined | null, max = 50): string =>
17
- s && s.length > max ? s.slice(0, max - 1) + '…' : s || ''
36
+ s && s.length > max ? `${s.slice(0, max - 1)}…` : s || ''
18
37
 
19
- const clear = (): boolean => process.stdout.write('\r' + ' '.repeat(80) + '\r')
38
+ const clear = (): boolean => process.stdout.write(`\r${' '.repeat(80)}\r`)
39
+
40
+ /**
41
+ * Metrics to display after command completion
42
+ * Shows value provided by prjct (compression, agent count, etc.)
43
+ */
44
+ interface OutputMetrics {
45
+ agents?: number // Number of agents used
46
+ reduction?: number // Context reduction percentage
47
+ tokens?: number // Token count (in thousands)
48
+ }
20
49
 
21
50
  interface Output {
22
51
  start(): Output
23
52
  end(): Output
24
53
  spin(msg: string): Output
25
- done(msg: string): Output
54
+ done(msg: string, metrics?: OutputMetrics): Output
26
55
  fail(msg: string): Output
27
56
  warn(msg: string): Output
28
57
  stop(): Output
@@ -33,18 +62,19 @@ interface Output {
33
62
  const out: Output = {
34
63
  // Branding: Show header at start
35
64
  start() {
36
- console.log(branding.cli.header())
65
+ if (!quietMode) console.log(branding.cli.header())
37
66
  return this
38
67
  },
39
68
 
40
69
  // Branding: Show footer at end
41
70
  end() {
42
- console.log(branding.cli.footer())
71
+ if (!quietMode) console.log(branding.cli.footer())
43
72
  return this
44
73
  },
45
74
 
46
75
  // Branded spinner: prjct message...
47
76
  spin(msg: string) {
77
+ if (quietMode) return this
48
78
  this.stop()
49
79
  interval = setInterval(() => {
50
80
  process.stdout.write(`\r${branding.cli.spin(frame++, truncate(msg, 45))}`)
@@ -52,21 +82,35 @@ const out: Output = {
52
82
  return this
53
83
  },
54
84
 
55
- done(msg: string) {
85
+ done(msg: string, metrics?: OutputMetrics) {
56
86
  this.stop()
57
- console.log(`${chalk.green('✓')} ${truncate(msg, 65)}`)
87
+ if (!quietMode) {
88
+ // Build metrics suffix if provided: [2a | 97% | 45K]
89
+ let suffix = ''
90
+ if (metrics) {
91
+ const parts: string[] = []
92
+ if (metrics.agents !== undefined) parts.push(`${metrics.agents}a`)
93
+ if (metrics.reduction !== undefined) parts.push(`${metrics.reduction}%`)
94
+ if (metrics.tokens !== undefined) parts.push(`${Math.round(metrics.tokens)}K`)
95
+ if (parts.length > 0) {
96
+ suffix = chalk.dim(` [${parts.join(' | ')}]`)
97
+ }
98
+ }
99
+ console.log(`${chalk.green('✓')} ${truncate(msg, 50)}${suffix}`)
100
+ }
58
101
  return this
59
102
  },
60
103
 
104
+ // Errors go to stderr even in quiet mode
61
105
  fail(msg: string) {
62
106
  this.stop()
63
- console.log(`${chalk.red('✗')} ${truncate(msg, 65)}`)
107
+ console.error(`${chalk.red('✗')} ${truncate(msg, 65)}`)
64
108
  return this
65
109
  },
66
110
 
67
111
  warn(msg: string) {
68
112
  this.stop()
69
- console.log(`${chalk.yellow('⚠')} ${truncate(msg, 65)}`)
113
+ if (!quietMode) console.log(`${chalk.yellow('⚠')} ${truncate(msg, 65)}`)
70
114
  return this
71
115
  },
72
116
 
@@ -81,6 +125,7 @@ const out: Output = {
81
125
 
82
126
  // Step counter: [3/7] Running tests...
83
127
  step(current: number, total: number, msg: string) {
128
+ if (quietMode) return this
84
129
  this.stop()
85
130
  const counter = chalk.dim(`[${current}/${total}]`)
86
131
  interval = setInterval(() => {
@@ -91,6 +136,7 @@ const out: Output = {
91
136
 
92
137
  // Progress bar: [████░░░░] 50% Analyzing...
93
138
  progress(current: number, total: number, msg?: string) {
139
+ if (quietMode) return this
94
140
  this.stop()
95
141
  const percent = Math.round((current / total) * 100)
96
142
  const filled = Math.round(percent / 10)
@@ -101,7 +147,8 @@ const out: Output = {
101
147
  process.stdout.write(`\r${branding.cli.spin(frame++, `[${bar}] ${percent}%${text}`)}`)
102
148
  }, SPEED)
103
149
  return this
104
- }
150
+ },
105
151
  }
106
152
 
153
+ export type { OutputMetrics }
107
154
  export default out
@@ -1,7 +1,6 @@
1
- import path from 'path'
2
-
3
- import * as fileHelper from './file-helper'
1
+ import path from 'node:path'
4
2
  import type { DetectedProjectCommands } from '../types'
3
+ import * as fileHelper from './file-helper'
5
4
 
6
5
  type PackageManager = 'npm' | 'pnpm' | 'yarn' | 'bun'
7
6
  type DetectedStack = 'js' | 'python' | 'go' | 'rust' | 'dotnet' | 'java' | 'unknown'
@@ -21,7 +20,10 @@ interface PackageJson {
21
20
  *
22
21
  * Reason: installed users may not have Bun, and many projects use pnpm/yarn.
23
22
  */
24
- async function detectPackageManager(projectPath: string, pkg: PackageJson | null): Promise<PackageManager> {
23
+ async function detectPackageManager(
24
+ projectPath: string,
25
+ pkg: PackageJson | null
26
+ ): Promise<PackageManager> {
25
27
  const declared = pkg?.packageManager?.trim().toLowerCase()
26
28
  if (declared?.startsWith('pnpm@')) return 'pnpm'
27
29
  if (declared?.startsWith('yarn@')) return 'yarn'
@@ -128,5 +130,3 @@ export async function detectProjectCommands(projectPath: string): Promise<Detect
128
130
 
129
131
  return { stack: 'unknown' }
130
132
  }
131
-
132
-
@@ -11,10 +11,10 @@
11
11
  * This allows different projects to use different Linear workspaces.
12
12
  */
13
13
 
14
- import fs from 'fs'
15
- import path from 'path'
16
- import os from 'os'
17
- import { getCredential, type CredentialKey } from './keychain'
14
+ import fs from 'node:fs'
15
+ import os from 'node:os'
16
+ import path from 'node:path'
17
+ import { type CredentialKey, getCredential } from './keychain'
18
18
 
19
19
  interface LinearCredentials {
20
20
  apiKey: string
@@ -31,14 +31,7 @@ export interface ProjectCredentials {
31
31
  * Get path to project credentials file
32
32
  */
33
33
  function getCredentialsPath(projectId: string): string {
34
- return path.join(
35
- os.homedir(),
36
- '.prjct-cli',
37
- 'projects',
38
- projectId,
39
- 'config',
40
- 'credentials.json'
41
- )
34
+ return path.join(os.homedir(), '.prjct-cli', 'projects', projectId, 'config', 'credentials.json')
42
35
  }
43
36
 
44
37
  /**