Skip to content

Commit 4c163ca

Browse files
committed
fix: display banner when launching socket with no arguments
Prevent Python CLI forwarding for no-args case to ensure the Socket ASCII banner displays when running socket without any commands
1 parent 433431d commit 4c163ca

File tree

2 files changed

+321
-11
lines changed

2 files changed

+321
-11
lines changed

src/utils/ask-mode.mts

Lines changed: 306 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,306 @@
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: /^(scan|check|analyze)(\s|$)/i,
121+
action: COMMON_ACTIONS.find(a => a.name === 'Scan this project')!,
122+
},
123+
{
124+
pattern: /^fix(\s|$)/i,
125+
action: COMMON_ACTIONS.find(a => a.name === 'Fix vulnerabilities')!,
126+
},
127+
{
128+
pattern: /^optimize(\s|$)/i,
129+
action: COMMON_ACTIONS.find(a => a.name === 'Optimize dependencies')!,
130+
},
131+
{
132+
pattern: /^(login|authenticate)(\s|$)/i,
133+
action: COMMON_ACTIONS.find(a => a.name === 'Log in to Socket')!,
134+
},
135+
{
136+
pattern: /^(help|\?)(\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+
}

src/utils/meow-with-subcommands.mts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,9 @@ export function emitBanner(
274274
// you can do something like `socket scan view xyz | jq | process`.
275275
// The spinner also emits over stderr for example.
276276
const banner = getAsciiHeader(name, orgFlag, compactMode)
277-
// For now, output to both stdout and stderr to ensure it shows
278-
// TODO: Fix stderr output issue
279-
console.log(banner)
280-
// Add extra newline for visual spacing
281-
console.log('')
277+
// Output only to stderr for consistency
282278
process.stderr.write(banner + '\n')
283-
// Add extra newline for visual spacing
279+
// Add single newline for proper spacing before next output
284280
process.stderr.write('\n')
285281
}
286282

@@ -339,6 +335,10 @@ export async function meowWithSubcommands(
339335
name === 'socket' &&
340336
(!commandOrAliasName || commandOrAliasName?.startsWith('-'))
341337

338+
// When no command is provided, we should NOT forward to Python CLI
339+
// Instead, we should show our banner and enter JavaScript ask mode
340+
const isNoArgsCase = isRootCommand && !commandOrAliasName
341+
342342
// Try to support `socket <purl>` as a shorthand for `socket package score <purl>`.
343343
if (!isRootCommand) {
344344
if (commandOrAliasName?.startsWith('pkg:')) {
@@ -538,7 +538,9 @@ export async function meowWithSubcommands(
538538
// If first arg is a flag (starts with --), try Python CLI forwarding.
539539
// This enables: socket --repo owner/repo --target-path .
540540
// Skip forwarding for help/version/dry-run flags to ensure Node.js CLI handles them.
541+
// IMPORTANT: Do NOT forward to Python when no args are provided at all (isNoArgsCase)
541542
if (
543+
!isNoArgsCase &&
542544
commandOrAliasName?.startsWith('--') &&
543545
commandOrAliasName !== '--help' &&
544546
commandOrAliasName !== '--help-full' &&
@@ -808,14 +810,16 @@ export async function meowWithSubcommands(
808810
// eslint-disable-next-line n/no-process-exit
809811
process.exit(found ? 0 : 2)
810812
} else {
811-
// Show banner before traditional help
813+
// No command provided and no help flag - enter "ask" mode
812814
if (!shouldSuppressBanner(cli2.flags)) {
813815
emitBanner(name, orgFlag, compactMode)
814-
// Meow will add newline so don't add stderr spacing here.
816+
logger.error('')
815817
}
816-
// When you explicitly request --help, the command should be successful
817-
// so we exit(0). If we do it because we need more input, we exit(2).
818-
cli2.showHelp(helpFlag ? 0 : 2)
818+
// Import and run the ask mode
819+
const { runAskMode } = await import('./ask-mode.mts')
820+
await runAskMode()
821+
// eslint-disable-next-line n/no-process-exit
822+
process.exit(0)
819823
}
820824
}
821825

0 commit comments

Comments
 (0)