@hasna/hooks 0.10.1 → 0.10.3

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.
@@ -899,8 +899,9 @@ function evaluate(input, deps) {
899
899
  if (false) {}
900
900
 
901
901
  // hooks/hook-workspace-repos-guard/src/hook.ts
902
+ import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
902
903
  import { homedir } from "os";
903
- import { isAbsolute as isAbsolute2, join as join2, normalize as normalize2, relative, resolve as resolve2, sep as sep2 } from "path";
904
+ import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, normalize as normalize2, relative, resolve as resolve2, sep as sep2 } from "path";
904
905
  var FILE_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
905
906
  var PATCH_TOOLS = new Set(["apply_patch", "ApplyPatch", "functions.apply_patch"]);
906
907
  var DEFAULT_ORGS = ["hasna", "hasnaxyz", "hasna-products"];
@@ -914,11 +915,47 @@ function legacyRoot(home) {
914
915
  function protectedRoots(home) {
915
916
  return [reposRoot(home), legacyRoot(home)];
916
917
  }
917
- function resolveAllowedOrgs(env = process.env) {
918
+ function allowedOrgsFile(home) {
919
+ return join2(home, ".hasna", "hooks", "config", "workspace-repos-guard-orgs");
920
+ }
921
+ var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
922
+ function ownedAndClosed(stat) {
923
+ const uid = process.getuid?.();
924
+ return uid !== undefined && (stat.uid === 0 || stat.uid === uid) && (stat.mode & 18) === 0;
925
+ }
926
+ function readAllowedOrgsFile(home) {
927
+ const file = allowedOrgsFile(home);
928
+ let text;
929
+ try {
930
+ const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
931
+ try {
932
+ const stat = fstatSync(fd);
933
+ if (!stat.isFile() || !ownedAndClosed(stat) || stat.size > 4096)
934
+ return null;
935
+ text = readFileSync2(fd, "utf8");
936
+ } finally {
937
+ closeSync(fd);
938
+ }
939
+ const top = normalize2(home);
940
+ for (let dir = dirname2(file);dir !== top; dir = dirname2(dir)) {
941
+ const stat = lstatSync(dir);
942
+ if (!stat.isDirectory() || !ownedAndClosed(stat) || dir === dirname2(dir))
943
+ return null;
944
+ }
945
+ if (!ownedAndClosed(statSync2(top)))
946
+ return null;
947
+ } catch {
948
+ return null;
949
+ }
950
+ const names = text.replace(/#[^\n]*/g, "").split(/[\s,]+/).filter(Boolean);
951
+ return names.length > 0 && names.every((name) => ORG_NAME.test(name)) ? new Set(names) : null;
952
+ }
953
+ function resolveAllowedOrgs(env = process.env, home = homedir()) {
918
954
  const raw = env.WORKSPACE_REPOS_GUARD_ORGS;
919
- if (typeof raw !== "string" || !raw.trim())
920
- return new Set(DEFAULT_ORGS);
921
- return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
955
+ if (typeof raw === "string" && raw.trim()) {
956
+ return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
957
+ }
958
+ return readAllowedOrgsFile(home) ?? new Set(DEFAULT_ORGS);
922
959
  }
923
960
  function classifyPath(target, root, orgs, op) {
924
961
  if (op === "read")
@@ -952,6 +989,11 @@ function classifyPath(target, root, orgs, op) {
952
989
  return { blocked: false };
953
990
  }
954
991
  var WRITE_FLAGS = /\b(?:-o|--output|-O|--output-document|-out)\b/;
992
+ var FD_DUPLICATION = /\d*[<>]&\s*(?:\d+|-)(?=$|[\s;&|)])/g;
993
+ var OUTPUT_REDIRECT = /(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;
994
+ function withoutFdDuplication(segment) {
995
+ return segment.replace(FD_DUPLICATION, " ");
996
+ }
955
997
  function segmentOperation(segment) {
956
998
  const trimmed = segment.trim();
957
999
  if (!trimmed)
@@ -980,7 +1022,7 @@ function segmentOperation(segment) {
980
1022
  } else if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(trimmed)) {
981
1023
  return "write";
982
1024
  }
983
- if (/(?:^|[;&|])\s*echo\b/.test(trimmed) || /[>&]/.test(trimmed))
1025
+ if (/(?:^|[;&|])\s*echo\b/.test(trimmed) || />/.test(withoutFdDuplication(trimmed)))
984
1026
  return "write";
985
1027
  return "read";
986
1028
  }
@@ -1017,22 +1059,51 @@ function unwrapSegment(segment) {
1017
1059
  function regexEscape(text) {
1018
1060
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1019
1061
  }
1062
+ function cdDestinations(operand, bases, home) {
1063
+ if (operand === undefined)
1064
+ return [home];
1065
+ let word = expandHomeSpelling2(operand, home);
1066
+ const quoted = word.match(/^(['"])(.*)\1$/);
1067
+ if (quoted)
1068
+ word = quoted[2];
1069
+ if (!word || word === "-" || /[$\x60*?[\]{}~\\'"]/.test(word))
1070
+ return null;
1071
+ return [...new Set(bases.map((base) => resolve2(base, word)))];
1072
+ }
1073
+ var REL_OPERAND = /(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/;
1020
1074
  function bashTargets(command, home, cwd) {
1021
1075
  if (!command)
1022
1076
  return [];
1023
1077
  const roots = protectedRoots(home);
1024
- const segments = command.split(/\s*&&\s*|\s*\|\|\s*|;\s*|\n+/);
1078
+ const underRoot = (path) => roots.some((root) => path === root || path.startsWith(`${root}${sep2}`));
1079
+ const parts = command.split(/(\s*&&\s*|\s*\|\|\s*|;\s*|\n+)/);
1025
1080
  const targets = [];
1026
- let cwdUnderRepos = null;
1027
- for (const rawSegment of segments) {
1081
+ let current = [cwd];
1082
+ let reachable = new Set([cwd]);
1083
+ let certain = true;
1084
+ const subshells = [];
1085
+ for (let index = 0;index < parts.length; index += 2) {
1086
+ const separator = index === 0 ? "" : parts[index - 1].trim();
1087
+ if (separator === "||")
1088
+ certain = false;
1089
+ else if (separator !== "&&")
1090
+ certain = true;
1091
+ if (separator !== "&&" || !certain)
1092
+ current = [...reachable];
1093
+ const rawSegment = parts[index];
1094
+ const opens = (rawSegment.match(/^[\s({]*/)?.[0].match(/\(/g) ?? []).length;
1095
+ const closes = (rawSegment.match(/[\s)}]*$/)?.[0].match(/\)/g) ?? []).length;
1096
+ for (let i = 0;i < opens; i++)
1097
+ subshells.push({ current: [...current], reachable: new Set(reachable), certain });
1028
1098
  const segment = unwrapSegment(rawSegment);
1029
1099
  const op = segmentOperation(segment);
1030
- const cdMatch = segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
1031
- if (cdMatch) {
1032
- const cdTarget = expandHomeSpelling2(cdMatch[1], home);
1033
- if (cdTarget && roots.some((root) => cdTarget === root || cdTarget.startsWith(`${root}${sep2}`))) {
1034
- cwdUnderRepos = cdTarget;
1035
- }
1100
+ const plainCd = segment.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/);
1101
+ const embeddedCd = plainCd ? null : segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
1102
+ if (embeddedCd) {
1103
+ for (const dir of cdDestinations(embeddedCd[1], current, home) ?? [])
1104
+ reachable.add(dir);
1105
+ certain = false;
1106
+ current = [...reachable];
1036
1107
  }
1037
1108
  const homeLiteral = regexEscape(home);
1038
1109
  const suffix = roots.map((root) => regexEscape(root.slice(home.length + 1))).join("|");
@@ -1046,14 +1117,35 @@ function bashTargets(command, home, cwd) {
1046
1117
  targets.push({ path: normalize2(expanded).replace(/\/+$/, ""), op });
1047
1118
  }
1048
1119
  if (!foundExplicit && (op === "delete" || op === "write")) {
1049
- const base = cwdUnderRepos ?? (roots.some((root) => cwd === root || cwd.startsWith(`${root}${sep2}`)) ? cwd : null);
1050
- if (base) {
1051
- const relMatch = segment.match(/(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/);
1052
- if (relMatch) {
1053
- targets.push({ path: normalize2(resolve2(base, relMatch[1])), op });
1120
+ const operands = [];
1121
+ const commandPart = withoutFdDuplication(segment).replace(OUTPUT_REDIRECT, (_match, operand) => {
1122
+ if (operand)
1123
+ operands.push(operand);
1124
+ return " ";
1125
+ });
1126
+ const commandOp = segmentOperation(commandPart);
1127
+ const relMatch = commandOp === "read" ? null : commandPart.match(REL_OPERAND);
1128
+ for (const base of current.filter(underRoot)) {
1129
+ for (const operand of operands) {
1130
+ targets.push({ path: normalize2(resolve2(base, expandHomeSpelling2(operand, home))), op: "write" });
1054
1131
  }
1132
+ if (relMatch)
1133
+ targets.push({ path: normalize2(resolve2(base, relMatch[1])), op: commandOp });
1134
+ }
1135
+ }
1136
+ if (plainCd) {
1137
+ const next = cdDestinations(plainCd[1], current, home);
1138
+ if (next) {
1139
+ for (const dir of next)
1140
+ reachable.add(dir);
1141
+ if (certain)
1142
+ current = next;
1143
+ } else {
1144
+ certain = false;
1055
1145
  }
1056
1146
  }
1147
+ for (let i = 0;i < closes && subshells.length > 0; i++)
1148
+ ({ current, reachable, certain } = subshells.pop());
1057
1149
  }
1058
1150
  return targets;
1059
1151
  }
@@ -1067,14 +1159,14 @@ function patchTargets(patch, cwd) {
1067
1159
  }
1068
1160
  return targets;
1069
1161
  }
1070
- function evaluate2(input) {
1162
+ function evaluate2(input, options = {}) {
1071
1163
  const warnings = [];
1072
1164
  if (input.hook_event_name !== "PreToolUse")
1073
1165
  return { output: { continue: true }, warnings };
1074
1166
  const tool = typeof input.tool_name === "string" ? input.tool_name : "";
1075
- const home = homedir();
1167
+ const home = options.home ?? homedir();
1076
1168
  const roots = protectedRoots(home);
1077
- const orgs = resolveAllowedOrgs();
1169
+ const orgs = resolveAllowedOrgs(process.env, home);
1078
1170
  if (tool === "Bash") {
1079
1171
  const command = getCommand(input);
1080
1172
  if (!command)
package/bin/serve.js CHANGED
@@ -5362,7 +5362,7 @@ import { readFileSync as readFileSync5 } from "fs";
5362
5362
  import { dirname as dirname4, join as join8 } from "path";
5363
5363
  import { fileURLToPath as fileURLToPath5 } from "url";
5364
5364
 
5365
- // ../contracts/dist/client/transport.js
5365
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
5366
5366
  import { spawnSync } from "child_process";
5367
5367
  import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
5368
5368
  import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";