-
Notifications
You must be signed in to change notification settings - Fork 34
feat: introduce Crisp commands for code simplicity auditing and management: #247
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import chalk from "chalk" | ||
| import { execSync } from "node:child_process" | ||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||
| import { getCrispModeSync } from "src/cli/crisp/index.ts" | ||
| import type { SlashCommandResult } from "./index.ts" | ||
|
|
||
| export async function crispAuditCommand(argsStr: string): Promise<SlashCommandResult> { | ||
| const mode = getCrispModeSync() | ||
| const focus = argsStr.trim() || "all" | ||
| const modeLabel = mode === "off" ? "full" : mode | ||
|
|
||
| let fileList = "" | ||
| try { | ||
| fileList = execSync(`fd -e ts -e tsx -e js -e jsx --exclude node_modules --exclude dist --exclude .git --max-depth 6 . ${process.cwd()}`, { encoding: "utf-8" }) | ||
| } catch { | ||
| fileList = "(could not list files)" | ||
| } | ||
|
|
||
| const lines = fileList.split("\n").filter(Boolean) | ||
| const sampleFiles = lines.slice(0, 30).join("\n") | ||
| const totalFiles = lines.length | ||
|
|
||
| const header = | ||
| `Audit this workspace against the Crisp simplicity ladder (${modeLabel}). focus: ${focus}\n` + | ||
| `Scan the whole tree. Rank findings biggest cut first.\n` + | ||
| `One line per finding. Format: <tag> <what to cut>. <replacement>. [path]\n\n` + | ||
| `Tags:\n` + | ||
| ` delete: dead code, unused flexibility, speculative feature. Replacement: nothing.\n` + | ||
| ` stdlib: hand-rolled thing the stdlib ships. Name the function.\n` + | ||
| ` native: dependency or code doing what the platform already does. Name the feature.\n` + | ||
| ` yagni: abstraction with one implementation, config nobody sets, layer with one caller.\n` + | ||
| ` shrink: same logic, fewer lines. Show the shorter form.\n\n` + | ||
| `Prioritise: deps the stdlib or platform already ships, single-implementation interfaces, ` + | ||
| `factories with one product, wrappers that only delegate, files exporting one thing, ` + | ||
| `dead flags and config, hand-rolled stdlib.\n\n` + | ||
| `End with: net: -<N> lines, -<M> deps possible.\n` + | ||
| `If nothing to cut: "Lean already. Ship."\n\n` + | ||
| `Found ${totalFiles} source files. Sample (${Math.min(totalFiles, 30)} of ${totalFiles}):\n` + | ||
| sampleFiles | ||
|
|
||
| console.log( | ||
| frame( | ||
| `${chalk.hex(theme.amber).bold("Supercode Crisp Audit")} ${chalk.hex(theme.muted)(`mode: ${modeLabel} · focus: ${focus}`)}\n\n${chalk.hex(theme.greenDim)(`Scanning ${totalFiles} source files across the workspace...`)}`, | ||
| { title: "crisp-audit", borderColor: theme.amber, padding: 0 }, | ||
| ), | ||
| ) | ||
|
|
||
| return { type: "message", message: header } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,106 @@ | ||
| import chalk from "chalk" | ||
| import { execSync } from "node:child_process" | ||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||
| import type { SlashCommandResult } from "./index.ts" | ||
|
|
||
| export async function crispDebtCommand(argsStr: string): Promise<SlashCommandResult> { | ||
| let tagCounts: Record<string, number> = {} | ||
| let rawMatches = "" | ||
| let crispCommentMatches = "" | ||
| try { | ||
| rawMatches = execSync("rg -n '\\[crisp:\\d+\\]' --type ts --type tsx --type js --type jsx -g '!node_modules' -g '!dist'", { encoding: "utf-8" }) | ||
| const matches = rawMatches.split("\n").filter(Boolean) | ||
| for (const m of matches) { | ||
| const tag = m.match(/\[crisp:(\d+)\]/) | ||
| if (tag) { | ||
| const rung = tag[1]! | ||
| tagCounts[rung] = (tagCounts[rung] ?? 0) + 1 | ||
| } | ||
|
Comment on lines
+11
to
+18
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Scan and count every Crisp marker. The type filters exclude the Also applies to: 24-24 🤖 Prompt for AI Agents |
||
| } | ||
| } catch { | ||
| rawMatches = "" | ||
| } | ||
| try { | ||
| crispCommentMatches = execSync("rg -n '(//|#|--)\\s*crisp:' --type ts --type tsx --type js --type jsx -g '!node_modules' -g '!dist'", { encoding: "utf-8" }) | ||
| } catch { | ||
| crispCommentMatches = "" | ||
| } | ||
|
|
||
| const totalTags = Object.values(tagCounts).reduce((a, b) => a + b, 0) | ||
|
|
||
| const rungLabels: Record<string, string> = { | ||
| "1": "YAGNI", | ||
| "2": "Reuse what exists", | ||
| "3": "Standard library first", | ||
| "4": "Native platform APIs", | ||
| "5": "Dependencies are debt", | ||
| "6": "One line > many", | ||
| "7": "Minimum code to satisfy the spec", | ||
| } | ||
|
|
||
| const summaryLines = Object.entries(rungLabels) | ||
| .map(([rung, label]) => { | ||
| const count = tagCounts[rung] ?? 0 | ||
| const color = count > 0 ? theme.green : theme.muted | ||
| return ` ${chalk.hex(color)(`[crisp:${rung}]`)} ${chalk.hex(color)(label.padEnd(28))} ${count > 0 ? chalk.hex(theme.amber)(`${count} tags`) : chalk.hex(theme.muted)("—")}` | ||
| }) | ||
| .join("\n") | ||
|
|
||
| const detailLines = rawMatches | ||
| .split("\n") | ||
| .filter(Boolean) | ||
| .slice(0, 40) | ||
| .map((line) => { | ||
| const idx = line.indexOf(":") | ||
| if (idx > 0) { | ||
| const file = line.slice(0, idx) | ||
| const rest = line.slice(idx + 1) | ||
| return ` ${chalk.hex(theme.greenDim)(file)}:${chalk.hex(theme.muted)(rest.slice(0, 80))}` | ||
| } | ||
| return ` ${chalk.hex(theme.muted)(line.slice(0, 80))}` | ||
| }) | ||
| .join("\n") | ||
|
|
||
| const crispComments = crispCommentMatches | ||
| .split("\n") | ||
| .filter(Boolean) | ||
| .slice(0, 20) | ||
|
|
||
| const commentLines = crispComments.map((line) => { | ||
| const idx = line.indexOf(":") | ||
| if (idx > 0) { | ||
| const file = line.slice(0, idx) | ||
| const rest = line.slice(idx + 1) | ||
| const hasUpgrade = /\b(upgrade|when|if|once|until|trigger|todo)\b/i.test(rest) | ||
| const triggerTag = hasUpgrade ? "" : chalk.hex(theme.red)(" no-trigger") | ||
| return ` ${chalk.hex(theme.greenDim)(file)}:${chalk.hex(theme.muted)(rest.slice(0, 80))}${triggerTag}` | ||
| } | ||
| return ` ${chalk.hex(theme.muted)(line.slice(0, 80))}` | ||
| }).join("\n") | ||
|
|
||
| const noTriggerCount = crispComments.filter((l) => !/\b(upgrade|when|if|once|until|trigger|todo)\b/i.test(l)).length | ||
|
|
||
| const sections: string[] = [ | ||
| chalk.hex(theme.amber).bold("Crisp Debt Ledger"), | ||
| "", | ||
| `${chalk.hex(theme.muted)(`[crisp:N] tag ledger: ${totalTags} markers`)}`, | ||
| "", | ||
| ...(totalTags > 0 | ||
| ? [summaryLines, "", detailLines] | ||
| : [`${chalk.hex(theme.greenDim)("No [crisp:N] tags found. Clean — or crisp hasn't been applied yet.")}`]), | ||
| ] | ||
|
|
||
| if (crispComments.length > 0) { | ||
| sections.push( | ||
| "", | ||
| chalk.hex(theme.amber).bold(`Deliberate shortcuts (crisp: comments)`), | ||
| "", | ||
| commentLines, | ||
| "", | ||
| `${chalk.hex(theme.muted)(`${crispComments.length} shortcuts, ${noTriggerCount} with no upgrade trigger`)}`, | ||
| ) | ||
| } | ||
|
|
||
| console.log(frame(sections.join("\n"), { title: "crisp-debt", borderColor: theme.amber, padding: 0 })) | ||
| return { type: "help" } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,39 @@ | ||
| import chalk from "chalk" | ||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||
| import { getCrispModeSync } from "src/cli/crisp/index.ts" | ||
| import type { SlashCommandResult } from "./index.ts" | ||
|
|
||
| const bar = (pct: number, width = 20): string => { | ||
| const filled = Math.round((pct / 100) * width) | ||
| return "█".repeat(filled) + "·".repeat(width - filled) | ||
| } | ||
|
|
||
| export async function crispGainCommand(argsStr: string): Promise<SlashCommandResult> { | ||
| const mode = getCrispModeSync() | ||
|
|
||
| const content = [ | ||
| chalk.hex(theme.amber).bold("Crisp Gain"), | ||
| chalk.hex(theme.muted)("benchmark median · 5 tasks · 3 models"), | ||
| "", | ||
| chalk.hex(theme.green)("Lines of code"), | ||
| ` no-crisp ${chalk.hex(theme.white)(bar(100))} 100%`, | ||
| ` crisp ${chalk.hex(theme.green)(bar(13))}${chalk.hex(theme.muted)(bar(87))} 6-20% ${chalk.hex(theme.red)("▼ 80-94%")}`, | ||
| "", | ||
| chalk.hex(theme.green)("Cost"), | ||
| ` no-crisp ${chalk.hex(theme.white)(bar(100))} 100%`, | ||
| ` crisp ${chalk.hex(theme.green)(bar(38))}${chalk.hex(theme.muted)(bar(62))} 23-53% ${chalk.hex(theme.red)("▼ 47-77%")}`, | ||
| "", | ||
| chalk.hex(theme.green)("Speed"), | ||
| ` crisp ${chalk.hex(theme.amber)("▸ 3-6× faster")}`, | ||
| "", | ||
| chalk.hex(theme.muted)("These are published benchmark medians (see README)."), | ||
| chalk.hex(theme.muted)("They are measured, not computed from this repo."), | ||
| "", | ||
| chalk.hex(theme.amber)("This repo:"), | ||
| ` ${chalk.hex(theme.green)("/crisp-debt")} ${chalk.hex(theme.muted)("(shortcuts you deferred — the counted ledger)")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp-audit")} ${chalk.hex(theme.muted)("(what's still cuttable)")}`, | ||
| ].join("\n") | ||
|
|
||
| console.log(frame(content, { title: "crisp-gain", borderColor: theme.amber, padding: 0 })) | ||
| return { type: "help" } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,49 @@ | ||
| import chalk from "chalk" | ||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||
| import { getCrispModeSync } from "src/cli/crisp/index.ts" | ||
| import type { SlashCommandResult } from "./index.ts" | ||
|
|
||
| export async function crispHelpCommand(): Promise<SlashCommandResult> { | ||
| const current = getCrispModeSync() | ||
|
|
||
| const content = [ | ||
| chalk.hex(theme.amber).bold("Supercode Crisp — Simplicity Ladder Commands"), | ||
| "", | ||
| chalk.hex(theme.amber).bold("Levels"), | ||
| ` ${chalk.hex(theme.green)("/crisp")} ${chalk.hex(theme.muted)("Show current mode")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp full")} ${chalk.hex(theme.muted)("Ladder enforced. Every abstraction must be justified. Default.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp lite")} ${chalk.hex(theme.muted)("Build what's asked, name the lazier alternative. User picks.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp ultra")} ${chalk.hex(theme.muted)("YAGNI extremist. Deletion before addition. Challenges requirements.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp off")} ${chalk.hex(theme.muted)("Disable crisp mode.")}`, | ||
| "", | ||
| chalk.hex(theme.amber).bold("Skills (one-shot)"), | ||
| ` ${chalk.hex(theme.green)("/crisp-review")} ${chalk.hex(theme.muted)("Over-engineering review: L42: yagni: factory, one product. Inline.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp-audit")} ${chalk.hex(theme.muted)("Whole-repo over-engineering audit: ranked list of what to delete.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp-debt")} ${chalk.hex(theme.muted)("Harvest [crisp:N] tags and crisp: shortcut comments.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp-gain")} ${chalk.hex(theme.muted)("Measured-impact scoreboard: less code, less cost, more speed.")}`, | ||
| ` ${chalk.hex(theme.green)("/crisp-help")} ${chalk.hex(theme.muted)("This card.")}`, | ||
| "", | ||
| chalk.hex(theme.amber).bold("The ladder"), | ||
| " 1. YAGNI — don't build what you don't need", | ||
| " 2. Reuse what exists — stdlib > library > new code", | ||
| " 3. Standard library first — built-in APIs > third-party", | ||
| " 4. Native platform APIs — platform > framework > userland", | ||
| " 5. Dependencies are debt — every dep is a liability", | ||
| " 6. One line > many — single expression > temp var chain", | ||
| " 7. Minimum code to satisfy the spec — delete everything else", | ||
| "", | ||
| chalk.hex(theme.amber).bold("Deactivate"), | ||
| ` ${chalk.hex(theme.green)('"stop crisp"')} ${chalk.hex(theme.muted)("or")} ${chalk.hex(theme.green)("normal mode")} ${chalk.hex(theme.muted)("revert to normal. Resume anytime with /crisp.")}`, | ||
| "", | ||
| chalk.hex(theme.amber).bold("Configure default"), | ||
| chalk.hex(theme.muted)("Environment variable (highest priority):"), | ||
| ` ${chalk.hex(theme.greenDim)("export CRISP_DEFAULT_MODE=ultra")}`, | ||
| chalk.hex(theme.muted)("Config file (~/.config/supercode/crisp.json):"), | ||
| ` ${chalk.hex(theme.greenDim)('{"defaultMode": "lite"}')}`, | ||
|
Comment on lines
+38
to
+42
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Document the configuration mechanism that actually exists. Neither 🤖 Prompt for AI Agents |
||
| "", | ||
| chalk.hex(theme.greenDim)(`Current mode: ${chalk.hex(theme.amber)(current)}`), | ||
| ].join("\n") | ||
|
|
||
| console.log(frame(content, { title: "crisp-help", borderColor: theme.amber, padding: 0 })) | ||
| return { type: "help" } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,56 @@ | ||||||||||||||
| import chalk from "chalk" | ||||||||||||||
| import { execSync } from "node:child_process" | ||||||||||||||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||||||||||||||
| import { getCrispModeSync } from "src/cli/crisp/index.ts" | ||||||||||||||
| import type { SlashCommandResult } from "./index.ts" | ||||||||||||||
|
|
||||||||||||||
| export async function crispReviewCommand(argsStr: string): Promise<SlashCommandResult> { | ||||||||||||||
| const mode = getCrispModeSync() | ||||||||||||||
|
|
||||||||||||||
| let diff: string | ||||||||||||||
| try { | ||||||||||||||
| const staged = execSync("git diff --cached", { encoding: "utf-8" }) | ||||||||||||||
| const unstaged = execSync("git diff", { encoding: "utf-8" }) | ||||||||||||||
| diff = staged || unstaged | ||||||||||||||
|
Comment on lines
+12
to
+14
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win Include both staged and unstaged changes in the review. When both diffs exist, Proposed fix- diff = staged || unstaged
+ diff = [staged, unstaged].filter(Boolean).join("\n")📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| if (!diff) { | ||||||||||||||
| console.log( | ||||||||||||||
| frame( | ||||||||||||||
| `${chalk.hex(theme.amber).bold("No changes to review")}\n\n${chalk.hex(theme.muted)("There are no staged or unstaged diffs. Make some changes first, then run /crisp-review.")}`, | ||||||||||||||
| { title: "crisp-review", borderColor: theme.amber, padding: 0 }, | ||||||||||||||
| ), | ||||||||||||||
| ) | ||||||||||||||
| return { type: "help" } | ||||||||||||||
| } | ||||||||||||||
| } catch { | ||||||||||||||
| console.log( | ||||||||||||||
| frame( | ||||||||||||||
| `${chalk.hex(theme.red).bold("Git error")}\n\n${chalk.hex(theme.muted)("Could not read git diff. Are you in a git repository?")}`, | ||||||||||||||
| { title: "crisp-review", borderColor: theme.red, padding: 0 }, | ||||||||||||||
| ), | ||||||||||||||
| ) | ||||||||||||||
| return { type: "help" } | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
| const modeLabel = mode === "off" ? "full" : mode | ||||||||||||||
| const header = | ||||||||||||||
| `Review this diff against the Crisp simplicity ladder (${modeLabel}).\n` + | ||||||||||||||
| `One line per finding. Format: L<line>: <tag> <what>. <replacement>.\n\n` + | ||||||||||||||
| `Tags:\n` + | ||||||||||||||
| ` delete: dead code, unused flexibility, speculative feature. Replacement: nothing.\n` + | ||||||||||||||
| ` stdlib: hand-rolled thing the stdlib ships. Name the function.\n` + | ||||||||||||||
| ` native: dependency or code doing what the platform already does. Name the feature.\n` + | ||||||||||||||
| ` yagni: abstraction with one implementation, config nobody sets, layer with one caller.\n` + | ||||||||||||||
| ` shrink: same logic, fewer lines. Show the shorter form.\n\n` + | ||||||||||||||
| `End with: net: -<N> lines possible.\n` + | ||||||||||||||
| `If nothing to cut: "Lean already. Ship."\n\n` + | ||||||||||||||
| `${diff}` | ||||||||||||||
|
|
||||||||||||||
| console.log( | ||||||||||||||
| frame( | ||||||||||||||
| `${chalk.hex(theme.amber).bold("Supercode Crisp Review")} ${chalk.hex(theme.muted)(`mode: ${modeLabel}`)}\n\n${chalk.hex(theme.greenDim)("Diff loaded. Evaluating against simplicity ladder...")}`, | ||||||||||||||
| { title: "crisp-review", borderColor: theme.amber, padding: 0 }, | ||||||||||||||
| ), | ||||||||||||||
| ) | ||||||||||||||
|
|
||||||||||||||
| return { type: "message", message: header } | ||||||||||||||
| } | ||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,48 @@ | ||
| import chalk from "chalk" | ||
| import { CRISP_MODES, getCrispModeSync, setCrispMode, CRISP_MODE_LABELS } from "src/cli/crisp/index.ts" | ||
| import { theme, frame } from "src/cli/utils/tui.ts" | ||
| import type { SlashCommandResult } from "./index.ts" | ||
| import type { CrispMode } from "src/cli/crisp/config.ts" | ||
|
|
||
| export async function crispCommand(argsStr: string): Promise<SlashCommandResult> { | ||
| const parts = argsStr.trim().split(/\s+/).filter(Boolean) | ||
| const raw = parts[0] ?? "" | ||
|
|
||
| if (raw === "lite" || raw === "full" || raw === "ultra" || raw === "off" || raw === "on") { | ||
| const mode: CrispMode = raw === "on" ? "full" : raw | ||
| await setCrispMode(mode) | ||
| if (mode === "off") { | ||
| console.log(` ${chalk.hex(theme.green)("✓")} ${chalk.hex(theme.muted)("Crisp disabled")}`) | ||
| } else { | ||
| console.log( | ||
| frame( | ||
| `${chalk.hex(theme.amber).bold(`Supercode Crisp: ${mode}`)}\n\n${chalk.hex(theme.muted)(CRISP_MODE_LABELS[mode])}\n\nThe simplicity ladder will be injected into every agent session.\nTry: /crisp status`, | ||
| { title: `crisp:${mode}`, borderColor: theme.amber, padding: 0 }, | ||
| ), | ||
| ) | ||
| } | ||
| return { type: "help" } | ||
| } | ||
|
|
||
| // Default / "on" / "status" → show status | ||
| const current = getCrispModeSync() | ||
| const statusLines = [ | ||
| chalk.hex(theme.amber).bold(`Supercode Crisp: ${current}`), | ||
| "", | ||
| `${chalk.hex(theme.muted)("Usage:")} /crisp { lite | full | ultra | off | status }`, | ||
| "", | ||
| ...CRISP_MODES.map((m) => { | ||
| const indicator = m === current ? chalk.hex(theme.green)("▸") : " " | ||
| return ` ${indicator} ${chalk.hex(m === current ? theme.white : theme.muted)(`/${m}`).padEnd(18)} ${chalk.hex(m === current ? theme.green : theme.greenDim)(CRISP_MODE_LABELS[m])}` | ||
| }), | ||
| ].join("\n") | ||
|
|
||
| console.log( | ||
| frame(statusLines, { | ||
| title: "crisp", | ||
| borderColor: theme.amber, | ||
| padding: 0, | ||
| }), | ||
| ) | ||
| return { type: "help" } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
Repository: yashdev9274/supercli
Length of output: 5729
🌐 Web query:
Node.js child_process execSync shell command execution default shell template string path whitespace documentation💡 Result:
The Node.js child_process.execSync function executes a command inside a shell [1][2]. Because it operates via a shell, it interprets the provided command string as a set of arguments, where spaces typically act as delimiters [3][4]. Key technical details include: 1. Shell Execution and Whitespace: When you pass a command string containing whitespace to execSync, the underlying shell (e.g., /bin/sh or cmd.exe) parses those spaces as separators between the executable and its arguments [3][4]. To include paths containing whitespace, you must wrap the path in double quotes within the command string to ensure the shell interprets the entire path as a single argument [3][4]. 2. Default Shell: By default, execSync executes commands using '/bin/sh' on Unix-like systems and process.env.ComSpec (usually cmd.exe) on Windows [2][5]. This default behavior can be overridden using the shell option in the options object [2][6]. 3. Security Warning: Because execSync processes the command string directly through a shell, it is vulnerable to shell injection [2][5]. Node.js documentation explicitly warns against passing unsanitized user input to this function, as shell metacharacters (e.g., |, &,;, $) can be used to execute arbitrary commands [2][4]. 4. Recommended Alternatives: For scenarios involving paths with spaces or when user input is involved, it is generally safer and cleaner to use child_process.execFileSync or child_process.spawnSync [7][3]. These methods allow you to pass the executable and its arguments as separate array elements, which avoids shell interpretation entirely and eliminates the need for manual quoting or escaping of paths [7][3].
Citations:
Avoid shell interpolation of the workspace path.
execSyncruns through a shell, andprocess.cwd()is interpolated into the command string. A workspace path with spaces breaks the scan, and shell metacharacters in the path can execute commands. UseexecFileSyncwith fixedfdarguments and thecwdoption.🧰 Tools
🪛 OpenGrep (1.26.0)
[ERROR] 14-14: Dynamic command passed to child_process.exec/execSync. Use child_process.execFile or spawn with an argument array instead.
(coderabbit.command-injection.exec-js)
🤖 Prompt for AI Agents
Source: Linters/SAST tools