From 4149ad4f7671b981c4e68f1b3b583edbe96cf845 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Sun, 8 Nov 2026 09:23:39 +0000 Subject: [PATCH] fix(autofix): shell parser safety and check script fallback - Prevent unsafe shell commands with redirections, backgrounding, or substitutions from being classified as read-only - Fail closed on malformed quotes and incomplete command substitutions - Fall back to fmt:check when format:check cannot be safely classified as read-only - Regenerate dist/autofix.mjs bundle - Add regression test coverage for shell safety and check script candidate fallback --- dist/autofix.mjs | 14 +- .../adapters/javascript/script-adapter.ts | 357 +++++++++++++----- tests/autofix-architecture.test.ts | 149 ++++++++ 3 files changed, 409 insertions(+), 111 deletions(-) diff --git a/dist/autofix.mjs b/dist/autofix.mjs index 509f8ae3..7f36cdaa 100644 --- a/dist/autofix.mjs +++ b/dist/autofix.mjs @@ -304,10 +304,10 @@ const Vn=[`uv`,`pip`],Hn=[`.git`,`.venv`,`venv`,`env`,`.env`,`virtualenv`,`.virt `:` `,i++;return i=t+1&&e[a]===`\\`;)n++,a--;if(n%2==0)return r+=` `,[r,i+1]}r+=` `,i++}return[r,i]}function dr(e,t,n){let r=` `,i=t+1;for(;i!Un(e))}function mr(e,t){if(!Un(e[`build-system`]))return!1;if(typeof t==`string`){let e=fr(t);return/(?:^|\n)\s*\[\s*build-system\s*\]\s*(?:\n|$)/.test(e)}let n=Object.entries(e[`build-system`]);return n.length===0||n.some(([,e])=>!Un(e))}function hr(e,t){if(pr(e,t)||mr(e,t))return!0;if(Un(e.tool)&&Un(e.tool.uv)){let t=e.tool.uv;return Yn(t.workspace)||Array.isArray(t[`dev-dependencies`])}return!1}function gr(e){return Un(e.tool)&&Un(e.tool.uv)}function _r(e,t){return pr(e,t)||mr(e,t)}function vr(e,t){return p.relative(e,t)||`.`}function yr(e,t){if(!t)throw Error(`Invalid Python project configuration: 'uv.lock' was detected without a valid 'pyproject.toml' project or workspace configuration ('[project]', '[build-system]', or '[tool.uv.workspace]'). 'uv sync' requires a valid 'pyproject.toml' file.`);return{name:`uv`,lockfile:`uv.lock`,configFile:`pyproject.toml`,installCommands:[{executable:`uv`,args:[`sync`]}],lockCommand:`uv lock`,syncCommand:`uv sync`,execPrefix:`uv run`}}function br(e,t){if(e.hasRequirementsTxt&&e.requirementsDir){let t=vr(e.cwd,p.join(e.requirementsDir,`requirements.txt`));return{name:`uv`,configFile:e.pyprojectDir?`pyproject.toml`:`uv.toml`,installCommands:[{executable:`uv`,args:[`pip`,`install`,`-r`,t]}],execPrefix:`uv run`}}return t?{name:`uv`,configFile:e.pyprojectDir?`pyproject.toml`:`uv.toml`,installCommands:[{executable:`uv`,args:[`sync`]}],lockCommand:`uv lock`,syncCommand:`uv sync`,execPrefix:`uv run`}:null}function xr(e){let t=e.pyprojectToml?.success===!0&&hr(e.pyprojectToml.data,e.pyprojectToml.rawContent),n=e.workspaceRootToml?.success===!0&&hr(e.workspaceRootToml.data,e.workspaceRootToml.rawContent),r=t||n,i=e.uvToml?.success===!0,a=e.pyprojectToml?.success===!0&&gr(e.pyprojectToml.data)||e.workspaceRootToml?.success===!0&&gr(e.workspaceRootToml.data);return e.hasUvLock?yr(e,r):a||i?br(e,r):null}function Sr(e){let t=e.pyprojectToml?.success===!0&&_r(e.pyprojectToml.data,e.pyprojectToml.rawContent);if(!(e.hasRequirementsTxt||e.hasSetupPy||e.hasSetupCfg||t))return null;let n;t?n=`pyproject.toml`:e.hasSetupPy?n=`setup.py`:e.hasSetupCfg&&(n=`setup.cfg`);let r=[],i,a=e.hasSetupPy||e.hasSetupCfg||t,o=e.requirementsDir?vr(e.cwd,p.join(e.requirementsDir,`requirements.txt`)):`requirements.txt`,s=vr(e.cwd,e.projectDir);return e.hasRequirementsTxt&&a?(r=[{executable:`pip`,args:[`install`,`-r`,o]},{executable:`pip`,args:[`install`,`-e`,s]}],i=`requirements.txt`):e.hasRequirementsTxt?(r=[{executable:`pip`,args:[`install`,`-r`,o]}],i=`requirements.txt`):a&&(r=[{executable:`pip`,args:[`install`,`-e`,s]}]),{name:`pip`,lockfile:i,configFile:n,installCommands:r,execPrefix:``}}function Cr(e=process.cwd(),t){if(t?.hasSource??or(e))return!0;let n=t?.ctx??ir(e);if(n.projectDir===n.cwd&&tr(n.cwd)&&(n.pyprojectToml?.success===!0&&hr(n.pyprojectToml.data,n.pyprojectToml.rawContent)||n.hasUvLock||n.hasRequirementsTxt||n.hasSetupPy||n.hasSetupCfg))return!0;if(n.workspaceRootDir&&n.workspaceRootToml?.success===!0){let t=Zn(n.workspaceRootDir);if(t&&Xn(n.workspaceRootDir,p.resolve(e),t.config))return!0}return!1}function wr(e,t){t!=null&&(e.pyprojectToml=t);for(let t of Vn)if(t===`uv`){let t=xr(e);if(t)return t}else if(t===`pip`){let t=Sr(e);if(t)return t}return{name:`pip`,installCommands:[],execPrefix:``,isStandalone:!0}}function Tr(e=process.cwd(),t,n){let r=n?.ctx??ir(e);return n?.isProject??Cr(e,{ctx:r,hasSource:n?.hasSource})?wr(r,t):null}function Er(e){if(!Array.isArray(e))throw Error(`Invalid Python environment: "pmInfo.installCommands" must be an array`);for(let t of e){if(!t||typeof t!=`object`)throw Error(`Invalid Python environment: malformed "installCommands" item`);let e=t;if(typeof e.executable!=`string`||!e.executable.trim()||!Array.isArray(e.args)||!e.args.every(e=>typeof e==`string`))throw Error(`Invalid Python environment: malformed "installCommands" item`)}}function Dr(e,t=`Invalid Python environment: "pmInfo" must be an object`){if(!e||typeof e!=`object`||Array.isArray(e))throw Error(t);let n=e;if(n.name!==`uv`&&n.name!==`pip`)throw Error(`Invalid Python environment: pmInfo name must be "uv" or "pip"`);return Er(n.installCommands),n}function Or(e){if(!process.argv[1])return!1;try{return l(e)===f.realpathSync(process.argv[1])}catch{return!1}}async function kr(e,t){let n=p.join(e,`node_modules`,`.bin`,t);if(f.existsSync(n))return n;let r=await Vt(t,!1);if(r)return r}var Ar=class{async resolveTaskExecutable(e){if(e.toolId===`npm-scripts`)return e.executable;if(e.toolId===`ruff`){if(!await Vt(`ruff`,!1))throw Error(`Required Python binary 'ruff' is not available in PATH.`);return`ruff`}let t=await kr(e.cwd,e.executable);if(!t){let t=e.toolName||e.toolId,n=e.requiredPackage||e.executable;throw Error(`Configured tool "${t}" could not run: executable "${e.executable}" is not available in node_modules/.bin or PATH. Please install "${n}" in project dependencies.`)}return t}async executeTasks(e){let t=[];for(let n of e){let e=`${n.executable} ${n.args.join(` `)}`.trim();try{let r=await this.resolveTaskExecutable(n);en(`Executing: ${e}`);let i=n.env?Object.fromEntries(Object.entries({...process.env,...n.env}).filter(e=>e[1]!==void 0)):void 0,a=await Yt(r,n.args,{cwd:n.cwd,env:i,ignoreReturnCode:!0});if(a!==0){let r=n.toolId===`ruff`?`Ruff command "${e}" failed. Check Python and Ruff configuration.`:n.toolId===`npm-scripts`?`Script "${n.args[1]||e}" failed while executing "${e}".`:`Task "${n.id}" (${e}) failed with exit code ${a}.`;t.push({task:n,exitCode:a,error:Error(r)})}else t.push({task:n,exitCode:a})}catch(e){let r=e instanceof Error?e:Error(String(e));t.push({task:n,exitCode:1,error:r})}}return t}};function jr(e,t){return t.has(`autofix`)?!0:e===`autofix`?t.has(`format`)&&t.has(`lint`)&&t.has(`assist`):t.has(e)}function Mr(e,t){let n=[],r=[];for(let i of e)jr(i.capability,t)?r.push(i):n.push(i);return{kept:n,skipped:r}}async function Nr(e,t,n,r,i,a,o){for(let s of e){let e=s.adapter,c=await e.plan(n,r);if(c.length===0)continue;let{kept:l,skipped:u}=Mr(c,t);if(l.length===0)a.push({id:e.id,name:e.name,reason:`all capabilities already covered by higher priority tool`});else{i.push({id:e.id,name:e.name,reason:s.reason});for(let e of l)if(t.add(e.capability),e.capabilities)for(let n of e.capabilities)t.add(n);if(o.push(...l),u.length>0){let t=Array.from(new Set(u.map(e=>e.capability))).join(`, `);a.push({id:e.id,name:e.name,reason:`partially skipped (${t} covered by higher priority tool)`})}}}}async function Pr(e,t,n,r,i){for(let a of e.getStandaloneAdapters()){let e=await a.detect(t);if(e.detected){r.push({id:a.id,name:a.name,reason:e.reason});let o=await a.plan(t,n);i.push(...o)}}}function Fr(e){let t=[],n=/* @__PURE__ */ new Set;for(let r of e)n.has(r.id)||(n.add(r.id),t.push(r));return t}async function Ir(e,t,n=`fix`){let r=t.hasPythonSource??or(t.cwd),i=t.jsEnv??fn(t.cwd),a={...t,jsEnv:i,hasPythonSource:r},o=[],s=[],c=[],l=e.getProviders(),u=/* @__PURE__ */ new Map;for(let e of l){if(!await e.detect(a))continue;let t=/* @__PURE__ */ new Set;u.set(e.id,t);let r=e.getAdapters(a),i=[];for(let e of r){let t=await e.detect(a);t.detected?i.push({adapter:e,reason:t.reason}):t.reason&&!t.reason.startsWith(`no `)&&s.push({id:e.id,name:e.name,reason:t.reason})}await Nr(i,t,a,n,o,s,c)}await Pr(e,a,n,o,c);let d=Fr(c);return{tasks:d,selectedTools:o,skippedTools:s,summary:Lr(e,a,o,s,d)}}function Lr(e,t,n,r,i){let a=[];a.push(`Detected environments:`);let o=t.hasPythonSource??!1,s=t.jsEnv?.hasPackageJson??!1;if(a.push(` Python: ${o}`),a.push(` JavaScript: ${s}`),o&&(a.push(``),a.push(`Python toolchain:`),n.filter(e=>e.id===`ruff`).length>0?a.push(` Ruff: default tool`):a.push(` None`)),s){a.push(``),a.push(`JavaScript toolchain:`);let e=n.filter(e=>e.id!==`ruff`);for(let t of e)a.push(` ${t.name}: selected`);let t=r.filter(e=>e.id!==`ruff`);for(let e of t)a.push(` ${e.name}: skipped (${e.reason})`)}if(a.push(``),a.push(`Execution plan:`),i.length===0?a.push(` (no tasks to execute)`):i.forEach((e,t)=>{let n=e.description||`${e.executable} ${e.args.join(` `)}`;a.push(` ${t+1}. ${n}`)}),r.length>0){a.push(``),a.push(`Skipped:`);for(let e of r)a.push(` - ${e.name}: ${e.reason}`)}return a.join(` -`)}function Rr(e){for(let t of[`format:write`,`format:fix`,`format`,`fmt`])if(e[t]&&Di(e[t],e))return t}function zr(e,t){if(t===`fix`)return Rr(e);let n=e[`format:check`]?`format:check`:e[`fmt:check`]?`fmt:check`:void 0;if(n&&Ei(e[n],e)===`read-only`)return n}function Br(e,t){if(t===`fix`)return e[`lint:fix`]?`lint:fix`:e[`lint:write`]?`lint:write`:void 0;if(e[`lint:check`]&&Ei(e[`lint:check`],e)===`read-only`)return`lint:check`;if(e.lint&&Ei(e.lint,e)===`read-only`)return`lint`}function Vr(e,t,n){if(n.inSingleQuote||n.inDoubleQuote||n.isEscaped)return 0;let r=e[t];return r===`;`||r===` -`?1:r===`&`&&e[t+1]===`&`||r===`|`&&e[t+1]===`|`?2:+(r===`|`)}function Hr(e,t){let n=t;for(;nt?n-1:t}function Ur(e,t){return t.inSingleQuote||t.inDoubleQuote||t.isEscaped?!1:e.length===0||/\s/.test(e[e.length-1])}function Wr(e){let t=[],n=``,r={inSingleQuote:!1,inDoubleQuote:!1,isEscaped:!1};for(let i=0;i0?(n.trim()&&t.push(n.trim()),n=``,i+=o-1):n+=a}return n.trim()&&t.push(n.trim()),t}function Gr(e,t,n){return t.isEscaped=!1,n+e}function Kr(e,t){return e===`'`&&!t.inDoubleQuote?(t.inSingleQuote=!t.inSingleQuote,!0):e===`"`&&!t.inSingleQuote&&(t.inDoubleQuote=!t.inDoubleQuote,!0)}function qr(e){let t=[],n=``,r=!1,i={inSingleQuote:!1,inDoubleQuote:!1,isEscaped:!1};for(let a=0;a0&&n.push(t)}return n}function $r(e,t){let n=Qr(e);for(let e of n)if(Zr(e,t))return!0;return!1}function ei(e,t,n){return!n||n===t?!1:!$r(e[t]||``,n)}function ti(e,t,n){return!n||n===t?!1:!$r(e[t]||``,n)}function ni(e){return e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function ri(e){let[t]=e;return t===`prettier`&&e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function ii(e){let[t]=e;return t===`oxfmt`&&e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function ai(e){let[t,n]=e;return t!==`biome`||n!==`format`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function oi(e){let[t]=e;return t===`eslint`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function si(e){let[t]=e;return t===`oxlint`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function ci(e){let[t,n]=e;return t!==`biome`||n!==`lint`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function li(e){let[t,n]=e;return t!==`biome`||n!==`check`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function ui(e){let[t,n]=e;return t!==`vp`||n!==`fmt`?!1:!e.some(e=>e===`--check`||e===`-c`)}function di(e){let[t,n]=e;return t!==`vp`||n!==`lint`?!1:e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function fi(e){let[t]=e;if(t!==`prettier`)return!1;let n=e.some(e=>e===`--check`||e===`-c`||e===`--list-different`||e===`-l`),r=e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`));return n&&!r}function pi(e){let[t]=e;if(t!==`oxfmt`)return!1;let n=e.some(e=>e===`--check`||e===`-c`),r=e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`));return n&&!r}function mi(e){let[t,n]=e;return t!==`biome`||n!==`format`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function hi(e){let[t]=e;return t===`eslint`&&!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function gi(e){let[t]=e;return t===`oxlint`&&!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function _i(e){let[t,n]=e;return t!==`biome`||n!==`lint`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function vi(e){let[t,n]=e;return t!==`biome`||n!==`check`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function yi(e){let[t,n]=e;return t!==`vp`||n!==`fmt`?!1:e.some(e=>e===`--check`||e===`-c`)}function bi(e){let[t,n]=e;return t!==`vp`||n!==`lint`?!1:!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function xi(e){let[t,n]=e;return t!==`vp`||n!==`check`?!1:!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Si(e){if(e.length===0)return;let[t,n,r]=e;if((t===`npm`||t===`pnpm`||t===`yarn`||t===`bun`)&&n===`run`&&r)return r;if((t===`pnpm`||t===`bun`||t===`yarn`)&&n&&!n.startsWith(`-`))return n}function Ci(e){if(e.length>=3&&(e[0]===`sh`||e[0]===`bash`)&&e[1]===`-c`)return e[2]}function wi(e,t,n){if(e.length===0)return`unknown`;if(ni(e))return`write-capable`;let r=Ci(e);if(r!==void 0)return Ti(r,t,n);let i=Si(e);if(i){if(n.has(i))return`unknown`;n.add(i);let e=t[i];return e?Ti(e,t,n):`unknown`}return ri(e)||ii(e)||ai(e)||oi(e)||si(e)||ci(e)||li(e)||ui(e)||di(e)?`write-capable`:fi(e)||pi(e)||mi(e)||hi(e)||gi(e)||_i(e)||vi(e)||yi(e)||bi(e)||xi(e)?`read-only`:`unknown`}function Ti(e,t,n){let r=Qr(e);if(r.length===0)return`unknown`;let i=!1;for(let e of r){let r=wi(e,t,n);if(r===`write-capable`)return`write-capable`;if(r===`unknown`)return`unknown`;r===`read-only`&&(i=!0)}return i?`read-only`:`unknown`}function Ei(e,t={}){return Ti(e,t,/* @__PURE__ */ new Set)}function Di(e,t={},n=/* @__PURE__ */ new Set){return Qr(e).some(e=>{let r=Si(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?Di(e,t,n):!1}let i=Ci(e);return i===void 0?ri(e)||ii(e)||ai(e)||ui(e):Di(i,t,n)})}function Oi(e,t={},n=/* @__PURE__ */ new Set){return Qr(e).some(e=>{let r=Si(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?Oi(e,t,n):!1}let i=Ci(e);return i===void 0?oi(e)||si(e)||ci(e)||di(e):Oi(i,t,n)})}function ki(e,t={},n=/* @__PURE__ */ new Set){return Qr(e).some(e=>{let r=Si(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?ki(e,t,n):!1}let i=Ci(e);if(i!==void 0)return ki(i,t,n);if(li(e))return!0;let[a,o]=e;return a===`vp`&&o===`check`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))})}function Ai(e,t,n){return t&&$r(e,t)?!0:n(e)}function ji(e,t,n,r,i){let a=Ai(t,r,t=>Di(t,e)),o=Ai(t,i,t=>Oi(t,e)),s=ki(t,e);return a&&o?[{capability:`format`,capabilities:[`format`,`lint`],scriptName:n}]:a&&!s?[{capability:`format`,scriptName:n}]:o&&!s?[{capability:`lint`,scriptName:n}]:s?[{capability:`autofix`,scriptName:n}]:[{capability:`typecheck`,capabilities:[],scriptName:n}]}function Mi(e,t){return Di(t[e],t)?{capability:`format`,scriptName:e}:{capability:`typecheck`,capabilities:[],scriptName:e}}function Ni(e,t){return Oi(t[e],t)?{capability:`lint`,scriptName:e}:{capability:`typecheck`,capabilities:[],scriptName:e}}function Pi(e,t,n){let r=[];return e&&r.push(Mi(e,n)),t&&r.push(Ni(t,n)),r}function Fi(e,t,n,r,i){r&&ei(t,n,r)&&e.push(Mi(r,t)),i&&ti(t,n,i)&&e.push(Ni(i,t))}function Ii(e){let t=e.fix?`fix`:e[`check:fix`]?`check:fix`:void 0,n=zr(e,`fix`),r=Br(e,`fix`);if(!t)return Pi(n,r,e);let i=ji(e,e[t]||``,t,n,r);return Fi(i,e,t,n,r),i}function Li(e){let t=[],n=zr(e,`check`),r=Br(e,`check`);return n&&t.push({capability:`format`,scriptName:n}),r&&t.push({capability:`lint`,scriptName:r}),t}function Ri(e,t){return t===`fix`?Ii(e):Li(e)}var zi=class{id=`npm-scripts`;name=`package.json scripts`;capabilities=[`autofix`,`format`,`lint`];detect(e){let t=e.jsEnv?.packageJson?.scripts;if(!t||Object.keys(t).length===0)return{detected:!1,reason:`no scripts declared in package.json`};let n=Ri(t,`fix`),r=Ri(t,`check`),i=[...n,...r];return i.length>0?{detected:!0,reason:`found supported scripts: ${Array.from(new Set(i.map(e=>`"${e.scriptName}"`))).join(`, `)}`}:{detected:!1,reason:`no supported autofix or check scripts found in package.json`}}plan(e,t){let n=Ri(e.jsEnv?.packageJson?.scripts||{},t),r=cn(e.jsPm||`npm`);return n.map(n=>({id:`script-${n.scriptName}`,toolId:this.id,capability:n.capability,capabilities:n.capabilities,executable:r,args:[`run`,n.scriptName],cwd:e.cwd,mode:t,description:`package.json script: ${r} run ${n.scriptName}`}))}};function Bi(e,t){return e?!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t]||e.optionalDependencies?.[t]):!1}function Vi(e,t){for(let n of t)if(f.existsSync(p.join(e,n)))return!0;return!1}var Hi=class{id=`vite-plus`;name=`Vite+`;capabilities=[`autofix`,`format`,`lint`];detect(e){let t=e.jsEnv?.packageJson,n=sn(t),r=Vi(e.cwd,[`vite.config.ts`,`vite.config.js`,`vite.config.mjs`,`vite.config.cjs`]);return n||r&&t?.scripts&&Object.values(t.scripts).some(e=>e.includes(`vp `))?{detected:!0,reason:`Vite+ dependency and/or configuration detected`}:{detected:!1,reason:`no Vite+ dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`vite-plus-fmt`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`format`,executable:`vp`,args:[`fmt`],cwd:e.cwd,mode:`fix`,description:`Vite+: vp fmt`},{id:`vite-plus-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`lint`,executable:`vp`,args:[`lint`,`--fix`],cwd:e.cwd,mode:`fix`,description:`Vite+: vp lint --fix`}]:[{id:`vite-plus-fmt-check`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`format`,executable:`vp`,args:[`fmt`,`--check`],cwd:e.cwd,mode:`check`,description:`Vite+: vp fmt --check`},{id:`vite-plus-lint`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`lint`,executable:`vp`,args:[`lint`],cwd:e.cwd,mode:`check`,description:`Vite+: vp lint`}]}},Ui=class{id=`biome`;name=`Biome`;capabilities=[`format`,`lint`,`assist`];detect(e){let t=e.jsEnv?.packageJson,n=Bi(t,`@biomejs/biome`),r=Vi(e.cwd,[`biome.json`,`biome.jsonc`]);return n&&r?{detected:!0,reason:`Biome dependency and configuration detected`}:r?{detected:!0,reason:`Biome configuration detected`}:n?{detected:!1,reason:`Biome dependency present without configuration file`}:{detected:!1,reason:`no Biome dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`biome-format-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`format`,executable:`biome`,args:[`format`,`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: format --write .`},{id:`biome-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`lint`,executable:`biome`,args:[`lint`,`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: lint --write .`},{id:`biome-assist-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`assist`,executable:`biome`,args:[`check`,`--write`,`--formatter-enabled=false`,`--linter-enabled=false`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: safe assist actions (--write)`}]:[{id:`biome-format-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`format`,executable:`biome`,args:[`format`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: format .`},{id:`biome-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`lint`,executable:`biome`,args:[`lint`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: lint .`},{id:`biome-assist-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`assist`,executable:`biome`,args:[`check`,`--formatter-enabled=false`,`--linter-enabled=false`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: check assist actions`}]}},Wi=class{id=`prettier`;name=`Prettier`;capabilities=[`format`];detect(e){let t=e.jsEnv?.packageJson,n=Bi(t,`prettier`),r=!!t?.prettier,i=Vi(e.cwd,[`.prettierrc`,`.prettierrc.json`,`.prettierrc.yml`,`.prettierrc.yaml`,`.prettierrc.json5`,`.prettierrc.js`,`.prettierrc.cjs`,`.prettierrc.mjs`,`prettier.config.js`,`prettier.config.cjs`,`prettier.config.mjs`]);return n&&(r||i)?{detected:!0,reason:`Prettier dependency and configuration detected`}:r||i?{detected:!0,reason:`Prettier configuration detected`}:n?{detected:!1,reason:`Prettier dependency present without configuration`}:{detected:!1,reason:`no Prettier dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`prettier-format`,toolId:this.id,toolName:this.name,requiredPackage:`prettier`,capability:`format`,executable:`prettier`,args:[`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Prettier: format`}]:[{id:`prettier-check`,toolId:this.id,toolName:this.name,requiredPackage:`prettier`,capability:`format`,executable:`prettier`,args:[`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Prettier: format check`}]}},Gi=class{id=`eslint`;name=`ESLint`;capabilities=[`lint`];detect(e){let t=e.jsEnv?.packageJson,n=Bi(t,`eslint`),r=!!t?.eslintConfig,i=Vi(e.cwd,[`eslint.config.js`,`eslint.config.mjs`,`eslint.config.cjs`,`eslint.config.ts`,`.eslintrc`,`.eslintrc.js`,`.eslintrc.cjs`,`.eslintrc.json`,`.eslintrc.yaml`,`.eslintrc.yml`]);return n&&(r||i)?{detected:!0,reason:`ESLint dependency and configuration detected`}:r||i?{detected:!0,reason:`ESLint configuration detected`}:n?{detected:!1,reason:`ESLint dependency present without configuration`}:{detected:!1,reason:`no ESLint dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`eslint-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`eslint`,capability:`lint`,executable:`eslint`,args:[`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`ESLint: lint autofix`}]:[{id:`eslint-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`eslint`,capability:`lint`,executable:`eslint`,args:[`.`],cwd:e.cwd,mode:`check`,description:`ESLint: lint check`}]}},Ki=class{id=`oxlint`;name=`Oxlint`;capabilities=[`lint`];detect(e){let t=e.jsEnv?.packageJson,n=Bi(t,`oxlint`),r=Vi(e.cwd,[`.oxlintrc.json`,`oxlint.json`,`oxlint.config.ts`,`oxlint.config.js`]);return n&&r?{detected:!0,reason:`Oxlint dependency and configuration detected`}:r?{detected:!0,reason:`Oxlint configuration detected`}:n?{detected:!1,reason:`Oxlint dependency present without configuration`}:{detected:!1,reason:`no Oxlint dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`oxlint-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`oxlint`,capability:`lint`,executable:`oxlint`,args:[`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`Oxlint: lint autofix`}]:[{id:`oxlint-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`oxlint`,capability:`lint`,executable:`oxlint`,args:[`.`],cwd:e.cwd,mode:`check`,description:`Oxlint: lint check`}]}},qi=class{id=`oxfmt`;name=`Oxfmt`;capabilities=[`format`];detect(e){let t=e.jsEnv?.packageJson,n=Bi(t,`oxfmt`),r=Vi(e.cwd,[`.oxfmtrc.json`,`oxfmt.json`,`oxfmt.config.ts`,`oxfmt.config.js`]);return n&&r?{detected:!0,reason:`Oxfmt dependency and configuration detected`}:r?{detected:!0,reason:`Oxfmt configuration detected`}:n?{detected:!1,reason:`Oxfmt dependency present without configuration`}:{detected:!1,reason:`no Oxfmt dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`oxfmt-format`,toolId:this.id,toolName:this.name,requiredPackage:`oxfmt`,capability:`format`,executable:`oxfmt`,args:[`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Oxfmt: format`}]:[{id:`oxfmt-check`,toolId:this.id,toolName:this.name,requiredPackage:`oxfmt`,capability:`format`,executable:`oxfmt`,args:[`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Oxfmt: format check`}]}},Ji=class{id=`javascript`;name=`JavaScript/TypeScript`;scriptAdapter=new zi;vitePlusAdapter=new Hi;biomeAdapter=new Ui;prettierAdapter=new Wi;eslintAdapter=new Gi;oxlintAdapter=new Ki;oxfmtAdapter=new qi;detect(e){return!!e.jsEnv?.hasPackageJson}getAdapters(e){return[this.scriptAdapter,this.vitePlusAdapter,this.biomeAdapter,this.oxfmtAdapter,this.prettierAdapter,this.oxlintAdapter,this.eslintAdapter]}},Yi=class{id=`ruff`;name=`Ruff`;capabilities=[`lint`,`format`];detect(e){return e.hasPythonSource??or(e.cwd)?{detected:!0,reason:`default Python toolchain`}:{detected:!1,reason:`no Python source files detected`}}plan(e,t){let n=[];return t===`fix`?n.push({id:`ruff-lint-fix`,toolId:this.id,capability:`lint`,executable:`ruff`,args:[`check`,`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`Ruff: lint autofix`},{id:`ruff-format`,toolId:this.id,capability:`format`,executable:`ruff`,args:[`format`,`.`],cwd:e.cwd,mode:`fix`,description:`Ruff: format`}):n.push({id:`ruff-lint-check`,toolId:this.id,capability:`lint`,executable:`ruff`,args:[`check`,`.`],cwd:e.cwd,mode:`check`,description:`Ruff: lint check`},{id:`ruff-format-check`,toolId:this.id,capability:`format`,executable:`ruff`,args:[`format`,`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Ruff: format check`}),n}},Xi=class{id=`python`;name=`Python`;ruffAdapter=new Yi;detect(e){return e.hasPythonSource??or(e.cwd)}getAdapters(e){return[this.ruffAdapter]}},Zi=class{providers=/* @__PURE__ */ new Map;standaloneAdapters=/* @__PURE__ */ new Map;registerProvider(e){this.providers.set(e.id,e)}getProvider(e){return this.providers.get(e)}getProviders(){return Array.from(this.providers.values())}registerAdapter(e){this.standaloneAdapters.set(e.id,e)}getAdapter(e){return this.standaloneAdapters.get(e)}getStandaloneAdapters(){return Array.from(this.standaloneAdapters.values())}clear(){this.providers.clear(),this.standaloneAdapters.clear()}};function Qi(){let e=new Zi;return e.registerProvider(new Xi),e.registerProvider(new Ji),e}const $i=Qi();async function ea(e,t=process.cwd()){let n=`${e.command} ${e.args.join(` `)}`.trim();if(en(`Installing JS dependencies: ${n}`),await Yt(e.command,e.args,{cwd:t,ignoreReturnCode:!0})!==0)throw Error(`JS dependency installation failed executing "${n}". Check dependency constraints or lockfile compatibility.`)}async function ta(e=process.cwd()){let t=fn(e);if(!t.hasPackageJson){en(`No package.json found. Skipping scripts.`);return}if(t.packageJsonError)throw Error(t.packageJsonError);let n=cn(process.env.PACKAGE_MANAGER||_n(t).name),r=(await Ir($i,{cwd:e,jsEnv:t,jsPm:n,hasPythonSource:!1},`fix`)).tasks.filter(e=>e.toolId!==`ruff`);if(r.length===0){en(`No matching scripts (fix, format, lint, check, etc.) found in package.json.`);return}await ea(vn(n),e);let i=(await new Ar().executeTasks(r)).filter(e=>e.error);if(i.length>0){let e=i.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` -`))}}async function na(e,t,n=process.cwd()){let r=`${e} ${t.join(` `)}`.trim();if(en(`Executing: ${r}`),await Yt(e,t,{cwd:n,ignoreReturnCode:!0})!==0)throw Error(`Python command "${r}" failed. Check Python environment and dependency configuration.`)}async function ra(e,t=process.cwd()){Dr(e,`Invalid Python environment: "pmInfo" must be an object`);let n=e.name===`pip`?`pip`:e.name;if(!await Vt(n,!1)&&!e.isStandalone){let e=`Required Python package manager '${n}' is not available in PATH.`;throw Error(e)}if(!e.isStandalone&&e.installCommands.length>0){let n=e.installCommands;en(`Installing Python dependencies: ${n.map(e=>`${e.executable} ${e.args.join(` `)}`).join(` && `)}`);for(let{executable:e,args:r}of n)await na(e,r,t)}}async function ia(e=process.cwd(),t,n){if(!(t??or(e)))return;let r;if(n)r=n;else{if(!await Vt(`ruff`,!1))throw Error(`Required Python binary 'ruff' is not available in PATH.`);r=(await Ir($i,{cwd:e,hasPythonSource:!0},`fix`)).tasks.filter(e=>e.toolId===`ruff`)}let i=(await new Ar().executeTasks(r)).filter(e=>e.error);if(i.length>0){let e=i.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` -`))}}async function aa(e,t){if(process.env.JS_SETUP_OUTCOME===`failure`||process.env.NODE_SETUP_OUTCOME===`failure`||process.env.BUN_SETUP_OUTCOME===`failure`||process.env.PNPM_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`JavaScript runtime or package manager setup failed.`);try{if(t){let n=fn(e);if(!n.hasPackageJson)return en(`No package.json found. Skipping scripts.`),null;if(n.packageJsonError)throw Error(n.packageJsonError);if(t.length===0)return en(`No matching scripts (fix, format, lint, check, etc.) found in package.json.`),null;await ea(vn(cn(process.env.PACKAGE_MANAGER||_n(n).name)),e);let r=(await new Ar().executeTasks(t)).filter(e=>e.error);if(r.length>0){let e=r.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` -`))}return null}return await ta(e),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function oa(e,t){if(process.env.PYTHON_SETUP_OUTCOME===`failure`||process.env.UV_SETUP_OUTCOME===`failure`||process.env.PY_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`Python runtime or package manager setup failed.`);let n=t&&`pmInfo`in t?t.pmInfo:Tr(e,void 0,{ctx:t?.ctx,hasSource:t?.hasSource});if(!n)return null;try{return await ra(n,e),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function sa(e,t,n){if(process.env.RUFF_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`Ruff setup failed.`);try{return await ia(e,t,n),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function ca(e,t,n){let r=t??or(e);if(!r)return[];let i=[],a=ir(e),o=await oa(e,{ctx:a,hasSource:!0,pmInfo:Tr(e,void 0,{ctx:a,hasSource:!0,isProject:!0})});o&&i.push(o);let s=await sa(e,r,n);return s&&i.push(s),i}async function la(e=process.cwd()){let t=or(e),n;try{let r=fn(e),i={cwd:e,jsEnv:r,jsPm:cn(process.env.PACKAGE_MANAGER||_n(r).name),hasPythonSource:t};n=await Ir($i,i,`fix`),en(n.summary)}catch{}let r=[],i=n?n.tasks.filter(e=>e.toolId!==`ruff`):void 0,a=n?n.tasks.filter(e=>e.toolId===`ruff`):void 0,o=await aa(e,i);o&&r.push(o);let s=await ca(e,t,a);r.push(...s),r.length>0&&(Zt(`Failed to run scripts: ${r.map(e=>e.message).join(` -`)}`),process.env.NODE_ENV!==`test`&&process.exit(1))}process.env.NODE_ENV!==`test`&&Or(import.meta.url)&&la();export{ta as executeJsWorkflow,ra as executePythonWorkflow,ia as executeRuffAutofix,la as run,cn as sanitizePackageManager}; \ No newline at end of file +`)}function Rr(e){for(let t of[`format:write`,`format:fix`,`format`,`fmt`])if(e[t]&&Bi(e[t],e))return t}function zr(e){if(e[`format:check`]&&zi(e[`format:check`],e)===`read-only`)return`format:check`;if(e[`fmt:check`]&&zi(e[`fmt:check`],e)===`read-only`)return`fmt:check`}function Br(e,t){return t===`fix`?Rr(e):zr(e)}function Vr(e,t){if(t===`fix`)return e[`lint:fix`]?`lint:fix`:e[`lint:write`]?`lint:write`:void 0;if(e[`lint:check`]&&zi(e[`lint:check`],e)===`read-only`)return`lint:check`;if(e.lint&&zi(e.lint,e)===`read-only`)return`lint`}function Hr(e,t,n){if(n.inSingleQuote||n.inDoubleQuote||n.isEscaped)return 0;let r=e[t];return r===`;`||r===` +`?1:r===`&`&&e[t+1]===`&`||r===`|`&&e[t+1]===`|`?2:+(r===`|`)}function Ur(e,t){let n=t;for(;nt?n-1:t}function Wr(e,t){return t.inSingleQuote||t.inDoubleQuote||t.isEscaped?!1:e.length===0||/\s/.test(e[e.length-1])}function Gr(e,t){return t.isEscaped=!1,{ch:e,advance:!0}}function Kr(e,t,n,r){let i=e[t];if(n.isEscaped)return{newCmd:r+Gr(i,n).ch,nextIndex:t,sepLen:0};if(i===`\\`&&!n.inSingleQuote)return n.isEscaped=!0,{newCmd:r+i,nextIndex:t,sepLen:0};if(i===`'`&&!n.inDoubleQuote)return n.inSingleQuote=!n.inSingleQuote,{newCmd:r+i,nextIndex:t,sepLen:0};if(i===`"`&&!n.inSingleQuote)return n.inDoubleQuote=!n.inDoubleQuote,{newCmd:r+i,nextIndex:t,sepLen:0};if(i===`#`&&Wr(r,n))return{newCmd:r,nextIndex:Ur(e,t),sepLen:0};let a=Hr(e,t,n);return{newCmd:r+(a===0?i:``),nextIndex:t,sepLen:a}}function qr(e){let t=[],n=``,r={inSingleQuote:!1,inDoubleQuote:!1,isEscaped:!1,hasSyntaxError:!1};for(let i=0;i0?(n.trim()&&t.push(n.trim()),n=``,i+=a.sepLen-1):(n=a.newCmd,i=a.nextIndex)}return(r.inSingleQuote||r.inDoubleQuote||r.isEscaped)&&(r.hasSyntaxError=!0),n.trim()&&t.push(n.trim()),{commands:t,hasSyntaxError:r.hasSyntaxError}}function Jr(e){return qr(e).commands}function Yr(e,t,n){return t.isEscaped=!1,n+e}function Xr(e,t){return e===`'`&&!t.inDoubleQuote?(t.inSingleQuote=!t.inSingleQuote,!0):e===`"`&&!t.inSingleQuote&&(t.inDoubleQuote=!t.inDoubleQuote,!0)}function Zr(e,t,n){if(n.inSingleQuote||n.inDoubleQuote||n.isEscaped||e[t]!==`&`)return!1;let r=t>0?e[t-1]:``,i=t+1`}function Qr(e,t,n){if(n.inSingleQuote||n.isEscaped)return!1;let r=e[t];return r==="`"||r===`$`&&t+1`)&&t+1`||r===`<`||r===`&`&&t+1`}function ti(e,t,n){return Zr(e,t,n.state)||Qr(e,t,n.state)||$r(e,t,n.state)?(n.inspection.hasUnsupportedSyntax=!0,!0):ei(e,t,n.state)?(n.inspection.hasRedirection=!0,n.inspection.hasUnsupportedSyntax=!0,!0):!1}function ni(e,t,n,r){let i=e[t];return n.state.isEscaped?{word:Yr(i,n.state,r),hasChars:!0,skipped:!1}:i===`\\`&&!n.state.inSingleQuote?(n.state.isEscaped=!0,{word:r,hasChars:!1,skipped:!0}):(ti(e,t,n),Xr(i,n.state)?{word:r,hasChars:!0,skipped:!0}:{word:r+i,hasChars:!0,skipped:!1})}function ri(e){let t={tokens:[],hasUnsupportedSyntax:!1,hasRedirection:!1},n={inSingleQuote:!1,inDoubleQuote:!1,isEscaped:!1},r={inspection:t,state:n},i=``,a=!1;for(let o=0;o0&&n.push(t)}return n}function ui(e,t){let n=li(e);for(let e of n)if(ci(e,t))return!0;return!1}function di(e,t,n){return!n||n===t?!1:!ui(e[t]||``,n)}function fi(e,t,n){return!n||n===t?!1:!ui(e[t]||``,n)}function pi(e){return e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function mi(e){let[t]=e;return t===`prettier`&&e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function hi(e){let[t]=e;return t===`oxfmt`&&e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function gi(e){let[t,n]=e;return t!==`biome`||n!==`format`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function _i(e){let[t]=e;return t===`eslint`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function vi(e){let[t]=e;return t===`oxlint`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function yi(e){let[t,n]=e;return t!==`biome`||n!==`lint`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function bi(e){let[t,n]=e;return t!==`biome`||n!==`check`?!1:e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function xi(e){let[t,n]=e;return t!==`vp`||n!==`fmt`?!1:!e.some(e=>e===`--check`||e===`-c`)}function Si(e){let[t,n]=e;return t!==`vp`||n!==`lint`?!1:e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Ci(e){let[t]=e;if(t!==`prettier`)return!1;let n=e.some(e=>e===`--check`||e===`-c`||e===`--list-different`||e===`-l`),r=e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`));return n&&!r}function wi(e){let[t]=e;if(t!==`oxfmt`)return!1;let n=e.some(e=>e===`--check`||e===`-c`),r=e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`));return n&&!r}function Ti(e){let[t,n]=e;return t!==`biome`||n!==`format`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`))}function Ei(e){let[t]=e;return t===`eslint`&&!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Di(e){let[t]=e;return t===`oxlint`&&!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Oi(e){let[t,n]=e;return t!==`biome`||n!==`lint`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function ki(e){let[t,n]=e;return t!==`biome`||n!==`check`?!1:!e.some(e=>e===`-w`||e===`--write`||e.startsWith(`--write=`)||e===`--fix`||e.startsWith(`--fix=`))}function Ai(e){let[t,n]=e;return t!==`vp`||n!==`fmt`?!1:e.some(e=>e===`--check`||e===`-c`)}function ji(e){let[t,n]=e;return t!==`vp`||n!==`lint`?!1:!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Mi(e){let[t,n]=e;return t!==`vp`||n!==`check`?!1:!e.some(e=>e===`--fix`||e.startsWith(`--fix=`))}function Ni(e){if(e.length===0)return;let[t,n,r]=e;if((t===`npm`||t===`pnpm`||t===`yarn`||t===`bun`)&&n===`run`&&r)return r;if((t===`pnpm`||t===`bun`||t===`yarn`)&&n&&!n.startsWith(`-`))return n}function Pi(e){if(e.length>=3&&(e[0]===`sh`||e[0]===`bash`)&&e[1]===`-c`)return e[2]}function Fi(e){return mi(e)||hi(e)||gi(e)||_i(e)||vi(e)||yi(e)||bi(e)||xi(e)||Si(e)}function Ii(e){return Ci(e)||wi(e)||Ti(e)||Ei(e)||Di(e)||Oi(e)||ki(e)||Ai(e)||ji(e)||Mi(e)}function Li(e,t,n){if(e.length===0)return`unknown`;if(pi(e))return`write-capable`;let r=Pi(e);if(r!==void 0)return Ri(r,t,n);let i=Ni(e);if(i){if(n.has(i))return`unknown`;n.add(i);let e=t[i];return e?Ri(e,t,n):`unknown`}return Fi(e)?`write-capable`:Ii(e)?`read-only`:`unknown`}function Ri(e,t,n){let r=qr(e);if(r.hasSyntaxError)return`unknown`;let{commands:i}=r;if(i.length===0)return`unknown`;let a=!1;for(let e of i){let r=ri(e),i=Li(si(r.tokens),t,n);if(i===`write-capable`)return`write-capable`;if(r.hasUnsupportedSyntax||i===`unknown`)return`unknown`;i===`read-only`&&(a=!0)}return a?`read-only`:`unknown`}function zi(e,t={}){return Ri(e,t,/* @__PURE__ */ new Set)}function Bi(e,t={},n=/* @__PURE__ */ new Set){return li(e).some(e=>{let r=Ni(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?Bi(e,t,n):!1}let i=Pi(e);return i===void 0?mi(e)||hi(e)||gi(e)||xi(e):Bi(i,t,n)})}function Vi(e,t={},n=/* @__PURE__ */ new Set){return li(e).some(e=>{let r=Ni(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?Vi(e,t,n):!1}let i=Pi(e);return i===void 0?_i(e)||vi(e)||yi(e)||Si(e):Vi(i,t,n)})}function Hi(e,t={},n=/* @__PURE__ */ new Set){return li(e).some(e=>{let r=Ni(e);if(r){if(n.has(r))return!1;n.add(r);let e=t[r];return e?Hi(e,t,n):!1}let i=Pi(e);if(i!==void 0)return Hi(i,t,n);if(bi(e))return!0;let[a,o]=e;return a===`vp`&&o===`check`&&e.some(e=>e===`--fix`||e.startsWith(`--fix=`))})}function Ui(e,t,n){return t&&ui(e,t)?!0:n(e)}function Wi(e,t,n,r,i){let a=Ui(t,r,t=>Bi(t,e)),o=Ui(t,i,t=>Vi(t,e)),s=Hi(t,e);return a&&o?[{capability:`format`,capabilities:[`format`,`lint`],scriptName:n}]:a&&!s?[{capability:`format`,scriptName:n}]:o&&!s?[{capability:`lint`,scriptName:n}]:s?[{capability:`autofix`,scriptName:n}]:[{capability:`typecheck`,capabilities:[],scriptName:n}]}function Gi(e,t){return Bi(t[e],t)?{capability:`format`,scriptName:e}:{capability:`typecheck`,capabilities:[],scriptName:e}}function Ki(e,t){return Vi(t[e],t)?{capability:`lint`,scriptName:e}:{capability:`typecheck`,capabilities:[],scriptName:e}}function qi(e,t,n){let r=[];return e&&r.push(Gi(e,n)),t&&r.push(Ki(t,n)),r}function Ji(e,t,n,r,i){r&&di(t,n,r)&&e.push(Gi(r,t)),i&&fi(t,n,i)&&e.push(Ki(i,t))}function Yi(e){let t=e.fix?`fix`:e[`check:fix`]?`check:fix`:void 0,n=Br(e,`fix`),r=Vr(e,`fix`);if(!t)return qi(n,r,e);let i=Wi(e,e[t]||``,t,n,r);return Ji(i,e,t,n,r),i}function Xi(e){let t=[],n=Br(e,`check`),r=Vr(e,`check`);return n&&t.push({capability:`format`,scriptName:n}),r&&t.push({capability:`lint`,scriptName:r}),t}function Zi(e,t){return t===`fix`?Yi(e):Xi(e)}var Qi=class{id=`npm-scripts`;name=`package.json scripts`;capabilities=[`autofix`,`format`,`lint`];detect(e){let t=e.jsEnv?.packageJson?.scripts;if(!t||Object.keys(t).length===0)return{detected:!1,reason:`no scripts declared in package.json`};let n=Zi(t,`fix`),r=Zi(t,`check`),i=[...n,...r];return i.length>0?{detected:!0,reason:`found supported scripts: ${Array.from(new Set(i.map(e=>`"${e.scriptName}"`))).join(`, `)}`}:{detected:!1,reason:`no supported autofix or check scripts found in package.json`}}plan(e,t){let n=Zi(e.jsEnv?.packageJson?.scripts||{},t),r=cn(e.jsPm||`npm`);return n.map(n=>({id:`script-${n.scriptName}`,toolId:this.id,capability:n.capability,capabilities:n.capabilities,executable:r,args:[`run`,n.scriptName],cwd:e.cwd,mode:t,description:`package.json script: ${r} run ${n.scriptName}`}))}};function $i(e,t){return e?!!(e.dependencies?.[t]||e.devDependencies?.[t]||e.peerDependencies?.[t]||e.optionalDependencies?.[t]):!1}function ea(e,t){for(let n of t)if(f.existsSync(p.join(e,n)))return!0;return!1}var ta=class{id=`vite-plus`;name=`Vite+`;capabilities=[`autofix`,`format`,`lint`];detect(e){let t=e.jsEnv?.packageJson,n=sn(t),r=ea(e.cwd,[`vite.config.ts`,`vite.config.js`,`vite.config.mjs`,`vite.config.cjs`]);return n||r&&t?.scripts&&Object.values(t.scripts).some(e=>e.includes(`vp `))?{detected:!0,reason:`Vite+ dependency and/or configuration detected`}:{detected:!1,reason:`no Vite+ dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`vite-plus-fmt`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`format`,executable:`vp`,args:[`fmt`],cwd:e.cwd,mode:`fix`,description:`Vite+: vp fmt`},{id:`vite-plus-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`lint`,executable:`vp`,args:[`lint`,`--fix`],cwd:e.cwd,mode:`fix`,description:`Vite+: vp lint --fix`}]:[{id:`vite-plus-fmt-check`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`format`,executable:`vp`,args:[`fmt`,`--check`],cwd:e.cwd,mode:`check`,description:`Vite+: vp fmt --check`},{id:`vite-plus-lint`,toolId:this.id,toolName:this.name,requiredPackage:`vite-plus`,capability:`lint`,executable:`vp`,args:[`lint`],cwd:e.cwd,mode:`check`,description:`Vite+: vp lint`}]}},na=class{id=`biome`;name=`Biome`;capabilities=[`format`,`lint`,`assist`];detect(e){let t=e.jsEnv?.packageJson,n=$i(t,`@biomejs/biome`),r=ea(e.cwd,[`biome.json`,`biome.jsonc`]);return n&&r?{detected:!0,reason:`Biome dependency and configuration detected`}:r?{detected:!0,reason:`Biome configuration detected`}:n?{detected:!1,reason:`Biome dependency present without configuration file`}:{detected:!1,reason:`no Biome dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`biome-format-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`format`,executable:`biome`,args:[`format`,`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: format --write .`},{id:`biome-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`lint`,executable:`biome`,args:[`lint`,`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: lint --write .`},{id:`biome-assist-fix`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`assist`,executable:`biome`,args:[`check`,`--write`,`--formatter-enabled=false`,`--linter-enabled=false`,`.`],cwd:e.cwd,mode:`fix`,description:`Biome: safe assist actions (--write)`}]:[{id:`biome-format-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`format`,executable:`biome`,args:[`format`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: format .`},{id:`biome-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`lint`,executable:`biome`,args:[`lint`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: lint .`},{id:`biome-assist-check`,toolId:this.id,toolName:this.name,requiredPackage:`@biomejs/biome`,capability:`assist`,executable:`biome`,args:[`check`,`--formatter-enabled=false`,`--linter-enabled=false`,`.`],cwd:e.cwd,mode:`check`,description:`Biome: check assist actions`}]}},ra=class{id=`prettier`;name=`Prettier`;capabilities=[`format`];detect(e){let t=e.jsEnv?.packageJson,n=$i(t,`prettier`),r=!!t?.prettier,i=ea(e.cwd,[`.prettierrc`,`.prettierrc.json`,`.prettierrc.yml`,`.prettierrc.yaml`,`.prettierrc.json5`,`.prettierrc.js`,`.prettierrc.cjs`,`.prettierrc.mjs`,`prettier.config.js`,`prettier.config.cjs`,`prettier.config.mjs`]);return n&&(r||i)?{detected:!0,reason:`Prettier dependency and configuration detected`}:r||i?{detected:!0,reason:`Prettier configuration detected`}:n?{detected:!1,reason:`Prettier dependency present without configuration`}:{detected:!1,reason:`no Prettier dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`prettier-format`,toolId:this.id,toolName:this.name,requiredPackage:`prettier`,capability:`format`,executable:`prettier`,args:[`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Prettier: format`}]:[{id:`prettier-check`,toolId:this.id,toolName:this.name,requiredPackage:`prettier`,capability:`format`,executable:`prettier`,args:[`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Prettier: format check`}]}},ia=class{id=`eslint`;name=`ESLint`;capabilities=[`lint`];detect(e){let t=e.jsEnv?.packageJson,n=$i(t,`eslint`),r=!!t?.eslintConfig,i=ea(e.cwd,[`eslint.config.js`,`eslint.config.mjs`,`eslint.config.cjs`,`eslint.config.ts`,`.eslintrc`,`.eslintrc.js`,`.eslintrc.cjs`,`.eslintrc.json`,`.eslintrc.yaml`,`.eslintrc.yml`]);return n&&(r||i)?{detected:!0,reason:`ESLint dependency and configuration detected`}:r||i?{detected:!0,reason:`ESLint configuration detected`}:n?{detected:!1,reason:`ESLint dependency present without configuration`}:{detected:!1,reason:`no ESLint dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`eslint-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`eslint`,capability:`lint`,executable:`eslint`,args:[`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`ESLint: lint autofix`}]:[{id:`eslint-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`eslint`,capability:`lint`,executable:`eslint`,args:[`.`],cwd:e.cwd,mode:`check`,description:`ESLint: lint check`}]}},aa=class{id=`oxlint`;name=`Oxlint`;capabilities=[`lint`];detect(e){let t=e.jsEnv?.packageJson,n=$i(t,`oxlint`),r=ea(e.cwd,[`.oxlintrc.json`,`oxlint.json`,`oxlint.config.ts`,`oxlint.config.js`]);return n&&r?{detected:!0,reason:`Oxlint dependency and configuration detected`}:r?{detected:!0,reason:`Oxlint configuration detected`}:n?{detected:!1,reason:`Oxlint dependency present without configuration`}:{detected:!1,reason:`no Oxlint dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`oxlint-lint-fix`,toolId:this.id,toolName:this.name,requiredPackage:`oxlint`,capability:`lint`,executable:`oxlint`,args:[`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`Oxlint: lint autofix`}]:[{id:`oxlint-lint-check`,toolId:this.id,toolName:this.name,requiredPackage:`oxlint`,capability:`lint`,executable:`oxlint`,args:[`.`],cwd:e.cwd,mode:`check`,description:`Oxlint: lint check`}]}},oa=class{id=`oxfmt`;name=`Oxfmt`;capabilities=[`format`];detect(e){let t=e.jsEnv?.packageJson,n=$i(t,`oxfmt`),r=ea(e.cwd,[`.oxfmtrc.json`,`oxfmt.json`,`oxfmt.config.ts`,`oxfmt.config.js`]);return n&&r?{detected:!0,reason:`Oxfmt dependency and configuration detected`}:r?{detected:!0,reason:`Oxfmt configuration detected`}:n?{detected:!1,reason:`Oxfmt dependency present without configuration`}:{detected:!1,reason:`no Oxfmt dependency or configuration`}}plan(e,t){return t===`fix`?[{id:`oxfmt-format`,toolId:this.id,toolName:this.name,requiredPackage:`oxfmt`,capability:`format`,executable:`oxfmt`,args:[`--write`,`.`],cwd:e.cwd,mode:`fix`,description:`Oxfmt: format`}]:[{id:`oxfmt-check`,toolId:this.id,toolName:this.name,requiredPackage:`oxfmt`,capability:`format`,executable:`oxfmt`,args:[`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Oxfmt: format check`}]}},sa=class{id=`javascript`;name=`JavaScript/TypeScript`;scriptAdapter=new Qi;vitePlusAdapter=new ta;biomeAdapter=new na;prettierAdapter=new ra;eslintAdapter=new ia;oxlintAdapter=new aa;oxfmtAdapter=new oa;detect(e){return!!e.jsEnv?.hasPackageJson}getAdapters(e){return[this.scriptAdapter,this.vitePlusAdapter,this.biomeAdapter,this.oxfmtAdapter,this.prettierAdapter,this.oxlintAdapter,this.eslintAdapter]}},ca=class{id=`ruff`;name=`Ruff`;capabilities=[`lint`,`format`];detect(e){return e.hasPythonSource??or(e.cwd)?{detected:!0,reason:`default Python toolchain`}:{detected:!1,reason:`no Python source files detected`}}plan(e,t){let n=[];return t===`fix`?n.push({id:`ruff-lint-fix`,toolId:this.id,capability:`lint`,executable:`ruff`,args:[`check`,`--fix`,`.`],cwd:e.cwd,mode:`fix`,description:`Ruff: lint autofix`},{id:`ruff-format`,toolId:this.id,capability:`format`,executable:`ruff`,args:[`format`,`.`],cwd:e.cwd,mode:`fix`,description:`Ruff: format`}):n.push({id:`ruff-lint-check`,toolId:this.id,capability:`lint`,executable:`ruff`,args:[`check`,`.`],cwd:e.cwd,mode:`check`,description:`Ruff: lint check`},{id:`ruff-format-check`,toolId:this.id,capability:`format`,executable:`ruff`,args:[`format`,`--check`,`.`],cwd:e.cwd,mode:`check`,description:`Ruff: format check`}),n}},la=class{id=`python`;name=`Python`;ruffAdapter=new ca;detect(e){return e.hasPythonSource??or(e.cwd)}getAdapters(e){return[this.ruffAdapter]}},ua=class{providers=/* @__PURE__ */ new Map;standaloneAdapters=/* @__PURE__ */ new Map;registerProvider(e){this.providers.set(e.id,e)}getProvider(e){return this.providers.get(e)}getProviders(){return Array.from(this.providers.values())}registerAdapter(e){this.standaloneAdapters.set(e.id,e)}getAdapter(e){return this.standaloneAdapters.get(e)}getStandaloneAdapters(){return Array.from(this.standaloneAdapters.values())}clear(){this.providers.clear(),this.standaloneAdapters.clear()}};function da(){let e=new ua;return e.registerProvider(new la),e.registerProvider(new sa),e}const fa=da();async function pa(e,t=process.cwd()){let n=`${e.command} ${e.args.join(` `)}`.trim();if(en(`Installing JS dependencies: ${n}`),await Yt(e.command,e.args,{cwd:t,ignoreReturnCode:!0})!==0)throw Error(`JS dependency installation failed executing "${n}". Check dependency constraints or lockfile compatibility.`)}async function ma(e=process.cwd()){let t=fn(e);if(!t.hasPackageJson){en(`No package.json found. Skipping scripts.`);return}if(t.packageJsonError)throw Error(t.packageJsonError);let n=cn(process.env.PACKAGE_MANAGER||_n(t).name),r=(await Ir(fa,{cwd:e,jsEnv:t,jsPm:n,hasPythonSource:!1},`fix`)).tasks.filter(e=>e.toolId!==`ruff`);if(r.length===0){en(`No matching scripts (fix, format, lint, check, etc.) found in package.json.`);return}await pa(vn(n),e);let i=(await new Ar().executeTasks(r)).filter(e=>e.error);if(i.length>0){let e=i.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` +`))}}async function ha(e,t,n=process.cwd()){let r=`${e} ${t.join(` `)}`.trim();if(en(`Executing: ${r}`),await Yt(e,t,{cwd:n,ignoreReturnCode:!0})!==0)throw Error(`Python command "${r}" failed. Check Python environment and dependency configuration.`)}async function ga(e,t=process.cwd()){Dr(e,`Invalid Python environment: "pmInfo" must be an object`);let n=e.name===`pip`?`pip`:e.name;if(!await Vt(n,!1)&&!e.isStandalone){let e=`Required Python package manager '${n}' is not available in PATH.`;throw Error(e)}if(!e.isStandalone&&e.installCommands.length>0){let n=e.installCommands;en(`Installing Python dependencies: ${n.map(e=>`${e.executable} ${e.args.join(` `)}`).join(` && `)}`);for(let{executable:e,args:r}of n)await ha(e,r,t)}}async function _a(e=process.cwd(),t,n){if(!(t??or(e)))return;let r;if(n)r=n;else{if(!await Vt(`ruff`,!1))throw Error(`Required Python binary 'ruff' is not available in PATH.`);r=(await Ir(fa,{cwd:e,hasPythonSource:!0},`fix`)).tasks.filter(e=>e.toolId===`ruff`)}let i=(await new Ar().executeTasks(r)).filter(e=>e.error);if(i.length>0){let e=i.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` +`))}}async function va(e,t){if(process.env.JS_SETUP_OUTCOME===`failure`||process.env.NODE_SETUP_OUTCOME===`failure`||process.env.BUN_SETUP_OUTCOME===`failure`||process.env.PNPM_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`JavaScript runtime or package manager setup failed.`);try{if(t){let n=fn(e);if(!n.hasPackageJson)return en(`No package.json found. Skipping scripts.`),null;if(n.packageJsonError)throw Error(n.packageJsonError);if(t.length===0)return en(`No matching scripts (fix, format, lint, check, etc.) found in package.json.`),null;await pa(vn(cn(process.env.PACKAGE_MANAGER||_n(n).name)),e);let r=(await new Ar().executeTasks(t)).filter(e=>e.error);if(r.length>0){let e=r.map(e=>e.error?.message||`Task ${e.task.id} failed`);throw Error(e.join(` +`))}return null}return await ma(e),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function ya(e,t){if(process.env.PYTHON_SETUP_OUTCOME===`failure`||process.env.UV_SETUP_OUTCOME===`failure`||process.env.PY_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`Python runtime or package manager setup failed.`);let n=t&&`pmInfo`in t?t.pmInfo:Tr(e,void 0,{ctx:t?.ctx,hasSource:t?.hasSource});if(!n)return null;try{return await ga(n,e),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function ba(e,t,n){if(process.env.RUFF_SETUP_OUTCOME===`failure`)return/* @__PURE__ */ Error(`Ruff setup failed.`);try{return await _a(e,t,n),null}catch(e){return e instanceof Error?e:Error(String(e))}}async function xa(e,t,n){let r=t??or(e);if(!r)return[];let i=[],a=ir(e),o=await ya(e,{ctx:a,hasSource:!0,pmInfo:Tr(e,void 0,{ctx:a,hasSource:!0,isProject:!0})});o&&i.push(o);let s=await ba(e,r,n);return s&&i.push(s),i}async function Sa(e=process.cwd()){let t=or(e),n;try{let r=fn(e),i={cwd:e,jsEnv:r,jsPm:cn(process.env.PACKAGE_MANAGER||_n(r).name),hasPythonSource:t};n=await Ir(fa,i,`fix`),en(n.summary)}catch{}let r=[],i=n?n.tasks.filter(e=>e.toolId!==`ruff`):void 0,a=n?n.tasks.filter(e=>e.toolId===`ruff`):void 0,o=await va(e,i);o&&r.push(o);let s=await xa(e,t,a);r.push(...s),r.length>0&&(Zt(`Failed to run scripts: ${r.map(e=>e.message).join(` +`)}`),process.env.NODE_ENV!==`test`&&process.exit(1))}process.env.NODE_ENV!==`test`&&Or(import.meta.url)&&Sa();export{ma as executeJsWorkflow,ga as executePythonWorkflow,_a as executeRuffAutofix,Sa as run,cn as sanitizePackageManager}; \ No newline at end of file diff --git a/src/autofix/adapters/javascript/script-adapter.ts b/src/autofix/adapters/javascript/script-adapter.ts index 5bc5666b..26802d53 100644 --- a/src/autofix/adapters/javascript/script-adapter.ts +++ b/src/autofix/adapters/javascript/script-adapter.ts @@ -27,6 +27,18 @@ function resolveFormatFixCandidate(scripts: Record): string | un return undefined; } +function resolveFormatCheckCandidate(scripts: Record): string | undefined { + // In check mode, positively require scripts to be "read-only". + // Fall back to "fmt:check" when "format:check" is missing or cannot be safely verified as read-only. + if (scripts["format:check"] && classifyScript(scripts["format:check"], scripts) === "read-only") { + return "format:check"; + } + if (scripts["fmt:check"] && classifyScript(scripts["fmt:check"], scripts) === "read-only") { + return "fmt:check"; + } + return undefined; +} + function resolveFormatScript( scripts: Record, mode: ExecutionMode, @@ -34,18 +46,7 @@ function resolveFormatScript( if (mode === "fix") { return resolveFormatFixCandidate(scripts); } - // In check mode, positively require scripts to be "read-only". - // Reject unknown commands, unsupported shell wrappers, unresolved package-manager delegations, - // parser failures, and any script with write flags. - const candidate = scripts["format:check"] - ? "format:check" - : scripts["fmt:check"] - ? "fmt:check" - : undefined; - if (candidate && classifyScript(scripts[candidate], scripts) === "read-only") { - return candidate; - } - return undefined; + return resolveFormatCheckCandidate(scripts); } function resolveLintScript( @@ -76,6 +77,10 @@ interface TokenizerState { isEscaped: boolean; } +interface SplitState extends TokenizerState { + hasSyntaxError: boolean; +} + function getSeparatorLength(text: string, index: number, state: TokenizerState): number { if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) return 0; const ch = text[index]; @@ -91,7 +96,6 @@ function skipComment(text: string, index: number): number { while (i < text.length && text[i] !== "\n") { i++; } - // Return index immediately before newline so outer loop processes the newline separator return i > index ? i - 1 : index; } @@ -101,63 +105,93 @@ function isShellCommentStart(currentCmd: string, state: TokenizerState): boolean return /\s/.test(currentCmd[currentCmd.length - 1]); } +interface SplitResult { + commands: string[]; + hasSyntaxError: boolean; +} + +function handleEscapeInSplit(ch: string, state: SplitState): { ch: string; advance: boolean } { + state.isEscaped = false; + return { ch, advance: true }; +} + +function processSplitChar( + shellText: string, + i: number, + state: SplitState, + currentCmd: string, +): { newCmd: string; nextIndex: number; sepLen: number } { + const ch = shellText[i]; + + if (state.isEscaped) { + const res = handleEscapeInSplit(ch, state); + return { newCmd: currentCmd + res.ch, nextIndex: i, sepLen: 0 }; + } + + if (ch === "\\" && !state.inSingleQuote) { + state.isEscaped = true; + return { newCmd: currentCmd + ch, nextIndex: i, sepLen: 0 }; + } + + if (ch === "'" && !state.inDoubleQuote) { + state.inSingleQuote = !state.inSingleQuote; + return { newCmd: currentCmd + ch, nextIndex: i, sepLen: 0 }; + } + + if (ch === '"' && !state.inSingleQuote) { + state.inDoubleQuote = !state.inDoubleQuote; + return { newCmd: currentCmd + ch, nextIndex: i, sepLen: 0 }; + } + + if (ch === "#" && isShellCommentStart(currentCmd, state)) { + const nextIdx = skipComment(shellText, i); + return { newCmd: currentCmd, nextIndex: nextIdx, sepLen: 0 }; + } + + const sepLen = getSeparatorLength(shellText, i, state); + return { newCmd: currentCmd + (sepLen === 0 ? ch : ""), nextIndex: i, sepLen }; +} + /** * Splits shell command text into chained commands (;, &&, ||, |, newline) * while respecting quotes ('...', "..."), escapes (\), and comments. + * Tracks syntax errors such as unterminated quotes or trailing escape. */ -function splitChainedCommands(shellText: string): string[] { +function splitChainedCommandsDetailed(shellText: string): SplitResult { const commands: string[] = []; let currentCmd = ""; - const state: TokenizerState = { inSingleQuote: false, inDoubleQuote: false, isEscaped: false }; + const state: SplitState = { + inSingleQuote: false, + inDoubleQuote: false, + isEscaped: false, + hasSyntaxError: false, + }; for (let i = 0; i < shellText.length; i++) { - const ch = shellText[i]; - - if (state.isEscaped) { - currentCmd += ch; - state.isEscaped = false; - continue; - } - - if (ch === "\\" && !state.inSingleQuote) { - state.isEscaped = true; - currentCmd += ch; - continue; - } - - if (ch === "'" && !state.inDoubleQuote) { - state.inSingleQuote = !state.inSingleQuote; - currentCmd += ch; - continue; - } - - if (ch === '"' && !state.inSingleQuote) { - state.inDoubleQuote = !state.inDoubleQuote; - currentCmd += ch; - continue; - } - - if (ch === "#" && isShellCommentStart(currentCmd, state)) { - i = skipComment(shellText, i); - continue; - } - - const sepLen = getSeparatorLength(shellText, i, state); - if (sepLen > 0) { + const processed = processSplitChar(shellText, i, state, currentCmd); + if (processed.sepLen > 0) { if (currentCmd.trim()) commands.push(currentCmd.trim()); currentCmd = ""; - i += sepLen - 1; + i += processed.sepLen - 1; continue; } + currentCmd = processed.newCmd; + i = processed.nextIndex; + } - currentCmd += ch; + if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) { + state.hasSyntaxError = true; } if (currentCmd.trim()) { commands.push(currentCmd.trim()); } - return commands; + return { commands, hasSyntaxError: state.hasSyntaxError }; +} + +function splitChainedCommands(shellText: string): string[] { + return splitChainedCommandsDetailed(shellText).commands; } function handleEscapeInWord(ch: string, state: TokenizerState, currentWord: string): string { @@ -177,53 +211,145 @@ function handleQuoteToggle(ch: string, state: TokenizerState): boolean { return false; } +interface CommandInspection { + tokens: string[]; + hasUnsupportedSyntax: boolean; + hasRedirection: boolean; +} + +function isBackgroundOp(cmd: string, index: number, state: TokenizerState): boolean { + if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) return false; + if (cmd[index] !== "&") return false; + const prev = index > 0 ? cmd[index - 1] : ""; + const next = index + 1 < cmd.length ? cmd[index + 1] : ""; + return prev !== "&" && next !== "&" && next !== ">"; +} + +function isCommandSubstitutionStart(cmd: string, index: number, state: TokenizerState): boolean { + if (state.inSingleQuote || state.isEscaped) return false; + const ch = cmd[index]; + if (ch === "`") return true; + if (ch === "$" && index + 1 < cmd.length && cmd[index + 1] === "(") return true; + return false; +} + +function isProcessSubstitutionStart(cmd: string, index: number, state: TokenizerState): boolean { + if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) return false; + const ch = cmd[index]; + if ((ch === "<" || ch === ">") && index + 1 < cmd.length && cmd[index + 1] === "(") { + return true; + } + return false; +} + +function isRedirectionChar(cmd: string, index: number, state: TokenizerState): boolean { + if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) return false; + const ch = cmd[index]; + if (ch === ">") return true; + if (ch === "<") return true; + if (ch === "&" && index + 1 < cmd.length && cmd[index + 1] === ">") return true; + return false; +} + +interface TokenizeScanContext { + inspection: CommandInspection; + state: TokenizerState; +} + +function inspectSpecialConstructs(cmd: string, i: number, ctx: TokenizeScanContext): boolean { + if (isBackgroundOp(cmd, i, ctx.state)) { + ctx.inspection.hasUnsupportedSyntax = true; + return true; + } + if (isCommandSubstitutionStart(cmd, i, ctx.state)) { + ctx.inspection.hasUnsupportedSyntax = true; + return true; + } + if (isProcessSubstitutionStart(cmd, i, ctx.state)) { + ctx.inspection.hasUnsupportedSyntax = true; + return true; + } + if (isRedirectionChar(cmd, i, ctx.state)) { + ctx.inspection.hasRedirection = true; + ctx.inspection.hasUnsupportedSyntax = true; + return true; + } + return false; +} + +function stepTokenizeChar( + cmd: string, + i: number, + ctx: TokenizeScanContext, + currentWord: string, +): { word: string; hasChars: boolean; skipped: boolean } { + const ch = cmd[i]; + + if (ctx.state.isEscaped) { + const word = handleEscapeInWord(ch, ctx.state, currentWord); + return { word, hasChars: true, skipped: false }; + } + + if (ch === "\\" && !ctx.state.inSingleQuote) { + ctx.state.isEscaped = true; + return { word: currentWord, hasChars: false, skipped: true }; + } + + inspectSpecialConstructs(cmd, i, ctx); + + if (handleQuoteToggle(ch, ctx.state)) { + return { word: currentWord, hasChars: true, skipped: true }; + } + + return { word: currentWord + ch, hasChars: true, skipped: false }; +} + /** * Splits a single command line into word tokens, respecting quotes and escapes. - * Strips outer quotes so `"format:write"` or `'format:write'` becomes `format:write`. + * Detects unsupported shell constructs (backgrounding, command substitutions, + * process substitutions, output/input redirections, unclosed quotes/escapes). */ -function tokenizeCommandLine(cmd: string): string[] { - const tokens: string[] = []; +function inspectAndTokenizeCommandLine(cmd: string): CommandInspection { + const inspection: CommandInspection = { + tokens: [], + hasUnsupportedSyntax: false, + hasRedirection: false, + }; + const state: TokenizerState = { inSingleQuote: false, inDoubleQuote: false, isEscaped: false }; + const ctx: TokenizeScanContext = { inspection, state }; + let currentWord = ""; let hasCharsInWord = false; - const state: TokenizerState = { inSingleQuote: false, inDoubleQuote: false, isEscaped: false }; for (let i = 0; i < cmd.length; i++) { const ch = cmd[i]; - - if (state.isEscaped) { - currentWord = handleEscapeInWord(ch, state, currentWord); - hasCharsInWord = true; - continue; - } - - if (ch === "\\" && !state.inSingleQuote) { - state.isEscaped = true; - continue; - } - - if (handleQuoteToggle(ch, state)) { - hasCharsInWord = true; - continue; - } - - if (!state.inSingleQuote && !state.inDoubleQuote && /\s/.test(ch)) { + if (!state.inSingleQuote && !state.inDoubleQuote && !state.isEscaped && /\s/.test(ch)) { if (hasCharsInWord) { - tokens.push(currentWord); + inspection.tokens.push(currentWord); currentWord = ""; hasCharsInWord = false; } continue; } - currentWord += ch; - hasCharsInWord = true; + const step = stepTokenizeChar(cmd, i, ctx, currentWord); + currentWord = step.word; + if (step.hasChars) hasCharsInWord = true; + } + + if (state.inSingleQuote || state.inDoubleQuote || state.isEscaped) { + inspection.hasUnsupportedSyntax = true; } if (hasCharsInWord) { - tokens.push(currentWord); + inspection.tokens.push(currentWord); } - return tokens; + return inspection; +} + +function tokenizeCommandLine(cmd: string): string[] { + return inspectAndTokenizeCommandLine(cmd).tokens; } const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=/; @@ -499,6 +625,35 @@ function extractShellWrapperScript(tokens: string[]): string | undefined { return undefined; } +function isKnownWriteCommand(tokens: string[]): boolean { + return ( + isPrettierWrite(tokens) || + isOxfmtWrite(tokens) || + isBiomeFormatWrite(tokens) || + isEslintFix(tokens) || + isOxlintFix(tokens) || + isBiomeLintWrite(tokens) || + isBiomeCheckWrite(tokens) || + isVpFmtWrite(tokens) || + isVpLintFix(tokens) + ); +} + +function isKnownReadOnlyCommand(tokens: string[]): boolean { + return ( + isPrettierCheck(tokens) || + isOxfmtCheck(tokens) || + isBiomeFormatCheck(tokens) || + isEslintCheck(tokens) || + isOxlintCheck(tokens) || + isBiomeLintCheck(tokens) || + isBiomeCheckCheck(tokens) || + isVpFmtCheck(tokens) || + isVpLintCheck(tokens) || + isVpCheckCheck(tokens) + ); +} + function classifyInvocation( tokens: string[], scripts: Record, @@ -528,33 +683,12 @@ function classifyInvocation( } // Check known write-capable commands - if ( - isPrettierWrite(tokens) || - isOxfmtWrite(tokens) || - isBiomeFormatWrite(tokens) || - isEslintFix(tokens) || - isOxlintFix(tokens) || - isBiomeLintWrite(tokens) || - isBiomeCheckWrite(tokens) || - isVpFmtWrite(tokens) || - isVpLintFix(tokens) - ) { + if (isKnownWriteCommand(tokens)) { return "write-capable"; } // Check known read-only commands - if ( - isPrettierCheck(tokens) || - isOxfmtCheck(tokens) || - isBiomeFormatCheck(tokens) || - isEslintCheck(tokens) || - isOxlintCheck(tokens) || - isBiomeLintCheck(tokens) || - isBiomeCheckCheck(tokens) || - isVpFmtCheck(tokens) || - isVpLintCheck(tokens) || - isVpCheckCheck(tokens) - ) { + if (isKnownReadOnlyCommand(tokens)) { return "read-only"; } @@ -566,16 +700,31 @@ function classifyScriptInternal( scripts: Record, visited: Set, ): ScriptClassification { - const invocations = parseCommandInvocations(command); - if (invocations.length === 0) return "unknown"; + const splitResult = splitChainedCommandsDetailed(command); + if (splitResult.hasSyntaxError) { + return "unknown"; + } + + const { commands } = splitResult; + if (commands.length === 0) return "unknown"; let hasReadOnly = false; - for (const tokens of invocations) { + for (const cmd of commands) { + const inspection = inspectAndTokenizeCommandLine(cmd); + const tokens = getCommandInvocationTokens(inspection.tokens); const classification = classifyInvocation(tokens, scripts, visited); + if (classification === "write-capable") { return "write-capable"; } + + // Fail closed: If command contains unsupported shell syntax, redirections, or substitutions, + // it must NEVER be classified as read-only. + if (inspection.hasUnsupportedSyntax) { + return "unknown"; + } + if (classification === "unknown") { return "unknown"; } diff --git a/tests/autofix-architecture.test.ts b/tests/autofix-architecture.test.ts index 1c07742c..c4276941 100644 --- a/tests/autofix-architecture.test.ts +++ b/tests/autofix-architecture.test.ts @@ -1461,6 +1461,155 @@ describe("Refactored Autofix Architecture & Tool Adapters", () => { const fixTasks = resolveScriptTasks({ fmt: "vp fmt" }, "fix"); expect(fixTasks).toEqual([{ capability: "format", scriptName: "fmt" }]); }); + + it("prevents unsafe shell constructs, redirections, backgrounding, and substitutions from being classified as read-only", () => { + // Background execution: & + expect(classifyScript("eslint . & touch marker.txt")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": "eslint . & touch marker.txt", + }, + "check", + ), + ).toEqual([]); + + // Output redirection: >, >>, 2>, 2>>, &> + expect(classifyScript("prettier --check . > package.json")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "format:check": "prettier --check . > package.json", + }, + "check", + ), + ).toEqual([]); + + expect(classifyScript("eslint . >> output.log")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": "eslint . >> output.log", + }, + "check", + ), + ).toEqual([]); + + expect(classifyScript("eslint . 2> errors.log")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": "eslint . 2> errors.log", + }, + "check", + ), + ).toEqual([]); + + expect(classifyScript("eslint . 2>> errors.log")).toBe("unknown"); + expect(classifyScript("eslint . &> errors.log")).toBe("unknown"); + + // Command substitutions: $(...) and `...` + expect(classifyScript('eslint "$(touch marker.txt)"')).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": 'eslint "$(touch marker.txt)"', + }, + "check", + ), + ).toEqual([]); + + expect(classifyScript("eslint `touch marker.txt`")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": "eslint `touch marker.txt`", + }, + "check", + ), + ).toEqual([]); + + // Process substitutions: <(...) and >(...) + expect(classifyScript("eslint <(touch marker.txt)")).toBe("unknown"); + expect( + resolveScriptTasks( + { + "lint:check": "eslint <(touch marker.txt)", + }, + "check", + ), + ).toEqual([]); + + expect(classifyScript("eslint >(touch marker.txt)")).toBe("unknown"); + + // Malformed quotes or unclosed command substitutions + expect(classifyScript('eslint "unterminated')).toBe("unknown"); + expect(classifyScript("eslint 'unterminated")).toBe("unknown"); + expect(classifyScript("eslint $(touch marker.txt")).toBe("unknown"); + expect(classifyScript("eslint `touch marker.txt")).toBe("unknown"); + + // Valid chained read-only commands continue to work + expect(classifyScript("eslint . && prettier --check .")).toBe("read-only"); + + // Valid quoted arguments containing characters that should not be treated as shell operators + expect(classifyScript('eslint --rule "semi: [error, always]"')).toBe("read-only"); + expect(classifyScript("prettier --check 'src/**/*.{js,ts}'")).toBe("read-only"); + expect(classifyScript('echo "> not redirected"')).toBe("unknown"); // echo is unknown command, not read-only + + // Escaped shell characters are literal + expect(classifyScript("eslint \\>")).toBe("read-only"); + }); + + it("falls back to fmt:check when format:check cannot be safely classified as read-only", () => { + // 1. Unknown format:check with valid fmt:check -> selects fmt:check + const unknownFormatWithValidFmt = { + "format:check": "node scripts/custom-format.js", + "fmt:check": "prettier --check .", + }; + expect(resolveScriptTasks(unknownFormatWithValidFmt, "check")).toEqual([ + { capability: "format", scriptName: "fmt:check" }, + ]); + + // 2. Write-capable format:check with valid fmt:check -> selects fmt:check + const writeFormatWithValidFmt = { + "format:check": "prettier --write .", + "fmt:check": "prettier --check .", + }; + expect(resolveScriptTasks(writeFormatWithValidFmt, "check")).toEqual([ + { capability: "format", scriptName: "fmt:check" }, + ]); + + // 3. Read-only format:check takes precedence over fmt:check + const bothReadOnly = { + "format:check": "prettier --check .", + "fmt:check": "vp fmt --check", + }; + expect(resolveScriptTasks(bothReadOnly, "check")).toEqual([ + { capability: "format", scriptName: "format:check" }, + ]); + + // 4. Both candidates unknown -> none selected + const bothUnknown = { + "format:check": "node scripts/format.js", + "fmt:check": "python -m format", + }; + expect(resolveScriptTasks(bothUnknown, "check")).toEqual([]); + + // 5. Both candidates write-capable -> none selected + const bothWriteCapable = { + "format:check": "prettier --write .", + "fmt:check": "vp fmt", + }; + expect(resolveScriptTasks(bothWriteCapable, "check")).toEqual([]); + + // 6. Only one valid candidate exists + expect(resolveScriptTasks({ "fmt:check": "prettier --check ." }, "check")).toEqual([ + { capability: "format", scriptName: "fmt:check" }, + ]); + expect(resolveScriptTasks({ "format:check": "prettier --check ." }, "check")).toEqual([ + { capability: "format", scriptName: "format:check" }, + ]); + }); }); describe("Registry Extensibility", () => {