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.
- package/CHANGELOG.md +123 -1
- package/bin/prjct.ts +23 -14
- package/core/__tests__/agentic/command-executor.test.ts +19 -19
- package/core/__tests__/agentic/prompt-builder.test.ts +16 -16
- package/core/__tests__/ai-tools/formatters.test.ts +118 -0
- package/core/agentic/command-executor.ts +18 -17
- package/core/agentic/prompt-builder.ts +18 -17
- package/core/agentic/template-executor.ts +2 -2
- package/core/ai-tools/formatters.ts +18 -0
- package/core/ai-tools/registry.ts +17 -14
- package/core/cli/start.ts +18 -17
- package/core/commands/analysis.ts +1 -1
- package/core/commands/setup.ts +8 -8
- package/core/commands/uninstall.ts +11 -11
- package/core/index.ts +103 -21
- package/core/infrastructure/agent-detector.ts +8 -8
- package/core/infrastructure/ai-provider.ts +49 -37
- package/core/infrastructure/command-installer.ts +18 -10
- package/core/infrastructure/path-manager.ts +4 -4
- package/core/infrastructure/setup.ts +124 -119
- package/core/infrastructure/update-checker.ts +14 -13
- package/core/integrations/linear/sync.ts +4 -4
- package/core/services/context-generator.ts +12 -3
- package/core/services/hooks-service.ts +78 -68
- package/core/services/sync-service.ts +64 -6
- package/core/utils/citations.ts +53 -0
- package/core/utils/error-messages.ts +11 -0
- package/core/utils/fs-helpers.ts +14 -0
- package/core/utils/project-credentials.ts +8 -7
- package/dist/bin/prjct.mjs +854 -643
- package/dist/core/infrastructure/command-installer.js +118 -87
- package/dist/core/infrastructure/setup.js +246 -210
- 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 =
|
|
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 =
|
|
155
|
+
const claudeMdExists = await fileExists(claudeMdPath)
|
|
156
156
|
let hasPrjctSection = false
|
|
157
157
|
|
|
158
158
|
if (claudeMdExists) {
|
|
159
159
|
try {
|
|
160
|
-
const content =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
222
|
+
const geminiMdExists = await fileExists(geminiMdPath)
|
|
223
223
|
let hasGeminiPrjctSection = false
|
|
224
224
|
|
|
225
225
|
if (geminiMdExists) {
|
|
226
226
|
try {
|
|
227
|
-
const content =
|
|
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 (
|
|
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
|
-
|
|
57
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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.
|
|
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 =
|
|
236
|
-
|
|
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
|
-
//
|
|
239
|
-
const
|
|
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 (
|
|
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 (
|
|
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 {
|
|
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
|
|
179
|
-
return
|
|
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
|
|
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 =
|
|
193
|
-
return match ? match[0] :
|
|
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
|
-
|
|
235
|
-
|
|
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(
|
|
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
|
|
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 =
|
|
322
|
-
|
|
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(
|
|
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 =
|
|
358
|
-
|
|
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 =
|
|
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
|
|
293
|
-
claudeConfigPath
|
|
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.
|
|
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
|