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