prjct-cli 1.4.0 → 1.5.1

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 (33) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/bin/prjct.ts +23 -14
  3. package/core/__tests__/agentic/command-executor.test.ts +19 -19
  4. package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
  5. package/core/__tests__/ai-tools/formatters.test.ts +118 -0
  6. package/core/agentic/command-executor.ts +18 -17
  7. package/core/agentic/prompt-builder.ts +18 -17
  8. package/core/agentic/template-executor.ts +2 -2
  9. package/core/ai-tools/formatters.ts +18 -0
  10. package/core/ai-tools/registry.ts +17 -14
  11. package/core/cli/start.ts +18 -17
  12. package/core/commands/analysis.ts +1 -1
  13. package/core/commands/setup.ts +8 -8
  14. package/core/commands/uninstall.ts +11 -11
  15. package/core/index.ts +103 -21
  16. package/core/infrastructure/agent-detector.ts +8 -8
  17. package/core/infrastructure/ai-provider.ts +49 -37
  18. package/core/infrastructure/command-installer.ts +18 -10
  19. package/core/infrastructure/path-manager.ts +4 -4
  20. package/core/infrastructure/setup.ts +124 -119
  21. package/core/infrastructure/update-checker.ts +14 -13
  22. package/core/integrations/linear/sync.ts +4 -4
  23. package/core/services/context-generator.ts +12 -3
  24. package/core/services/hooks-service.ts +78 -68
  25. package/core/services/sync-service.ts +64 -6
  26. package/core/utils/citations.ts +53 -0
  27. package/core/utils/error-messages.ts +11 -0
  28. package/core/utils/fs-helpers.ts +14 -0
  29. package/core/utils/project-credentials.ts +8 -7
  30. package/dist/bin/prjct.mjs +854 -643
  31. package/dist/core/infrastructure/command-installer.js +118 -87
  32. package/dist/core/infrastructure/setup.js +246 -210
  33. package/package.json +1 -1
@@ -8,7 +8,6 @@
8
8
  */
9
9
 
10
10
  import { execSync } from 'node:child_process'
11
- import fsSync from 'node:fs'
12
11
  import fs from 'node:fs/promises'
13
12
  import os from 'node:os'
14
13
  import path from 'node:path'
@@ -17,6 +16,7 @@ import chalk from 'chalk'
17
16
  import { getProviderPaths } from '../infrastructure/command-installer'
18
17
  import pathManager from '../infrastructure/path-manager'
19
18
  import type { CommandResult, UninstallOptions } from '../types'
19
+ import { fileExists } from '../utils/fs-helpers'
20
20
  import { PrjctCommandsBase } from './base'
21
21
 
22
22
  // Markers for prjct section in CLAUDE.md
@@ -135,7 +135,7 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
135
135
 
136
136
  // 1. ~/.prjct-cli/ (main data directory)
137
137
  const prjctCliPath = pathManager.getGlobalBasePath()
138
- const prjctCliExists = fsSync.existsSync(prjctCliPath)
138
+ const prjctCliExists = await fileExists(prjctCliPath)
139
139
  const projectCount = prjctCliExists
140
140
  ? await countDirectoryItems(path.join(prjctCliPath, 'projects'))
141
141
  : 0
@@ -152,12 +152,12 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
152
152
 
153
153
  // 2. ~/.claude/CLAUDE.md (prjct section only)
154
154
  const claudeMdPath = path.join(providerPaths.claude.config, 'CLAUDE.md')
155
- const claudeMdExists = fsSync.existsSync(claudeMdPath)
155
+ const claudeMdExists = await fileExists(claudeMdPath)
156
156
  let hasPrjctSection = false
157
157
 
158
158
  if (claudeMdExists) {
159
159
  try {
160
- const content = fsSync.readFileSync(claudeMdPath, 'utf-8')
160
+ const content = await fs.readFile(claudeMdPath, 'utf-8')
161
161
  hasPrjctSection = content.includes(PRJCT_START_MARKER) && content.includes(PRJCT_END_MARKER)
162
162
  } catch {
163
163
  // Can't read file
@@ -173,7 +173,7 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
173
173
 
174
174
  // 3. ~/.claude/commands/p/ (prjct commands)
175
175
  const claudeCommandsPath = providerPaths.claude.commands
176
- const claudeCommandsExists = fsSync.existsSync(claudeCommandsPath)
176
+ const claudeCommandsExists = await fileExists(claudeCommandsPath)
177
177
  const claudeCommandsSize = claudeCommandsExists ? await getDirectorySize(claudeCommandsPath) : 0
178
178
 
179
179
  items.push({
@@ -186,7 +186,7 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
186
186
 
187
187
  // 4. ~/.claude/commands/p.md (router)
188
188
  const claudeRouterPath = providerPaths.claude.router
189
- const claudeRouterExists = fsSync.existsSync(claudeRouterPath)
189
+ const claudeRouterExists = await fileExists(claudeRouterPath)
190
190
 
191
191
  items.push({
192
192
  path: claudeRouterPath,
@@ -197,7 +197,7 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
197
197
 
198
198
  // 5. ~/.claude/prjct-statusline.sh (status line script)
199
199
  const statusLinePath = path.join(providerPaths.claude.config, 'prjct-statusline.sh')
200
- const statusLineExists = fsSync.existsSync(statusLinePath)
200
+ const statusLineExists = await fileExists(statusLinePath)
201
201
 
202
202
  items.push({
203
203
  path: statusLinePath,
@@ -208,7 +208,7 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
208
208
 
209
209
  // 6. ~/.gemini/commands/p.toml (Gemini router, if exists)
210
210
  const geminiRouterPath = providerPaths.gemini.router
211
- const geminiRouterExists = fsSync.existsSync(geminiRouterPath)
211
+ const geminiRouterExists = await fileExists(geminiRouterPath)
212
212
 
213
213
  items.push({
214
214
  path: geminiRouterPath,
@@ -219,12 +219,12 @@ async function gatherUninstallItems(): Promise<UninstallItem[]> {
219
219
 
220
220
  // 7. ~/.gemini/GEMINI.md (prjct section only, if exists)
221
221
  const geminiMdPath = path.join(providerPaths.gemini.config, 'GEMINI.md')
222
- const geminiMdExists = fsSync.existsSync(geminiMdPath)
222
+ const geminiMdExists = await fileExists(geminiMdPath)
223
223
  let hasGeminiPrjctSection = false
224
224
 
225
225
  if (geminiMdExists) {
226
226
  try {
227
- const content = fsSync.readFileSync(geminiMdPath, 'utf-8')
227
+ const content = await fs.readFile(geminiMdPath, 'utf-8')
228
228
  hasGeminiPrjctSection =
229
229
  content.includes(PRJCT_START_MARKER) && content.includes(PRJCT_END_MARKER)
230
230
  } catch {
@@ -288,7 +288,7 @@ async function createBackup(): Promise<string | null> {
288
288
 
289
289
  const prjctCliPath = pathManager.getGlobalBasePath()
290
290
 
291
- if (fsSync.existsSync(prjctCliPath)) {
291
+ if (await fileExists(prjctCliPath)) {
292
292
  // Copy entire .prjct-cli directory
293
293
  await copyDirectory(prjctCliPath, path.join(backupDir, '.prjct-cli'))
294
294
  }
package/core/index.ts CHANGED
@@ -7,7 +7,6 @@
7
7
  import { PrjctCommands } from './commands/index'
8
8
  import { commandRegistry } from './commands/registry'
9
9
  import './commands/register' // Ensure commands are registered
10
- import fs from 'node:fs'
11
10
  import os from 'node:os'
12
11
  import path from 'node:path'
13
12
  import chalk from 'chalk'
@@ -15,6 +14,8 @@ import type { CommandMeta } from './commands/registry'
15
14
  import { detectAllProviders, detectAntigravity } from './infrastructure/ai-provider'
16
15
  import configManager from './infrastructure/config-manager'
17
16
  import { sessionTracker } from './services/session-tracker'
17
+ import { getError } from './utils/error-messages'
18
+ import { fileExists } from './utils/fs-helpers'
18
19
  import out from './utils/output'
19
20
 
20
21
  interface ParsedCommandArgs {
@@ -34,7 +35,7 @@ async function main(): Promise<void> {
34
35
 
35
36
  if (['-v', '--version', 'version'].includes(commandName)) {
36
37
  const packageJson = await import('../package.json')
37
- displayVersion(packageJson.version)
38
+ await displayVersion(packageJson.version)
38
39
  process.exit(0)
39
40
  }
40
41
 
@@ -53,27 +54,34 @@ async function main(): Promise<void> {
53
54
  const cmd = commandRegistry.getByName(commandName)
54
55
 
55
56
  if (!cmd) {
56
- console.error(`Unknown command: ${commandName}`)
57
- console.error(`\nUse 'prjct --help' to see available commands.`)
57
+ const suggestion = findClosestCommand(commandName)
58
+ const hint = suggestion
59
+ ? `Did you mean 'prjct ${suggestion}'? Run 'prjct --help' for all commands`
60
+ : "Run 'prjct --help' to see available commands"
61
+ out.failWithHint(
62
+ getError('UNKNOWN_COMMAND', { message: `Unknown command: ${commandName}`, hint })
63
+ )
58
64
  out.end()
59
65
  process.exit(1)
60
66
  }
61
67
 
62
68
  // 2. Check if deprecated
63
69
  if (cmd.deprecated) {
64
- console.error(`Command '${commandName}' is deprecated.`)
65
- if (cmd.replacedBy) {
66
- console.error(`Use 'prjct ${cmd.replacedBy}' instead.`)
67
- }
70
+ const hint = cmd.replacedBy
71
+ ? `Use 'prjct ${cmd.replacedBy}' instead`
72
+ : "Run 'prjct --help' to see available commands"
73
+ out.failWithHint({ message: `Command '${commandName}' is deprecated`, hint })
68
74
  out.end()
69
75
  process.exit(1)
70
76
  }
71
77
 
72
78
  // 3. Check if implemented
73
79
  if (!cmd.implemented) {
74
- console.error(`Command '${commandName}' exists but is not yet implemented.`)
75
- console.error(`Check the roadmap or contribute: https://github.com/jlopezlira/prjct-cli`)
76
- console.error(`\nUse 'prjct --help' to see available commands.`)
80
+ out.failWithHint({
81
+ message: `Command '${commandName}' is not yet implemented`,
82
+ hint: "Run 'prjct --help' to see available commands",
83
+ docs: 'https://github.com/jlopezlira/prjct-cli',
84
+ })
77
85
  out.end()
78
86
  process.exit(1)
79
87
  }
@@ -81,7 +89,15 @@ async function main(): Promise<void> {
81
89
  // 4. Parse arguments
82
90
  const { parsedArgs, options } = parseCommandArgs(cmd, rawArgs)
83
91
 
84
- // 4.5. Session tracking — touch/create session before command execution
92
+ // 4.5. Validate required params
93
+ const paramError = validateCommandParams(cmd, parsedArgs)
94
+ if (paramError) {
95
+ out.failWithHint(paramError)
96
+ out.end()
97
+ process.exit(1)
98
+ }
99
+
100
+ // 4.6. Session tracking — touch/create session before command execution
85
101
  let projectId: string | null = null
86
102
  const commandStartTime = Date.now()
87
103
  try {
@@ -193,6 +209,71 @@ async function main(): Promise<void> {
193
209
  }
194
210
  }
195
211
 
212
+ /**
213
+ * Validate that required params are provided
214
+ * Parses CommandMeta.params: <required> vs [optional]
215
+ */
216
+ function validateCommandParams(
217
+ cmd: CommandMeta,
218
+ parsedArgs: string[]
219
+ ): import('./utils/error-messages').ErrorWithHint | null {
220
+ if (!cmd.params) return null
221
+
222
+ // Extract required params: tokens wrapped in <angle brackets>
223
+ const requiredParams = cmd.params.match(/<[^>]+>/g)
224
+ if (!requiredParams || requiredParams.length === 0) return null
225
+
226
+ // Check if enough positional args provided
227
+ if (parsedArgs.length < requiredParams.length) {
228
+ const paramNames = requiredParams.map((p) => p.slice(1, -1)).join(', ')
229
+ const usage = cmd.usage.terminal || `prjct ${cmd.name} ${cmd.params}`
230
+ return getError('MISSING_PARAM', {
231
+ message: `Missing required parameter: ${paramNames}`,
232
+ hint: `Usage: ${usage}`,
233
+ })
234
+ }
235
+
236
+ return null
237
+ }
238
+
239
+ /**
240
+ * Find closest matching command name for did-you-mean suggestions
241
+ * Uses Levenshtein edit distance — suggests if distance <= 2
242
+ */
243
+ function findClosestCommand(input: string): string | null {
244
+ const allNames = commandRegistry.getAll().map((c) => c.name)
245
+ let best: string | null = null
246
+ let bestDist = Infinity
247
+
248
+ for (const name of allNames) {
249
+ const dist = editDistance(input.toLowerCase(), name.toLowerCase())
250
+ if (dist < bestDist) {
251
+ bestDist = dist
252
+ best = name
253
+ }
254
+ }
255
+
256
+ // Only suggest if edit distance is at most 2
257
+ return bestDist <= 2 ? best : null
258
+ }
259
+
260
+ function editDistance(a: string, b: string): number {
261
+ const m = a.length
262
+ const n = b.length
263
+ const dp: number[][] = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0))
264
+ for (let i = 0; i <= m; i++) dp[i][0] = i
265
+ for (let j = 0; j <= n; j++) dp[0][j] = j
266
+ for (let i = 1; i <= m; i++) {
267
+ for (let j = 1; j <= n; j++) {
268
+ dp[i][j] =
269
+ a[i - 1] === b[j - 1]
270
+ ? dp[i - 1][j - 1]
271
+ : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1])
272
+ }
273
+ }
274
+ return dp[m][n]
275
+ }
276
+
196
277
  /**
197
278
  * Parse command arguments dynamically
198
279
  */
@@ -226,18 +307,21 @@ function parseCommandArgs(_cmd: CommandMeta, rawArgs: string[]): ParsedCommandAr
226
307
  /**
227
308
  * Display version with provider status
228
309
  */
229
- function displayVersion(version: string): void {
230
- const detection = detectAllProviders()
310
+ async function displayVersion(version: string): Promise<void> {
311
+ const detection = await detectAllProviders()
231
312
 
232
313
  // Check if prjct commands are installed for each provider
233
314
  const claudeCommandPath = path.join(os.homedir(), '.claude', 'commands', 'p.md')
234
315
  const geminiCommandPath = path.join(os.homedir(), '.gemini', 'commands', 'p.toml')
235
- const claudeConfigured = fs.existsSync(claudeCommandPath)
236
- const geminiConfigured = fs.existsSync(geminiCommandPath)
316
+ const [claudeConfigured, geminiConfigured, cursorConfigured, cursorExists] = await Promise.all([
317
+ fileExists(claudeCommandPath),
318
+ fileExists(geminiCommandPath),
319
+ fileExists(path.join(process.cwd(), '.cursor', 'commands', 'sync.md')),
320
+ fileExists(path.join(process.cwd(), '.cursor')),
321
+ ])
237
322
 
238
- // Check current project for Cursor
239
- const cursorConfigured = fs.existsSync(path.join(process.cwd(), '.cursor', 'commands', 'sync.md'))
240
- const cursorExists = fs.existsSync(path.join(process.cwd(), '.cursor'))
323
+ // Antigravity status (global, skills-based)
324
+ const antigravityDetection = await detectAntigravity()
241
325
 
242
326
  console.log(`
243
327
  ${chalk.cyan('p/')} prjct v${version}
@@ -263,8 +347,6 @@ ${chalk.dim('Providers:')}`)
263
347
  console.log(` Gemini CLI ${chalk.dim('○ not installed')}`)
264
348
  }
265
349
 
266
- // Antigravity status (global, skills-based)
267
- const antigravityDetection = detectAntigravity()
268
350
  if (antigravityDetection.installed) {
269
351
  const status = antigravityDetection.skillInstalled
270
352
  ? chalk.green('✓ ready')
@@ -5,9 +5,9 @@
5
5
  * @module infrastructure/agent-detector
6
6
  */
7
7
 
8
- import fs from 'node:fs'
9
8
  import path from 'node:path'
10
9
  import type { DetectedAgent } from '../types'
10
+ import { fileExists } from '../utils/fs-helpers'
11
11
 
12
12
  declare const global: typeof globalThis & {
13
13
  mcp?: { filesystem?: unknown }
@@ -79,7 +79,7 @@ const TERMINAL_AGENT: DetectedAgent = {
79
79
 
80
80
  // ============ Detection Functions ============
81
81
 
82
- export function isClaudeEnvironment(): boolean {
82
+ export async function isClaudeEnvironment(): Promise<boolean> {
83
83
  // Environment variables
84
84
  if (process.env.CLAUDE_AGENT || process.env.ANTHROPIC_CLAUDE) return true
85
85
 
@@ -88,11 +88,11 @@ export function isClaudeEnvironment(): boolean {
88
88
 
89
89
  // Configuration files
90
90
  const projectRoot = process.cwd()
91
- if (fs.existsSync(path.join(projectRoot, 'CLAUDE.md'))) return true
91
+ if (await fileExists(path.join(projectRoot, 'CLAUDE.md'))) return true
92
92
 
93
93
  // Claude directory in home
94
94
  const homeDir = process.env.HOME || process.env.USERPROFILE || ''
95
- if (fs.existsSync(path.join(homeDir, '.claude'))) return true
95
+ if (await fileExists(path.join(homeDir, '.claude'))) return true
96
96
 
97
97
  // Filesystem paths
98
98
  const cwd = process.cwd()
@@ -112,7 +112,7 @@ export function getTerminalAgent(): DetectedAgent {
112
112
  export async function detect(): Promise<DetectedAgent> {
113
113
  if (cachedAgent) return cachedAgent
114
114
 
115
- cachedAgent = isClaudeEnvironment() ? getClaudeAgent() : getTerminalAgent()
115
+ cachedAgent = (await isClaudeEnvironment()) ? getClaudeAgent() : getTerminalAgent()
116
116
  return cachedAgent
117
117
  }
118
118
 
@@ -125,13 +125,13 @@ export function reset(): void {
125
125
  cachedAgent = null
126
126
  }
127
127
 
128
- export function isClaude(): boolean {
128
+ export async function isClaude(): Promise<boolean> {
129
129
  if (cachedAgent) return cachedAgent.type === 'claude'
130
130
  return isClaudeEnvironment()
131
131
  }
132
132
 
133
- export function isTerminal(): boolean {
134
- return !isClaude()
133
+ export async function isTerminal(): Promise<boolean> {
134
+ return !(await isClaude())
135
135
  }
136
136
 
137
137
  // ============ Default Export (backwards compat) ============
@@ -17,10 +17,14 @@
17
17
  * @see https://docs.windsurf.com/windsurf/cascade/memories
18
18
  */
19
19
 
20
- import { execSync } from 'node:child_process'
21
- import fs from 'node:fs'
20
+ import { exec } from 'node:child_process'
22
21
  import os from 'node:os'
23
22
  import path from 'node:path'
23
+ import { promisify } from 'node:util'
24
+ import { fileExists } from '../utils/fs-helpers'
25
+
26
+ const execAsync = promisify(exec)
27
+
24
28
  import type {
25
29
  AIProviderConfig,
26
30
  AIProviderName,
@@ -173,10 +177,10 @@ export const Providers: Record<AIProviderName, AIProviderConfig> = {
173
177
  /**
174
178
  * Check if a CLI command is available
175
179
  */
176
- function whichCommand(command: string): string | null {
180
+ async function whichCommand(command: string): Promise<string | null> {
177
181
  try {
178
- const result = execSync(`which ${command}`, { stdio: 'pipe', encoding: 'utf-8' })
179
- return result.trim()
182
+ const { stdout } = await execAsync(`which ${command}`)
183
+ return stdout.trim()
180
184
  } catch {
181
185
  return null
182
186
  }
@@ -185,12 +189,12 @@ function whichCommand(command: string): string | null {
185
189
  /**
186
190
  * Get CLI version
187
191
  */
188
- function getCliVersion(command: string): string | null {
192
+ async function getCliVersion(command: string): Promise<string | null> {
189
193
  try {
190
- const result = execSync(`${command} --version`, { stdio: 'pipe', encoding: 'utf-8' })
194
+ const { stdout } = await execAsync(`${command} --version`)
191
195
  // Extract version number from output (e.g., "claude 1.0.0" -> "1.0.0")
192
- const match = result.match(/\d+\.\d+\.\d+/)
193
- return match ? match[0] : result.trim()
196
+ const match = stdout.match(/\d+\.\d+\.\d+/)
197
+ return match ? match[0] : stdout.trim()
194
198
  } catch {
195
199
  return null
196
200
  }
@@ -200,7 +204,7 @@ function getCliVersion(command: string): string | null {
200
204
  * Detect if a specific CLI-based provider is installed
201
205
  * Note: Cursor is NOT a CLI, use detectCursorProject() instead
202
206
  */
203
- export function detectProvider(provider: AIProviderName): ProviderDetectionResult {
207
+ export async function detectProvider(provider: AIProviderName): Promise<ProviderDetectionResult> {
204
208
  const config = Providers[provider]
205
209
 
206
210
  // Cursor is not a CLI - return not installed for CLI detection
@@ -208,13 +212,13 @@ export function detectProvider(provider: AIProviderName): ProviderDetectionResul
208
212
  return { installed: false }
209
213
  }
210
214
 
211
- const cliPath = whichCommand(config.cliCommand)
215
+ const cliPath = await whichCommand(config.cliCommand)
212
216
 
213
217
  if (!cliPath) {
214
218
  return { installed: false }
215
219
  }
216
220
 
217
- const version = getCliVersion(config.cliCommand)
221
+ const version = await getCliVersion(config.cliCommand)
218
222
 
219
223
  return {
220
224
  installed: true,
@@ -227,14 +231,12 @@ export function detectProvider(provider: AIProviderName): ProviderDetectionResul
227
231
  * Detect all available CLI-based providers
228
232
  * Note: Cursor detection is project-level, use detectCursorProject() separately
229
233
  */
230
- export function detectAllProviders(): {
234
+ export async function detectAllProviders(): Promise<{
231
235
  claude: ProviderDetectionResult
232
236
  gemini: ProviderDetectionResult
233
- } {
234
- return {
235
- claude: detectProvider('claude'),
236
- gemini: detectProvider('gemini'),
237
- }
237
+ }> {
238
+ const [claude, gemini] = await Promise.all([detectProvider('claude'), detectProvider('gemini')])
239
+ return { claude, gemini }
238
240
  }
239
241
 
240
242
  /**
@@ -245,14 +247,16 @@ export function detectAllProviders(): {
245
247
  * 2. Auto-detect single installed provider
246
248
  * 3. Default to Claude if both installed (backward compatibility)
247
249
  */
248
- export function getActiveProvider(projectProvider?: AIProviderName): AIProviderConfig {
250
+ export async function getActiveProvider(
251
+ projectProvider?: AIProviderName
252
+ ): Promise<AIProviderConfig> {
249
253
  // If project has a saved preference, use it
250
254
  if (projectProvider && Providers[projectProvider]) {
251
255
  return Providers[projectProvider]
252
256
  }
253
257
 
254
258
  // Auto-detect
255
- const detection = detectAllProviders()
259
+ const detection = await detectAllProviders()
256
260
 
257
261
  // If only one is installed, use it
258
262
  if (detection.claude.installed && !detection.gemini.installed) {
@@ -270,12 +274,12 @@ export function getActiveProvider(projectProvider?: AIProviderName): AIProviderC
270
274
  * Check if config directory exists for a provider
271
275
  * Returns false for project-level providers (Cursor)
272
276
  */
273
- export function hasProviderConfig(provider: AIProviderName): boolean {
277
+ export async function hasProviderConfig(provider: AIProviderName): Promise<boolean> {
274
278
  const config = Providers[provider]
275
279
  if (!config.configDir) {
276
280
  return false // Cursor has no global config directory
277
281
  }
278
- return fs.existsSync(config.configDir)
282
+ return fileExists(config.configDir)
279
283
  }
280
284
 
281
285
  // =============================================================================
@@ -313,13 +317,15 @@ export function getProviderBranding(provider: AIProviderName): ProviderBranding
313
317
  * Cursor has NO global config (~/.cursor/ doesn't exist).
314
318
  * Detection is based on project-level .cursor/ directory.
315
319
  */
316
- export function detectCursorProject(projectRoot: string): CursorProjectDetection {
320
+ export async function detectCursorProject(projectRoot: string): Promise<CursorProjectDetection> {
317
321
  const cursorDir = path.join(projectRoot, '.cursor')
318
322
  const rulesDir = path.join(cursorDir, 'rules')
319
323
  const routerPath = path.join(rulesDir, 'prjct.mdc')
320
324
 
321
- const detected = fs.existsSync(cursorDir)
322
- const routerInstalled = fs.existsSync(routerPath)
325
+ const [detected, routerInstalled] = await Promise.all([
326
+ fileExists(cursorDir),
327
+ fileExists(routerPath),
328
+ ])
323
329
 
324
330
  return {
325
331
  detected,
@@ -331,8 +337,8 @@ export function detectCursorProject(projectRoot: string): CursorProjectDetection
331
337
  /**
332
338
  * Check if Cursor routers need to be regenerated
333
339
  */
334
- export function needsCursorRouterRegeneration(projectRoot: string): boolean {
335
- const detection = detectCursorProject(projectRoot)
340
+ export async function needsCursorRouterRegeneration(projectRoot: string): Promise<boolean> {
341
+ const detection = await detectCursorProject(projectRoot)
336
342
 
337
343
  // Only check if .cursor/ exists (project uses Cursor)
338
344
  // and prjct router is missing
@@ -349,13 +355,17 @@ export function needsCursorRouterRegeneration(projectRoot: string): boolean {
349
355
  * Windsurf has NO global config (~/.windsurf/ doesn't exist).
350
356
  * Detection is based on project-level .windsurf/ directory.
351
357
  */
352
- export function detectWindsurfProject(projectRoot: string): WindsurfProjectDetection {
358
+ export async function detectWindsurfProject(
359
+ projectRoot: string
360
+ ): Promise<WindsurfProjectDetection> {
353
361
  const windsurfDir = path.join(projectRoot, '.windsurf')
354
362
  const rulesDir = path.join(windsurfDir, 'rules')
355
363
  const routerPath = path.join(rulesDir, 'prjct.md')
356
364
 
357
- const detected = fs.existsSync(windsurfDir)
358
- const routerInstalled = fs.existsSync(routerPath)
365
+ const [detected, routerInstalled] = await Promise.all([
366
+ fileExists(windsurfDir),
367
+ fileExists(routerPath),
368
+ ])
359
369
 
360
370
  return {
361
371
  detected,
@@ -367,8 +377,8 @@ export function detectWindsurfProject(projectRoot: string): WindsurfProjectDetec
367
377
  /**
368
378
  * Check if Windsurf routers need to be regenerated
369
379
  */
370
- export function needsWindsurfRouterRegeneration(projectRoot: string): boolean {
371
- const detection = detectWindsurfProject(projectRoot)
380
+ export async function needsWindsurfRouterRegeneration(projectRoot: string): Promise<boolean> {
381
+ const detection = await detectWindsurfProject(projectRoot)
372
382
 
373
383
  // Only check if .windsurf/ exists (project uses Windsurf)
374
384
  // and prjct router is missing
@@ -399,15 +409,17 @@ export interface AntigravityDetection {
399
409
  * Antigravity is NOT a CLI command - it's a GUI platform.
400
410
  * Detection is based on ~/.gemini/antigravity/ directory.
401
411
  */
402
- export function detectAntigravity(): AntigravityDetection {
412
+ export async function detectAntigravity(): Promise<AntigravityDetection> {
403
413
  const configPath = AntigravityProvider.configDir
404
414
  if (!configPath) {
405
415
  return { installed: false, skillInstalled: false }
406
416
  }
407
417
 
408
- const installed = fs.existsSync(configPath)
409
418
  const skillPath = path.join(configPath, 'skills', 'prjct', 'SKILL.md')
410
- const skillInstalled = fs.existsSync(skillPath)
419
+ const [installed, skillInstalled] = await Promise.all([
420
+ fileExists(configPath),
421
+ fileExists(skillPath),
422
+ ])
411
423
 
412
424
  return {
413
425
  installed,
@@ -475,8 +487,8 @@ export function getProjectCommandsPath(provider: AIProviderName, projectRoot: st
475
487
  * Determine which provider to use during setup
476
488
  * Returns selection result with detection details
477
489
  */
478
- export function selectProvider(): ProviderSelectionResult {
479
- const detection = detectAllProviders()
490
+ export async function selectProvider(): Promise<ProviderSelectionResult> {
491
+ const detection = await detectAllProviders()
480
492
 
481
493
  const claudeInstalled = detection.claude.installed
482
494
  const geminiInstalled = detection.gemini.installed
@@ -159,11 +159,11 @@ export async function installDocs(): Promise<{ success: boolean; error?: string
159
159
  */
160
160
  export async function installGlobalConfig(): Promise<GlobalConfigResult> {
161
161
  const aiProvider = require('./ai-provider')
162
- const activeProvider = aiProvider.getActiveProvider()
162
+ const activeProvider = await aiProvider.getActiveProvider()
163
163
  const providerName = activeProvider.name
164
164
 
165
165
  // Check if provider is installed
166
- const detection = aiProvider.detectProvider(providerName)
166
+ const detection = await aiProvider.detectProvider(providerName)
167
167
  if (!detection.installed && !activeProvider.configDir) {
168
168
  return {
169
169
  success: false,
@@ -289,15 +289,21 @@ export async function installGlobalConfig(): Promise<GlobalConfigResult> {
289
289
 
290
290
  export class CommandInstaller {
291
291
  homeDir: string
292
- claudeCommandsPath: string
293
- claudeConfigPath: string
292
+ claudeCommandsPath = ''
293
+ claudeConfigPath = ''
294
294
  templatesDir: string
295
+ private _initialized = false
295
296
 
296
297
  constructor() {
297
298
  this.homeDir = os.homedir()
299
+ this.templatesDir = path.join(getPackageRoot(), 'templates', 'commands')
300
+ }
301
+
302
+ private async ensureInit(): Promise<void> {
303
+ if (this._initialized) return
298
304
 
299
305
  const aiProvider = require('./ai-provider')
300
- const activeProvider = aiProvider.getActiveProvider()
306
+ const activeProvider = await aiProvider.getActiveProvider()
301
307
 
302
308
  // Command paths are provider-specific
303
309
  if (activeProvider.name === 'gemini') {
@@ -308,13 +314,14 @@ export class CommandInstaller {
308
314
  }
309
315
 
310
316
  this.claudeConfigPath = activeProvider.configDir
311
- this.templatesDir = path.join(getPackageRoot(), 'templates', 'commands')
317
+ this._initialized = true
312
318
  }
313
319
 
314
320
  /**
315
321
  * Detect if active provider is installed
316
322
  */
317
323
  async detectActiveProvider(): Promise<boolean> {
324
+ await this.ensureInit()
318
325
  try {
319
326
  await fs.access(this.claudeConfigPath)
320
327
  return true
@@ -372,7 +379,7 @@ export class CommandInstaller {
372
379
  async installCommands(): Promise<InstallResult> {
373
380
  const providerDetected = await this.detectActiveProvider()
374
381
  const aiProvider = require('./ai-provider')
375
- const activeProvider = aiProvider.getActiveProvider()
382
+ const activeProvider = await aiProvider.getActiveProvider()
376
383
 
377
384
  if (!providerDetected) {
378
385
  return {
@@ -522,7 +529,8 @@ export class CommandInstaller {
522
529
  /**
523
530
  * Get installation path for Claude commands
524
531
  */
525
- getInstallPath(): string {
532
+ async getInstallPath(): Promise<string> {
533
+ await this.ensureInit()
526
534
  return this.claudeCommandsPath
527
535
  }
528
536
 
@@ -548,7 +556,7 @@ export class CommandInstaller {
548
556
  */
549
557
  async installRouter(): Promise<boolean> {
550
558
  const aiProvider = require('./ai-provider')
551
- const activeProvider = aiProvider.getActiveProvider()
559
+ const activeProvider = await aiProvider.getActiveProvider()
552
560
  const routerFile = activeProvider.name === 'gemini' ? 'p.toml' : 'p.md'
553
561
 
554
562
  try {
@@ -575,7 +583,7 @@ export class CommandInstaller {
575
583
  */
576
584
  async removeLegacyCommands(): Promise<number> {
577
585
  const aiProvider = require('./ai-provider')
578
- const activeProvider = aiProvider.getActiveProvider()
586
+ const activeProvider = await aiProvider.getActiveProvider()
579
587
  const commandsRoot = path.join(activeProvider.configDir, 'commands')
580
588
 
581
589
  let removed = 0