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,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1112,7 +1112,19 @@ function formatFindingIgnoreCommand(finding) {
|
|||||||
function quoteCommandArg(value) {
|
function quoteCommandArg(value) {
|
||||||
const text = String(value || '').trim();
|
const text = String(value || '').trim();
|
||||||
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
if (/^[A-Za-z0-9._:-]+$/.test(text)) return text;
|
||||||
return `"${text.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
|
// 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) {
|
function relativize(filePath, cwd) {
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
* within the first ~300 characters — catches non-git projects.
|
* 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 fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
|
|
||||||
@@ -41,7 +41,10 @@ export function isGeneratedFile(filePath, options = {}) {
|
|||||||
|
|
||||||
function isGitIgnored(absPath, cwd) {
|
function isGitIgnored(absPath, cwd) {
|
||||||
try {
|
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,
|
cwd,
|
||||||
stdio: 'ignore',
|
stdio: 'ignore',
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -244,7 +244,8 @@ const HOOK_MARKER = /skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs/;
|
|||||||
// * bundle-relative: node ".agents/.../hook.mjs"
|
// * bundle-relative: node ".agents/.../hook.mjs"
|
||||||
// * legacy unquoted: node .claude/.../hook.mjs
|
// * legacy unquoted: node .claude/.../hook.mjs
|
||||||
// * guarded (#399): [ ! -f "PATH" ] || node "PATH" (PATH twice, identical)
|
// * 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"
|
// * github portable: node "$(git rev-parse --show-toplevel)/.../hook.mjs"
|
||||||
// A quoted path wins; the guard's two occurrences are identical, so the first
|
// 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-
|
// 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;
|
if (!HOOK_MARKER.test(str)) return null;
|
||||||
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
const quoted = str.match(/"([^"]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)"/);
|
||||||
if (quoted) return quoted[1];
|
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)/);
|
const bare = str.match(/([^\s"'|&;()]*skills\/impeccable\/scripts\/hook(?:-before-edit)?\.mjs)/);
|
||||||
return bare ? bare[1] : null;
|
return bare ? bare[1] : null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@
|
|||||||
* node live.mjs --help
|
* node live.mjs --help
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { execSync } from 'node:child_process';
|
import { execFileSync } from 'node:child_process';
|
||||||
import fs from 'node:fs';
|
import fs from 'node:fs';
|
||||||
import path from 'node:path';
|
import path from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
@@ -316,11 +316,17 @@ function globToRegex(pattern) {
|
|||||||
|
|
||||||
function runScript(name, args, options = {}) {
|
function runScript(name, args, options = {}) {
|
||||||
const scriptPath = path.join(__dirname, name);
|
const scriptPath = path.join(__dirname, name);
|
||||||
const cmd = `node "${scriptPath}" ${args.map(a => `"${a}"`).join(' ')}`;
|
|
||||||
try {
|
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) {
|
} 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 || '';
|
return err.stdout || err.message || '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user