Skip to content

Commit 7f5ad4e

Browse files
committed
feat: add project context awareness and rich progress utilities
- Add project context detection for package managers and frameworks - Add rich progress indicators for better UX during long operations - Create foundation for Claude CLI-like enhancements - Support for multi-progress bars, spinners, and file progress - Auto-detect npm/yarn/pnpm and provide contextual suggestions
1 parent bc1da0e commit 7f5ad4e

File tree

2 files changed

+428
-0
lines changed

2 files changed

+428
-0
lines changed

src/utils/project-context.mts

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
/** @fileoverview Project context awareness for better CLI UX. */
2+
3+
import { existsSync } from 'node:fs'
4+
import { readFile } from 'node:fs/promises'
5+
import { join, dirname } from 'node:path'
6+
7+
interface ProjectContext {
8+
type: 'npm' | 'yarn' | 'pnpm' | 'unknown'
9+
root: string
10+
packageManager?: string
11+
hasLockFile: boolean
12+
framework?: string
13+
isMonorepo: boolean
14+
workspaces?: string[]
15+
}
16+
17+
/**
18+
* Detect the package manager being used in the project
19+
*/
20+
export async function detectPackageManager(cwd: string): Promise<ProjectContext['type']> {
21+
// Check for lock files
22+
if (existsSync(join(cwd, 'pnpm-lock.yaml'))) return 'pnpm'
23+
if (existsSync(join(cwd, 'yarn.lock'))) return 'yarn'
24+
if (existsSync(join(cwd, 'package-lock.json'))) return 'npm'
25+
26+
// Check packageManager field in package.json
27+
const pkgPath = join(cwd, 'package.json')
28+
if (existsSync(pkgPath)) {
29+
try {
30+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
31+
if (pkg.packageManager) {
32+
if (pkg.packageManager.startsWith('pnpm')) return 'pnpm'
33+
if (pkg.packageManager.startsWith('yarn')) return 'yarn'
34+
if (pkg.packageManager.startsWith('npm')) return 'npm'
35+
}
36+
} catch {
37+
// Ignore parse errors
38+
}
39+
}
40+
41+
return 'unknown'
42+
}
43+
44+
/**
45+
* Find the project root by looking for package.json
46+
*/
47+
export async function findProjectRoot(startDir: string): Promise<string | null> {
48+
let currentDir = startDir
49+
50+
while (currentDir !== dirname(currentDir)) {
51+
if (existsSync(join(currentDir, 'package.json'))) {
52+
return currentDir
53+
}
54+
currentDir = dirname(currentDir)
55+
}
56+
57+
return null
58+
}
59+
60+
/**
61+
* Detect if this is a monorepo
62+
*/
63+
async function isMonorepo(root: string): Promise<boolean> {
64+
const pkgPath = join(root, 'package.json')
65+
if (!existsSync(pkgPath)) return false
66+
67+
try {
68+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
69+
// Check for workspaces (npm/yarn/pnpm)
70+
if (pkg.workspaces) return true
71+
72+
// Check for lerna
73+
if (existsSync(join(root, 'lerna.json'))) return true
74+
75+
// Check for rush
76+
if (existsSync(join(root, 'rush.json'))) return true
77+
78+
// Check for nx
79+
if (existsSync(join(root, 'nx.json'))) return true
80+
81+
// Check for pnpm workspaces
82+
if (existsSync(join(root, 'pnpm-workspace.yaml'))) return true
83+
} catch {
84+
// Ignore errors
85+
}
86+
87+
return false
88+
}
89+
90+
/**
91+
* Detect the framework being used
92+
*/
93+
async function detectFramework(root: string): Promise<string | undefined> {
94+
const pkgPath = join(root, 'package.json')
95+
if (!existsSync(pkgPath)) return undefined
96+
97+
try {
98+
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'))
99+
const deps = { ...pkg.dependencies, ...pkg.devDependencies }
100+
101+
// React-based
102+
if (deps['next']) return 'next'
103+
if (deps['react']) return 'react'
104+
105+
// Vue-based
106+
if (deps['nuxt']) return 'nuxt'
107+
if (deps['vue']) return 'vue'
108+
109+
// Angular
110+
if (deps['@angular/core']) return 'angular'
111+
112+
// Svelte
113+
if (deps['svelte'] || deps['@sveltejs/kit']) return 'svelte'
114+
115+
// Node.js frameworks
116+
if (deps['express']) return 'express'
117+
if (deps['fastify']) return 'fastify'
118+
if (deps['koa']) return 'koa'
119+
120+
// Static site generators
121+
if (deps['gatsby']) return 'gatsby'
122+
if (deps['@11ty/eleventy']) return 'eleventy'
123+
124+
} catch {
125+
// Ignore errors
126+
}
127+
128+
return undefined
129+
}
130+
131+
/**
132+
* Get the full project context
133+
*/
134+
export async function getProjectContext(cwd: string = process.cwd()): Promise<ProjectContext | null> {
135+
const root = await findProjectRoot(cwd)
136+
if (!root) {
137+
return null
138+
}
139+
140+
const [packageManager, monorepo, framework] = await Promise.all([
141+
detectPackageManager(root),
142+
isMonorepo(root),
143+
detectFramework(root),
144+
])
145+
146+
const hasLockFile = ['npm', 'yarn', 'pnpm'].includes(packageManager)
147+
148+
return {
149+
type: packageManager,
150+
root,
151+
hasLockFile,
152+
framework,
153+
isMonorepo: monorepo,
154+
}
155+
}
156+
157+
/**
158+
* Get smart suggestions based on project context
159+
*/
160+
export function getContextualSuggestions(context: ProjectContext): string[] {
161+
const suggestions: string[] = []
162+
163+
// Package manager specific
164+
if (context.type === 'pnpm' && context.isMonorepo) {
165+
suggestions.push('Use `socket pnpm --recursive` to scan all workspaces')
166+
}
167+
168+
// Framework specific
169+
if (context.framework === 'next') {
170+
suggestions.push('Consider using `socket scan --prod` to exclude dev dependencies')
171+
}
172+
173+
// Lock file missing
174+
if (!context.hasLockFile) {
175+
suggestions.push(`Run \`${context.type} install\` to generate a lock file for accurate scanning`)
176+
}
177+
178+
return suggestions
179+
}

0 commit comments

Comments
 (0)