mirror of
https://github.com/pbakaus/impeccable.git
synced 2026-09-11 21:57:14 +03:00
Sync generated provider output
This commit is contained in:
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1112,8 +1112,20 @@ function formatFindingIgnoreCommand(finding) {
|
||||
function quoteCommandArg(value) {
|
||||
const text = String(value || '').trim();
|
||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||
// The suggestion is meant to be run on this same machine, so quote for its
|
||||
// shell. POSIX /bin/sh still expands $(...), backticks, and ${} inside
|
||||
// double quotes, and these values come from scanned file content (a
|
||||
// font-family name) or a file path, so untrusted input must be
|
||||
// single-quoted (issue #476). Windows cmd.exe performs no such command
|
||||
// substitution, but it treats a single quote as a literal character rather
|
||||
// than a grouping delimiter, so a value or path containing spaces has to
|
||||
// stay double-quoted there (Greptile #533). Keep the pre-existing
|
||||
// double-quote escaping on Windows so that path's behavior is unchanged.
|
||||
if (process.platform === 'win32') {
|
||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
||||
}
|
||||
return `'${text.replace(/'/g, `'\\''`)}'`;
|
||||
}
|
||||
|
||||
function relativize(filePath, cwd) {
|
||||
try {
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
* within the first ~300 characters — catches non-git projects.
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
|
||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
||||
|
||||
function isGitIgnored(absPath, cwd) {
|
||||
try {
|
||||
execSync(`git check-ignore --quiet ${JSON.stringify(absPath)}`, {
|
||||
// argv form, never a shell: this runs on every file the live-mode source
|
||||
// walk reaches, so a hostile filename embedding $(...) or backticks must
|
||||
// not be interpretable (issue #476). JSON.stringify is not shell quoting.
|
||||
execFileSync('git', ['check-ignore', '--quiet', absPath], {
|
||||
cwd,
|
||||
stdio: 'ignore',
|
||||
});
|
||||
|
||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||
// * legacy unquoted: node .claude/.../hook.mjs
|
||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
||||
// * absolute: node "/Users/.../hook.mjs" (user-level installs)
|
||||
// * absolute (#476): [ ! -f 'PATH' ] || node 'PATH' (single-quoted since
|
||||
// the shell-injection fix; older installs double-quote)
|
||||
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
||||
// quoted match is the path. Otherwise fall back to the whitespace/metachar-
|
||||
@@ -255,6 +256,12 @@ function hookScriptTokenFrom(command) {
|
||||
if (!HOOK_MARKER.test(str)) return null;
|
||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||
if (quoted) return quoted[1];
|
||||
// A path containing an apostrophe serializes as '\'' inside single quotes;
|
||||
// no regex reassembles that, and the bare fallback would misread a fragment
|
||||
// of it, so return null: the caller never asserts on a path it can't parse.
|
||||
if (str.includes("'\\''")) return null;
|
||||
const singleQuoted = str.match(/'([^']*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)'/);
|
||||
if (singleQuoted) return singleQuoted[1];
|
||||
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||
return bare ? bare[1] : null;
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
* node live.mjs --help
|
||||
*/
|
||||
|
||||
import { execSync } from 'node:child_process';
|
||||
import { execFileSync } from 'node:child_process';
|
||||
import fs from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
||||
|
||||
function runScript(name, args, options = {}) {
|
||||
const scriptPath = path.join(__dirname, name);
|
||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
||||
try {
|
||||
return execSync(cmd, { encoding: 'utf-8', cwd: options.cwd || process.cwd(), timeout: 15_000 });
|
||||
// argv form, never a shell: string interpolation into double quotes would
|
||||
// let a `"` or `$(...)` in any future caller's arg escape into the shell
|
||||
// (issue #476).
|
||||
return execFileSync(process.execPath, [scriptPath, ...args], {
|
||||
encoding: 'utf-8',
|
||||
cwd: options.cwd || process.cwd(),
|
||||
timeout: 15_000,
|
||||
});
|
||||
} catch (err) {
|
||||
// execSync throws on non-zero exit; return stdout if any
|
||||
// execFileSync throws on non-zero exit; return stdout if any
|
||||
return err.stdout || err.message || '';
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user