1+ /** @fileoverview Interactive ask mode for no-command scenarios */
2+
3+ import { spawn } from 'node:child_process'
4+ import process from 'node:process'
5+
6+ import colors from 'yoctocolors-cjs'
7+
8+ import isInteractive from '@socketregistry/is-interactive/index.cjs'
9+ import { logger } from '@socketsecurity/registry/lib/logger'
10+ import { confirm , input , select } from '@socketsecurity/registry/lib/prompts'
11+
12+ import type { Choice } from '@socketsecurity/registry/lib/prompts'
13+
14+ interface CommonAction {
15+ name : string
16+ description : string
17+ command : string [ ]
18+ category : string
19+ }
20+
21+ const COMMON_ACTIONS : CommonAction [ ] = [
22+ // Security Scanning
23+ {
24+ name : 'Scan this project' ,
25+ description : 'Create a security scan of current directory' ,
26+ command : [ 'scan' , 'create' , '.' ] ,
27+ category : 'Scanning' ,
28+ } ,
29+ {
30+ name : 'Scan production dependencies only' ,
31+ description : 'Scan only production deps' ,
32+ command : [ 'scan' , 'create' , '.' , '--prod' ] ,
33+ category : 'Scanning' ,
34+ } ,
35+ {
36+ name : 'View recent scans' ,
37+ description : 'List your recent security scans' ,
38+ command : [ 'scan' , 'list' ] ,
39+ category : 'Scanning' ,
40+ } ,
41+
42+ // Fixing & Optimization
43+ {
44+ name : 'Fix vulnerabilities' ,
45+ description : 'Interactive vulnerability fixing' ,
46+ command : [ 'fix' , 'interactive' ] ,
47+ category : 'Fixing' ,
48+ } ,
49+ {
50+ name : 'Auto-fix safe issues' ,
51+ description : 'Automatically apply safe fixes' ,
52+ command : [ 'fix' , '--auto' ] ,
53+ category : 'Fixing' ,
54+ } ,
55+ {
56+ name : 'Optimize dependencies' ,
57+ description : 'Clean up and optimize packages' ,
58+ command : [ 'optimize' , '.' ] ,
59+ category : 'Optimization' ,
60+ } ,
61+
62+ // Package Management
63+ {
64+ name : 'Check package security' ,
65+ description : 'Get security score for a package' ,
66+ command : [ 'package' , 'score' ] ,
67+ category : 'Packages' ,
68+ } ,
69+ {
70+ name : 'Install with npm wrapper' ,
71+ description : 'Secure npm install' ,
72+ command : [ 'npm' , 'install' ] ,
73+ category : 'Packages' ,
74+ } ,
75+
76+ // Authentication & Config
77+ {
78+ name : 'Log in to Socket' ,
79+ description : 'Authenticate with Socket.dev' ,
80+ command : [ 'login' ] ,
81+ category : 'Setup' ,
82+ } ,
83+ {
84+ name : 'View configuration' ,
85+ description : 'Show current CLI configuration' ,
86+ command : [ 'config' , 'list' ] ,
87+ category : 'Setup' ,
88+ } ,
89+ {
90+ name : 'Who am I?' ,
91+ description : 'Show current user information' ,
92+ command : [ 'whoami' ] ,
93+ category : 'Setup' ,
94+ } ,
95+
96+ // Help
97+ {
98+ name : 'View help documentation' ,
99+ description : 'Interactive help system' ,
100+ command : [ '--help' ] ,
101+ category : 'Help' ,
102+ } ,
103+ {
104+ name : 'Natural language query' ,
105+ description : 'Describe what you want in plain English' ,
106+ command : [ 'ask' ] ,
107+ category : 'Help' ,
108+ } ,
109+ ]
110+
111+ /**
112+ * Parse natural language input to find matching commands
113+ */
114+ function parseNaturalInput ( query : string ) : CommonAction | null {
115+ const normalized = query . toLowerCase ( ) . trim ( )
116+
117+ // Direct command patterns
118+ const patterns : Array < { pattern : RegExp ; action : CommonAction } > = [
119+ {
120+ pattern : / ^ ( s c a n | c h e c k | a n a l y z e ) ( \s | $ ) / i,
121+ action : COMMON_ACTIONS . find ( a => a . name === 'Scan this project' ) ! ,
122+ } ,
123+ {
124+ pattern : / ^ f i x ( \s | $ ) / i,
125+ action : COMMON_ACTIONS . find ( a => a . name === 'Fix vulnerabilities' ) ! ,
126+ } ,
127+ {
128+ pattern : / ^ o p t i m i z e ( \s | $ ) / i,
129+ action : COMMON_ACTIONS . find ( a => a . name === 'Optimize dependencies' ) ! ,
130+ } ,
131+ {
132+ pattern : / ^ ( l o g i n | a u t h e n t i c a t e ) ( \s | $ ) / i,
133+ action : COMMON_ACTIONS . find ( a => a . name === 'Log in to Socket' ) ! ,
134+ } ,
135+ {
136+ pattern : / ^ ( h e l p | \? ) ( \s | $ ) / i,
137+ action : COMMON_ACTIONS . find ( a => a . name === 'View help documentation' ) ! ,
138+ } ,
139+ ]
140+
141+ for ( const { action, pattern } of patterns ) {
142+ if ( pattern . test ( normalized ) ) {
143+ return action
144+ }
145+ }
146+
147+ // Try to match against action names
148+ for ( const action of COMMON_ACTIONS ) {
149+ if ( normalized . includes ( action . name . toLowerCase ( ) ) ) {
150+ return action
151+ }
152+ }
153+
154+ return null
155+ }
156+
157+ /**
158+ * Execute a Socket CLI command
159+ */
160+ async function executeCommand ( args : string [ ] ) : Promise < void > {
161+ return await new Promise ( ( resolve , reject ) => {
162+ const child = spawn ( process . argv [ 0 ] ! , [ process . argv [ 1 ] ! , ...args ] , {
163+ stdio : 'inherit' ,
164+ env : process . env ,
165+ } )
166+
167+ child . on ( 'error' , reject )
168+ child . on ( 'exit' , ( code : number | null ) => {
169+ process . exitCode = code || 0
170+ resolve ( )
171+ } )
172+ } )
173+ }
174+
175+ /**
176+ * Run the interactive ask mode
177+ */
178+ export async function runAskMode ( ) : Promise < void > {
179+ // Non-interactive fallback
180+ if ( ! isInteractive ( ) ) {
181+ // Add spacing before the prompt for better visual separation
182+ logger . log ( '' )
183+ logger . log ( colors . bold ( 'What would you like to do?' ) )
184+ logger . log ( '' )
185+ logger . log ( 'Common actions:' )
186+ logger . log ( ' socket scan create . # Scan this project' )
187+ logger . log ( ' socket fix interactive # Fix vulnerabilities' )
188+ logger . log ( ' socket optimize . # Optimize dependencies' )
189+ logger . log ( ' socket login # Authenticate' )
190+ logger . log ( ' socket --help # View help' )
191+ logger . log ( '' )
192+ logger . log ( colors . gray ( '💡 Run in an interactive terminal for a better experience' ) )
193+ return
194+ }
195+
196+ // First, ask what they want to do
197+ const query = await input ( {
198+ message : 'What would you like to do? (describe in plain English or press Enter for options)' ,
199+ } )
200+
201+ let selectedAction : CommonAction | null = null
202+
203+ if ( query && query . trim ( ) ) {
204+ // Try to parse natural language
205+ selectedAction = parseNaturalInput ( query )
206+
207+ if ( ! selectedAction ) {
208+ // If we can't parse it, pass it to the ask command
209+ logger . log ( '' )
210+ logger . log ( colors . cyan ( 'Running natural language interpreter...' ) )
211+ logger . log ( '' )
212+ await executeCommand ( [ 'ask' , query ] )
213+ return
214+ }
215+ } else {
216+ // Show action menu
217+ const categories = [ ...new Set ( COMMON_ACTIONS . map ( a => a . category ) ) ]
218+ const choices : Array < Choice < CommonAction | string > > = [ ]
219+
220+ for ( const category of categories ) {
221+ // Add category separator
222+ choices . push ( {
223+ name : colors . bold ( colors . cyan ( `── ${ category } ──` ) ) ,
224+ value : `separator-${ category } ` ,
225+ disabled : true ,
226+ } as any )
227+
228+ // Add actions in this category
229+ const categoryActions = COMMON_ACTIONS . filter ( a => a . category === category )
230+ for ( const action of categoryActions ) {
231+ choices . push ( {
232+ name : ` ${ action . name } ` ,
233+ value : action ,
234+ short : action . name ,
235+ description : colors . gray ( action . description ) ,
236+ } )
237+ }
238+ }
239+
240+ const selected = await select ( {
241+ message : 'What would you like to do?' ,
242+ choices,
243+ } )
244+
245+ if ( ! selected || typeof selected === 'string' ) {
246+ return
247+ }
248+
249+ selectedAction = selected as CommonAction
250+ }
251+
252+ if ( ! selectedAction ) {
253+ return
254+ }
255+
256+ // Show the command that will be executed
257+ const commandStr = `socket ${ selectedAction . command . join ( ' ' ) } `
258+
259+ logger . log ( '' )
260+ logger . log ( colors . cyan ( 'Command:' ) + ` ${ colors . bold ( commandStr ) } ` )
261+
262+ // Special handling for commands that need additional input
263+ const finalCommand = [ ...selectedAction . command ]
264+
265+ if ( selectedAction . name === 'Check package security' ) {
266+ const pkg = await input ( {
267+ message : 'Which package would you like to check?' ,
268+ } )
269+ if ( ! pkg ) {
270+ return
271+ }
272+ finalCommand . push ( pkg )
273+ } else if ( selectedAction . name === 'Install with npm wrapper' ) {
274+ const pkg = await input ( {
275+ message : 'What would you like to install? (package name or leave empty for all)' ,
276+ } )
277+ if ( pkg ) {
278+ finalCommand . push ( pkg )
279+ }
280+ } else if ( selectedAction . name === 'Natural language query' ) {
281+ const nlQuery = await input ( {
282+ message : 'What would you like to do? (describe in plain English)' ,
283+ } )
284+ if ( ! nlQuery ) {
285+ return
286+ }
287+ finalCommand . push ( nlQuery )
288+ }
289+
290+ // Update command display if modified
291+ if ( finalCommand . length !== selectedAction . command . length ) {
292+ const updatedCommandStr = `socket ${ finalCommand . join ( ' ' ) } `
293+ logger . log ( colors . gray ( 'Updated:' ) + ` ${ colors . bold ( updatedCommandStr ) } ` )
294+ }
295+
296+ // Confirm execution
297+ const shouldExecute = await confirm ( {
298+ message : 'Execute this command?' ,
299+ default : true ,
300+ } )
301+
302+ if ( shouldExecute ) {
303+ logger . log ( '' )
304+ await executeCommand ( finalCommand )
305+ }
306+ }
0 commit comments