prjct-cli 1.6.10 → 1.6.11
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 +34 -1
- package/bin/prjct.ts +11 -2
- package/core/infrastructure/ai-provider.ts +17 -5
- package/core/utils/provider-cache.ts +49 -0
- package/dist/bin/prjct.mjs +1103 -1044
- package/dist/core/infrastructure/command-installer.js +140 -95
- package/dist/core/infrastructure/setup.js +260 -215
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,12 +1,45 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.6.11] - 2026-02-07
|
|
4
|
+
|
|
5
|
+
### Performance
|
|
6
|
+
|
|
7
|
+
- cache provider detection to eliminate redundant shell spawns (PRJ-289) (#136)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## [1.6.13] - 2026-02-07
|
|
11
|
+
|
|
12
|
+
### Improvements
|
|
13
|
+
- **Cache provider detection to eliminate redundant shell spawns (PRJ-289)**: Provider detection results (Claude, Gemini CLI availability) are now cached to `~/.prjct-cli/cache/providers.json` with a 10-minute TTL. Subsequent CLI commands skip shell spawns entirely. Added 2-second timeout on `which`/`--version` spawns to prevent hangs. Added `--refresh` flag to force re-detection.
|
|
14
|
+
|
|
15
|
+
### Implementation Details
|
|
16
|
+
Created `core/utils/provider-cache.ts` with `readProviderCache()`, `writeProviderCache()`, and `invalidateProviderCache()`. Wired into `detectAllProviders()` — checks cache first, falls through to shell detection on miss or expiry, writes cache after detection. `bin/prjct.ts` parses `--refresh` early (like `--quiet`), invalidates cache, and passes refresh flag to detection.
|
|
17
|
+
|
|
18
|
+
### Learnings
|
|
19
|
+
- `execAsync` accepts a `timeout` option (milliseconds) that kills the child process on expiry — ideal for preventing hangs on broken CLI installations.
|
|
20
|
+
- Biome enforces `Array#indexOf()` over `Array#findIndex()` for simple equality checks (`useIndexOf` rule).
|
|
21
|
+
- Separating cache logic into its own module keeps `ai-provider.ts` focused on detection logic.
|
|
22
|
+
|
|
23
|
+
### Test Plan
|
|
24
|
+
|
|
25
|
+
#### For QA
|
|
26
|
+
1. Run `prjct --version` twice — second run should be near-instant (cache hit)
|
|
27
|
+
2. Delete `~/.prjct-cli/cache/providers.json`, run `prjct --version` — should re-detect and recreate cache
|
|
28
|
+
3. Run `prjct --version --refresh` — should take ~2s (forced re-detection)
|
|
29
|
+
4. Edit cache file to set timestamp 11 minutes ago — next command should re-detect (TTL expired)
|
|
30
|
+
5. Run `prjct sync` — should use cached providers, no shell spawns
|
|
31
|
+
|
|
32
|
+
#### For Users
|
|
33
|
+
**What changed:** Provider detection is now cached for 10 minutes. CLI startup is ~30x faster for cached commands (~66ms vs ~2100ms).
|
|
34
|
+
**How to use:** Automatic. Use `--refresh` to force re-detection after installing a new CLI.
|
|
35
|
+
**Breaking changes:** None.
|
|
36
|
+
|
|
3
37
|
## [1.6.10] - 2026-02-07
|
|
4
38
|
|
|
5
39
|
### Bug Fixes
|
|
6
40
|
|
|
7
41
|
- resolve signal handler and EventBus listener accumulation leaks (PRJ-287) (#135)
|
|
8
42
|
|
|
9
|
-
|
|
10
43
|
## [1.6.12] - 2026-02-07
|
|
11
44
|
|
|
12
45
|
### Bug Fixes
|
package/bin/prjct.ts
CHANGED
|
@@ -16,6 +16,7 @@ import configManager from '../core/infrastructure/config-manager'
|
|
|
16
16
|
import editorsConfig from '../core/infrastructure/editors-config'
|
|
17
17
|
import { DEFAULT_PORT, startServer } from '../core/server/server'
|
|
18
18
|
import { fileExists } from '../core/utils/fs-helpers'
|
|
19
|
+
import { invalidateProviderCache } from '../core/utils/provider-cache'
|
|
19
20
|
import { VERSION } from '../core/utils/version'
|
|
20
21
|
|
|
21
22
|
/**
|
|
@@ -62,6 +63,14 @@ if (isQuietMode) {
|
|
|
62
63
|
setQuietMode(true)
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
// Parse --refresh flag (force re-detection of providers, invalidate cache)
|
|
67
|
+
const refreshIndex = args.indexOf('--refresh')
|
|
68
|
+
const isRefresh = refreshIndex !== -1
|
|
69
|
+
if (isRefresh) {
|
|
70
|
+
args.splice(refreshIndex, 1)
|
|
71
|
+
await invalidateProviderCache()
|
|
72
|
+
}
|
|
73
|
+
|
|
65
74
|
// Colors for output (chalk respects NO_COLOR env)
|
|
66
75
|
|
|
67
76
|
// Session tracking for commands that bypass core/index.ts
|
|
@@ -217,8 +226,8 @@ if (args[0] === 'start' || args[0] === 'setup') {
|
|
|
217
226
|
console.log(getHelp(topic))
|
|
218
227
|
process.exitCode = 0
|
|
219
228
|
} else if (args[0] === 'version' || args[0] === '-v' || args[0] === '--version') {
|
|
220
|
-
// Show version with provider status
|
|
221
|
-
const detection = await detectAllProviders()
|
|
229
|
+
// Show version with provider status (uses cached detection unless --refresh)
|
|
230
|
+
const detection = await detectAllProviders(isRefresh)
|
|
222
231
|
const home = os.homedir()
|
|
223
232
|
const cwd = process.cwd()
|
|
224
233
|
const [
|
|
@@ -22,8 +22,10 @@ import os from 'node:os'
|
|
|
22
22
|
import path from 'node:path'
|
|
23
23
|
import { promisify } from 'node:util'
|
|
24
24
|
import { fileExists } from '../utils/fs-helpers'
|
|
25
|
+
import { readProviderCache, writeProviderCache } from '../utils/provider-cache'
|
|
25
26
|
|
|
26
27
|
const execAsync = promisify(exec)
|
|
28
|
+
const SPAWN_TIMEOUT_MS = 2000
|
|
27
29
|
|
|
28
30
|
import type {
|
|
29
31
|
AIProviderConfig,
|
|
@@ -179,7 +181,7 @@ export const Providers: Record<AIProviderName, AIProviderConfig> = {
|
|
|
179
181
|
*/
|
|
180
182
|
async function whichCommand(command: string): Promise<string | null> {
|
|
181
183
|
try {
|
|
182
|
-
const { stdout } = await execAsync(`which ${command}
|
|
184
|
+
const { stdout } = await execAsync(`which ${command}`, { timeout: SPAWN_TIMEOUT_MS })
|
|
183
185
|
return stdout.trim()
|
|
184
186
|
} catch {
|
|
185
187
|
return null
|
|
@@ -191,7 +193,7 @@ async function whichCommand(command: string): Promise<string | null> {
|
|
|
191
193
|
*/
|
|
192
194
|
async function getCliVersion(command: string): Promise<string | null> {
|
|
193
195
|
try {
|
|
194
|
-
const { stdout } = await execAsync(`${command} --version
|
|
196
|
+
const { stdout } = await execAsync(`${command} --version`, { timeout: SPAWN_TIMEOUT_MS })
|
|
195
197
|
// Extract version number from output (e.g., "claude 1.0.0" -> "1.0.0")
|
|
196
198
|
const match = stdout.match(/\d+\.\d+\.\d+/)
|
|
197
199
|
return match ? match[0] : stdout.trim()
|
|
@@ -229,14 +231,24 @@ export async function detectProvider(provider: AIProviderName): Promise<Provider
|
|
|
229
231
|
|
|
230
232
|
/**
|
|
231
233
|
* Detect all available CLI-based providers
|
|
232
|
-
*
|
|
234
|
+
* Results are cached to disk with a 10-minute TTL to avoid redundant shell spawns.
|
|
235
|
+
* Pass refresh=true to force re-detection.
|
|
233
236
|
*/
|
|
234
|
-
export async function detectAllProviders(): Promise<{
|
|
237
|
+
export async function detectAllProviders(refresh = false): Promise<{
|
|
235
238
|
claude: ProviderDetectionResult
|
|
236
239
|
gemini: ProviderDetectionResult
|
|
237
240
|
}> {
|
|
241
|
+
if (!refresh) {
|
|
242
|
+
const cached = await readProviderCache()
|
|
243
|
+
if (cached) return cached
|
|
244
|
+
}
|
|
245
|
+
|
|
238
246
|
const [claude, gemini] = await Promise.all([detectProvider('claude'), detectProvider('gemini')])
|
|
239
|
-
|
|
247
|
+
const detection = { claude, gemini }
|
|
248
|
+
|
|
249
|
+
await writeProviderCache(detection).catch(() => {})
|
|
250
|
+
|
|
251
|
+
return detection
|
|
240
252
|
}
|
|
241
253
|
|
|
242
254
|
/**
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import fs from 'node:fs/promises'
|
|
2
|
+
import os from 'node:os'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import type { ProviderDetectionResult } from '../types/provider'
|
|
5
|
+
|
|
6
|
+
const CACHE_DIR = path.join(os.homedir(), '.prjct-cli', 'cache')
|
|
7
|
+
const CACHE_FILE = path.join(CACHE_DIR, 'providers.json')
|
|
8
|
+
const TTL_MS = 10 * 60 * 1000 // 10 minutes
|
|
9
|
+
|
|
10
|
+
interface ProviderCache {
|
|
11
|
+
timestamp: string
|
|
12
|
+
detection: {
|
|
13
|
+
claude: ProviderDetectionResult
|
|
14
|
+
gemini: ProviderDetectionResult
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function readProviderCache(): Promise<ProviderCache['detection'] | null> {
|
|
19
|
+
try {
|
|
20
|
+
const raw = await fs.readFile(CACHE_FILE, 'utf-8')
|
|
21
|
+
const cache: ProviderCache = JSON.parse(raw)
|
|
22
|
+
|
|
23
|
+
if (!cache.timestamp || !cache.detection) return null
|
|
24
|
+
|
|
25
|
+
const age = Date.now() - new Date(cache.timestamp).getTime()
|
|
26
|
+
if (age > TTL_MS) return null
|
|
27
|
+
|
|
28
|
+
return cache.detection
|
|
29
|
+
} catch {
|
|
30
|
+
return null
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export async function writeProviderCache(detection: ProviderCache['detection']): Promise<void> {
|
|
35
|
+
const cache: ProviderCache = {
|
|
36
|
+
timestamp: new Date().toISOString(),
|
|
37
|
+
detection,
|
|
38
|
+
}
|
|
39
|
+
await fs.mkdir(CACHE_DIR, { recursive: true })
|
|
40
|
+
await fs.writeFile(CACHE_FILE, JSON.stringify(cache, null, 2))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export async function invalidateProviderCache(): Promise<void> {
|
|
44
|
+
try {
|
|
45
|
+
await fs.unlink(CACHE_FILE)
|
|
46
|
+
} catch {
|
|
47
|
+
// File doesn't exist — fine
|
|
48
|
+
}
|
|
49
|
+
}
|