cc-safety-net 0.8.2 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +69 -16
- package/dist/bin/cc-safety-net.js +1775 -260
- package/dist/bin/doctor/hooks.d.ts +2 -0
- package/dist/bin/doctor/types.d.ts +5 -1
- package/dist/core/analyze/analyze-command.d.ts +10 -0
- package/dist/core/analyze/parallel.d.ts +4 -1
- package/dist/core/analyze/segment.d.ts +2 -2
- package/dist/core/analyze/xargs.d.ts +2 -0
- package/dist/core/path.d.ts +1 -0
- package/dist/core/rules-git.d.ts +14 -2
- package/dist/core/shell.d.ts +8 -2
- package/dist/core/worktree.d.ts +15 -0
- package/dist/index.js +1435 -107
- package/dist/types.d.ts +13 -0
- package/package.json +1 -1
|
@@ -871,6 +871,11 @@ var ENV_VARS = [
|
|
|
871
871
|
name: "SAFETY_NET_PARANOID_INTERPRETERS",
|
|
872
872
|
description: "Block interpreter one-liners",
|
|
873
873
|
defaultBehavior: "off"
|
|
874
|
+
},
|
|
875
|
+
{
|
|
876
|
+
name: "SAFETY_NET_WORKTREE",
|
|
877
|
+
description: "Allow local git discards in linked worktrees",
|
|
878
|
+
defaultBehavior: "off"
|
|
874
879
|
}
|
|
875
880
|
];
|
|
876
881
|
function getEnvironmentInfo() {
|
|
@@ -969,7 +974,8 @@ var PLATFORM_NAMES = {
|
|
|
969
974
|
"claude-code": "Claude Code",
|
|
970
975
|
opencode: "OpenCode",
|
|
971
976
|
"gemini-cli": "Gemini CLI",
|
|
972
|
-
"copilot-cli": "Copilot CLI"
|
|
977
|
+
"copilot-cli": "Copilot CLI",
|
|
978
|
+
codex: "Codex"
|
|
973
979
|
};
|
|
974
980
|
function formatHooksSection(hooks) {
|
|
975
981
|
const lines = [];
|
|
@@ -1009,7 +1015,7 @@ function formatHooksSection(hooks) {
|
|
|
1009
1015
|
lines.push(` Warning (${w.platform}): ${w.message}`);
|
|
1010
1016
|
}
|
|
1011
1017
|
for (const e of errors) {
|
|
1012
|
-
lines.push(` Error (${e.platform}): ${e.message}`);
|
|
1018
|
+
lines.push(colors.red(` Error (${e.platform}): ${e.message}`));
|
|
1013
1019
|
}
|
|
1014
1020
|
return lines.join(`
|
|
1015
1021
|
`);
|
|
@@ -1375,9 +1381,9 @@ All checks passed.`);
|
|
|
1375
1381
|
}
|
|
1376
1382
|
|
|
1377
1383
|
// src/bin/doctor/hooks.ts
|
|
1378
|
-
import { existsSync as
|
|
1384
|
+
import { existsSync as existsSync6, readdirSync as readdirSync2, readFileSync as readFileSync6 } from "node:fs";
|
|
1379
1385
|
import { homedir as homedir4, tmpdir as tmpdir3 } from "node:os";
|
|
1380
|
-
import { join as
|
|
1386
|
+
import { join as join5 } from "node:path";
|
|
1381
1387
|
|
|
1382
1388
|
// src/core/analyze/dangerous-text.ts
|
|
1383
1389
|
function dangerousInText(text) {
|
|
@@ -1563,16 +1569,336 @@ function hasRecursiveForceFlags(tokens) {
|
|
|
1563
1569
|
return hasRecursive && hasForce;
|
|
1564
1570
|
}
|
|
1565
1571
|
|
|
1572
|
+
// src/core/shell.ts
|
|
1573
|
+
import { realpathSync as realpathSync3 } from "node:fs";
|
|
1574
|
+
import { isAbsolute as isAbsolute3, parse as parsePath2 } from "node:path";
|
|
1575
|
+
|
|
1566
1576
|
// node_modules/shell-quote/index.js
|
|
1567
1577
|
var $quote = require_quote();
|
|
1568
1578
|
var $parse = require_parse();
|
|
1569
1579
|
|
|
1580
|
+
// src/core/path.ts
|
|
1581
|
+
import { lstatSync, realpathSync } from "node:fs";
|
|
1582
|
+
import { dirname, isAbsolute, parse as parsePath, sep } from "node:path";
|
|
1583
|
+
function resolveChdirTarget(baseCwd, target) {
|
|
1584
|
+
const root = isAbsolute(target) ? getPathRoot(target) : "";
|
|
1585
|
+
let current = root || baseCwd;
|
|
1586
|
+
for (const component of getPathComponents(root ? target.slice(root.length) : target)) {
|
|
1587
|
+
if (component === "" || component === ".") {
|
|
1588
|
+
continue;
|
|
1589
|
+
}
|
|
1590
|
+
if (component === "..") {
|
|
1591
|
+
current = dirname(current);
|
|
1592
|
+
continue;
|
|
1593
|
+
}
|
|
1594
|
+
const candidate = appendPathWithoutNormalizing(current, component);
|
|
1595
|
+
current = lstatSync(candidate).isSymbolicLink() ? realpathSync(candidate) : candidate;
|
|
1596
|
+
}
|
|
1597
|
+
return current;
|
|
1598
|
+
}
|
|
1599
|
+
function appendPathWithoutNormalizing(base, target) {
|
|
1600
|
+
return base.endsWith("/") || base.endsWith("\\") ? `${base}${target}` : `${base}${sep}${target}`;
|
|
1601
|
+
}
|
|
1602
|
+
function getPathRoot(target) {
|
|
1603
|
+
return parsePath(target).root;
|
|
1604
|
+
}
|
|
1605
|
+
function getPathComponents(target) {
|
|
1606
|
+
const separator = process.platform === "win32" ? /[\\/]+/ : /\/+/;
|
|
1607
|
+
return target.split(separator);
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/core/worktree.ts
|
|
1611
|
+
import { existsSync as existsSync4, lstatSync as lstatSync2, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync } from "node:fs";
|
|
1612
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join3, resolve as resolve2 } from "node:path";
|
|
1613
|
+
var GIT_GLOBAL_OPTS_WITH_VALUE = new Set([
|
|
1614
|
+
"-c",
|
|
1615
|
+
"-C",
|
|
1616
|
+
"--git-dir",
|
|
1617
|
+
"--work-tree",
|
|
1618
|
+
"--namespace",
|
|
1619
|
+
"--super-prefix",
|
|
1620
|
+
"--config-env"
|
|
1621
|
+
]);
|
|
1622
|
+
var GIT_CONTEXT_ENV_OVERRIDES = [
|
|
1623
|
+
"GIT_DIR",
|
|
1624
|
+
"GIT_WORK_TREE",
|
|
1625
|
+
"GIT_COMMON_DIR",
|
|
1626
|
+
"GIT_INDEX_FILE"
|
|
1627
|
+
];
|
|
1628
|
+
var GIT_CONFIG_AFFECTING_ENV_NAMES = new Set([
|
|
1629
|
+
"GIT_CONFIG_GLOBAL",
|
|
1630
|
+
"GIT_CONFIG_NOSYSTEM",
|
|
1631
|
+
"GIT_CONFIG_SYSTEM",
|
|
1632
|
+
"HOME",
|
|
1633
|
+
"XDG_CONFIG_HOME"
|
|
1634
|
+
]);
|
|
1635
|
+
function hasGitContextEnvOverride(envAssignments) {
|
|
1636
|
+
for (const name of GIT_CONTEXT_ENV_OVERRIDES) {
|
|
1637
|
+
if (envAssignments?.has(name) || Object.hasOwn(process.env, name)) {
|
|
1638
|
+
return true;
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
return false;
|
|
1642
|
+
}
|
|
1643
|
+
function getGitExecutionContext(tokens, cwd) {
|
|
1644
|
+
if (!cwd) {
|
|
1645
|
+
return { gitCwd: null, hasExplicitGitContext: false };
|
|
1646
|
+
}
|
|
1647
|
+
let gitCwd;
|
|
1648
|
+
try {
|
|
1649
|
+
gitCwd = realpathSync2(resolve2(cwd));
|
|
1650
|
+
} catch {
|
|
1651
|
+
return { gitCwd: null, hasExplicitGitContext: false };
|
|
1652
|
+
}
|
|
1653
|
+
if (!isDirectory(gitCwd)) {
|
|
1654
|
+
return { gitCwd: null, hasExplicitGitContext: false };
|
|
1655
|
+
}
|
|
1656
|
+
let hasExplicitGitContext = false;
|
|
1657
|
+
let i = 1;
|
|
1658
|
+
while (i < tokens.length) {
|
|
1659
|
+
const token = tokens[i];
|
|
1660
|
+
if (!token)
|
|
1661
|
+
break;
|
|
1662
|
+
if (token === "--") {
|
|
1663
|
+
break;
|
|
1664
|
+
}
|
|
1665
|
+
if (!token.startsWith("-")) {
|
|
1666
|
+
break;
|
|
1667
|
+
}
|
|
1668
|
+
if (token === "-C") {
|
|
1669
|
+
const target = tokens[i + 1];
|
|
1670
|
+
if (!target) {
|
|
1671
|
+
return { gitCwd: null, hasExplicitGitContext };
|
|
1672
|
+
}
|
|
1673
|
+
const resolvedCwd = resolveGitCwd(gitCwd, target);
|
|
1674
|
+
if (!resolvedCwd) {
|
|
1675
|
+
return { gitCwd: null, hasExplicitGitContext };
|
|
1676
|
+
}
|
|
1677
|
+
gitCwd = resolvedCwd;
|
|
1678
|
+
i += 2;
|
|
1679
|
+
continue;
|
|
1680
|
+
}
|
|
1681
|
+
if (token.startsWith("-C") && token.length > 2) {
|
|
1682
|
+
const resolvedCwd = resolveGitCwd(gitCwd, token.slice(2));
|
|
1683
|
+
if (!resolvedCwd) {
|
|
1684
|
+
return { gitCwd: null, hasExplicitGitContext };
|
|
1685
|
+
}
|
|
1686
|
+
gitCwd = resolvedCwd;
|
|
1687
|
+
i++;
|
|
1688
|
+
continue;
|
|
1689
|
+
}
|
|
1690
|
+
if (token === "--git-dir" || token === "--work-tree") {
|
|
1691
|
+
hasExplicitGitContext = true;
|
|
1692
|
+
i += 2;
|
|
1693
|
+
continue;
|
|
1694
|
+
}
|
|
1695
|
+
if (token.startsWith("--git-dir=") || token.startsWith("--work-tree=")) {
|
|
1696
|
+
hasExplicitGitContext = true;
|
|
1697
|
+
i++;
|
|
1698
|
+
continue;
|
|
1699
|
+
}
|
|
1700
|
+
if (GIT_GLOBAL_OPTS_WITH_VALUE.has(token)) {
|
|
1701
|
+
i += 2;
|
|
1702
|
+
} else if (token.startsWith("-c") && token.length > 2) {
|
|
1703
|
+
i++;
|
|
1704
|
+
} else {
|
|
1705
|
+
i++;
|
|
1706
|
+
}
|
|
1707
|
+
}
|
|
1708
|
+
return { gitCwd, hasExplicitGitContext };
|
|
1709
|
+
}
|
|
1710
|
+
function isLinkedWorktree(cwd) {
|
|
1711
|
+
const dotGitPath = findDotGit(cwd);
|
|
1712
|
+
if (!dotGitPath) {
|
|
1713
|
+
return false;
|
|
1714
|
+
}
|
|
1715
|
+
try {
|
|
1716
|
+
const stat = lstatSync2(dotGitPath);
|
|
1717
|
+
if (stat.isSymbolicLink() || !stat.isFile()) {
|
|
1718
|
+
return false;
|
|
1719
|
+
}
|
|
1720
|
+
const content = readFileSync4(dotGitPath, "utf-8");
|
|
1721
|
+
const firstLine = content.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1722
|
+
if (!firstLine.startsWith("gitdir:")) {
|
|
1723
|
+
return false;
|
|
1724
|
+
}
|
|
1725
|
+
const rawGitDir = firstLine.slice("gitdir:".length).trim();
|
|
1726
|
+
if (rawGitDir === "") {
|
|
1727
|
+
return false;
|
|
1728
|
+
}
|
|
1729
|
+
const gitDir = isAbsolute2(rawGitDir) ? rawGitDir : resolve2(dirname2(dotGitPath), rawGitDir);
|
|
1730
|
+
if (!existsSync4(join3(gitDir, "commondir"))) {
|
|
1731
|
+
return false;
|
|
1732
|
+
}
|
|
1733
|
+
if (!worktreeGitdirBacklinkMatches(gitDir, dotGitPath)) {
|
|
1734
|
+
return false;
|
|
1735
|
+
}
|
|
1736
|
+
return worktreeConfigMatchesRoot(gitDir, dirname2(dotGitPath));
|
|
1737
|
+
} catch {
|
|
1738
|
+
return false;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
function worktreeGitdirBacklinkMatches(gitDir, dotGitPath) {
|
|
1742
|
+
const backlinkPath = join3(gitDir, "gitdir");
|
|
1743
|
+
if (!existsSync4(backlinkPath)) {
|
|
1744
|
+
return false;
|
|
1745
|
+
}
|
|
1746
|
+
const rawBacklink = readFileSync4(backlinkPath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
1747
|
+
if (rawBacklink === "") {
|
|
1748
|
+
return false;
|
|
1749
|
+
}
|
|
1750
|
+
const linkedDotGitPath = isAbsolute2(rawBacklink) ? rawBacklink : resolve2(gitDir, rawBacklink);
|
|
1751
|
+
try {
|
|
1752
|
+
return sameFilesystemPath(linkedDotGitPath, dotGitPath);
|
|
1753
|
+
} catch {
|
|
1754
|
+
return false;
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
function worktreeConfigMatchesRoot(gitDir, worktreeRoot) {
|
|
1758
|
+
const configWorktreePath = join3(gitDir, "config.worktree");
|
|
1759
|
+
if (!existsSync4(configWorktreePath)) {
|
|
1760
|
+
return true;
|
|
1761
|
+
}
|
|
1762
|
+
const configuredWorktree = readCoreWorktree(configWorktreePath);
|
|
1763
|
+
if (configuredWorktree === null) {
|
|
1764
|
+
return true;
|
|
1765
|
+
}
|
|
1766
|
+
const resolvedConfiguredWorktree = isAbsolute2(configuredWorktree) ? configuredWorktree : resolve2(gitDir, configuredWorktree);
|
|
1767
|
+
try {
|
|
1768
|
+
return sameFilesystemPath(resolvedConfiguredWorktree, worktreeRoot);
|
|
1769
|
+
} catch {
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
function sameFilesystemPath(left, right) {
|
|
1774
|
+
try {
|
|
1775
|
+
const leftStat = statSync(left);
|
|
1776
|
+
const rightStat = statSync(right);
|
|
1777
|
+
if (leftStat.ino !== 0 && rightStat.ino !== 0 && leftStat.dev === rightStat.dev && leftStat.ino === rightStat.ino) {
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1780
|
+
} catch {}
|
|
1781
|
+
return getCanonicalPathForComparison(left) === getCanonicalPathForComparison(right);
|
|
1782
|
+
}
|
|
1783
|
+
function getCanonicalPathForComparison(path) {
|
|
1784
|
+
return normalizePathForComparison(realpathSync2.native(path));
|
|
1785
|
+
}
|
|
1786
|
+
function normalizePathForComparison(path) {
|
|
1787
|
+
let normalized = path.replace(/^\\\\\?\\UNC\\/i, "//").replace(/^\\\\\?\\/i, "");
|
|
1788
|
+
normalized = normalized.replace(/\\/g, "/");
|
|
1789
|
+
if (normalized.length > 1 && normalized.endsWith("/")) {
|
|
1790
|
+
normalized = normalized.slice(0, -1);
|
|
1791
|
+
}
|
|
1792
|
+
return process.platform === "win32" ? normalized.toLowerCase() : normalized;
|
|
1793
|
+
}
|
|
1794
|
+
function readCoreWorktree(configPath) {
|
|
1795
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
1796
|
+
let inCore = false;
|
|
1797
|
+
let configuredWorktree = null;
|
|
1798
|
+
for (const line of content.split(/\r?\n/)) {
|
|
1799
|
+
const trimmed = line.trim();
|
|
1800
|
+
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";")) {
|
|
1801
|
+
continue;
|
|
1802
|
+
}
|
|
1803
|
+
if (trimmed.startsWith("[")) {
|
|
1804
|
+
inCore = /^\[core\]$/i.test(trimmed);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
if (!inCore) {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const match = trimmed.match(/^worktree\s*=\s*(.*)$/i);
|
|
1811
|
+
if (match) {
|
|
1812
|
+
configuredWorktree = parseGitConfigValue(match[1] ?? "");
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
return configuredWorktree;
|
|
1816
|
+
}
|
|
1817
|
+
function parseGitConfigValue(value) {
|
|
1818
|
+
const trimmed = value.trim();
|
|
1819
|
+
if (!trimmed.startsWith('"') || !trimmed.endsWith('"')) {
|
|
1820
|
+
return trimmed;
|
|
1821
|
+
}
|
|
1822
|
+
return unescapeDoubleQuotedGitConfigValue(trimmed.slice(1, -1));
|
|
1823
|
+
}
|
|
1824
|
+
function unescapeDoubleQuotedGitConfigValue(value) {
|
|
1825
|
+
let result = "";
|
|
1826
|
+
for (let i = 0;i < value.length; i++) {
|
|
1827
|
+
const char = value[i];
|
|
1828
|
+
if (char !== "\\") {
|
|
1829
|
+
result += char;
|
|
1830
|
+
continue;
|
|
1831
|
+
}
|
|
1832
|
+
const next = value[i + 1];
|
|
1833
|
+
if (next === undefined) {
|
|
1834
|
+
result += char;
|
|
1835
|
+
continue;
|
|
1836
|
+
}
|
|
1837
|
+
switch (next) {
|
|
1838
|
+
case "\\":
|
|
1839
|
+
case '"':
|
|
1840
|
+
result += next;
|
|
1841
|
+
break;
|
|
1842
|
+
case "n":
|
|
1843
|
+
result += `
|
|
1844
|
+
`;
|
|
1845
|
+
break;
|
|
1846
|
+
case "t":
|
|
1847
|
+
result += "\t";
|
|
1848
|
+
break;
|
|
1849
|
+
case "b":
|
|
1850
|
+
result += "\b";
|
|
1851
|
+
break;
|
|
1852
|
+
default:
|
|
1853
|
+
result += `\\${next}`;
|
|
1854
|
+
break;
|
|
1855
|
+
}
|
|
1856
|
+
i++;
|
|
1857
|
+
}
|
|
1858
|
+
return result;
|
|
1859
|
+
}
|
|
1860
|
+
function resolveGitCwd(baseCwd, target) {
|
|
1861
|
+
try {
|
|
1862
|
+
const resolved = resolveChdirTarget(baseCwd, target);
|
|
1863
|
+
return isDirectory(resolved) ? resolved : null;
|
|
1864
|
+
} catch {
|
|
1865
|
+
return null;
|
|
1866
|
+
}
|
|
1867
|
+
}
|
|
1868
|
+
function isDirectory(path) {
|
|
1869
|
+
try {
|
|
1870
|
+
return statSync(path).isDirectory();
|
|
1871
|
+
} catch {
|
|
1872
|
+
return false;
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
function findDotGit(cwd) {
|
|
1876
|
+
let current;
|
|
1877
|
+
try {
|
|
1878
|
+
current = realpathSync2(cwd);
|
|
1879
|
+
} catch {
|
|
1880
|
+
return null;
|
|
1881
|
+
}
|
|
1882
|
+
while (true) {
|
|
1883
|
+
const dotGitPath = join3(current, ".git");
|
|
1884
|
+
if (existsSync4(dotGitPath)) {
|
|
1885
|
+
return dotGitPath;
|
|
1886
|
+
}
|
|
1887
|
+
const parent = dirname2(current);
|
|
1888
|
+
if (parent === current) {
|
|
1889
|
+
return null;
|
|
1890
|
+
}
|
|
1891
|
+
current = parent;
|
|
1892
|
+
}
|
|
1893
|
+
}
|
|
1894
|
+
|
|
1570
1895
|
// src/core/shell.ts
|
|
1571
1896
|
var ENV_PROXY = new Proxy({}, {
|
|
1572
1897
|
get: (_, name) => `$${String(name)}`
|
|
1573
1898
|
});
|
|
1574
1899
|
var ARITHMETIC_SENTINEL = "__CC_SAFETY_NET_ARITH_SENTINEL__";
|
|
1575
1900
|
var BACKTICK_ATTACHED_SUFFIX_SENTINEL = "__CC_SAFETY_NET_BACKTICK_SUFFIX__";
|
|
1901
|
+
var DYNAMIC_SUBSTITUTION_TOKEN = "$__CC_SAFETY_NET_DYNAMIC_SUBSTITUTION__";
|
|
1576
1902
|
function splitShellCommands(command) {
|
|
1577
1903
|
if (hasUnclosedQuotes(command)) {
|
|
1578
1904
|
return [[command]];
|
|
@@ -1616,9 +1942,14 @@ function splitShellCommands(command) {
|
|
|
1616
1942
|
const { innerSegments, endIndex } = extractCommandSubstitution(tokens, i + 2);
|
|
1617
1943
|
const attachedSuffix = _getBacktickAttachedSuffix(tokens[endIndex + 1]);
|
|
1618
1944
|
const shouldKeepCurrent = attachedSuffix !== null && !_isRedirectOp(tokens[i - 1]) && !isOperatorToken(tokens[i - 1]);
|
|
1619
|
-
if (
|
|
1620
|
-
|
|
1621
|
-
|
|
1945
|
+
if (current.length > 0) {
|
|
1946
|
+
if (_containsGitCommandToken(current)) {
|
|
1947
|
+
current.push(DYNAMIC_SUBSTITUTION_TOKEN);
|
|
1948
|
+
}
|
|
1949
|
+
if (!shouldKeepCurrent) {
|
|
1950
|
+
segments.push(current);
|
|
1951
|
+
current = [];
|
|
1952
|
+
}
|
|
1622
1953
|
}
|
|
1623
1954
|
for (const seg of innerSegments) {
|
|
1624
1955
|
segments.push(seg);
|
|
@@ -1637,6 +1968,9 @@ function splitShellCommands(command) {
|
|
|
1637
1968
|
current.push(prefix);
|
|
1638
1969
|
}
|
|
1639
1970
|
}
|
|
1971
|
+
if (_containsGitCommandToken(current)) {
|
|
1972
|
+
current.push(DYNAMIC_SUBSTITUTION_TOKEN);
|
|
1973
|
+
}
|
|
1640
1974
|
const { innerSegments, endIndex } = extractCommandSubstitution(tokens, i + 2);
|
|
1641
1975
|
for (const seg of innerSegments) {
|
|
1642
1976
|
segments.push(seg);
|
|
@@ -1723,6 +2057,9 @@ function _getCommandTokenText(token) {
|
|
|
1723
2057
|
}
|
|
1724
2058
|
return null;
|
|
1725
2059
|
}
|
|
2060
|
+
function _containsGitCommandToken(tokens) {
|
|
2061
|
+
return tokens.some((token) => (token.split("/").pop() ?? token).toLowerCase() === "git");
|
|
2062
|
+
}
|
|
1726
2063
|
function extractCommandSubstitution(tokens, startIndex) {
|
|
1727
2064
|
if (tokens[startIndex] === ARITHMETIC_SENTINEL) {
|
|
1728
2065
|
return _extractArithmeticSubstitution(tokens, startIndex);
|
|
@@ -2032,6 +2369,8 @@ function _stripAttachedIoNumbers(command) {
|
|
|
2032
2369
|
return result;
|
|
2033
2370
|
}
|
|
2034
2371
|
var ENV_ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
|
|
2372
|
+
var ENV_APPEND_ASSIGNMENT_RE = /^([A-Za-z_][A-Za-z0-9_]*)\+=/;
|
|
2373
|
+
var GIT_CONTEXT_ENV_OVERRIDE_NAMES = new Set(GIT_CONTEXT_ENV_OVERRIDES);
|
|
2035
2374
|
function parseEnvAssignment(token) {
|
|
2036
2375
|
if (!ENV_ASSIGNMENT_RE.test(token)) {
|
|
2037
2376
|
return null;
|
|
@@ -2039,6 +2378,21 @@ function parseEnvAssignment(token) {
|
|
|
2039
2378
|
const eqIdx = token.indexOf("=");
|
|
2040
2379
|
return { name: token.slice(0, eqIdx), value: token.slice(eqIdx + 1) };
|
|
2041
2380
|
}
|
|
2381
|
+
function parseGitContextAppendEnvAssignment(token) {
|
|
2382
|
+
const match = token.match(ENV_APPEND_ASSIGNMENT_RE);
|
|
2383
|
+
const name = match?.[1];
|
|
2384
|
+
if (!name || !isTrackedGitEnvName(name)) {
|
|
2385
|
+
return null;
|
|
2386
|
+
}
|
|
2387
|
+
const eqIdx = token.indexOf("=");
|
|
2388
|
+
return { name, value: token.slice(eqIdx + 1) };
|
|
2389
|
+
}
|
|
2390
|
+
function isTrackedGitEnvName(name) {
|
|
2391
|
+
return GIT_CONTEXT_ENV_OVERRIDE_NAMES.has(name) || GIT_CONFIG_AFFECTING_ENV_NAMES.has(name) || isGitConfigEnvName(name);
|
|
2392
|
+
}
|
|
2393
|
+
function isGitConfigEnvName(name) {
|
|
2394
|
+
return name === "GIT_CONFIG_COUNT" || name === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(name);
|
|
2395
|
+
}
|
|
2042
2396
|
function stripEnvAssignmentsWithInfo(tokens) {
|
|
2043
2397
|
const envAssignments = new Map;
|
|
2044
2398
|
let i = 0;
|
|
@@ -2056,12 +2410,13 @@ function stripEnvAssignmentsWithInfo(tokens) {
|
|
|
2056
2410
|
}
|
|
2057
2411
|
return { tokens: tokens.slice(i), envAssignments };
|
|
2058
2412
|
}
|
|
2059
|
-
function stripWrappers(tokens) {
|
|
2060
|
-
return stripWrappersWithInfo(tokens).tokens;
|
|
2413
|
+
function stripWrappers(tokens, cwd) {
|
|
2414
|
+
return stripWrappersWithInfo(tokens, cwd).tokens;
|
|
2061
2415
|
}
|
|
2062
|
-
function stripWrappersWithInfo(tokens) {
|
|
2416
|
+
function stripWrappersWithInfo(tokens, cwd) {
|
|
2063
2417
|
let result = [...tokens];
|
|
2064
2418
|
const allEnvAssignments = new Map;
|
|
2419
|
+
let currentCwd = cwd;
|
|
2065
2420
|
for (let iteration = 0;iteration < MAX_STRIP_ITERATIONS; iteration++) {
|
|
2066
2421
|
const before = result.join(" ");
|
|
2067
2422
|
const { tokens: strippedTokens, envAssignments } = stripEnvAssignmentsWithInfo(result);
|
|
@@ -2072,6 +2427,10 @@ function stripWrappersWithInfo(tokens) {
|
|
|
2072
2427
|
if (result.length === 0)
|
|
2073
2428
|
break;
|
|
2074
2429
|
while (result.length > 0 && result[0]?.includes("=") && !ENV_ASSIGNMENT_RE.test(result[0] ?? "")) {
|
|
2430
|
+
const appendAssignment = parseGitContextAppendEnvAssignment(result[0] ?? "");
|
|
2431
|
+
if (appendAssignment) {
|
|
2432
|
+
allEnvAssignments.set(appendAssignment.name, appendAssignment.value);
|
|
2433
|
+
}
|
|
2075
2434
|
result = result.slice(1);
|
|
2076
2435
|
}
|
|
2077
2436
|
if (result.length === 0)
|
|
@@ -2081,11 +2440,18 @@ function stripWrappersWithInfo(tokens) {
|
|
|
2081
2440
|
break;
|
|
2082
2441
|
}
|
|
2083
2442
|
if (head === "sudo") {
|
|
2084
|
-
|
|
2443
|
+
const sudoResult = stripSudoWithInfo(result, currentCwd);
|
|
2444
|
+
result = sudoResult.tokens;
|
|
2445
|
+
if (sudoResult.cwd !== undefined) {
|
|
2446
|
+
currentCwd = sudoResult.cwd;
|
|
2447
|
+
}
|
|
2085
2448
|
}
|
|
2086
2449
|
if (head === "env") {
|
|
2087
|
-
const envResult = stripEnvWithInfo(result);
|
|
2450
|
+
const envResult = stripEnvWithInfo(result, currentCwd);
|
|
2088
2451
|
result = envResult.tokens;
|
|
2452
|
+
if (envResult.cwd !== undefined) {
|
|
2453
|
+
currentCwd = envResult.cwd;
|
|
2454
|
+
}
|
|
2089
2455
|
for (const [k, v] of envResult.envAssignments) {
|
|
2090
2456
|
allEnvAssignments.set(k, v);
|
|
2091
2457
|
}
|
|
@@ -2100,28 +2466,50 @@ function stripWrappersWithInfo(tokens) {
|
|
|
2100
2466
|
for (const [k, v] of finalAssignments) {
|
|
2101
2467
|
allEnvAssignments.set(k, v);
|
|
2102
2468
|
}
|
|
2103
|
-
return { tokens: finalTokens, envAssignments: allEnvAssignments };
|
|
2469
|
+
return { tokens: finalTokens, envAssignments: allEnvAssignments, cwd: currentCwd };
|
|
2104
2470
|
}
|
|
2105
2471
|
var SUDO_OPTS_WITH_VALUE = new Set(["-u", "-g", "-C", "-D", "-h", "-p", "-r", "-t", "-T", "-U"]);
|
|
2106
|
-
function
|
|
2472
|
+
function stripSudoWithInfo(tokens, cwd) {
|
|
2107
2473
|
let i = 1;
|
|
2474
|
+
let currentCwd = cwd;
|
|
2108
2475
|
while (i < tokens.length) {
|
|
2109
2476
|
const token = tokens[i];
|
|
2110
2477
|
if (!token)
|
|
2111
2478
|
break;
|
|
2112
2479
|
if (token === "--") {
|
|
2113
|
-
return tokens.slice(i + 1);
|
|
2480
|
+
return { tokens: tokens.slice(i + 1), cwd: currentCwd };
|
|
2114
2481
|
}
|
|
2115
2482
|
if (!token.startsWith("-")) {
|
|
2116
2483
|
break;
|
|
2117
2484
|
}
|
|
2485
|
+
if (token === "-D" || token === "--chdir") {
|
|
2486
|
+
const target = tokens[i + 1];
|
|
2487
|
+
currentCwd = target ? resolveWrapperCwd(currentCwd, target) : null;
|
|
2488
|
+
i += 2;
|
|
2489
|
+
continue;
|
|
2490
|
+
}
|
|
2491
|
+
if (token.startsWith("--chdir=")) {
|
|
2492
|
+
currentCwd = resolveWrapperCwd(currentCwd, token.slice("--chdir=".length));
|
|
2493
|
+
i++;
|
|
2494
|
+
continue;
|
|
2495
|
+
}
|
|
2496
|
+
if (token.startsWith("-D") && token.length > 2) {
|
|
2497
|
+
currentCwd = resolveWrapperCwd(currentCwd, token.slice(2));
|
|
2498
|
+
i++;
|
|
2499
|
+
continue;
|
|
2500
|
+
}
|
|
2501
|
+
if (token === "-i" || token === "--login") {
|
|
2502
|
+
currentCwd = null;
|
|
2503
|
+
i++;
|
|
2504
|
+
continue;
|
|
2505
|
+
}
|
|
2118
2506
|
if (SUDO_OPTS_WITH_VALUE.has(token)) {
|
|
2119
2507
|
i += 2;
|
|
2120
2508
|
continue;
|
|
2121
2509
|
}
|
|
2122
2510
|
i++;
|
|
2123
2511
|
}
|
|
2124
|
-
return tokens.slice(i);
|
|
2512
|
+
return { tokens: tokens.slice(i), cwd: currentCwd };
|
|
2125
2513
|
}
|
|
2126
2514
|
var ENV_OPTS_NO_VALUE = new Set(["-i", "-0", "--null"]);
|
|
2127
2515
|
var ENV_OPTS_WITH_VALUE = new Set([
|
|
@@ -2133,21 +2521,70 @@ var ENV_OPTS_WITH_VALUE = new Set([
|
|
|
2133
2521
|
"--split-string",
|
|
2134
2522
|
"-P"
|
|
2135
2523
|
]);
|
|
2136
|
-
function stripEnvWithInfo(tokens) {
|
|
2524
|
+
function stripEnvWithInfo(tokens, cwd) {
|
|
2137
2525
|
const envAssignments = new Map;
|
|
2526
|
+
let currentCwd = cwd;
|
|
2527
|
+
let expandedTokens = tokens;
|
|
2138
2528
|
let i = 1;
|
|
2139
|
-
while (i <
|
|
2140
|
-
const token =
|
|
2529
|
+
while (i < expandedTokens.length) {
|
|
2530
|
+
const token = expandedTokens[i];
|
|
2141
2531
|
if (!token)
|
|
2142
2532
|
break;
|
|
2143
2533
|
if (token === "--") {
|
|
2144
|
-
return { tokens:
|
|
2534
|
+
return { tokens: expandedTokens.slice(i + 1), envAssignments, cwd: currentCwd };
|
|
2145
2535
|
}
|
|
2146
2536
|
if (ENV_OPTS_NO_VALUE.has(token)) {
|
|
2147
2537
|
i++;
|
|
2148
2538
|
continue;
|
|
2149
2539
|
}
|
|
2540
|
+
if (token === "-S" || token === "--split-string") {
|
|
2541
|
+
const splitValue = expandedTokens[i + 1];
|
|
2542
|
+
const splitTokens = splitValue !== undefined ? parseEnvSplitString(splitValue) : null;
|
|
2543
|
+
if (!splitTokens) {
|
|
2544
|
+
currentCwd = null;
|
|
2545
|
+
i += 2;
|
|
2546
|
+
continue;
|
|
2547
|
+
}
|
|
2548
|
+
expandedTokens = [
|
|
2549
|
+
...expandedTokens.slice(0, i),
|
|
2550
|
+
...splitTokens,
|
|
2551
|
+
...expandedTokens.slice(i + 2)
|
|
2552
|
+
];
|
|
2553
|
+
continue;
|
|
2554
|
+
}
|
|
2555
|
+
if (token.startsWith("-S") && token.length > 2) {
|
|
2556
|
+
const splitTokens = parseEnvSplitString(token.slice("-S".length));
|
|
2557
|
+
if (!splitTokens) {
|
|
2558
|
+
currentCwd = null;
|
|
2559
|
+
i++;
|
|
2560
|
+
continue;
|
|
2561
|
+
}
|
|
2562
|
+
expandedTokens = [
|
|
2563
|
+
...expandedTokens.slice(0, i),
|
|
2564
|
+
...splitTokens,
|
|
2565
|
+
...expandedTokens.slice(i + 1)
|
|
2566
|
+
];
|
|
2567
|
+
continue;
|
|
2568
|
+
}
|
|
2569
|
+
if (token.startsWith("--split-string=")) {
|
|
2570
|
+
const splitTokens = parseEnvSplitString(token.slice("--split-string=".length));
|
|
2571
|
+
if (!splitTokens) {
|
|
2572
|
+
currentCwd = null;
|
|
2573
|
+
i++;
|
|
2574
|
+
continue;
|
|
2575
|
+
}
|
|
2576
|
+
expandedTokens = [
|
|
2577
|
+
...expandedTokens.slice(0, i),
|
|
2578
|
+
...splitTokens,
|
|
2579
|
+
...expandedTokens.slice(i + 1)
|
|
2580
|
+
];
|
|
2581
|
+
continue;
|
|
2582
|
+
}
|
|
2150
2583
|
if (ENV_OPTS_WITH_VALUE.has(token)) {
|
|
2584
|
+
if (token === "-C" || token === "--chdir") {
|
|
2585
|
+
const target = expandedTokens[i + 1];
|
|
2586
|
+
currentCwd = target ? resolveWrapperCwd(currentCwd, target) : null;
|
|
2587
|
+
}
|
|
2151
2588
|
i += 2;
|
|
2152
2589
|
continue;
|
|
2153
2590
|
}
|
|
@@ -2155,7 +2592,9 @@ function stripEnvWithInfo(tokens) {
|
|
|
2155
2592
|
i++;
|
|
2156
2593
|
continue;
|
|
2157
2594
|
}
|
|
2158
|
-
if (token.startsWith("-C
|
|
2595
|
+
if (token.startsWith("-C") && token.length > 2 || token.startsWith("--chdir=")) {
|
|
2596
|
+
const target = token.startsWith("--chdir=") ? token.slice("--chdir=".length) : token.startsWith("-C=") ? token.slice("-C=".length) : token.slice("-C".length);
|
|
2597
|
+
currentCwd = resolveWrapperCwd(currentCwd, target);
|
|
2159
2598
|
i++;
|
|
2160
2599
|
continue;
|
|
2161
2600
|
}
|
|
@@ -2174,7 +2613,39 @@ function stripEnvWithInfo(tokens) {
|
|
|
2174
2613
|
envAssignments.set(assignment.name, assignment.value);
|
|
2175
2614
|
i++;
|
|
2176
2615
|
}
|
|
2177
|
-
return { tokens:
|
|
2616
|
+
return { tokens: expandedTokens.slice(i), envAssignments, cwd: currentCwd };
|
|
2617
|
+
}
|
|
2618
|
+
function parseEnvSplitString(value) {
|
|
2619
|
+
if (hasUnclosedQuotes(value)) {
|
|
2620
|
+
return null;
|
|
2621
|
+
}
|
|
2622
|
+
const parsed = $parse(value, ENV_PROXY);
|
|
2623
|
+
const result = [];
|
|
2624
|
+
for (const entry of parsed) {
|
|
2625
|
+
const token = _getCommandTokenText(entry);
|
|
2626
|
+
if (token === null) {
|
|
2627
|
+
return null;
|
|
2628
|
+
}
|
|
2629
|
+
result.push(token);
|
|
2630
|
+
}
|
|
2631
|
+
return result;
|
|
2632
|
+
}
|
|
2633
|
+
function resolveWrapperCwd(cwd, target) {
|
|
2634
|
+
if (target === "") {
|
|
2635
|
+
return null;
|
|
2636
|
+
}
|
|
2637
|
+
try {
|
|
2638
|
+
if (!cwd && !isAbsolute3(target)) {
|
|
2639
|
+
return null;
|
|
2640
|
+
}
|
|
2641
|
+
const baseCwd = isAbsolute3(target) ? getPathRoot2(target) : realpathSync3(cwd ?? "/");
|
|
2642
|
+
return resolveChdirTarget(baseCwd, target);
|
|
2643
|
+
} catch {
|
|
2644
|
+
return null;
|
|
2645
|
+
}
|
|
2646
|
+
}
|
|
2647
|
+
function getPathRoot2(target) {
|
|
2648
|
+
return parsePath2(target).root;
|
|
2178
2649
|
}
|
|
2179
2650
|
function stripCommand(tokens) {
|
|
2180
2651
|
let i = 1;
|
|
@@ -2527,6 +2998,9 @@ function extractDashCArg(tokens) {
|
|
|
2527
2998
|
}
|
|
2528
2999
|
|
|
2529
3000
|
// src/core/rules-git.ts
|
|
3001
|
+
import { execFileSync } from "node:child_process";
|
|
3002
|
+
import { existsSync as existsSync5, readFileSync as readFileSync5 } from "node:fs";
|
|
3003
|
+
import { dirname as dirname3, isAbsolute as isAbsolute4, join as join4, resolve as resolve3 } from "node:path";
|
|
2530
3004
|
var REASON_CHECKOUT_DOUBLE_DASH = "git checkout -- discards uncommitted changes permanently. Use 'git stash' first.";
|
|
2531
3005
|
var REASON_CHECKOUT_FORCE = "git checkout --force discards uncommitted changes. Use 'git stash' first.";
|
|
2532
3006
|
var REASON_CHECKOUT_REF_PATH = "git checkout <ref> -- <path> overwrites working tree with ref version. Use 'git stash' first.";
|
|
@@ -2544,15 +3018,6 @@ var REASON_BRANCH_DELETE = "git branch -D force-deletes without merge check. Use
|
|
|
2544
3018
|
var REASON_STASH_DROP = "git stash drop permanently deletes stashed changes. Consider 'git stash list' first.";
|
|
2545
3019
|
var REASON_STASH_CLEAR = "git stash clear deletes ALL stashed changes permanently.";
|
|
2546
3020
|
var REASON_WORKTREE_REMOVE_FORCE = "git worktree remove --force can delete uncommitted changes. Remove --force flag.";
|
|
2547
|
-
var GIT_GLOBAL_OPTS_WITH_VALUE = new Set([
|
|
2548
|
-
"-c",
|
|
2549
|
-
"-C",
|
|
2550
|
-
"--git-dir",
|
|
2551
|
-
"--work-tree",
|
|
2552
|
-
"--namespace",
|
|
2553
|
-
"--super-prefix",
|
|
2554
|
-
"--config-env"
|
|
2555
|
-
]);
|
|
2556
3021
|
var CHECKOUT_OPTS_WITH_VALUE = new Set([
|
|
2557
3022
|
"-b",
|
|
2558
3023
|
"-B",
|
|
@@ -2565,6 +3030,13 @@ var CHECKOUT_OPTS_WITH_VALUE = new Set([
|
|
|
2565
3030
|
var CHECKOUT_OPTS_WITH_OPTIONAL_VALUE = new Set(["--recurse-submodules", "--track", "-t"]);
|
|
2566
3031
|
var CHECKOUT_SHORT_OPTS_WITH_VALUE = new Set(["-b", "-B", "-U"]);
|
|
2567
3032
|
var SWITCH_SHORT_OPTS_WITH_VALUE = new Set(["-c", "-C"]);
|
|
3033
|
+
var TRUSTED_GIT_BINARIES = [
|
|
3034
|
+
"/usr/bin/git",
|
|
3035
|
+
"/usr/local/bin/git",
|
|
3036
|
+
"/opt/homebrew/bin/git",
|
|
3037
|
+
"C:\\Program Files\\Git\\cmd\\git.exe",
|
|
3038
|
+
"C:\\Program Files\\Git\\bin\\git.exe"
|
|
3039
|
+
];
|
|
2568
3040
|
var CHECKOUT_KNOWN_OPTS_NO_VALUE = new Set([
|
|
2569
3041
|
"-q",
|
|
2570
3042
|
"--quiet",
|
|
@@ -2611,34 +3083,76 @@ function splitAtDoubleDash(tokens) {
|
|
|
2611
3083
|
after: tokens.slice(index + 1)
|
|
2612
3084
|
};
|
|
2613
3085
|
}
|
|
2614
|
-
function analyzeGit(tokens) {
|
|
3086
|
+
function analyzeGit(tokens, options = {}) {
|
|
3087
|
+
const match = analyzeGitRule(tokens);
|
|
3088
|
+
if (!match) {
|
|
3089
|
+
return null;
|
|
3090
|
+
}
|
|
3091
|
+
if (getGitWorktreeRelaxationForMatch(tokens, match, options)) {
|
|
3092
|
+
return null;
|
|
3093
|
+
}
|
|
3094
|
+
return match.reason;
|
|
3095
|
+
}
|
|
3096
|
+
function getGitWorktreeRelaxation(tokens, options = {}) {
|
|
3097
|
+
const match = analyzeGitRule(tokens);
|
|
3098
|
+
if (!match) {
|
|
3099
|
+
return null;
|
|
3100
|
+
}
|
|
3101
|
+
return getGitWorktreeRelaxationForMatch(tokens, match, options);
|
|
3102
|
+
}
|
|
3103
|
+
function analyzeGitRule(tokens) {
|
|
2615
3104
|
const { subcommand, rest } = extractGitSubcommandAndRest(tokens);
|
|
2616
3105
|
if (!subcommand) {
|
|
2617
3106
|
return null;
|
|
2618
3107
|
}
|
|
2619
3108
|
switch (subcommand.toLowerCase()) {
|
|
2620
3109
|
case "checkout":
|
|
2621
|
-
return analyzeGitCheckout(rest);
|
|
3110
|
+
return localDiscard(analyzeGitCheckout(rest));
|
|
2622
3111
|
case "switch":
|
|
2623
|
-
return analyzeGitSwitch(rest);
|
|
3112
|
+
return localDiscard(analyzeGitSwitch(rest));
|
|
2624
3113
|
case "restore":
|
|
2625
|
-
return analyzeGitRestore(rest);
|
|
3114
|
+
return localDiscard(analyzeGitRestore(rest));
|
|
2626
3115
|
case "reset":
|
|
2627
3116
|
return analyzeGitReset(rest);
|
|
2628
3117
|
case "clean":
|
|
2629
|
-
return analyzeGitClean(rest);
|
|
3118
|
+
return localDiscard(analyzeGitClean(rest));
|
|
2630
3119
|
case "push":
|
|
2631
|
-
return analyzeGitPush(rest);
|
|
3120
|
+
return sharedState(analyzeGitPush(rest));
|
|
2632
3121
|
case "branch":
|
|
2633
|
-
return analyzeGitBranch(rest);
|
|
3122
|
+
return sharedState(analyzeGitBranch(rest));
|
|
2634
3123
|
case "stash":
|
|
2635
|
-
return analyzeGitStash(rest);
|
|
3124
|
+
return sharedState(analyzeGitStash(rest));
|
|
2636
3125
|
case "worktree":
|
|
2637
|
-
return analyzeGitWorktree(rest);
|
|
3126
|
+
return sharedState(analyzeGitWorktree(rest));
|
|
2638
3127
|
default:
|
|
2639
3128
|
return null;
|
|
2640
3129
|
}
|
|
2641
3130
|
}
|
|
3131
|
+
function localDiscard(reason) {
|
|
3132
|
+
return reason ? { reason, localDiscard: true } : null;
|
|
3133
|
+
}
|
|
3134
|
+
function sharedState(reason) {
|
|
3135
|
+
return reason ? { reason, localDiscard: false } : null;
|
|
3136
|
+
}
|
|
3137
|
+
function getGitWorktreeRelaxationForMatch(tokens, match, options) {
|
|
3138
|
+
if (!match.localDiscard || !options.worktreeMode || hasGitContextEnvOverride(options.envAssignments)) {
|
|
3139
|
+
return null;
|
|
3140
|
+
}
|
|
3141
|
+
const context = getGitExecutionContext(tokens, options.cwd);
|
|
3142
|
+
if (!context.gitCwd || context.hasExplicitGitContext) {
|
|
3143
|
+
return null;
|
|
3144
|
+
}
|
|
3145
|
+
if (!isLinkedWorktree(context.gitCwd)) {
|
|
3146
|
+
return null;
|
|
3147
|
+
}
|
|
3148
|
+
if (isNonRelaxableLocalDiscard(tokens, options, context.gitCwd)) {
|
|
3149
|
+
return null;
|
|
3150
|
+
}
|
|
3151
|
+
return {
|
|
3152
|
+
originalReason: match.reason,
|
|
3153
|
+
gitCwd: context.gitCwd
|
|
3154
|
+
};
|
|
3155
|
+
}
|
|
2642
3156
|
function extractGitSubcommandAndRest(tokens) {
|
|
2643
3157
|
if (tokens.length === 0) {
|
|
2644
3158
|
return { subcommand: null, rest: [] };
|
|
@@ -2776,15 +3290,32 @@ function analyzeGitRestore(tokens) {
|
|
|
2776
3290
|
return hasStaged ? null : REASON_RESTORE;
|
|
2777
3291
|
}
|
|
2778
3292
|
function analyzeGitReset(tokens) {
|
|
3293
|
+
let reason = null;
|
|
2779
3294
|
for (const token of tokens) {
|
|
2780
3295
|
if (token === "--hard") {
|
|
2781
|
-
|
|
3296
|
+
reason = REASON_RESET_HARD;
|
|
3297
|
+
break;
|
|
2782
3298
|
}
|
|
2783
3299
|
if (token === "--merge") {
|
|
2784
|
-
|
|
3300
|
+
reason = REASON_RESET_MERGE;
|
|
3301
|
+
break;
|
|
2785
3302
|
}
|
|
2786
3303
|
}
|
|
2787
|
-
|
|
3304
|
+
if (!reason) {
|
|
3305
|
+
return null;
|
|
3306
|
+
}
|
|
3307
|
+
return resetHasRef(tokens) ? sharedState(reason) : localDiscard(reason);
|
|
3308
|
+
}
|
|
3309
|
+
function resetHasRef(tokens) {
|
|
3310
|
+
for (const token of tokens) {
|
|
3311
|
+
if (token === "--") {
|
|
3312
|
+
return false;
|
|
3313
|
+
}
|
|
3314
|
+
if (!token.startsWith("-")) {
|
|
3315
|
+
return true;
|
|
3316
|
+
}
|
|
3317
|
+
}
|
|
3318
|
+
return false;
|
|
2788
3319
|
}
|
|
2789
3320
|
function analyzeGitClean(tokens) {
|
|
2790
3321
|
for (const token of tokens) {
|
|
@@ -2798,6 +3329,351 @@ function analyzeGitClean(tokens) {
|
|
|
2798
3329
|
}
|
|
2799
3330
|
return null;
|
|
2800
3331
|
}
|
|
3332
|
+
function isNonRelaxableLocalDiscard(tokens, options, gitCwd) {
|
|
3333
|
+
const { subcommand, rest } = extractGitSubcommandAndRest(tokens);
|
|
3334
|
+
const normalizedSubcommand = subcommand?.toLowerCase();
|
|
3335
|
+
if (hasDynamicGitArgument(rest) || hasRecursiveSubmoduleConfig(tokens, options, gitCwd) || hasRecurseSubmodulesOption(rest) || isForcedBranchReset(normalizedSubcommand, rest)) {
|
|
3336
|
+
return true;
|
|
3337
|
+
}
|
|
3338
|
+
return normalizedSubcommand === "clean" && countCleanForceFlags(rest) > 1;
|
|
3339
|
+
}
|
|
3340
|
+
function hasDynamicGitArgument(tokens) {
|
|
3341
|
+
return tokens.some((token) => /[$*?[]/.test(token));
|
|
3342
|
+
}
|
|
3343
|
+
function hasRecursiveSubmoduleConfig(tokens, options, gitCwd) {
|
|
3344
|
+
const commandLineConfig = commandLineRecursiveSubmoduleConfig(tokens, options.envAssignments);
|
|
3345
|
+
if (commandLineConfig !== null) {
|
|
3346
|
+
return commandLineConfig;
|
|
3347
|
+
}
|
|
3348
|
+
const envConfig = envRecursiveSubmoduleConfig(options.envAssignments);
|
|
3349
|
+
if (envConfig !== null) {
|
|
3350
|
+
return envConfig;
|
|
3351
|
+
}
|
|
3352
|
+
if (hasConfigAffectingEnvAssignment(options.envAssignments)) {
|
|
3353
|
+
return true;
|
|
3354
|
+
}
|
|
3355
|
+
return effectiveGitConfigEnablesRecursiveSubmodules(gitCwd);
|
|
3356
|
+
}
|
|
3357
|
+
function commandLineRecursiveSubmoduleConfig(tokens, envAssignments) {
|
|
3358
|
+
let recursiveSubmoduleConfig = null;
|
|
3359
|
+
let i = 1;
|
|
3360
|
+
while (i < tokens.length) {
|
|
3361
|
+
const token = tokens[i];
|
|
3362
|
+
if (!token || token === "--") {
|
|
3363
|
+
return recursiveSubmoduleConfig;
|
|
3364
|
+
}
|
|
3365
|
+
if (!token.startsWith("-")) {
|
|
3366
|
+
return recursiveSubmoduleConfig;
|
|
3367
|
+
}
|
|
3368
|
+
if (token === "-c") {
|
|
3369
|
+
const configValue = recursiveSubmoduleConfigValue(tokens[i + 1]);
|
|
3370
|
+
if (configValue !== null) {
|
|
3371
|
+
recursiveSubmoduleConfig = configValue;
|
|
3372
|
+
}
|
|
3373
|
+
i += 2;
|
|
3374
|
+
continue;
|
|
3375
|
+
}
|
|
3376
|
+
if (token.startsWith("-c") && token.length > 2) {
|
|
3377
|
+
const configValue = recursiveSubmoduleConfigValue(token.slice(2));
|
|
3378
|
+
if (configValue !== null) {
|
|
3379
|
+
recursiveSubmoduleConfig = configValue;
|
|
3380
|
+
}
|
|
3381
|
+
i++;
|
|
3382
|
+
continue;
|
|
3383
|
+
}
|
|
3384
|
+
if (token === "--config-env") {
|
|
3385
|
+
const configValue = recursiveSubmoduleConfigEnvValue(tokens[i + 1], envAssignments);
|
|
3386
|
+
if (configValue !== null) {
|
|
3387
|
+
recursiveSubmoduleConfig = configValue;
|
|
3388
|
+
}
|
|
3389
|
+
i += 2;
|
|
3390
|
+
continue;
|
|
3391
|
+
}
|
|
3392
|
+
if (token.startsWith("--config-env=")) {
|
|
3393
|
+
const configValue = recursiveSubmoduleConfigEnvValue(token.slice("--config-env=".length), envAssignments);
|
|
3394
|
+
if (configValue !== null) {
|
|
3395
|
+
recursiveSubmoduleConfig = configValue;
|
|
3396
|
+
}
|
|
3397
|
+
i++;
|
|
3398
|
+
continue;
|
|
3399
|
+
}
|
|
3400
|
+
if (GIT_GLOBAL_OPTS_WITH_VALUE.has(token)) {
|
|
3401
|
+
i += 2;
|
|
3402
|
+
} else {
|
|
3403
|
+
i++;
|
|
3404
|
+
}
|
|
3405
|
+
}
|
|
3406
|
+
return recursiveSubmoduleConfig;
|
|
3407
|
+
}
|
|
3408
|
+
function envRecursiveSubmoduleConfig(envAssignments) {
|
|
3409
|
+
if (getEnvConfigValue("GIT_CONFIG_PARAMETERS", envAssignments) !== undefined) {
|
|
3410
|
+
return true;
|
|
3411
|
+
}
|
|
3412
|
+
const countValue = getEnvConfigValue("GIT_CONFIG_COUNT", envAssignments);
|
|
3413
|
+
if (countValue === undefined) {
|
|
3414
|
+
return null;
|
|
3415
|
+
}
|
|
3416
|
+
const count = Number.parseInt(countValue, 10);
|
|
3417
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
3418
|
+
return true;
|
|
3419
|
+
}
|
|
3420
|
+
let recursiveSubmoduleConfig = null;
|
|
3421
|
+
for (let i = 0;i < count; i++) {
|
|
3422
|
+
const key = getEnvConfigValue(`GIT_CONFIG_KEY_${i}`, envAssignments);
|
|
3423
|
+
if (key?.toLowerCase() !== "submodule.recurse") {
|
|
3424
|
+
continue;
|
|
3425
|
+
}
|
|
3426
|
+
const value = getEnvConfigValue(`GIT_CONFIG_VALUE_${i}`, envAssignments);
|
|
3427
|
+
recursiveSubmoduleConfig = value === undefined || gitConfigValueEnablesRecursiveSubmodules(value);
|
|
3428
|
+
}
|
|
3429
|
+
return recursiveSubmoduleConfig;
|
|
3430
|
+
}
|
|
3431
|
+
function hasConfigAffectingEnvAssignment(envAssignments) {
|
|
3432
|
+
if (!envAssignments) {
|
|
3433
|
+
return false;
|
|
3434
|
+
}
|
|
3435
|
+
for (const key of envAssignments.keys()) {
|
|
3436
|
+
if (GIT_CONFIG_AFFECTING_ENV_NAMES.has(key)) {
|
|
3437
|
+
return true;
|
|
3438
|
+
}
|
|
3439
|
+
}
|
|
3440
|
+
return false;
|
|
3441
|
+
}
|
|
3442
|
+
function getEnvConfigValue(name, envAssignments) {
|
|
3443
|
+
return envAssignments?.get(name) ?? process.env[name];
|
|
3444
|
+
}
|
|
3445
|
+
function effectiveGitConfigEnablesRecursiveSubmodules(cwd, gitBinary = getTrustedGitBinary()) {
|
|
3446
|
+
const localConfigResult = localGitConfigEnablesRecursiveSubmodules(cwd);
|
|
3447
|
+
if (localConfigResult === null || localConfigResult) {
|
|
3448
|
+
return true;
|
|
3449
|
+
}
|
|
3450
|
+
if (gitBinary === null) {
|
|
3451
|
+
return true;
|
|
3452
|
+
}
|
|
3453
|
+
try {
|
|
3454
|
+
const value = execFileSync(gitBinary, ["config", "--get", "submodule.recurse"], {
|
|
3455
|
+
cwd,
|
|
3456
|
+
encoding: "utf8",
|
|
3457
|
+
env: withoutGitConfigEnv(process.env),
|
|
3458
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
3459
|
+
}).trim();
|
|
3460
|
+
return gitConfigValueEnablesRecursiveSubmodules(value);
|
|
3461
|
+
} catch (error) {
|
|
3462
|
+
return !isGitConfigUnsetError(error);
|
|
3463
|
+
}
|
|
3464
|
+
}
|
|
3465
|
+
function localGitConfigEnablesRecursiveSubmodules(cwd) {
|
|
3466
|
+
const configPaths = getLocalGitConfigPaths(cwd);
|
|
3467
|
+
if (configPaths === null) {
|
|
3468
|
+
return null;
|
|
3469
|
+
}
|
|
3470
|
+
for (const configPath of configPaths) {
|
|
3471
|
+
if (!existsSync5(configPath)) {
|
|
3472
|
+
continue;
|
|
3473
|
+
}
|
|
3474
|
+
const result = gitConfigFileEnablesRecursiveSubmodules(configPath);
|
|
3475
|
+
if (result) {
|
|
3476
|
+
return true;
|
|
3477
|
+
}
|
|
3478
|
+
}
|
|
3479
|
+
return false;
|
|
3480
|
+
}
|
|
3481
|
+
function getTrustedGitBinary() {
|
|
3482
|
+
for (const gitBinary of TRUSTED_GIT_BINARIES) {
|
|
3483
|
+
if (existsSync5(gitBinary)) {
|
|
3484
|
+
return gitBinary;
|
|
3485
|
+
}
|
|
3486
|
+
}
|
|
3487
|
+
return null;
|
|
3488
|
+
}
|
|
3489
|
+
function withoutGitConfigEnv(env) {
|
|
3490
|
+
const nextEnv = { ...env };
|
|
3491
|
+
for (const key of Object.keys(nextEnv)) {
|
|
3492
|
+
if (key === "GIT_CONFIG_COUNT" || key === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(key)) {
|
|
3493
|
+
delete nextEnv[key];
|
|
3494
|
+
}
|
|
3495
|
+
}
|
|
3496
|
+
return nextEnv;
|
|
3497
|
+
}
|
|
3498
|
+
function isGitConfigUnsetError(error) {
|
|
3499
|
+
return typeof error === "object" && error !== null && "status" in error && error.status === 1;
|
|
3500
|
+
}
|
|
3501
|
+
function getLocalGitConfigPaths(cwd) {
|
|
3502
|
+
const dotGitPath = findDotGitPath(cwd);
|
|
3503
|
+
if (dotGitPath === null) {
|
|
3504
|
+
return null;
|
|
3505
|
+
}
|
|
3506
|
+
const gitDir = resolveGitDirFromDotGit(dotGitPath);
|
|
3507
|
+
if (gitDir === null) {
|
|
3508
|
+
return null;
|
|
3509
|
+
}
|
|
3510
|
+
const commonDir = resolveCommonGitDir(gitDir);
|
|
3511
|
+
if (commonDir === null) {
|
|
3512
|
+
return null;
|
|
3513
|
+
}
|
|
3514
|
+
return [join4(commonDir, "config"), join4(gitDir, "config.worktree")];
|
|
3515
|
+
}
|
|
3516
|
+
function findDotGitPath(cwd) {
|
|
3517
|
+
let current = cwd;
|
|
3518
|
+
while (true) {
|
|
3519
|
+
const dotGitPath = join4(current, ".git");
|
|
3520
|
+
if (existsSync5(dotGitPath)) {
|
|
3521
|
+
return dotGitPath;
|
|
3522
|
+
}
|
|
3523
|
+
const parent = dirname3(current);
|
|
3524
|
+
if (parent === current) {
|
|
3525
|
+
return null;
|
|
3526
|
+
}
|
|
3527
|
+
current = parent;
|
|
3528
|
+
}
|
|
3529
|
+
}
|
|
3530
|
+
function resolveGitDirFromDotGit(dotGitPath) {
|
|
3531
|
+
try {
|
|
3532
|
+
const content = readFileSync5(dotGitPath, "utf-8");
|
|
3533
|
+
const firstLine = content.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
3534
|
+
if (!firstLine.startsWith("gitdir:")) {
|
|
3535
|
+
return dotGitPath;
|
|
3536
|
+
}
|
|
3537
|
+
const rawGitDir = firstLine.slice("gitdir:".length).trim();
|
|
3538
|
+
if (rawGitDir === "") {
|
|
3539
|
+
return null;
|
|
3540
|
+
}
|
|
3541
|
+
return isAbsolute4(rawGitDir) ? rawGitDir : resolve3(dirname3(dotGitPath), rawGitDir);
|
|
3542
|
+
} catch {
|
|
3543
|
+
return null;
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
function resolveCommonGitDir(gitDir) {
|
|
3547
|
+
const commonDirPath = join4(gitDir, "commondir");
|
|
3548
|
+
if (!existsSync5(commonDirPath)) {
|
|
3549
|
+
return gitDir;
|
|
3550
|
+
}
|
|
3551
|
+
try {
|
|
3552
|
+
const rawCommonDir = readFileSync5(commonDirPath, "utf-8").split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
3553
|
+
if (rawCommonDir === "") {
|
|
3554
|
+
return null;
|
|
3555
|
+
}
|
|
3556
|
+
return isAbsolute4(rawCommonDir) ? rawCommonDir : resolve3(gitDir, rawCommonDir);
|
|
3557
|
+
} catch {
|
|
3558
|
+
return null;
|
|
3559
|
+
}
|
|
3560
|
+
}
|
|
3561
|
+
function gitConfigFileEnablesRecursiveSubmodules(configPath) {
|
|
3562
|
+
let content;
|
|
3563
|
+
try {
|
|
3564
|
+
content = readFileSync5(configPath, "utf-8");
|
|
3565
|
+
} catch {
|
|
3566
|
+
return true;
|
|
3567
|
+
}
|
|
3568
|
+
let section = "";
|
|
3569
|
+
let recursiveSubmoduleConfig = false;
|
|
3570
|
+
for (const line of content.split(/\r?\n/)) {
|
|
3571
|
+
const trimmed = line.trim();
|
|
3572
|
+
if (trimmed === "" || trimmed.startsWith("#") || trimmed.startsWith(";")) {
|
|
3573
|
+
continue;
|
|
3574
|
+
}
|
|
3575
|
+
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
|
|
3576
|
+
if (sectionMatch) {
|
|
3577
|
+
section = sectionMatch[1]?.trim().toLowerCase() ?? "";
|
|
3578
|
+
continue;
|
|
3579
|
+
}
|
|
3580
|
+
const eqIdx = trimmed.indexOf("=");
|
|
3581
|
+
const key = (eqIdx === -1 ? trimmed : trimmed.slice(0, eqIdx)).trim().toLowerCase();
|
|
3582
|
+
const value = eqIdx === -1 ? "true" : trimmed.slice(eqIdx + 1).trim();
|
|
3583
|
+
if (isIncludeConfigSection(section) && key === "path") {
|
|
3584
|
+
return true;
|
|
3585
|
+
}
|
|
3586
|
+
if (section === "submodule" && key === "recurse") {
|
|
3587
|
+
recursiveSubmoduleConfig = gitConfigValueEnablesRecursiveSubmodules(value);
|
|
3588
|
+
}
|
|
3589
|
+
}
|
|
3590
|
+
return recursiveSubmoduleConfig;
|
|
3591
|
+
}
|
|
3592
|
+
function isIncludeConfigSection(section) {
|
|
3593
|
+
return section === "include" || section.startsWith("includeif ");
|
|
3594
|
+
}
|
|
3595
|
+
function recursiveSubmoduleConfigValue(config) {
|
|
3596
|
+
if (!config) {
|
|
3597
|
+
return null;
|
|
3598
|
+
}
|
|
3599
|
+
const eqIdx = config.indexOf("=");
|
|
3600
|
+
const key = (eqIdx === -1 ? config : config.slice(0, eqIdx)).toLowerCase();
|
|
3601
|
+
if (isIncludeConfigKey(key)) {
|
|
3602
|
+
return true;
|
|
3603
|
+
}
|
|
3604
|
+
if (key !== "submodule.recurse") {
|
|
3605
|
+
return null;
|
|
3606
|
+
}
|
|
3607
|
+
const value = eqIdx === -1 ? "true" : config.slice(eqIdx + 1).toLowerCase();
|
|
3608
|
+
return gitConfigValueEnablesRecursiveSubmodules(value);
|
|
3609
|
+
}
|
|
3610
|
+
function gitConfigValueEnablesRecursiveSubmodules(value) {
|
|
3611
|
+
const normalizedValue = value.toLowerCase();
|
|
3612
|
+
return normalizedValue !== "false" && normalizedValue !== "no" && normalizedValue !== "off" && normalizedValue !== "0";
|
|
3613
|
+
}
|
|
3614
|
+
function recursiveSubmoduleConfigEnvValue(configEnv, envAssignments) {
|
|
3615
|
+
const eqIdx = configEnv?.indexOf("=") ?? -1;
|
|
3616
|
+
if (!configEnv || eqIdx === -1) {
|
|
3617
|
+
return null;
|
|
3618
|
+
}
|
|
3619
|
+
const key = configEnv.slice(0, eqIdx).toLowerCase();
|
|
3620
|
+
if (isIncludeConfigKey(key)) {
|
|
3621
|
+
return true;
|
|
3622
|
+
}
|
|
3623
|
+
if (key !== "submodule.recurse") {
|
|
3624
|
+
return null;
|
|
3625
|
+
}
|
|
3626
|
+
const value = getEnvConfigValue(configEnv.slice(eqIdx + 1), envAssignments);
|
|
3627
|
+
return value === undefined || gitConfigValueEnablesRecursiveSubmodules(value);
|
|
3628
|
+
}
|
|
3629
|
+
function isIncludeConfigKey(key) {
|
|
3630
|
+
return key === "include.path" || key.startsWith("includeif.") && key.endsWith(".path");
|
|
3631
|
+
}
|
|
3632
|
+
function isForcedBranchReset(subcommand, rest) {
|
|
3633
|
+
if (subcommand === "checkout") {
|
|
3634
|
+
const { before } = splitAtDoubleDash(rest);
|
|
3635
|
+
const shortOpts = extractShortOpts(before, {
|
|
3636
|
+
shortOptsWithValue: CHECKOUT_SHORT_OPTS_WITH_VALUE
|
|
3637
|
+
});
|
|
3638
|
+
const hasForce = before.includes("--force") || shortOpts.has("-f");
|
|
3639
|
+
const hasBranchReset = shortOpts.has("-B") || before.some((token) => token === "-B" || token.startsWith("-B"));
|
|
3640
|
+
return hasForce && hasBranchReset;
|
|
3641
|
+
}
|
|
3642
|
+
if (subcommand === "switch") {
|
|
3643
|
+
const { before } = splitAtDoubleDash(rest);
|
|
3644
|
+
const shortOpts = extractShortOpts(before, {
|
|
3645
|
+
shortOptsWithValue: SWITCH_SHORT_OPTS_WITH_VALUE
|
|
3646
|
+
});
|
|
3647
|
+
const hasForce = before.includes("--force") || before.includes("--discard-changes") || shortOpts.has("-f");
|
|
3648
|
+
const hasForceCreate = before.some((token) => token === "-C" || token.startsWith("-C") || isForceCreateOption(token)) || shortOpts.has("-C");
|
|
3649
|
+
return hasForce && hasForceCreate;
|
|
3650
|
+
}
|
|
3651
|
+
return false;
|
|
3652
|
+
}
|
|
3653
|
+
function isForceCreateOption(token) {
|
|
3654
|
+
const optionName = token.split("=", 1)[0] ?? token;
|
|
3655
|
+
return optionName === "--force-create" || optionName.length >= "--force-c".length && "--force-create".startsWith(optionName);
|
|
3656
|
+
}
|
|
3657
|
+
function hasRecurseSubmodulesOption(tokens) {
|
|
3658
|
+
return tokens.some((token) => token.startsWith("--recurse-sub"));
|
|
3659
|
+
}
|
|
3660
|
+
function countCleanForceFlags(tokens) {
|
|
3661
|
+
let count = 0;
|
|
3662
|
+
for (const token of tokens) {
|
|
3663
|
+
if (token === "--force") {
|
|
3664
|
+
count++;
|
|
3665
|
+
continue;
|
|
3666
|
+
}
|
|
3667
|
+
if (token.startsWith("-") && !token.startsWith("--")) {
|
|
3668
|
+
for (const opt of token.slice(1)) {
|
|
3669
|
+
if (opt === "f") {
|
|
3670
|
+
count++;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
return count;
|
|
3676
|
+
}
|
|
2801
3677
|
function analyzeGitPush(tokens) {
|
|
2802
3678
|
let hasForceWithLease = false;
|
|
2803
3679
|
const shortOpts = extractShortOpts(tokens.filter((t) => t !== "--"));
|
|
@@ -2844,11 +3720,11 @@ function analyzeGitWorktree(tokens) {
|
|
|
2844
3720
|
}
|
|
2845
3721
|
|
|
2846
3722
|
// src/core/rules-rm.ts
|
|
2847
|
-
import { realpathSync } from "node:fs";
|
|
3723
|
+
import { realpathSync as realpathSync4 } from "node:fs";
|
|
2848
3724
|
import { homedir as homedir3, tmpdir } from "node:os";
|
|
2849
|
-
import { normalize, resolve as
|
|
3725
|
+
import { normalize, resolve as resolve4, sep as sep2 } from "node:path";
|
|
2850
3726
|
var IS_WINDOWS = process.platform === "win32";
|
|
2851
|
-
function
|
|
3727
|
+
function normalizePathForComparison2(p) {
|
|
2852
3728
|
let normalized = normalize(p);
|
|
2853
3729
|
if (IS_WINDOWS) {
|
|
2854
3730
|
normalized = normalized.replace(/\//g, "\\");
|
|
@@ -2988,9 +3864,9 @@ function isTempTarget(path, allowTmpdirVar) {
|
|
|
2988
3864
|
return true;
|
|
2989
3865
|
}
|
|
2990
3866
|
const systemTmpdir = tmpdir();
|
|
2991
|
-
const normalizedTmpdir =
|
|
2992
|
-
const pathToCompare =
|
|
2993
|
-
if (pathToCompare.startsWith(`${normalizedTmpdir}${
|
|
3867
|
+
const normalizedTmpdir = normalizePathForComparison2(systemTmpdir);
|
|
3868
|
+
const pathToCompare = normalizePathForComparison2(normalized);
|
|
3869
|
+
if (pathToCompare.startsWith(`${normalizedTmpdir}${sep2}`) || pathToCompare === normalizedTmpdir) {
|
|
2994
3870
|
return true;
|
|
2995
3871
|
}
|
|
2996
3872
|
if (allowTmpdirVar) {
|
|
@@ -3008,7 +3884,7 @@ function getHomeDirForRmPolicy() {
|
|
|
3008
3884
|
}
|
|
3009
3885
|
function isCwdHomeForRmPolicy(cwd, homeDir) {
|
|
3010
3886
|
try {
|
|
3011
|
-
return
|
|
3887
|
+
return normalizePathForComparison2(cwd) === normalizePathForComparison2(homeDir);
|
|
3012
3888
|
} catch {
|
|
3013
3889
|
return false;
|
|
3014
3890
|
}
|
|
@@ -3018,14 +3894,14 @@ function isCwdSelfTarget(target, cwd) {
|
|
|
3018
3894
|
return true;
|
|
3019
3895
|
}
|
|
3020
3896
|
try {
|
|
3021
|
-
const resolved =
|
|
3022
|
-
const realCwd =
|
|
3023
|
-
const realResolved =
|
|
3024
|
-
return
|
|
3897
|
+
const resolved = resolve4(cwd, target);
|
|
3898
|
+
const realCwd = realpathSync4(cwd);
|
|
3899
|
+
const realResolved = realpathSync4(resolved);
|
|
3900
|
+
return normalizePathForComparison2(realResolved) === normalizePathForComparison2(realCwd);
|
|
3025
3901
|
} catch {
|
|
3026
3902
|
try {
|
|
3027
|
-
const resolved =
|
|
3028
|
-
return
|
|
3903
|
+
const resolved = resolve4(cwd, target);
|
|
3904
|
+
return normalizePathForComparison2(resolved) === normalizePathForComparison2(cwd);
|
|
3029
3905
|
} catch {
|
|
3030
3906
|
return false;
|
|
3031
3907
|
}
|
|
@@ -3041,8 +3917,8 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
|
|
|
3041
3917
|
}
|
|
3042
3918
|
if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target)) {
|
|
3043
3919
|
try {
|
|
3044
|
-
const normalizedTarget =
|
|
3045
|
-
const normalizedCwd = `${
|
|
3920
|
+
const normalizedTarget = normalizePathForComparison2(target);
|
|
3921
|
+
const normalizedCwd = `${normalizePathForComparison2(originalCwd)}${sep2}`;
|
|
3046
3922
|
return normalizedTarget.startsWith(normalizedCwd);
|
|
3047
3923
|
} catch {
|
|
3048
3924
|
return false;
|
|
@@ -3050,10 +3926,10 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
|
|
|
3050
3926
|
}
|
|
3051
3927
|
if (target.startsWith("./") || target.startsWith(".\\") || !target.includes("/") && !target.includes("\\")) {
|
|
3052
3928
|
try {
|
|
3053
|
-
const resolved =
|
|
3054
|
-
const normalizedResolved =
|
|
3055
|
-
const normalizedOriginalCwd =
|
|
3056
|
-
return normalizedResolved.startsWith(`${normalizedOriginalCwd}${
|
|
3929
|
+
const resolved = resolve4(resolveCwd, target);
|
|
3930
|
+
const normalizedResolved = normalizePathForComparison2(resolved);
|
|
3931
|
+
const normalizedOriginalCwd = normalizePathForComparison2(originalCwd);
|
|
3932
|
+
return normalizedResolved.startsWith(`${normalizedOriginalCwd}${sep2}`) || normalizedResolved === normalizedOriginalCwd;
|
|
3057
3933
|
} catch {
|
|
3058
3934
|
return false;
|
|
3059
3935
|
}
|
|
@@ -3062,10 +3938,10 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
|
|
|
3062
3938
|
return false;
|
|
3063
3939
|
}
|
|
3064
3940
|
try {
|
|
3065
|
-
const resolved =
|
|
3066
|
-
const normalizedResolved =
|
|
3067
|
-
const normalizedCwd =
|
|
3068
|
-
return normalizedResolved.startsWith(`${normalizedCwd}${
|
|
3941
|
+
const resolved = resolve4(resolveCwd, target);
|
|
3942
|
+
const normalizedResolved = normalizePathForComparison2(resolved);
|
|
3943
|
+
const normalizedCwd = normalizePathForComparison2(originalCwd);
|
|
3944
|
+
return normalizedResolved.startsWith(`${normalizedCwd}${sep2}`) || normalizedResolved === normalizedCwd;
|
|
3069
3945
|
} catch {
|
|
3070
3946
|
return false;
|
|
3071
3947
|
}
|
|
@@ -3074,22 +3950,32 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
|
|
|
3074
3950
|
// src/core/analyze/parallel.ts
|
|
3075
3951
|
var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
|
|
3076
3952
|
var REASON_PARALLEL_SHELL = "parallel with shell -c can execute arbitrary commands from dynamic input.";
|
|
3953
|
+
var PARALLEL_PLACEHOLDER_RE = /\{[^{}\s]*\}/;
|
|
3077
3954
|
function analyzeParallel(tokens, context) {
|
|
3078
3955
|
const parseResult = parseParallelCommand(tokens);
|
|
3079
3956
|
if (!parseResult) {
|
|
3080
3957
|
return null;
|
|
3081
3958
|
}
|
|
3082
|
-
const { template, args, hasPlaceholder } = parseResult;
|
|
3959
|
+
const { template, args, hasPlaceholder, runsRemotely, usesStdin } = parseResult;
|
|
3960
|
+
const hasDynamicStdinPlaceholder = usesStdin && hasPlaceholder;
|
|
3083
3961
|
if (template.length === 0) {
|
|
3962
|
+
const nestedOverrides2 = buildCommandsModeOverrides(context, runsRemotely);
|
|
3084
3963
|
for (const arg of args) {
|
|
3085
|
-
const reason = context.analyzeNested(arg);
|
|
3964
|
+
const reason = context.analyzeNested(arg, nestedOverrides2);
|
|
3086
3965
|
if (reason) {
|
|
3087
3966
|
return reason;
|
|
3088
3967
|
}
|
|
3089
3968
|
}
|
|
3090
3969
|
return null;
|
|
3091
3970
|
}
|
|
3092
|
-
|
|
3971
|
+
const childWrapperInfo = stripWrappersWithInfo([...template], context.cwd);
|
|
3972
|
+
let childTokens = childWrapperInfo.tokens;
|
|
3973
|
+
const childEnvAssignments = new Map(context.envAssignments ?? []);
|
|
3974
|
+
for (const [k, v] of childWrapperInfo.envAssignments) {
|
|
3975
|
+
childEnvAssignments.set(k, v);
|
|
3976
|
+
}
|
|
3977
|
+
const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
|
|
3978
|
+
const nestedOverrides = buildNestedOverrides(childEnvAssignments, childWrapperInfo.cwd, runsRemotely || hasDynamicStdinPlaceholder);
|
|
3093
3979
|
let head = getBasename(childTokens[0] ?? "").toLowerCase();
|
|
3094
3980
|
if (head === "busybox" && childTokens.length > 1) {
|
|
3095
3981
|
childTokens = childTokens.slice(1);
|
|
@@ -3098,27 +3984,27 @@ function analyzeParallel(tokens, context) {
|
|
|
3098
3984
|
if (SHELL_WRAPPERS.has(head)) {
|
|
3099
3985
|
const dashCArg = extractDashCArg(childTokens);
|
|
3100
3986
|
if (dashCArg) {
|
|
3101
|
-
if (dashCArg
|
|
3987
|
+
if (isOnlyParallelPlaceholder(dashCArg)) {
|
|
3102
3988
|
return REASON_PARALLEL_SHELL;
|
|
3103
3989
|
}
|
|
3104
|
-
if (dashCArg
|
|
3990
|
+
if (hasParallelPlaceholder(dashCArg)) {
|
|
3105
3991
|
if (args.length > 0) {
|
|
3106
3992
|
for (const arg of args) {
|
|
3107
|
-
const expandedScript = dashCArg
|
|
3108
|
-
const reason3 = context.analyzeNested(expandedScript);
|
|
3993
|
+
const expandedScript = replaceParallelPlaceholder(dashCArg, arg);
|
|
3994
|
+
const reason3 = context.analyzeNested(expandedScript, nestedOverrides);
|
|
3109
3995
|
if (reason3) {
|
|
3110
3996
|
return reason3;
|
|
3111
3997
|
}
|
|
3112
3998
|
}
|
|
3113
3999
|
return null;
|
|
3114
4000
|
}
|
|
3115
|
-
const reason2 = context.analyzeNested(dashCArg);
|
|
4001
|
+
const reason2 = context.analyzeNested(dashCArg, nestedOverrides);
|
|
3116
4002
|
if (reason2) {
|
|
3117
4003
|
return reason2;
|
|
3118
4004
|
}
|
|
3119
4005
|
return null;
|
|
3120
4006
|
}
|
|
3121
|
-
const reason = context.analyzeNested(dashCArg);
|
|
4007
|
+
const reason = context.analyzeNested(dashCArg, nestedOverrides);
|
|
3122
4008
|
if (reason) {
|
|
3123
4009
|
return reason;
|
|
3124
4010
|
}
|
|
@@ -3140,7 +4026,7 @@ function analyzeParallel(tokens, context) {
|
|
|
3140
4026
|
for (const arg of args) {
|
|
3141
4027
|
const expandedTokens = childTokens.map((t) => t.replace(/{}/g, arg));
|
|
3142
4028
|
const rmResult = analyzeRm(expandedTokens, {
|
|
3143
|
-
cwd:
|
|
4029
|
+
cwd: childCwd,
|
|
3144
4030
|
originalCwd: context.originalCwd,
|
|
3145
4031
|
paranoid: context.paranoidRm,
|
|
3146
4032
|
allowTmpdirVar: context.allowTmpdirVar
|
|
@@ -3154,7 +4040,7 @@ function analyzeParallel(tokens, context) {
|
|
|
3154
4040
|
if (args.length > 0) {
|
|
3155
4041
|
const expandedTokens = [...childTokens, args[0] ?? ""];
|
|
3156
4042
|
const rmResult = analyzeRm(expandedTokens, {
|
|
3157
|
-
cwd:
|
|
4043
|
+
cwd: childCwd,
|
|
3158
4044
|
originalCwd: context.originalCwd,
|
|
3159
4045
|
paranoid: context.paranoidRm,
|
|
3160
4046
|
allowTmpdirVar: context.allowTmpdirVar
|
|
@@ -3173,13 +4059,53 @@ function analyzeParallel(tokens, context) {
|
|
|
3173
4059
|
}
|
|
3174
4060
|
}
|
|
3175
4061
|
if (head === "git") {
|
|
3176
|
-
const
|
|
3177
|
-
|
|
3178
|
-
|
|
4062
|
+
const gitTokenSets = hasPlaceholder && args.length > 0 ? args.map((arg) => childTokens.map((token) => replaceParallelPlaceholder(token, arg))) : !hasPlaceholder && args.length > 0 ? args.map((arg) => [...childTokens, arg]) : [childTokens];
|
|
4063
|
+
const dynamicGitArgs = usesStdin || hasPlaceholder;
|
|
4064
|
+
for (const gitTokens of gitTokenSets) {
|
|
4065
|
+
const gitResult = analyzeGit(gitTokens, {
|
|
4066
|
+
cwd: childCwd,
|
|
4067
|
+
envAssignments: childEnvAssignments,
|
|
4068
|
+
worktreeMode: runsRemotely || dynamicGitArgs ? false : context.worktreeMode
|
|
4069
|
+
});
|
|
4070
|
+
if (gitResult) {
|
|
4071
|
+
return gitResult;
|
|
4072
|
+
}
|
|
3179
4073
|
}
|
|
3180
4074
|
}
|
|
3181
4075
|
return null;
|
|
3182
4076
|
}
|
|
4077
|
+
function buildNestedOverrides(envAssignments, cwd, runsRemotely) {
|
|
4078
|
+
const overrides = { envAssignments };
|
|
4079
|
+
if (cwd !== undefined) {
|
|
4080
|
+
overrides.effectiveCwd = cwd;
|
|
4081
|
+
}
|
|
4082
|
+
if (runsRemotely) {
|
|
4083
|
+
overrides.worktreeMode = false;
|
|
4084
|
+
}
|
|
4085
|
+
return overrides;
|
|
4086
|
+
}
|
|
4087
|
+
function buildCommandsModeOverrides(context, runsRemotely) {
|
|
4088
|
+
const overrides = {};
|
|
4089
|
+
if (context.envAssignments) {
|
|
4090
|
+
overrides.envAssignments = context.envAssignments;
|
|
4091
|
+
}
|
|
4092
|
+
if (context.cwd !== undefined) {
|
|
4093
|
+
overrides.effectiveCwd = context.cwd;
|
|
4094
|
+
}
|
|
4095
|
+
if (runsRemotely) {
|
|
4096
|
+
overrides.worktreeMode = false;
|
|
4097
|
+
}
|
|
4098
|
+
return Object.keys(overrides).length > 0 ? overrides : undefined;
|
|
4099
|
+
}
|
|
4100
|
+
function replaceParallelPlaceholder(token, arg) {
|
|
4101
|
+
return token.replace(/\{[^{}\s]*\}/g, arg);
|
|
4102
|
+
}
|
|
4103
|
+
function hasParallelPlaceholder(token) {
|
|
4104
|
+
return PARALLEL_PLACEHOLDER_RE.test(token);
|
|
4105
|
+
}
|
|
4106
|
+
function isOnlyParallelPlaceholder(token) {
|
|
4107
|
+
return /^\{[^{}\s]*\}$/.test(token);
|
|
4108
|
+
}
|
|
3183
4109
|
function parseParallelCommand(tokens) {
|
|
3184
4110
|
const parallelOptsWithValue = new Set([
|
|
3185
4111
|
"-S",
|
|
@@ -3198,6 +4124,7 @@ function parseParallelCommand(tokens) {
|
|
|
3198
4124
|
let i = 1;
|
|
3199
4125
|
const templateTokens = [];
|
|
3200
4126
|
let markerIndex = -1;
|
|
4127
|
+
let runsRemotely = false;
|
|
3201
4128
|
while (i < tokens.length) {
|
|
3202
4129
|
const token = tokens[i];
|
|
3203
4130
|
if (!token)
|
|
@@ -3221,6 +4148,21 @@ function parseParallelCommand(tokens) {
|
|
|
3221
4148
|
break;
|
|
3222
4149
|
}
|
|
3223
4150
|
if (token.startsWith("-")) {
|
|
4151
|
+
if (token === "-S" || token === "--sshlogin" || token === "--slf" || token === "--sshloginfile") {
|
|
4152
|
+
runsRemotely = true;
|
|
4153
|
+
i += 2;
|
|
4154
|
+
continue;
|
|
4155
|
+
}
|
|
4156
|
+
if (token.startsWith("-S") && token.length > 2) {
|
|
4157
|
+
runsRemotely = true;
|
|
4158
|
+
i++;
|
|
4159
|
+
continue;
|
|
4160
|
+
}
|
|
4161
|
+
if (token.startsWith("--sshlogin=") || token.startsWith("--slf=") || token.startsWith("--sshloginfile=")) {
|
|
4162
|
+
runsRemotely = true;
|
|
4163
|
+
i++;
|
|
4164
|
+
continue;
|
|
4165
|
+
}
|
|
3224
4166
|
if (token.startsWith("-j") && token.length > 2 && /^\d+$/.test(token.slice(2))) {
|
|
3225
4167
|
i++;
|
|
3226
4168
|
continue;
|
|
@@ -3261,16 +4203,22 @@ function parseParallelCommand(tokens) {
|
|
|
3261
4203
|
}
|
|
3262
4204
|
}
|
|
3263
4205
|
}
|
|
3264
|
-
const hasPlaceholder = templateTokens.some(
|
|
4206
|
+
const hasPlaceholder = templateTokens.some(hasParallelPlaceholder);
|
|
3265
4207
|
if (templateTokens.length === 0 && markerIndex === -1) {
|
|
3266
4208
|
return null;
|
|
3267
4209
|
}
|
|
3268
|
-
return {
|
|
4210
|
+
return {
|
|
4211
|
+
template: templateTokens,
|
|
4212
|
+
args,
|
|
4213
|
+
hasPlaceholder,
|
|
4214
|
+
runsRemotely,
|
|
4215
|
+
usesStdin: markerIndex === -1
|
|
4216
|
+
};
|
|
3269
4217
|
}
|
|
3270
4218
|
|
|
3271
4219
|
// src/core/analyze/tmpdir.ts
|
|
3272
4220
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
3273
|
-
import { normalize as normalize2, sep as
|
|
4221
|
+
import { normalize as normalize2, sep as sep3 } from "node:path";
|
|
3274
4222
|
function isTmpdirOverriddenToNonTemp(envAssignments) {
|
|
3275
4223
|
if (!envAssignments.has("TMPDIR")) {
|
|
3276
4224
|
return false;
|
|
@@ -3290,16 +4238,23 @@ function isPathOrSubpath(path, basePath) {
|
|
|
3290
4238
|
if (path === basePath) {
|
|
3291
4239
|
return true;
|
|
3292
4240
|
}
|
|
3293
|
-
const baseWithSlash = basePath.endsWith(
|
|
4241
|
+
const baseWithSlash = basePath.endsWith(sep3) ? basePath : `${basePath}${sep3}`;
|
|
3294
4242
|
return path.startsWith(baseWithSlash);
|
|
3295
4243
|
}
|
|
3296
4244
|
|
|
3297
4245
|
// src/core/analyze/xargs.ts
|
|
3298
4246
|
var REASON_XARGS_RM = "xargs rm -rf with dynamic input is dangerous. Use explicit file list instead.";
|
|
3299
4247
|
var REASON_XARGS_SHELL = "xargs with shell -c can execute arbitrary commands from dynamic input.";
|
|
4248
|
+
var XARGS_APPENDED_INPUT = "__CC_SAFETY_NET_XARGS_INPUT__";
|
|
3300
4249
|
function analyzeXargs(tokens, context) {
|
|
3301
|
-
const { childTokens: rawChildTokens } = extractXargsChildCommandWithInfo(tokens);
|
|
3302
|
-
|
|
4250
|
+
const { childTokens: rawChildTokens, replacementToken } = extractXargsChildCommandWithInfo(tokens);
|
|
4251
|
+
const childWrapperInfo = stripWrappersWithInfo(rawChildTokens, context.cwd);
|
|
4252
|
+
let childTokens = childWrapperInfo.tokens;
|
|
4253
|
+
const childEnvAssignments = new Map(context.envAssignments ?? []);
|
|
4254
|
+
for (const [k, v] of childWrapperInfo.envAssignments) {
|
|
4255
|
+
childEnvAssignments.set(k, v);
|
|
4256
|
+
}
|
|
4257
|
+
const childCwd = childWrapperInfo.cwd === null ? undefined : childWrapperInfo.cwd ?? context.cwd;
|
|
3303
4258
|
if (childTokens.length === 0) {
|
|
3304
4259
|
return null;
|
|
3305
4260
|
}
|
|
@@ -3313,7 +4268,7 @@ function analyzeXargs(tokens, context) {
|
|
|
3313
4268
|
}
|
|
3314
4269
|
if (head === "rm" && hasRecursiveForceFlags(childTokens)) {
|
|
3315
4270
|
const rmResult = analyzeRm(childTokens, {
|
|
3316
|
-
cwd:
|
|
4271
|
+
cwd: childCwd,
|
|
3317
4272
|
originalCwd: context.originalCwd,
|
|
3318
4273
|
paranoid: context.paranoidRm,
|
|
3319
4274
|
allowTmpdirVar: context.allowTmpdirVar
|
|
@@ -3330,7 +4285,13 @@ function analyzeXargs(tokens, context) {
|
|
|
3330
4285
|
}
|
|
3331
4286
|
}
|
|
3332
4287
|
if (head === "git") {
|
|
3333
|
-
const
|
|
4288
|
+
const gitTokens = replacementToken === null ? [...childTokens, XARGS_APPENDED_INPUT] : childTokens;
|
|
4289
|
+
const hasDynamicReplacement = replacementToken !== null && childTokens.some((token) => token.includes(replacementToken));
|
|
4290
|
+
const gitResult = analyzeGit(gitTokens, {
|
|
4291
|
+
cwd: childCwd,
|
|
4292
|
+
envAssignments: childEnvAssignments,
|
|
4293
|
+
worktreeMode: replacementToken === null || hasDynamicReplacement ? false : context.worktreeMode
|
|
4294
|
+
});
|
|
3334
4295
|
if (gitResult) {
|
|
3335
4296
|
return gitResult;
|
|
3336
4297
|
}
|
|
@@ -3500,9 +4461,17 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3500
4461
|
if (tokens.length === 0) {
|
|
3501
4462
|
return null;
|
|
3502
4463
|
}
|
|
4464
|
+
const { cwdForRm: baseCwdForRm, originalCwd } = deriveCwdContext(options);
|
|
3503
4465
|
const { tokens: strippedEnv, envAssignments: leadingEnvAssignments } = stripEnvAssignmentsWithInfo(tokens);
|
|
3504
|
-
const {
|
|
3505
|
-
|
|
4466
|
+
const {
|
|
4467
|
+
tokens: stripped,
|
|
4468
|
+
envAssignments: wrapperEnvAssignments,
|
|
4469
|
+
cwd: wrapperCwd
|
|
4470
|
+
} = stripWrappersWithInfo(strippedEnv, baseCwdForRm);
|
|
4471
|
+
const envAssignments = new Map(options.envAssignments ?? []);
|
|
4472
|
+
for (const [k, v] of leadingEnvAssignments) {
|
|
4473
|
+
envAssignments.set(k, v);
|
|
4474
|
+
}
|
|
3506
4475
|
for (const [k, v] of wrapperEnvAssignments) {
|
|
3507
4476
|
envAssignments.set(k, v);
|
|
3508
4477
|
}
|
|
@@ -3515,12 +4484,16 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3515
4484
|
}
|
|
3516
4485
|
const normalizedHead = normalizeCommandToken(head);
|
|
3517
4486
|
const basename = getBasename(head);
|
|
3518
|
-
const
|
|
4487
|
+
const cwdForRm = wrapperCwd === null ? undefined : wrapperCwd ?? baseCwdForRm;
|
|
4488
|
+
const nestedEffectiveCwd = wrapperCwd === undefined ? options.effectiveCwd : wrapperCwd;
|
|
3519
4489
|
const allowTmpdirVar = !isTmpdirOverriddenToNonTemp(envAssignments);
|
|
3520
4490
|
if (SHELL_WRAPPERS.has(normalizedHead)) {
|
|
3521
4491
|
const dashCArg = extractDashCArg(stripped);
|
|
3522
4492
|
if (dashCArg) {
|
|
3523
|
-
return options.analyzeNested(dashCArg
|
|
4493
|
+
return options.analyzeNested(dashCArg, {
|
|
4494
|
+
effectiveCwd: nestedEffectiveCwd,
|
|
4495
|
+
envAssignments
|
|
4496
|
+
});
|
|
3524
4497
|
}
|
|
3525
4498
|
}
|
|
3526
4499
|
if (INTERPRETERS.has(normalizedHead)) {
|
|
@@ -3529,7 +4502,10 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3529
4502
|
if (options.paranoidInterpreters) {
|
|
3530
4503
|
return REASON_INTERPRETER_BLOCKED + PARANOID_INTERPRETERS_SUFFIX;
|
|
3531
4504
|
}
|
|
3532
|
-
const innerReason = options.analyzeNested(codeArg
|
|
4505
|
+
const innerReason = options.analyzeNested(codeArg, {
|
|
4506
|
+
effectiveCwd: nestedEffectiveCwd,
|
|
4507
|
+
envAssignments
|
|
4508
|
+
});
|
|
3533
4509
|
if (innerReason) {
|
|
3534
4510
|
return innerReason;
|
|
3535
4511
|
}
|
|
@@ -3539,7 +4515,11 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3539
4515
|
}
|
|
3540
4516
|
}
|
|
3541
4517
|
if (normalizedHead === "busybox" && stripped.length > 1) {
|
|
3542
|
-
return analyzeSegment(stripped.slice(1), depth,
|
|
4518
|
+
return analyzeSegment(stripped.slice(1), depth, {
|
|
4519
|
+
...options,
|
|
4520
|
+
effectiveCwd: nestedEffectiveCwd,
|
|
4521
|
+
envAssignments
|
|
4522
|
+
});
|
|
3543
4523
|
}
|
|
3544
4524
|
const isGit = basename.toLowerCase() === "git";
|
|
3545
4525
|
const isRm = basename === "rm";
|
|
@@ -3547,7 +4527,11 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3547
4527
|
const isXargs = basename === "xargs";
|
|
3548
4528
|
const isParallel = basename === "parallel";
|
|
3549
4529
|
if (isGit) {
|
|
3550
|
-
const gitResult = analyzeGit(stripped
|
|
4530
|
+
const gitResult = analyzeGit(stripped, {
|
|
4531
|
+
cwd: cwdForRm,
|
|
4532
|
+
envAssignments,
|
|
4533
|
+
worktreeMode: options.worktreeMode
|
|
4534
|
+
});
|
|
3551
4535
|
if (gitResult) {
|
|
3552
4536
|
return gitResult;
|
|
3553
4537
|
}
|
|
@@ -3574,7 +4558,9 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3574
4558
|
cwd: cwdForRm,
|
|
3575
4559
|
originalCwd,
|
|
3576
4560
|
paranoidRm: options.paranoidRm,
|
|
3577
|
-
allowTmpdirVar
|
|
4561
|
+
allowTmpdirVar,
|
|
4562
|
+
envAssignments,
|
|
4563
|
+
worktreeMode: options.worktreeMode
|
|
3578
4564
|
});
|
|
3579
4565
|
if (xargsResult) {
|
|
3580
4566
|
return xargsResult;
|
|
@@ -3586,6 +4572,8 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3586
4572
|
originalCwd,
|
|
3587
4573
|
paranoidRm: options.paranoidRm,
|
|
3588
4574
|
allowTmpdirVar,
|
|
4575
|
+
envAssignments,
|
|
4576
|
+
worktreeMode: options.worktreeMode,
|
|
3589
4577
|
analyzeNested: options.analyzeNested
|
|
3590
4578
|
});
|
|
3591
4579
|
if (parallelResult) {
|
|
@@ -3614,7 +4602,11 @@ function analyzeSegment(tokens, depth, options) {
|
|
|
3614
4602
|
}
|
|
3615
4603
|
if (cmd === "git") {
|
|
3616
4604
|
const gitTokens = ["git", ...stripped.slice(i + 1)];
|
|
3617
|
-
const reason = analyzeGit(gitTokens
|
|
4605
|
+
const reason = analyzeGit(gitTokens, {
|
|
4606
|
+
cwd: cwdForRm,
|
|
4607
|
+
envAssignments,
|
|
4608
|
+
worktreeMode: false
|
|
4609
|
+
});
|
|
3618
4610
|
if (reason) {
|
|
3619
4611
|
return reason;
|
|
3620
4612
|
}
|
|
@@ -3671,6 +4663,8 @@ function stripLeadingGrouping(tokens) {
|
|
|
3671
4663
|
// src/core/analyze/analyze-command.ts
|
|
3672
4664
|
var REASON_STRICT_UNPARSEABLE = "Command could not be safely analyzed (strict mode). Verify manually.";
|
|
3673
4665
|
var REASON_RECURSION_LIMIT = "Command exceeds maximum recursion depth and cannot be safely analyzed.";
|
|
4666
|
+
var GIT_CONTEXT_ENV_OVERRIDE_NAMES2 = new Set(GIT_CONTEXT_ENV_OVERRIDES);
|
|
4667
|
+
var GIT_CONTEXT_APPEND_ASSIGNMENT_RE = /^([A-Za-z_][A-Za-z0-9_]*)\+=/;
|
|
3674
4668
|
function analyzeCommandInternal(command, depth, options) {
|
|
3675
4669
|
if (depth >= MAX_RECURSION_DEPTH) {
|
|
3676
4670
|
return { reason: REASON_RECURSION_LIMIT, segment: command };
|
|
@@ -3681,8 +4675,10 @@ function analyzeCommandInternal(command, depth, options) {
|
|
|
3681
4675
|
}
|
|
3682
4676
|
const originalCwd = options.cwd;
|
|
3683
4677
|
let effectiveCwd = options.effectiveCwd !== undefined ? options.effectiveCwd : options.cwd;
|
|
4678
|
+
const shellGitContextState = createShellGitContextEnvState(options.envAssignments);
|
|
3684
4679
|
for (const segment of segments) {
|
|
3685
4680
|
const segmentStr = segment.join(" ");
|
|
4681
|
+
const segmentEnvAssignments = getSegmentGitContextEnvAssignments(segment, shellGitContextState);
|
|
3686
4682
|
if (segment.length === 1 && segment[0]?.includes(" ")) {
|
|
3687
4683
|
const textReason = dangerousInText(segment[0]);
|
|
3688
4684
|
if (textReason) {
|
|
@@ -3697,8 +4693,15 @@ function analyzeCommandInternal(command, depth, options) {
|
|
|
3697
4693
|
...options,
|
|
3698
4694
|
cwd: originalCwd,
|
|
3699
4695
|
effectiveCwd,
|
|
3700
|
-
|
|
3701
|
-
|
|
4696
|
+
envAssignments: segmentEnvAssignments,
|
|
4697
|
+
analyzeNested: (nestedCommand, overrides) => {
|
|
4698
|
+
const nestedEffectiveCwd = overrides && Object.hasOwn(overrides, "effectiveCwd") ? overrides.effectiveCwd : effectiveCwd;
|
|
4699
|
+
return analyzeCommandInternal(nestedCommand, depth + 1, {
|
|
4700
|
+
...options,
|
|
4701
|
+
effectiveCwd: nestedEffectiveCwd,
|
|
4702
|
+
envAssignments: overrides?.envAssignments ?? segmentEnvAssignments,
|
|
4703
|
+
worktreeMode: overrides?.worktreeMode ?? options.worktreeMode
|
|
4704
|
+
})?.reason ?? null;
|
|
3702
4705
|
}
|
|
3703
4706
|
});
|
|
3704
4707
|
if (reason) {
|
|
@@ -3707,9 +4710,338 @@ function analyzeCommandInternal(command, depth, options) {
|
|
|
3707
4710
|
if (segmentChangesCwd(segment)) {
|
|
3708
4711
|
effectiveCwd = null;
|
|
3709
4712
|
}
|
|
4713
|
+
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
3710
4714
|
}
|
|
3711
4715
|
return null;
|
|
3712
4716
|
}
|
|
4717
|
+
function createShellGitContextEnvState(effectiveEnvAssignments) {
|
|
4718
|
+
return {
|
|
4719
|
+
effectiveEnvAssignments,
|
|
4720
|
+
shellAssignments: new Map,
|
|
4721
|
+
exportedNames: getInitiallyExportedGitContextNames(effectiveEnvAssignments),
|
|
4722
|
+
allexport: false,
|
|
4723
|
+
keywordExport: false
|
|
4724
|
+
};
|
|
4725
|
+
}
|
|
4726
|
+
function applyShellGitContextEnvSegment(tokens, state) {
|
|
4727
|
+
const commandInfo = getShellCommandInfo(tokens);
|
|
4728
|
+
if (!commandInfo) {
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
const { command, commandIndex, leadingAssignments } = commandInfo;
|
|
4732
|
+
if (command === null) {
|
|
4733
|
+
for (const assignment of leadingAssignments.values()) {
|
|
4734
|
+
setShellGitContextAssignment(state, assignment);
|
|
4735
|
+
}
|
|
4736
|
+
return;
|
|
4737
|
+
}
|
|
4738
|
+
if (command === "set") {
|
|
4739
|
+
const changes = getSetOptionChanges(tokens, commandIndex);
|
|
4740
|
+
if (changes.allexport !== null) {
|
|
4741
|
+
state.allexport = changes.allexport;
|
|
4742
|
+
}
|
|
4743
|
+
if (changes.keywordExport !== null) {
|
|
4744
|
+
state.keywordExport = changes.keywordExport;
|
|
4745
|
+
}
|
|
4746
|
+
return;
|
|
4747
|
+
}
|
|
4748
|
+
if (command !== "export" && command !== "typeset" && command !== "declare" && command !== "readonly") {
|
|
4749
|
+
return;
|
|
4750
|
+
}
|
|
4751
|
+
for (const assignment of leadingAssignments.values()) {
|
|
4752
|
+
setShellGitContextAssignment(state, assignment);
|
|
4753
|
+
}
|
|
4754
|
+
if (command === "export") {
|
|
4755
|
+
const operandsStart = getExportOperandsStart(tokens, commandIndex);
|
|
4756
|
+
if (operandsStart === null) {
|
|
4757
|
+
return;
|
|
4758
|
+
}
|
|
4759
|
+
for (const token of tokens.slice(operandsStart)) {
|
|
4760
|
+
addExportedGitContextEnvAssignment(state, token);
|
|
4761
|
+
}
|
|
4762
|
+
return;
|
|
4763
|
+
}
|
|
4764
|
+
const operandsInfo = getTypesetOperandsInfo(tokens, commandIndex);
|
|
4765
|
+
if (operandsInfo === null) {
|
|
4766
|
+
return;
|
|
4767
|
+
}
|
|
4768
|
+
for (const token of tokens.slice(operandsInfo.operandsStart)) {
|
|
4769
|
+
addTypesetGitContextEnvAssignment(state, token, operandsInfo.exports, command === "readonly" ? leadingAssignments : undefined);
|
|
4770
|
+
}
|
|
4771
|
+
}
|
|
4772
|
+
function getSegmentGitContextEnvAssignments(tokens, state) {
|
|
4773
|
+
if (!state.keywordExport) {
|
|
4774
|
+
return state.effectiveEnvAssignments;
|
|
4775
|
+
}
|
|
4776
|
+
let nextEnvAssignments = null;
|
|
4777
|
+
for (const token of tokens) {
|
|
4778
|
+
const assignment = parseGitContextEnvAssignment(token);
|
|
4779
|
+
if (!assignment) {
|
|
4780
|
+
continue;
|
|
4781
|
+
}
|
|
4782
|
+
nextEnvAssignments ??= new Map(state.effectiveEnvAssignments ?? []);
|
|
4783
|
+
nextEnvAssignments.set(assignment.name, assignment.value);
|
|
4784
|
+
}
|
|
4785
|
+
return nextEnvAssignments ?? state.effectiveEnvAssignments;
|
|
4786
|
+
}
|
|
4787
|
+
function getShellCommandInfo(tokens) {
|
|
4788
|
+
const leadingAssignments = new Map;
|
|
4789
|
+
let i = 0;
|
|
4790
|
+
while (i < tokens.length) {
|
|
4791
|
+
const token = tokens[i];
|
|
4792
|
+
if (!token) {
|
|
4793
|
+
return null;
|
|
4794
|
+
}
|
|
4795
|
+
const assignment = parseShellAssignment(token);
|
|
4796
|
+
if (!assignment) {
|
|
4797
|
+
break;
|
|
4798
|
+
}
|
|
4799
|
+
if (isTrackedGitEnvName2(assignment.name)) {
|
|
4800
|
+
leadingAssignments.set(assignment.name, assignment);
|
|
4801
|
+
}
|
|
4802
|
+
i++;
|
|
4803
|
+
}
|
|
4804
|
+
if (i >= tokens.length) {
|
|
4805
|
+
return { command: null, commandIndex: i, leadingAssignments };
|
|
4806
|
+
}
|
|
4807
|
+
let commandIndex = i;
|
|
4808
|
+
let command = tokens[commandIndex] ?? null;
|
|
4809
|
+
if (command === "builtin") {
|
|
4810
|
+
commandIndex++;
|
|
4811
|
+
if (tokens[commandIndex] === "--") {
|
|
4812
|
+
commandIndex++;
|
|
4813
|
+
}
|
|
4814
|
+
command = tokens[commandIndex] ?? null;
|
|
4815
|
+
}
|
|
4816
|
+
if (command === "command") {
|
|
4817
|
+
const commandBuiltinInfo = getCommandBuiltinTarget(tokens, commandIndex);
|
|
4818
|
+
if (!commandBuiltinInfo) {
|
|
4819
|
+
return null;
|
|
4820
|
+
}
|
|
4821
|
+
commandIndex = commandBuiltinInfo.commandIndex;
|
|
4822
|
+
command = commandBuiltinInfo.command;
|
|
4823
|
+
}
|
|
4824
|
+
if (command === null) {
|
|
4825
|
+
return null;
|
|
4826
|
+
}
|
|
4827
|
+
return { command, commandIndex, leadingAssignments };
|
|
4828
|
+
}
|
|
4829
|
+
function getCommandBuiltinTarget(tokens, commandIndex) {
|
|
4830
|
+
let i = commandIndex + 1;
|
|
4831
|
+
while (i < tokens.length) {
|
|
4832
|
+
const token = tokens[i];
|
|
4833
|
+
if (!token) {
|
|
4834
|
+
return null;
|
|
4835
|
+
}
|
|
4836
|
+
if (token === "--") {
|
|
4837
|
+
i++;
|
|
4838
|
+
break;
|
|
4839
|
+
}
|
|
4840
|
+
if (token === "-p") {
|
|
4841
|
+
i++;
|
|
4842
|
+
continue;
|
|
4843
|
+
}
|
|
4844
|
+
if (token === "-v" || token === "-V") {
|
|
4845
|
+
return null;
|
|
4846
|
+
}
|
|
4847
|
+
break;
|
|
4848
|
+
}
|
|
4849
|
+
const command = tokens[i];
|
|
4850
|
+
return command ? { command, commandIndex: i } : null;
|
|
4851
|
+
}
|
|
4852
|
+
function parseShellAssignment(token) {
|
|
4853
|
+
return parseEnvAssignment(token) ?? parseGitContextAppendEnvAssignment2(token);
|
|
4854
|
+
}
|
|
4855
|
+
function parseGitContextEnvAssignment(token) {
|
|
4856
|
+
const assignment = parseEnvAssignment(token) ?? parseGitContextAppendEnvAssignment2(token);
|
|
4857
|
+
if (!assignment || !isTrackedGitEnvName2(assignment.name)) {
|
|
4858
|
+
return null;
|
|
4859
|
+
}
|
|
4860
|
+
return assignment;
|
|
4861
|
+
}
|
|
4862
|
+
function parseGitContextAppendEnvAssignment2(token) {
|
|
4863
|
+
const match = token.match(GIT_CONTEXT_APPEND_ASSIGNMENT_RE);
|
|
4864
|
+
const name = match?.[1];
|
|
4865
|
+
if (!name || !isTrackedGitEnvName2(name)) {
|
|
4866
|
+
return null;
|
|
4867
|
+
}
|
|
4868
|
+
const eqIdx = token.indexOf("=");
|
|
4869
|
+
return { name, value: token.slice(eqIdx + 1) };
|
|
4870
|
+
}
|
|
4871
|
+
function isTrackedGitEnvName2(name) {
|
|
4872
|
+
return GIT_CONTEXT_ENV_OVERRIDE_NAMES2.has(name) || GIT_CONFIG_AFFECTING_ENV_NAMES.has(name) || isGitConfigEnvName2(name);
|
|
4873
|
+
}
|
|
4874
|
+
function isGitConfigEnvName2(name) {
|
|
4875
|
+
return name === "GIT_CONFIG_COUNT" || name === "GIT_CONFIG_PARAMETERS" || /^GIT_CONFIG_(KEY|VALUE)_\d+$/.test(name);
|
|
4876
|
+
}
|
|
4877
|
+
function getInitiallyExportedGitContextNames(effectiveEnvAssignments) {
|
|
4878
|
+
const exportedNames = new Set;
|
|
4879
|
+
for (const name of Object.keys(process.env)) {
|
|
4880
|
+
if (isTrackedGitEnvName2(name)) {
|
|
4881
|
+
exportedNames.add(name);
|
|
4882
|
+
}
|
|
4883
|
+
}
|
|
4884
|
+
for (const name of effectiveEnvAssignments?.keys() ?? []) {
|
|
4885
|
+
if (isTrackedGitEnvName2(name)) {
|
|
4886
|
+
exportedNames.add(name);
|
|
4887
|
+
}
|
|
4888
|
+
}
|
|
4889
|
+
return exportedNames;
|
|
4890
|
+
}
|
|
4891
|
+
function setShellGitContextAssignment(state, assignment) {
|
|
4892
|
+
state.shellAssignments.set(assignment.name, assignment.value);
|
|
4893
|
+
if (state.allexport || state.exportedNames.has(assignment.name)) {
|
|
4894
|
+
setEffectiveGitContextAssignment(state, assignment);
|
|
4895
|
+
}
|
|
4896
|
+
}
|
|
4897
|
+
function setEffectiveGitContextAssignment(state, assignment) {
|
|
4898
|
+
const nextEnvAssignments = new Map(state.effectiveEnvAssignments ?? []);
|
|
4899
|
+
nextEnvAssignments.set(assignment.name, assignment.value);
|
|
4900
|
+
state.effectiveEnvAssignments = nextEnvAssignments;
|
|
4901
|
+
}
|
|
4902
|
+
function addExportedGitContextEnvAssignment(state, token) {
|
|
4903
|
+
const assignment = parseGitContextEnvAssignment(token);
|
|
4904
|
+
if (assignment) {
|
|
4905
|
+
state.shellAssignments.set(assignment.name, assignment.value);
|
|
4906
|
+
state.exportedNames.add(assignment.name);
|
|
4907
|
+
setEffectiveGitContextAssignment(state, assignment);
|
|
4908
|
+
return;
|
|
4909
|
+
}
|
|
4910
|
+
if (isTrackedGitEnvName2(token)) {
|
|
4911
|
+
state.exportedNames.add(token);
|
|
4912
|
+
const value = state.shellAssignments.get(token);
|
|
4913
|
+
if (value !== undefined) {
|
|
4914
|
+
setEffectiveGitContextAssignment(state, { name: token, value });
|
|
4915
|
+
} else {
|
|
4916
|
+
setEffectiveGitContextAssignment(state, { name: token, value: "" });
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
}
|
|
4920
|
+
function addTypesetGitContextEnvAssignment(state, token, exports, readonlyLeadingAssignments) {
|
|
4921
|
+
const assignment = parseGitContextEnvAssignment(token);
|
|
4922
|
+
if (assignment) {
|
|
4923
|
+
state.shellAssignments.set(assignment.name, assignment.value);
|
|
4924
|
+
if (exports) {
|
|
4925
|
+
state.exportedNames.add(assignment.name);
|
|
4926
|
+
setEffectiveGitContextAssignment(state, assignment);
|
|
4927
|
+
} else if (state.allexport || state.exportedNames.has(assignment.name)) {
|
|
4928
|
+
setEffectiveGitContextAssignment(state, assignment);
|
|
4929
|
+
}
|
|
4930
|
+
return;
|
|
4931
|
+
}
|
|
4932
|
+
const readonlyAssignment = readonlyLeadingAssignments?.get(token);
|
|
4933
|
+
if (readonlyAssignment) {
|
|
4934
|
+
state.exportedNames.add(token);
|
|
4935
|
+
setEffectiveGitContextAssignment(state, readonlyAssignment);
|
|
4936
|
+
return;
|
|
4937
|
+
}
|
|
4938
|
+
if (exports && isTrackedGitEnvName2(token)) {
|
|
4939
|
+
state.exportedNames.add(token);
|
|
4940
|
+
const value = state.shellAssignments.get(token);
|
|
4941
|
+
if (value !== undefined) {
|
|
4942
|
+
setEffectiveGitContextAssignment(state, { name: token, value });
|
|
4943
|
+
} else {
|
|
4944
|
+
setEffectiveGitContextAssignment(state, { name: token, value: "" });
|
|
4945
|
+
}
|
|
4946
|
+
}
|
|
4947
|
+
}
|
|
4948
|
+
function getExportOperandsStart(tokens, commandIndex) {
|
|
4949
|
+
let i = commandIndex + 1;
|
|
4950
|
+
while (i < tokens.length) {
|
|
4951
|
+
const token = tokens[i];
|
|
4952
|
+
if (!token) {
|
|
4953
|
+
return null;
|
|
4954
|
+
}
|
|
4955
|
+
if (token === "--") {
|
|
4956
|
+
return i + 1;
|
|
4957
|
+
}
|
|
4958
|
+
if (token === "-p") {
|
|
4959
|
+
i++;
|
|
4960
|
+
continue;
|
|
4961
|
+
}
|
|
4962
|
+
if (token.startsWith("-")) {
|
|
4963
|
+
return null;
|
|
4964
|
+
}
|
|
4965
|
+
return i;
|
|
4966
|
+
}
|
|
4967
|
+
return i;
|
|
4968
|
+
}
|
|
4969
|
+
function getTypesetOperandsInfo(tokens, commandIndex) {
|
|
4970
|
+
let i = commandIndex + 1;
|
|
4971
|
+
let hasExportFlag = false;
|
|
4972
|
+
while (i < tokens.length) {
|
|
4973
|
+
const token = tokens[i];
|
|
4974
|
+
if (!token) {
|
|
4975
|
+
return null;
|
|
4976
|
+
}
|
|
4977
|
+
if (token === "--") {
|
|
4978
|
+
return { operandsStart: i + 1, exports: hasExportFlag };
|
|
4979
|
+
}
|
|
4980
|
+
if (token.startsWith("-")) {
|
|
4981
|
+
if (token.slice(1).includes("x")) {
|
|
4982
|
+
hasExportFlag = true;
|
|
4983
|
+
}
|
|
4984
|
+
i++;
|
|
4985
|
+
continue;
|
|
4986
|
+
}
|
|
4987
|
+
if (token.startsWith("+")) {
|
|
4988
|
+
if (token.slice(1).includes("x")) {
|
|
4989
|
+
hasExportFlag = false;
|
|
4990
|
+
}
|
|
4991
|
+
i++;
|
|
4992
|
+
continue;
|
|
4993
|
+
}
|
|
4994
|
+
return { operandsStart: i, exports: hasExportFlag };
|
|
4995
|
+
}
|
|
4996
|
+
return { operandsStart: i, exports: hasExportFlag };
|
|
4997
|
+
}
|
|
4998
|
+
function getSetOptionChanges(tokens, commandIndex) {
|
|
4999
|
+
const changes = { allexport: null, keywordExport: null };
|
|
5000
|
+
let i = commandIndex + 1;
|
|
5001
|
+
while (i < tokens.length) {
|
|
5002
|
+
const token = tokens[i];
|
|
5003
|
+
if (!token) {
|
|
5004
|
+
return changes;
|
|
5005
|
+
}
|
|
5006
|
+
if (token === "--") {
|
|
5007
|
+
return changes;
|
|
5008
|
+
}
|
|
5009
|
+
if (token === "-o" || token === "+o") {
|
|
5010
|
+
if (tokens[i + 1] === "allexport") {
|
|
5011
|
+
changes.allexport = token === "-o";
|
|
5012
|
+
}
|
|
5013
|
+
if (tokens[i + 1] === "keyword") {
|
|
5014
|
+
changes.keywordExport = token === "-o";
|
|
5015
|
+
}
|
|
5016
|
+
i += 2;
|
|
5017
|
+
continue;
|
|
5018
|
+
}
|
|
5019
|
+
if (token.startsWith("-") && token.length > 1) {
|
|
5020
|
+
const flags = token.slice(1);
|
|
5021
|
+
if (flags.includes("a")) {
|
|
5022
|
+
changes.allexport = true;
|
|
5023
|
+
}
|
|
5024
|
+
if (flags.includes("k")) {
|
|
5025
|
+
changes.keywordExport = true;
|
|
5026
|
+
}
|
|
5027
|
+
i++;
|
|
5028
|
+
continue;
|
|
5029
|
+
}
|
|
5030
|
+
if (token.startsWith("+") && token.length > 1) {
|
|
5031
|
+
const flags = token.slice(1);
|
|
5032
|
+
if (flags.includes("a")) {
|
|
5033
|
+
changes.allexport = false;
|
|
5034
|
+
}
|
|
5035
|
+
if (flags.includes("k")) {
|
|
5036
|
+
changes.keywordExport = false;
|
|
5037
|
+
}
|
|
5038
|
+
i++;
|
|
5039
|
+
continue;
|
|
5040
|
+
}
|
|
5041
|
+
return changes;
|
|
5042
|
+
}
|
|
5043
|
+
return changes;
|
|
5044
|
+
}
|
|
3713
5045
|
|
|
3714
5046
|
// src/core/analyze.ts
|
|
3715
5047
|
function analyzeCommand(command, options = {}) {
|
|
@@ -3719,6 +5051,12 @@ function analyzeCommand(command, options = {}) {
|
|
|
3719
5051
|
|
|
3720
5052
|
// src/bin/doctor/hooks.ts
|
|
3721
5053
|
var COPILOT_PLUGIN_CONFIG_PATH = "copilot-plugin";
|
|
5054
|
+
var CLAUDE_PLUGIN_LIST_CONFIG_PATH = "claude plugin list";
|
|
5055
|
+
var CLAUDE_SAFETY_NET_PLUGIN_ID = "safety-net@cc-marketplace";
|
|
5056
|
+
var GEMINI_EXTENSIONS_LIST_CONFIG_PATH = "gemini extensions list";
|
|
5057
|
+
var GEMINI_SAFETY_NET_SOURCE = "https://github.com/kenryu42/gemini-safety-net";
|
|
5058
|
+
var CODEX_PLUGIN_HOOKS_WARNING = "Codex plugin hooks are behind a feature flag. Add `plugin_hooks = true` under [features] in $CODEX_HOME/config.toml.";
|
|
5059
|
+
var CODEX_SAFETY_NET_PLUGIN_ID = "safety-net@cc-marketplace";
|
|
3722
5060
|
var SELF_TEST_CASES = [
|
|
3723
5061
|
{ command: "git reset --hard", description: "git reset --hard", expectBlocked: true },
|
|
3724
5062
|
{ command: "rm -rf /", description: "rm -rf /", expectBlocked: true },
|
|
@@ -3726,7 +5064,7 @@ var SELF_TEST_CASES = [
|
|
|
3726
5064
|
];
|
|
3727
5065
|
var SELF_TEST_CONFIG = { version: 1, rules: [] };
|
|
3728
5066
|
function runSelfTest() {
|
|
3729
|
-
const selfTestCwd =
|
|
5067
|
+
const selfTestCwd = join5(tmpdir3(), "cc-safety-net-self-test");
|
|
3730
5068
|
const results = SELF_TEST_CASES.map((tc) => {
|
|
3731
5069
|
const result = analyzeCommand(tc.command, {
|
|
3732
5070
|
cwd: selfTestCwd,
|
|
@@ -3834,50 +5172,63 @@ function stripJsonComments(content) {
|
|
|
3834
5172
|
}
|
|
3835
5173
|
return result;
|
|
3836
5174
|
}
|
|
3837
|
-
function detectClaudeCode(
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
}
|
|
3862
|
-
} catch (e) {
|
|
3863
|
-
errors.push(`Failed to parse settings.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
3864
|
-
}
|
|
5175
|
+
function detectClaudeCode(pluginListOutput) {
|
|
5176
|
+
if (!pluginListOutput) {
|
|
5177
|
+
return { platform: "claude-code", status: "n/a" };
|
|
5178
|
+
}
|
|
5179
|
+
const pluginBlock = _findClaudeSafetyNetPluginBlock(pluginListOutput);
|
|
5180
|
+
if (!pluginBlock) {
|
|
5181
|
+
return { platform: "claude-code", status: "n/a" };
|
|
5182
|
+
}
|
|
5183
|
+
if (/^\s*Status:\s*.*\bdisabled\b\s*$/im.test(pluginBlock)) {
|
|
5184
|
+
return {
|
|
5185
|
+
platform: "claude-code",
|
|
5186
|
+
status: "disabled",
|
|
5187
|
+
method: "plugin list",
|
|
5188
|
+
configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH
|
|
5189
|
+
};
|
|
5190
|
+
}
|
|
5191
|
+
if (/^\s*Status:\s*.*\benabled\b\s*$/im.test(pluginBlock)) {
|
|
5192
|
+
return {
|
|
5193
|
+
platform: "claude-code",
|
|
5194
|
+
status: "configured",
|
|
5195
|
+
method: "plugin list",
|
|
5196
|
+
configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH,
|
|
5197
|
+
selfTest: runSelfTest()
|
|
5198
|
+
};
|
|
3865
5199
|
}
|
|
3866
5200
|
return {
|
|
3867
5201
|
platform: "claude-code",
|
|
3868
|
-
status: "
|
|
3869
|
-
|
|
5202
|
+
status: "disabled",
|
|
5203
|
+
method: "plugin list",
|
|
5204
|
+
configPath: CLAUDE_PLUGIN_LIST_CONFIG_PATH,
|
|
5205
|
+
errors: ["Status is not enabled"]
|
|
3870
5206
|
};
|
|
3871
5207
|
}
|
|
5208
|
+
function _findClaudeSafetyNetPluginBlock(output) {
|
|
5209
|
+
const pluginLinePattern = new RegExp(`^\\s*(?:[^\\w\\s@]+\\s+)?${_escapeRegExp(CLAUDE_SAFETY_NET_PLUGIN_ID)}\\s*$`);
|
|
5210
|
+
const pluginStartPattern = /^\s*(?:[^\w\s@]+\s+)?\S+@\S+\s*$/;
|
|
5211
|
+
const lines = output.split(`
|
|
5212
|
+
`);
|
|
5213
|
+
const startIndex = lines.findIndex((line) => pluginLinePattern.test(line));
|
|
5214
|
+
if (startIndex === -1)
|
|
5215
|
+
return;
|
|
5216
|
+
const endIndex = lines.findIndex((line, index) => index > startIndex && pluginStartPattern.test(line));
|
|
5217
|
+
return lines.slice(startIndex, endIndex === -1 ? undefined : endIndex).join(`
|
|
5218
|
+
`);
|
|
5219
|
+
}
|
|
5220
|
+
function _escapeRegExp(value) {
|
|
5221
|
+
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
5222
|
+
}
|
|
3872
5223
|
function detectOpenCode(homeDir) {
|
|
3873
5224
|
const errors = [];
|
|
3874
|
-
const configDir =
|
|
5225
|
+
const configDir = join5(homeDir, ".config", "opencode");
|
|
3875
5226
|
const candidates = ["opencode.json", "opencode.jsonc"];
|
|
3876
5227
|
for (const filename of candidates) {
|
|
3877
|
-
const configPath =
|
|
3878
|
-
if (
|
|
5228
|
+
const configPath = join5(configDir, filename);
|
|
5229
|
+
if (existsSync6(configPath)) {
|
|
3879
5230
|
try {
|
|
3880
|
-
const content =
|
|
5231
|
+
const content = readFileSync6(configPath, "utf-8");
|
|
3881
5232
|
const json = stripJsonComments(content);
|
|
3882
5233
|
const config = JSON.parse(json);
|
|
3883
5234
|
const plugins = config.plugin ?? [];
|
|
@@ -3903,79 +5254,143 @@ function detectOpenCode(homeDir) {
|
|
|
3903
5254
|
errors: errors.length > 0 ? errors : undefined
|
|
3904
5255
|
};
|
|
3905
5256
|
}
|
|
3906
|
-
function
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
5257
|
+
function detectGeminiCLI(extensionsListOutput) {
|
|
5258
|
+
if (!extensionsListOutput) {
|
|
5259
|
+
return { platform: "gemini-cli", status: "n/a" };
|
|
5260
|
+
}
|
|
5261
|
+
const extension = _parseGeminiExtensionsList(extensionsListOutput).find((item) => item.source?.includes(GEMINI_SAFETY_NET_SOURCE));
|
|
5262
|
+
if (!extension) {
|
|
5263
|
+
return { platform: "gemini-cli", status: "n/a" };
|
|
5264
|
+
}
|
|
5265
|
+
const effectiveEnabled = extension.enabledWorkspace ?? extension.enabledUser ?? true;
|
|
5266
|
+
const errors = effectiveEnabled ? [] : [
|
|
5267
|
+
extension.enabledWorkspace === false ? "Enabled (Workspace) is false" : "Enabled (User) is false"
|
|
3910
5268
|
];
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
5269
|
+
if (errors.length > 0) {
|
|
5270
|
+
return {
|
|
5271
|
+
platform: "gemini-cli",
|
|
5272
|
+
status: "disabled",
|
|
5273
|
+
method: "extension list",
|
|
5274
|
+
configPath: GEMINI_EXTENSIONS_LIST_CONFIG_PATH,
|
|
5275
|
+
errors
|
|
5276
|
+
};
|
|
5277
|
+
}
|
|
5278
|
+
return {
|
|
5279
|
+
platform: "gemini-cli",
|
|
5280
|
+
status: "configured",
|
|
5281
|
+
method: "extension list",
|
|
5282
|
+
configPath: GEMINI_EXTENSIONS_LIST_CONFIG_PATH,
|
|
5283
|
+
selfTest: runSelfTest()
|
|
5284
|
+
};
|
|
5285
|
+
}
|
|
5286
|
+
function _parseGeminiExtensionsList(output) {
|
|
5287
|
+
const blocks = output.split(`
|
|
5288
|
+
`).reduce((result, line) => {
|
|
5289
|
+
if (/^\S/.test(line) || result.length === 0) {
|
|
5290
|
+
result.push(line);
|
|
5291
|
+
return result;
|
|
3921
5292
|
}
|
|
5293
|
+
const index = result.length - 1;
|
|
5294
|
+
result[index] = `${result[index]}
|
|
5295
|
+
${line}`;
|
|
5296
|
+
return result;
|
|
5297
|
+
}, []);
|
|
5298
|
+
return blocks.map((block) => ({
|
|
5299
|
+
source: /^\s*Source:\s*(.+)$/m.exec(block)?.[1],
|
|
5300
|
+
enabledUser: _parseGeminiEnabledValue(block, "User"),
|
|
5301
|
+
enabledWorkspace: _parseGeminiEnabledValue(block, "Workspace")
|
|
5302
|
+
}));
|
|
5303
|
+
}
|
|
5304
|
+
function _parseGeminiEnabledValue(block, scope) {
|
|
5305
|
+
const match = new RegExp(`^\\s*Enabled \\(${scope}\\):\\s*(true|false)\\s*$`, "im").exec(block);
|
|
5306
|
+
if (!match)
|
|
5307
|
+
return;
|
|
5308
|
+
return match[1] === "true";
|
|
5309
|
+
}
|
|
5310
|
+
function _getCodexHome(homeDir) {
|
|
5311
|
+
return process.env.CODEX_HOME || join5(homeDir, ".codex");
|
|
5312
|
+
}
|
|
5313
|
+
function _parseCodexConfig(content) {
|
|
5314
|
+
const result = {};
|
|
5315
|
+
content.split(`
|
|
5316
|
+
`).reduce((activeSection, line) => {
|
|
5317
|
+
const trimmed = line.trim();
|
|
5318
|
+
if (trimmed === "" || trimmed.startsWith("#"))
|
|
5319
|
+
return activeSection;
|
|
5320
|
+
const sectionMatch = /^\[([^\]]+)]\s*(?:#.*)?$/.exec(trimmed);
|
|
5321
|
+
if (sectionMatch)
|
|
5322
|
+
return sectionMatch[1];
|
|
5323
|
+
if (activeSection === "features") {
|
|
5324
|
+
const pluginHooksMatch = /^plugin_hooks\s*=\s*(true|false)\s*(?:#.*)?$/.exec(trimmed);
|
|
5325
|
+
if (pluginHooksMatch)
|
|
5326
|
+
result.pluginHooks = pluginHooksMatch[1] === "true";
|
|
5327
|
+
}
|
|
5328
|
+
if (activeSection === `plugins."${CODEX_SAFETY_NET_PLUGIN_ID}"`) {
|
|
5329
|
+
const enabledMatch = /^enabled\s*=\s*(true|false)\s*(?:#.*)?$/.exec(trimmed);
|
|
5330
|
+
if (enabledMatch)
|
|
5331
|
+
result.safetyNetEnabled = enabledMatch[1] === "true";
|
|
5332
|
+
}
|
|
5333
|
+
return activeSection;
|
|
5334
|
+
}, undefined);
|
|
5335
|
+
return result;
|
|
5336
|
+
}
|
|
5337
|
+
function _readCodexConfig(configPath, errors) {
|
|
5338
|
+
try {
|
|
5339
|
+
return _parseCodexConfig(readFileSync6(configPath, "utf-8"));
|
|
5340
|
+
} catch (e) {
|
|
5341
|
+
errors.push(`Failed to read ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
5342
|
+
return {};
|
|
3922
5343
|
}
|
|
3923
|
-
return { enabled: false };
|
|
3924
5344
|
}
|
|
3925
|
-
function
|
|
5345
|
+
function detectCodex(homeDir) {
|
|
5346
|
+
const codexHome = _getCodexHome(homeDir);
|
|
5347
|
+
const pluginCachePath = join5(codexHome, "plugins", "cache", "cc-marketplace", "safety-net");
|
|
3926
5348
|
const errors = [];
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
return { platform: "gemini-cli", status: "n/a" };
|
|
5349
|
+
if (!existsSync6(pluginCachePath)) {
|
|
5350
|
+
return { platform: "codex", status: "n/a", configPath: pluginCachePath };
|
|
3930
5351
|
}
|
|
3931
|
-
let isInstalled = false;
|
|
3932
|
-
let isEnabled = false;
|
|
3933
5352
|
try {
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
if (pluginConfig) {
|
|
3937
|
-
isInstalled = true;
|
|
3938
|
-
const overrides = pluginConfig.overrides ?? [];
|
|
3939
|
-
isEnabled = overrides.some((o) => !o.startsWith("!"));
|
|
5353
|
+
if (readdirSync2(pluginCachePath).length === 0) {
|
|
5354
|
+
return { platform: "codex", status: "n/a", configPath: pluginCachePath };
|
|
3940
5355
|
}
|
|
3941
5356
|
} catch (e) {
|
|
3942
|
-
errors.push(`Failed to parse extension-enablement.json: ${e instanceof Error ? e.message : String(e)}`);
|
|
3943
|
-
}
|
|
3944
|
-
if (!isInstalled) {
|
|
3945
5357
|
return {
|
|
3946
|
-
platform: "
|
|
5358
|
+
platform: "codex",
|
|
3947
5359
|
status: "n/a",
|
|
3948
|
-
|
|
5360
|
+
configPath: pluginCachePath,
|
|
5361
|
+
errors: [`Failed to read ${pluginCachePath}: ${e instanceof Error ? e.message : String(e)}`]
|
|
3949
5362
|
};
|
|
3950
5363
|
}
|
|
3951
|
-
|
|
3952
|
-
|
|
5364
|
+
const configPath = join5(codexHome, "config.toml");
|
|
5365
|
+
const config = _readCodexConfig(configPath, errors);
|
|
5366
|
+
if (config.safetyNetEnabled !== true) {
|
|
3953
5367
|
return {
|
|
3954
|
-
platform: "
|
|
5368
|
+
platform: "codex",
|
|
3955
5369
|
status: "disabled",
|
|
3956
|
-
method: "
|
|
3957
|
-
configPath
|
|
3958
|
-
errors
|
|
5370
|
+
method: "plugin cache",
|
|
5371
|
+
configPath,
|
|
5372
|
+
errors: [
|
|
5373
|
+
...errors,
|
|
5374
|
+
`Codex plugin ${CODEX_SAFETY_NET_PLUGIN_ID} is not enabled. Add enabled = true under [plugins."${CODEX_SAFETY_NET_PLUGIN_ID}"] in $CODEX_HOME/config.toml.`
|
|
5375
|
+
]
|
|
3959
5376
|
};
|
|
3960
5377
|
}
|
|
3961
|
-
|
|
3962
|
-
if (hooksCheck.enabled) {
|
|
5378
|
+
if (config.pluginHooks !== true) {
|
|
3963
5379
|
return {
|
|
3964
|
-
platform: "
|
|
3965
|
-
status: "
|
|
3966
|
-
method: "
|
|
3967
|
-
configPath
|
|
3968
|
-
|
|
3969
|
-
errors: errors.length > 0 ? errors : undefined
|
|
5380
|
+
platform: "codex",
|
|
5381
|
+
status: "disabled",
|
|
5382
|
+
method: "plugin cache",
|
|
5383
|
+
configPath,
|
|
5384
|
+
errors: [...errors, CODEX_PLUGIN_HOOKS_WARNING]
|
|
3970
5385
|
};
|
|
3971
5386
|
}
|
|
3972
|
-
errors.push("Hooks are not enabled (set tools.enableHooks: true in settings.json)");
|
|
3973
5387
|
return {
|
|
3974
|
-
platform: "
|
|
3975
|
-
status: "
|
|
3976
|
-
method: "
|
|
3977
|
-
configPath
|
|
3978
|
-
|
|
5388
|
+
platform: "codex",
|
|
5389
|
+
status: "configured",
|
|
5390
|
+
method: "plugin cache",
|
|
5391
|
+
configPath,
|
|
5392
|
+
selfTest: runSelfTest(),
|
|
5393
|
+
errors: errors.length > 0 ? errors : undefined
|
|
3979
5394
|
};
|
|
3980
5395
|
}
|
|
3981
5396
|
function _isSafetyNetCopilotCommand(command) {
|
|
@@ -4018,7 +5433,7 @@ function _supportsCopilotInlineHooks(version) {
|
|
|
4018
5433
|
return comparison >= 0;
|
|
4019
5434
|
}
|
|
4020
5435
|
function _getCopilotConfigHome(homeDir) {
|
|
4021
|
-
return process.env.COPILOT_HOME ||
|
|
5436
|
+
return process.env.COPILOT_HOME || join5(homeDir, ".copilot");
|
|
4022
5437
|
}
|
|
4023
5438
|
function _hasSafetyNetCopilotHook(config) {
|
|
4024
5439
|
const preToolUseHooks = config.hooks?.preToolUse ?? [];
|
|
@@ -4030,7 +5445,7 @@ function _hasSafetyNetCopilotHook(config) {
|
|
|
4030
5445
|
}
|
|
4031
5446
|
function _readCopilotConfigFile(configPath, errors) {
|
|
4032
5447
|
try {
|
|
4033
|
-
return JSON.parse(
|
|
5448
|
+
return JSON.parse(stripJsonComments(readFileSync6(configPath, "utf-8")));
|
|
4034
5449
|
} catch (e) {
|
|
4035
5450
|
errors?.push(`Failed to parse ${configPath}: ${e instanceof Error ? e.message : String(e)}`);
|
|
4036
5451
|
return;
|
|
@@ -4045,11 +5460,11 @@ function _listJsonFiles(dirPath, errors) {
|
|
|
4045
5460
|
}
|
|
4046
5461
|
}
|
|
4047
5462
|
function _collectSafetyNetCopilotHookFiles(dirPath, errors) {
|
|
4048
|
-
if (!
|
|
5463
|
+
if (!existsSync6(dirPath))
|
|
4049
5464
|
return [];
|
|
4050
5465
|
const matches = [];
|
|
4051
5466
|
for (const filename of _listJsonFiles(dirPath, errors)) {
|
|
4052
|
-
const configPath =
|
|
5467
|
+
const configPath = join5(dirPath, filename);
|
|
4053
5468
|
const config = _readCopilotConfigFile(configPath, errors);
|
|
4054
5469
|
if (config && _hasSafetyNetCopilotHook(config)) {
|
|
4055
5470
|
matches.push(configPath);
|
|
@@ -4058,7 +5473,7 @@ function _collectSafetyNetCopilotHookFiles(dirPath, errors) {
|
|
|
4058
5473
|
return matches;
|
|
4059
5474
|
}
|
|
4060
5475
|
function _collectCopilotInlineConfig(configPath, errors) {
|
|
4061
|
-
if (!
|
|
5476
|
+
if (!existsSync6(configPath))
|
|
4062
5477
|
return;
|
|
4063
5478
|
const config = _readCopilotConfigFile(configPath, errors);
|
|
4064
5479
|
if (!config)
|
|
@@ -4088,15 +5503,15 @@ function _resolveCopilotInlineDisableSource(inlineSources) {
|
|
|
4088
5503
|
}
|
|
4089
5504
|
function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
|
|
4090
5505
|
const configHome = _getCopilotConfigHome(homeDir);
|
|
4091
|
-
const repoHookDir =
|
|
4092
|
-
const userHookDir =
|
|
4093
|
-
const repoConfigDir =
|
|
5506
|
+
const repoHookDir = join5(cwd, ".github", "hooks");
|
|
5507
|
+
const userHookDir = join5(configHome, "hooks");
|
|
5508
|
+
const repoConfigDir = join5(cwd, ".github", "copilot");
|
|
4094
5509
|
const inlineSupport = _supportsCopilotInlineHooks(copilotCliVersion);
|
|
4095
5510
|
const inlineErrors = inlineSupport === true ? errors : undefined;
|
|
4096
5511
|
const inlineSources = {
|
|
4097
|
-
userConfig: _collectCopilotInlineConfig(
|
|
4098
|
-
repoSettings: _collectCopilotInlineConfig(
|
|
4099
|
-
localSettings: _collectCopilotInlineConfig(
|
|
5512
|
+
userConfig: _collectCopilotInlineConfig(join5(configHome, "config.json"), inlineErrors),
|
|
5513
|
+
repoSettings: _collectCopilotInlineConfig(join5(repoConfigDir, "settings.json"), inlineErrors),
|
|
5514
|
+
localSettings: _collectCopilotInlineConfig(join5(repoConfigDir, "settings.local.json"), inlineErrors)
|
|
4100
5515
|
};
|
|
4101
5516
|
if (inlineSupport !== false) {
|
|
4102
5517
|
const disableSource = _resolveCopilotInlineDisableSource(inlineSources);
|
|
@@ -4110,10 +5525,10 @@ function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
|
|
|
4110
5525
|
const repoHookPaths = _collectSafetyNetCopilotHookFiles(repoHookDir, errors);
|
|
4111
5526
|
const userHookSupport = _supportsCopilotUserHookFiles(copilotCliVersion);
|
|
4112
5527
|
const userHookErrors = userHookSupport === true ? errors : undefined;
|
|
4113
|
-
const userHookFiles =
|
|
5528
|
+
const userHookFiles = existsSync6(userHookDir) ? _listJsonFiles(userHookDir, userHookErrors) : [];
|
|
4114
5529
|
const userHookPaths = [];
|
|
4115
5530
|
for (const filename of userHookFiles) {
|
|
4116
|
-
const configPath =
|
|
5531
|
+
const configPath = join5(userHookDir, filename);
|
|
4117
5532
|
const config = _readCopilotConfigFile(configPath, userHookErrors);
|
|
4118
5533
|
if (config && _hasSafetyNetCopilotHook(config)) {
|
|
4119
5534
|
userHookPaths.push(configPath);
|
|
@@ -4153,7 +5568,7 @@ function _checkCopilotEnabled(homeDir, cwd, copilotCliVersion, errors) {
|
|
|
4153
5568
|
}
|
|
4154
5569
|
function detectAllHooks(cwd, options) {
|
|
4155
5570
|
const homeDir = options?.homeDir ?? homedir4();
|
|
4156
|
-
const
|
|
5571
|
+
const detectCopilotCLI = () => {
|
|
4157
5572
|
const errors = [];
|
|
4158
5573
|
const hooksCheck = _checkCopilotEnabled(homeDir, cwd, options?.copilotCliVersion, errors);
|
|
4159
5574
|
if (hooksCheck.disabledBy) {
|
|
@@ -4186,16 +5601,17 @@ function detectAllHooks(cwd, options) {
|
|
|
4186
5601
|
};
|
|
4187
5602
|
};
|
|
4188
5603
|
return [
|
|
4189
|
-
detectClaudeCode(
|
|
5604
|
+
detectClaudeCode(options?.claudePluginListOutput),
|
|
4190
5605
|
detectOpenCode(homeDir),
|
|
4191
|
-
detectGeminiCLI(
|
|
4192
|
-
|
|
5606
|
+
detectGeminiCLI(options?.geminiExtensionsListOutput),
|
|
5607
|
+
detectCopilotCLI(),
|
|
5608
|
+
detectCodex(homeDir)
|
|
4193
5609
|
];
|
|
4194
5610
|
}
|
|
4195
5611
|
|
|
4196
5612
|
// src/bin/doctor/system-info.ts
|
|
4197
5613
|
import { spawn } from "node:child_process";
|
|
4198
|
-
var CURRENT_VERSION = "0.
|
|
5614
|
+
var CURRENT_VERSION = "0.9.0";
|
|
4199
5615
|
var VERSION_FETCH_TIMEOUT_MS = 2000;
|
|
4200
5616
|
function getPackageVersion() {
|
|
4201
5617
|
return CURRENT_VERSION;
|
|
@@ -4205,36 +5621,39 @@ var defaultVersionFetcher = async (args) => {
|
|
|
4205
5621
|
const [cmd, ...rest] = args;
|
|
4206
5622
|
if (!cmd)
|
|
4207
5623
|
return null;
|
|
4208
|
-
return new Promise((
|
|
5624
|
+
return new Promise((resolve5) => {
|
|
4209
5625
|
try {
|
|
4210
5626
|
const proc = spawn(cmd, rest, {
|
|
4211
5627
|
stdio: ["ignore", "pipe", "pipe"]
|
|
4212
5628
|
});
|
|
4213
5629
|
let isSettled = false;
|
|
4214
5630
|
let output = "";
|
|
5631
|
+
let errorOutput = "";
|
|
4215
5632
|
proc.stdout.on("data", (data) => {
|
|
4216
5633
|
output += data.toString();
|
|
4217
5634
|
});
|
|
4218
|
-
proc.stderr.on("data", () => {
|
|
5635
|
+
proc.stderr.on("data", (data) => {
|
|
5636
|
+
errorOutput += data.toString();
|
|
5637
|
+
});
|
|
4219
5638
|
const finish = (value) => {
|
|
4220
5639
|
if (isSettled)
|
|
4221
5640
|
return;
|
|
4222
5641
|
isSettled = true;
|
|
4223
5642
|
clearTimeout(timeoutId);
|
|
4224
|
-
|
|
5643
|
+
resolve5(value);
|
|
4225
5644
|
};
|
|
4226
5645
|
const timeoutId = setTimeout(() => {
|
|
4227
5646
|
proc.kill();
|
|
4228
5647
|
finish(null);
|
|
4229
5648
|
}, VERSION_FETCH_TIMEOUT_MS);
|
|
4230
5649
|
proc.on("close", (code) => {
|
|
4231
|
-
finish(code === 0 ? output.trim() || null : null);
|
|
5650
|
+
finish(code === 0 ? output.trim() || errorOutput.trim() || null : null);
|
|
4232
5651
|
});
|
|
4233
5652
|
proc.on("error", () => {
|
|
4234
5653
|
finish(null);
|
|
4235
5654
|
});
|
|
4236
5655
|
} catch {
|
|
4237
|
-
|
|
5656
|
+
resolve5(null);
|
|
4238
5657
|
}
|
|
4239
5658
|
});
|
|
4240
5659
|
};
|
|
@@ -4267,10 +5686,23 @@ async function getSystemInfo(fetcher = defaultVersionFetcher) {
|
|
|
4267
5686
|
}
|
|
4268
5687
|
return fallbackVersionPromise;
|
|
4269
5688
|
};
|
|
4270
|
-
const [
|
|
5689
|
+
const [
|
|
5690
|
+
claudeRaw,
|
|
5691
|
+
claudePluginListOutput,
|
|
5692
|
+
openCodeRaw,
|
|
5693
|
+
geminiRaw,
|
|
5694
|
+
geminiExtensionsListOutput,
|
|
5695
|
+
copilotRaw,
|
|
5696
|
+
nodeRaw,
|
|
5697
|
+
npmRaw,
|
|
5698
|
+
bunRaw,
|
|
5699
|
+
pluginListRaw
|
|
5700
|
+
] = await Promise.all([
|
|
4271
5701
|
fetcher(["claude", "--version"]),
|
|
5702
|
+
fetcher(["claude", "plugin", "list"]),
|
|
4272
5703
|
fetcher(["opencode", "--version"]),
|
|
4273
5704
|
fetcher(["gemini", "--version"]),
|
|
5705
|
+
fetcher(["gemini", "extensions", "list"]),
|
|
4274
5706
|
fetchCopilotVersion(),
|
|
4275
5707
|
fetcher(["node", "--version"]),
|
|
4276
5708
|
fetcher(["npm", "--version"]),
|
|
@@ -4280,8 +5712,10 @@ async function getSystemInfo(fetcher = defaultVersionFetcher) {
|
|
|
4280
5712
|
return {
|
|
4281
5713
|
version: CURRENT_VERSION,
|
|
4282
5714
|
claudeCodeVersion: parseVersion(claudeRaw),
|
|
5715
|
+
claudePluginListOutput,
|
|
4283
5716
|
openCodeVersion: parseVersion(openCodeRaw),
|
|
4284
5717
|
geminiCliVersion: parseVersion(geminiRaw),
|
|
5718
|
+
geminiExtensionsListOutput,
|
|
4285
5719
|
copilotCliVersion: parseVersion(copilotRaw),
|
|
4286
5720
|
nodeVersion: parseVersion(nodeRaw),
|
|
4287
5721
|
npmVersion: parseVersion(npmRaw),
|
|
@@ -4353,6 +5787,8 @@ async function runDoctor(options = {}) {
|
|
|
4353
5787
|
const cwd = options.cwd ?? process.cwd();
|
|
4354
5788
|
const system = await getSystemInfo();
|
|
4355
5789
|
const hooks = detectAllHooks(cwd, {
|
|
5790
|
+
claudePluginListOutput: system.claudePluginListOutput,
|
|
5791
|
+
geminiExtensionsListOutput: system.geminiExtensionsListOutput,
|
|
4356
5792
|
copilotCliVersion: system.copilotCliVersion,
|
|
4357
5793
|
copilotPluginInstalled: system.copilotPluginInstalled
|
|
4358
5794
|
});
|
|
@@ -4400,8 +5836,8 @@ function printReport(report) {
|
|
|
4400
5836
|
}
|
|
4401
5837
|
|
|
4402
5838
|
// src/bin/explain/config.ts
|
|
4403
|
-
import { existsSync as
|
|
4404
|
-
import { resolve as
|
|
5839
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
5840
|
+
import { resolve as resolve5 } from "node:path";
|
|
4405
5841
|
|
|
4406
5842
|
// src/core/env.ts
|
|
4407
5843
|
function envTruthy(name) {
|
|
@@ -4413,7 +5849,7 @@ function envTruthy(name) {
|
|
|
4413
5849
|
function getConfigSource(options) {
|
|
4414
5850
|
const projectPath = getProjectConfigPath(options?.cwd);
|
|
4415
5851
|
let invalidProjectPath = null;
|
|
4416
|
-
if (
|
|
5852
|
+
if (existsSync7(projectPath)) {
|
|
4417
5853
|
const validation = validateConfigFile(projectPath);
|
|
4418
5854
|
if (validation.errors.length === 0) {
|
|
4419
5855
|
return { configSource: projectPath, configValid: true };
|
|
@@ -4421,7 +5857,7 @@ function getConfigSource(options) {
|
|
|
4421
5857
|
invalidProjectPath = projectPath;
|
|
4422
5858
|
}
|
|
4423
5859
|
const userPath = options?.userConfigPath ?? getUserConfigPath();
|
|
4424
|
-
if (
|
|
5860
|
+
if (existsSync7(userPath)) {
|
|
4425
5861
|
const validation = validateConfigFile(userPath);
|
|
4426
5862
|
return { configSource: userPath, configValid: validation.errors.length === 0 };
|
|
4427
5863
|
}
|
|
@@ -4431,7 +5867,7 @@ function getConfigSource(options) {
|
|
|
4431
5867
|
return { configSource: null, configValid: true };
|
|
4432
5868
|
}
|
|
4433
5869
|
function buildAnalyzeOptions(explainOptions) {
|
|
4434
|
-
const cwd =
|
|
5870
|
+
const cwd = resolve5(explainOptions?.cwd ?? process.cwd());
|
|
4435
5871
|
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
|
|
4436
5872
|
return {
|
|
4437
5873
|
cwd,
|
|
@@ -4439,7 +5875,8 @@ function buildAnalyzeOptions(explainOptions) {
|
|
|
4439
5875
|
config: explainOptions?.config ?? loadConfig(cwd),
|
|
4440
5876
|
strict: explainOptions?.strict ?? envTruthy("SAFETY_NET_STRICT"),
|
|
4441
5877
|
paranoidRm: paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM"),
|
|
4442
|
-
paranoidInterpreters: paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS")
|
|
5878
|
+
paranoidInterpreters: paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS"),
|
|
5879
|
+
worktreeMode: envTruthy("SAFETY_NET_WORKTREE")
|
|
4443
5880
|
};
|
|
4444
5881
|
}
|
|
4445
5882
|
|
|
@@ -4488,6 +5925,7 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
|
|
|
4488
5925
|
return { reason: REASON_STRICT_UNPARSEABLE2 };
|
|
4489
5926
|
}
|
|
4490
5927
|
let effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
|
|
5928
|
+
const shellGitContextState = createShellGitContextEnvState(options.envAssignments);
|
|
4491
5929
|
for (const segment of innerSegments) {
|
|
4492
5930
|
if (segment.length === 1 && segment[0]?.includes(" ")) {
|
|
4493
5931
|
const textReason = dangerousInText(segment[0]);
|
|
@@ -4515,7 +5953,11 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
|
|
|
4515
5953
|
}
|
|
4516
5954
|
continue;
|
|
4517
5955
|
}
|
|
4518
|
-
const result = explainSegment(segment, depth + 1, {
|
|
5956
|
+
const result = explainSegment(segment, depth + 1, {
|
|
5957
|
+
...options,
|
|
5958
|
+
effectiveCwd,
|
|
5959
|
+
envAssignments: getSegmentGitContextEnvAssignments(segment, shellGitContextState)
|
|
5960
|
+
}, steps);
|
|
4519
5961
|
if (result)
|
|
4520
5962
|
return result;
|
|
4521
5963
|
if (segmentChangesCwd(segment)) {
|
|
@@ -4526,6 +5968,7 @@ function explainInnerSegments(innerCmd, depth, options, steps) {
|
|
|
4526
5968
|
});
|
|
4527
5969
|
effectiveCwd = null;
|
|
4528
5970
|
}
|
|
5971
|
+
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
4529
5972
|
}
|
|
4530
5973
|
return null;
|
|
4531
5974
|
}
|
|
@@ -4546,7 +5989,11 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4546
5989
|
output: envResult.tokens
|
|
4547
5990
|
});
|
|
4548
5991
|
}
|
|
4549
|
-
const
|
|
5992
|
+
const effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
|
|
5993
|
+
const cwdUnknown = effectiveCwd === null;
|
|
5994
|
+
const baseCwdForRm = cwdUnknown ? undefined : effectiveCwd ?? options.cwd;
|
|
5995
|
+
const originalCwd = cwdUnknown ? undefined : options.cwd;
|
|
5996
|
+
const wrapperResult = stripWrappersWithInfo(envResult.tokens, baseCwdForRm);
|
|
4550
5997
|
const removed = envResult.tokens.slice(0, envResult.tokens.length - wrapperResult.tokens.length);
|
|
4551
5998
|
if (removed.length > 0) {
|
|
4552
5999
|
steps.push({
|
|
@@ -4557,6 +6004,20 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4557
6004
|
});
|
|
4558
6005
|
}
|
|
4559
6006
|
const strippedTokens = wrapperResult.tokens;
|
|
6007
|
+
const envAssignments = new Map(options.envAssignments ?? []);
|
|
6008
|
+
for (const [k, v] of envResult.envAssignments) {
|
|
6009
|
+
envAssignments.set(k, v);
|
|
6010
|
+
}
|
|
6011
|
+
for (const [k, v] of wrapperResult.envAssignments) {
|
|
6012
|
+
envAssignments.set(k, v);
|
|
6013
|
+
}
|
|
6014
|
+
const cwdForRm = wrapperResult.cwd === null ? undefined : wrapperResult.cwd ?? baseCwdForRm;
|
|
6015
|
+
const nestedEffectiveCwd = wrapperResult.cwd === undefined ? options.effectiveCwd : wrapperResult.cwd;
|
|
6016
|
+
const nestedOptions = {
|
|
6017
|
+
...options,
|
|
6018
|
+
effectiveCwd: nestedEffectiveCwd,
|
|
6019
|
+
envAssignments
|
|
6020
|
+
};
|
|
4560
6021
|
if (strippedTokens.length === 0) {
|
|
4561
6022
|
return null;
|
|
4562
6023
|
}
|
|
@@ -4580,7 +6041,7 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4580
6041
|
innerCommand: redactedInnerCmd,
|
|
4581
6042
|
depth: depth + 1
|
|
4582
6043
|
});
|
|
4583
|
-
return explainInnerSegments(innerCmd, depth,
|
|
6044
|
+
return explainInnerSegments(innerCmd, depth, nestedOptions, steps);
|
|
4584
6045
|
}
|
|
4585
6046
|
}
|
|
4586
6047
|
if (INTERPRETERS.has(baseNameLower)) {
|
|
@@ -4603,7 +6064,7 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4603
6064
|
innerCommand: redactedCodeArg,
|
|
4604
6065
|
depth: depth + 1
|
|
4605
6066
|
});
|
|
4606
|
-
const nestedResult = explainInnerSegments(codeArg, depth,
|
|
6067
|
+
const nestedResult = explainInnerSegments(codeArg, depth, nestedOptions, steps);
|
|
4607
6068
|
if (nestedResult)
|
|
4608
6069
|
return nestedResult;
|
|
4609
6070
|
if (containsDangerousCode(codeArg)) {
|
|
@@ -4631,18 +6092,10 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4631
6092
|
innerCommand: redactEnvAssignmentsInString(busyboxInnerCmd),
|
|
4632
6093
|
depth: depth + 1
|
|
4633
6094
|
});
|
|
4634
|
-
return explainSegment(strippedTokens.slice(1), depth + 1,
|
|
4635
|
-
}
|
|
4636
|
-
const envAssignments = new Map(envResult.envAssignments);
|
|
4637
|
-
for (const [k, v] of wrapperResult.envAssignments) {
|
|
4638
|
-
envAssignments.set(k, v);
|
|
6095
|
+
return explainSegment(strippedTokens.slice(1), depth + 1, nestedOptions, steps);
|
|
4639
6096
|
}
|
|
4640
6097
|
const allowTmpdirVar = !isTmpdirOverriddenToNonTemp(envAssignments);
|
|
4641
6098
|
const tmpdirValue = envAssignments.get("TMPDIR") ?? process.env.TMPDIR ?? null;
|
|
4642
|
-
const effectiveCwd = options.effectiveCwd === undefined ? options.cwd : options.effectiveCwd;
|
|
4643
|
-
const cwdUnknown = effectiveCwd === null;
|
|
4644
|
-
const cwdForRm = cwdUnknown ? undefined : effectiveCwd ?? options.cwd;
|
|
4645
|
-
const originalCwd = cwdUnknown ? undefined : options.cwd;
|
|
4646
6099
|
const isGit = baseNameLower === "git";
|
|
4647
6100
|
const isRm = baseName === "rm";
|
|
4648
6101
|
const isFind = baseName === "find";
|
|
@@ -4657,14 +6110,27 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4657
6110
|
});
|
|
4658
6111
|
}
|
|
4659
6112
|
if (isGit) {
|
|
4660
|
-
const
|
|
6113
|
+
const gitOptions = {
|
|
6114
|
+
cwd: cwdForRm,
|
|
6115
|
+
envAssignments,
|
|
6116
|
+
worktreeMode: options.worktreeMode
|
|
6117
|
+
};
|
|
6118
|
+
const relaxation = getGitWorktreeRelaxation(strippedTokens, gitOptions);
|
|
6119
|
+
const reason = analyzeGit(strippedTokens, gitOptions);
|
|
4661
6120
|
steps.push({
|
|
4662
6121
|
type: "rule-check",
|
|
4663
6122
|
ruleModule: "rules-git.ts",
|
|
4664
6123
|
ruleFunction: "analyzeGit",
|
|
4665
|
-
matched: !!reason,
|
|
4666
|
-
reason: reason ??
|
|
6124
|
+
matched: !!reason || !!relaxation,
|
|
6125
|
+
reason: reason ?? relaxation?.originalReason
|
|
4667
6126
|
});
|
|
6127
|
+
if (relaxation) {
|
|
6128
|
+
steps.push({
|
|
6129
|
+
type: "worktree-relaxation",
|
|
6130
|
+
originalReason: relaxation.originalReason,
|
|
6131
|
+
gitCwd: relaxation.gitCwd
|
|
6132
|
+
});
|
|
6133
|
+
}
|
|
4668
6134
|
if (reason)
|
|
4669
6135
|
return { reason };
|
|
4670
6136
|
}
|
|
@@ -4702,7 +6168,9 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4702
6168
|
cwd: cwdForRm,
|
|
4703
6169
|
originalCwd,
|
|
4704
6170
|
paranoidRm: options.paranoidRm,
|
|
4705
|
-
allowTmpdirVar
|
|
6171
|
+
allowTmpdirVar,
|
|
6172
|
+
envAssignments,
|
|
6173
|
+
worktreeMode: options.worktreeMode
|
|
4706
6174
|
});
|
|
4707
6175
|
steps.push({
|
|
4708
6176
|
type: "rule-check",
|
|
@@ -4715,8 +6183,14 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4715
6183
|
return { reason };
|
|
4716
6184
|
}
|
|
4717
6185
|
if (isParallel) {
|
|
4718
|
-
const analyzeNested = (cmd) => {
|
|
4719
|
-
const
|
|
6186
|
+
const analyzeNested = (cmd, overrides) => {
|
|
6187
|
+
const overriddenOptions = {
|
|
6188
|
+
...nestedOptions,
|
|
6189
|
+
effectiveCwd: overrides && Object.hasOwn(overrides, "effectiveCwd") ? overrides.effectiveCwd : nestedOptions.effectiveCwd,
|
|
6190
|
+
envAssignments: overrides?.envAssignments ?? nestedOptions.envAssignments,
|
|
6191
|
+
worktreeMode: overrides?.worktreeMode ?? nestedOptions.worktreeMode
|
|
6192
|
+
};
|
|
6193
|
+
const result = explainInnerSegments(cmd, depth, overriddenOptions, steps);
|
|
4720
6194
|
return result?.reason ?? null;
|
|
4721
6195
|
};
|
|
4722
6196
|
const reason = analyzeParallel(strippedTokens, {
|
|
@@ -4724,6 +6198,8 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4724
6198
|
originalCwd,
|
|
4725
6199
|
paranoidRm: options.paranoidRm,
|
|
4726
6200
|
allowTmpdirVar,
|
|
6201
|
+
envAssignments,
|
|
6202
|
+
worktreeMode: options.worktreeMode,
|
|
4727
6203
|
analyzeNested
|
|
4728
6204
|
});
|
|
4729
6205
|
steps.push({
|
|
@@ -4739,6 +6215,7 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4739
6215
|
const matchedKnown = isGit || isRm || isFind || isXargs || isParallel;
|
|
4740
6216
|
const tokensScanned = [];
|
|
4741
6217
|
let fallbackReason = null;
|
|
6218
|
+
let fallbackRelaxation = null;
|
|
4742
6219
|
let embeddedCommandFound;
|
|
4743
6220
|
if (!matchedKnown && !DISPLAY_COMMANDS.has(normalizeCommandToken(head))) {
|
|
4744
6221
|
for (let i = 1;i < strippedTokens.length && !fallbackReason; i++) {
|
|
@@ -4760,7 +6237,13 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4760
6237
|
if (!fallbackReason && cmd === "git") {
|
|
4761
6238
|
embeddedCommandFound = "git";
|
|
4762
6239
|
const gitTokens = ["git", ...strippedTokens.slice(i + 1)];
|
|
4763
|
-
|
|
6240
|
+
const gitOptions = {
|
|
6241
|
+
cwd: cwdForRm,
|
|
6242
|
+
envAssignments,
|
|
6243
|
+
worktreeMode: false
|
|
6244
|
+
};
|
|
6245
|
+
fallbackRelaxation = getGitWorktreeRelaxation(gitTokens, gitOptions);
|
|
6246
|
+
fallbackReason = analyzeGit(gitTokens, gitOptions);
|
|
4764
6247
|
}
|
|
4765
6248
|
if (!fallbackReason && cmd === "find") {
|
|
4766
6249
|
embeddedCommandFound = "find";
|
|
@@ -4774,6 +6257,13 @@ function explainSegment(tokens, depth, options, steps) {
|
|
|
4774
6257
|
tokensScanned,
|
|
4775
6258
|
embeddedCommandFound
|
|
4776
6259
|
});
|
|
6260
|
+
if (fallbackRelaxation) {
|
|
6261
|
+
steps.push({
|
|
6262
|
+
type: "worktree-relaxation",
|
|
6263
|
+
originalReason: fallbackRelaxation.originalReason,
|
|
6264
|
+
gitCwd: fallbackRelaxation.gitCwd
|
|
6265
|
+
});
|
|
6266
|
+
}
|
|
4777
6267
|
if (fallbackReason)
|
|
4778
6268
|
return { reason: fallbackReason };
|
|
4779
6269
|
const shouldCheckCustomRules = depth === 0 || !matchedKnown;
|
|
@@ -4839,6 +6329,7 @@ function explainCommand2(command, options) {
|
|
|
4839
6329
|
let blockReason;
|
|
4840
6330
|
let blockSegment;
|
|
4841
6331
|
let effectiveCwd = analyzeOpts.effectiveCwd;
|
|
6332
|
+
const shellGitContextState = createShellGitContextEnvState(analyzeOpts.envAssignments);
|
|
4842
6333
|
for (let i = 0;i < segments.length; i++) {
|
|
4843
6334
|
const segment = segments[i];
|
|
4844
6335
|
if (!segment)
|
|
@@ -4884,7 +6375,11 @@ function explainCommand2(command, options) {
|
|
|
4884
6375
|
trace.segments.push({ index: i, steps: segmentSteps });
|
|
4885
6376
|
continue;
|
|
4886
6377
|
}
|
|
4887
|
-
const result = explainSegment(segment, 0, {
|
|
6378
|
+
const result = explainSegment(segment, 0, {
|
|
6379
|
+
...analyzeOpts,
|
|
6380
|
+
effectiveCwd,
|
|
6381
|
+
envAssignments: getSegmentGitContextEnvAssignments(segment, shellGitContextState)
|
|
6382
|
+
}, segmentSteps);
|
|
4888
6383
|
if (result) {
|
|
4889
6384
|
blocked = true;
|
|
4890
6385
|
blockReason = result.reason;
|
|
@@ -4898,6 +6393,7 @@ function explainCommand2(command, options) {
|
|
|
4898
6393
|
});
|
|
4899
6394
|
effectiveCwd = null;
|
|
4900
6395
|
}
|
|
6396
|
+
applyShellGitContextEnvSegment(segment, shellGitContextState);
|
|
4901
6397
|
trace.segments.push({ index: i, steps: segmentSteps });
|
|
4902
6398
|
}
|
|
4903
6399
|
return {
|
|
@@ -5077,6 +6573,14 @@ function formatStepStyleD(step, stepNum, box) {
|
|
|
5077
6573
|
}
|
|
5078
6574
|
return { lines, incrementStep: true };
|
|
5079
6575
|
}
|
|
6576
|
+
case "worktree-relaxation": {
|
|
6577
|
+
lines.push("");
|
|
6578
|
+
lines.push(`STEP ${stepNum} ${box.h} Worktree relaxation`);
|
|
6579
|
+
lines.push(` Mode: SAFETY_NET_WORKTREE`);
|
|
6580
|
+
lines.push(` Git cwd: ${step.gitCwd}`);
|
|
6581
|
+
lines.push(` Result: Allowed local discard in linked worktree`);
|
|
6582
|
+
return { lines, incrementStep: true };
|
|
6583
|
+
}
|
|
5080
6584
|
case "tmpdir-check":
|
|
5081
6585
|
return null;
|
|
5082
6586
|
case "fallback-scan": {
|
|
@@ -5268,7 +6772,7 @@ function formatTraceJson(result) {
|
|
|
5268
6772
|
return JSON.stringify(result, null, 2);
|
|
5269
6773
|
}
|
|
5270
6774
|
// src/bin/help.ts
|
|
5271
|
-
var version = "0.
|
|
6775
|
+
var version = "0.9.0";
|
|
5272
6776
|
var INDENT = " ";
|
|
5273
6777
|
var PROGRAM_NAME = "cc-safety-net";
|
|
5274
6778
|
function formatOptionFlags(option) {
|
|
@@ -5334,6 +6838,7 @@ function printHelp() {
|
|
|
5334
6838
|
lines.push(`${INDENT}SAFETY_NET_PARANOID=1 Enable all paranoid checks`);
|
|
5335
6839
|
lines.push(`${INDENT}SAFETY_NET_PARANOID_RM=1 Block non-temp rm -rf within cwd`);
|
|
5336
6840
|
lines.push(`${INDENT}SAFETY_NET_PARANOID_INTERPRETERS=1 Block interpreter one-liners`);
|
|
6841
|
+
lines.push(`${INDENT}SAFETY_NET_WORKTREE=1 Allow local git discards in linked worktrees`);
|
|
5337
6842
|
lines.push("");
|
|
5338
6843
|
lines.push("CONFIG FILES:");
|
|
5339
6844
|
lines.push(`${INDENT}~/.cc-safety-net/config.json User-scope config`);
|
|
@@ -5354,9 +6859,9 @@ function showCommandHelp(commandName) {
|
|
|
5354
6859
|
}
|
|
5355
6860
|
|
|
5356
6861
|
// src/core/audit.ts
|
|
5357
|
-
import { appendFileSync, existsSync as
|
|
6862
|
+
import { appendFileSync, existsSync as existsSync8, mkdirSync } from "node:fs";
|
|
5358
6863
|
import { homedir as homedir5 } from "node:os";
|
|
5359
|
-
import { join as
|
|
6864
|
+
import { join as join6 } from "node:path";
|
|
5360
6865
|
function sanitizeSessionIdForFilename(sessionId) {
|
|
5361
6866
|
const raw = sessionId.trim();
|
|
5362
6867
|
if (!raw) {
|
|
@@ -5375,12 +6880,12 @@ function writeAuditLog(sessionId, command, segment, reason, cwd, options = {}) {
|
|
|
5375
6880
|
return;
|
|
5376
6881
|
}
|
|
5377
6882
|
const home = options.homeDir ?? homedir5();
|
|
5378
|
-
const logsDir =
|
|
6883
|
+
const logsDir = join6(home, ".cc-safety-net", "logs");
|
|
5379
6884
|
try {
|
|
5380
|
-
if (!
|
|
6885
|
+
if (!existsSync8(logsDir)) {
|
|
5381
6886
|
mkdirSync(logsDir, { recursive: true });
|
|
5382
6887
|
}
|
|
5383
|
-
const logFile =
|
|
6888
|
+
const logFile = join6(logsDir, `${safeSessionId}.jsonl`);
|
|
5384
6889
|
const entry = {
|
|
5385
6890
|
ts: new Date().toISOString(),
|
|
5386
6891
|
command: redactSecrets(command).slice(0, 300),
|
|
@@ -5478,13 +6983,15 @@ async function runClaudeCodeHook() {
|
|
|
5478
6983
|
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
|
|
5479
6984
|
const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
|
|
5480
6985
|
const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
|
|
6986
|
+
const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
|
|
5481
6987
|
const config = loadConfig(cwd);
|
|
5482
6988
|
const result = analyzeCommand(command, {
|
|
5483
6989
|
cwd,
|
|
5484
6990
|
config,
|
|
5485
6991
|
strict,
|
|
5486
6992
|
paranoidRm,
|
|
5487
|
-
paranoidInterpreters
|
|
6993
|
+
paranoidInterpreters,
|
|
6994
|
+
worktreeMode
|
|
5488
6995
|
});
|
|
5489
6996
|
if (result) {
|
|
5490
6997
|
const sessionId = input.session_id;
|
|
@@ -5548,13 +7055,15 @@ async function runCopilotCliHook() {
|
|
|
5548
7055
|
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
|
|
5549
7056
|
const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
|
|
5550
7057
|
const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
|
|
7058
|
+
const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
|
|
5551
7059
|
const config = loadConfig(cwd);
|
|
5552
7060
|
const result = analyzeCommand(command, {
|
|
5553
7061
|
cwd,
|
|
5554
7062
|
config,
|
|
5555
7063
|
strict,
|
|
5556
7064
|
paranoidRm,
|
|
5557
|
-
paranoidInterpreters
|
|
7065
|
+
paranoidInterpreters,
|
|
7066
|
+
worktreeMode
|
|
5558
7067
|
});
|
|
5559
7068
|
if (result) {
|
|
5560
7069
|
const sessionId = `copilot-${input.timestamp ?? Date.now()}`;
|
|
@@ -5611,13 +7120,15 @@ async function runGeminiCLIHook() {
|
|
|
5611
7120
|
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
|
|
5612
7121
|
const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
|
|
5613
7122
|
const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
|
|
7123
|
+
const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
|
|
5614
7124
|
const config = loadConfig(cwd);
|
|
5615
7125
|
const result = analyzeCommand(command, {
|
|
5616
7126
|
cwd,
|
|
5617
7127
|
config,
|
|
5618
7128
|
strict,
|
|
5619
7129
|
paranoidRm,
|
|
5620
|
-
paranoidInterpreters
|
|
7130
|
+
paranoidInterpreters,
|
|
7131
|
+
worktreeMode
|
|
5621
7132
|
});
|
|
5622
7133
|
if (result) {
|
|
5623
7134
|
const sessionId = input.session_id;
|
|
@@ -5629,14 +7140,14 @@ async function runGeminiCLIHook() {
|
|
|
5629
7140
|
}
|
|
5630
7141
|
|
|
5631
7142
|
// src/bin/statusline.ts
|
|
5632
|
-
import { existsSync as
|
|
7143
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
|
|
5633
7144
|
import { homedir as homedir6 } from "node:os";
|
|
5634
|
-
import { join as
|
|
7145
|
+
import { join as join7 } from "node:path";
|
|
5635
7146
|
async function readStdinAsync() {
|
|
5636
7147
|
if (process.stdin.isTTY) {
|
|
5637
7148
|
return null;
|
|
5638
7149
|
}
|
|
5639
|
-
return new Promise((
|
|
7150
|
+
return new Promise((resolve6) => {
|
|
5640
7151
|
let data = "";
|
|
5641
7152
|
process.stdin.setEncoding("utf-8");
|
|
5642
7153
|
process.stdin.on("data", (chunk) => {
|
|
@@ -5644,10 +7155,10 @@ async function readStdinAsync() {
|
|
|
5644
7155
|
});
|
|
5645
7156
|
process.stdin.on("end", () => {
|
|
5646
7157
|
const trimmed = data.trim();
|
|
5647
|
-
|
|
7158
|
+
resolve6(trimmed || null);
|
|
5648
7159
|
});
|
|
5649
7160
|
process.stdin.on("error", () => {
|
|
5650
|
-
|
|
7161
|
+
resolve6(null);
|
|
5651
7162
|
});
|
|
5652
7163
|
});
|
|
5653
7164
|
}
|
|
@@ -5655,15 +7166,15 @@ function getSettingsPath() {
|
|
|
5655
7166
|
if (process.env.CLAUDE_SETTINGS_PATH) {
|
|
5656
7167
|
return process.env.CLAUDE_SETTINGS_PATH;
|
|
5657
7168
|
}
|
|
5658
|
-
return
|
|
7169
|
+
return join7(homedir6(), ".claude", "settings.json");
|
|
5659
7170
|
}
|
|
5660
7171
|
function isPluginEnabled() {
|
|
5661
7172
|
const settingsPath = getSettingsPath();
|
|
5662
|
-
if (!
|
|
7173
|
+
if (!existsSync9(settingsPath)) {
|
|
5663
7174
|
return false;
|
|
5664
7175
|
}
|
|
5665
7176
|
try {
|
|
5666
|
-
const content =
|
|
7177
|
+
const content = readFileSync7(settingsPath, "utf-8");
|
|
5667
7178
|
const settings = JSON.parse(content);
|
|
5668
7179
|
if (!settings.enabledPlugins) {
|
|
5669
7180
|
return false;
|
|
@@ -5687,6 +7198,7 @@ async function printStatusline() {
|
|
|
5687
7198
|
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
|
|
5688
7199
|
const paranoidRm = paranoidAll || envTruthy("SAFETY_NET_PARANOID_RM");
|
|
5689
7200
|
const paranoidInterpreters = paranoidAll || envTruthy("SAFETY_NET_PARANOID_INTERPRETERS");
|
|
7201
|
+
const worktreeMode = envTruthy("SAFETY_NET_WORKTREE");
|
|
5690
7202
|
let modeEmojis = "";
|
|
5691
7203
|
if (strict) {
|
|
5692
7204
|
modeEmojis += "\uD83D\uDD12";
|
|
@@ -5698,6 +7210,9 @@ async function printStatusline() {
|
|
|
5698
7210
|
} else if (paranoidInterpreters) {
|
|
5699
7211
|
modeEmojis += "\uD83D\uDC1A";
|
|
5700
7212
|
}
|
|
7213
|
+
if (worktreeMode) {
|
|
7214
|
+
modeEmojis += "\uD83C\uDF33";
|
|
7215
|
+
}
|
|
5701
7216
|
const statusEmoji = modeEmojis || "✅";
|
|
5702
7217
|
status = `\uD83D\uDEE1️ Safety Net ${statusEmoji}`;
|
|
5703
7218
|
}
|
|
@@ -5710,8 +7225,8 @@ async function printStatusline() {
|
|
|
5710
7225
|
}
|
|
5711
7226
|
|
|
5712
7227
|
// src/bin/verify-config.ts
|
|
5713
|
-
import { existsSync as
|
|
5714
|
-
import { resolve as
|
|
7228
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync } from "node:fs";
|
|
7229
|
+
import { resolve as resolve6 } from "node:path";
|
|
5715
7230
|
var HEADER = "Safety Net Config";
|
|
5716
7231
|
var SEPARATOR = "═".repeat(HEADER.length);
|
|
5717
7232
|
var SCHEMA_URL = "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json";
|
|
@@ -5747,7 +7262,7 @@ function printInvalidConfig(scope, path, errors) {
|
|
|
5747
7262
|
}
|
|
5748
7263
|
function addSchemaIfMissing(path) {
|
|
5749
7264
|
try {
|
|
5750
|
-
const content =
|
|
7265
|
+
const content = readFileSync8(path, "utf-8");
|
|
5751
7266
|
const parsed = JSON.parse(content);
|
|
5752
7267
|
if (parsed.$schema) {
|
|
5753
7268
|
return false;
|
|
@@ -5765,18 +7280,18 @@ function verifyConfig(options = {}) {
|
|
|
5765
7280
|
let hasErrors = false;
|
|
5766
7281
|
const configsChecked = [];
|
|
5767
7282
|
printHeader();
|
|
5768
|
-
if (
|
|
7283
|
+
if (existsSync10(userConfig)) {
|
|
5769
7284
|
const result = validateConfigFile(userConfig);
|
|
5770
7285
|
configsChecked.push({ scope: "User", path: userConfig, result });
|
|
5771
7286
|
if (result.errors.length > 0) {
|
|
5772
7287
|
hasErrors = true;
|
|
5773
7288
|
}
|
|
5774
7289
|
}
|
|
5775
|
-
if (
|
|
7290
|
+
if (existsSync10(projectConfig)) {
|
|
5776
7291
|
const result = validateConfigFile(projectConfig);
|
|
5777
7292
|
configsChecked.push({
|
|
5778
7293
|
scope: "Project",
|
|
5779
|
-
path:
|
|
7294
|
+
path: resolve6(projectConfig),
|
|
5780
7295
|
result
|
|
5781
7296
|
});
|
|
5782
7297
|
if (result.errors.length > 0) {
|