@scheduler-systems/gal-run 0.0.378 → 0.0.380

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.
Files changed (2) hide show
  1. package/dist/index.cjs +181 -46
  2. package/package.json +1 -1
package/dist/index.cjs CHANGED
@@ -3970,7 +3970,7 @@ var cliVersion, defaultApiUrl, BUILD_CONSTANTS, constants_default;
3970
3970
  var init_constants = __esm({
3971
3971
  "src/constants.ts"() {
3972
3972
  "use strict";
3973
- cliVersion = true ? "0.0.378" : "0.0.0-dev";
3973
+ cliVersion = true ? "0.0.380" : "0.0.0-dev";
3974
3974
  defaultApiUrl = true ? "https://api.gal.run" : "http://localhost:3000";
3975
3975
  BUILD_CONSTANTS = Object.freeze([cliVersion, defaultApiUrl]);
3976
3976
  constants_default = BUILD_CONSTANTS;
@@ -4880,7 +4880,7 @@ function detectEnvironment() {
4880
4880
  return "dev";
4881
4881
  }
4882
4882
  try {
4883
- const version2 = true ? "0.0.378" : void 0;
4883
+ const version2 = true ? "0.0.380" : void 0;
4884
4884
  if (version2 && version2.includes("-local")) {
4885
4885
  return "dev";
4886
4886
  }
@@ -5249,7 +5249,7 @@ function getId() {
5249
5249
  }
5250
5250
  function getCliVersion() {
5251
5251
  try {
5252
- return true ? "0.0.378" : "0.0.0-dev";
5252
+ return true ? "0.0.380" : "0.0.0-dev";
5253
5253
  } catch {
5254
5254
  return "0.0.0-dev";
5255
5255
  }
@@ -54045,7 +54045,8 @@ function generateSdlcPrePushHook(apiUrl, orgName) {
54045
54045
  # GAL SDLC Phase-Gate Pre-Push Hook
54046
54046
  # Installed by: gal enforce install --sdlc
54047
54047
  # Blocks pushes when the SDLC phase gate for the branch is not met.
54048
- # Fail-open: if the API is unreachable the push is allowed.
54048
+ # Uses the local GAL CLI for auth and API access.
54049
+ # Fail-open: if GAL or the API is unavailable the push is allowed.
54049
54050
  # To skip: git push --no-verify
54050
54051
 
54051
54052
  BRANCH=$(git rev-parse --abbrev-ref HEAD)
@@ -54058,25 +54059,15 @@ esac
54058
54059
  API_URL="${apiUrl}"
54059
54060
  ORG_NAME="${orgName}"
54060
54061
 
54061
- RESPONSE=$(curl -sf -w "\\n%{http_code}" \\
54062
- "\${API_URL}/organizations/\${ORG_NAME}/enforcement/sdlc/gate?branch=\${BRANCH}" \\
54063
- 2>/dev/null) || {
54064
- # API unreachable \u2014 fail open
54062
+ RESPONSE=$(gal enforce sdlc gate --branch "$BRANCH" --json 2>/dev/null) || {
54063
+ # GAL unavailable or API unreachable \u2014 fail open
54065
54064
  exit 0
54066
54065
  }
54067
54066
 
54068
- HTTP_CODE=$(echo "$RESPONSE" | tail -n1)
54069
- BODY=$(echo "$RESPONSE" | sed '$ d')
54070
-
54071
- if [ "$HTTP_CODE" != "200" ]; then
54072
- # Non-200 \u2014 fail open
54073
- exit 0
54074
- fi
54075
-
54076
- ALLOWED=$(echo "$BODY" | grep -o '"allowed"\\s*:\\s*\\(true\\|false\\)' | grep -o '\\(true\\|false\\)')
54067
+ ALLOWED=$(echo "$RESPONSE" | grep -o '"allowed"\\s*:\\s*\\(true\\|false\\)' | grep -o '\\(true\\|false\\)')
54077
54068
 
54078
54069
  if [ "$ALLOWED" = "false" ]; then
54079
- MESSAGE=$(echo "$BODY" | grep -o '"message"\\s*:\\s*"[^"]*"' | head -1 | sed 's/"message"\\s*:\\s*"//;s/"$//')
54070
+ MESSAGE=$(echo "$RESPONSE" | grep -o '"message"\\s*:\\s*"[^"]*"' | head -1 | sed 's/"message"\\s*:\\s*"//;s/"$//')
54080
54071
  echo ""
54081
54072
  echo "\\033[31m[GAL] SDLC phase gate blocked this push.\\033[0m"
54082
54073
  echo "\\033[33m Branch: $BRANCH\\033[0m"
@@ -54092,31 +54083,106 @@ exit 0
54092
54083
  `;
54093
54084
  }
54094
54085
  function generateSdlcAgentHook(apiUrl) {
54095
- const hook = {
54096
- type: "pre_tool_call",
54097
- description: "GAL SDLC phase-gate enforcement \u2014 blocks tool calls when the required SDLC phase is not met.",
54098
- api_url: apiUrl,
54099
- matchers: [
54100
- {
54101
- tool_name: "Bash",
54102
- command_pattern: "git\\s+push",
54103
- action: "check_sdlc_gate"
54104
- },
54105
- {
54106
- tool_name: "Edit",
54107
- file_pattern: "^\\.github/workflows/.*\\.yml$",
54108
- action: "check_sdlc_gate"
54109
- }
54110
- ],
54111
- check_sdlc_gate: {
54112
- method: "GET",
54113
- url: `${apiUrl}/enforcement/sdlc/gate`,
54114
- query_params: { branch: "{{current_branch}}" },
54115
- block_when: { allowed: false },
54116
- fail_open: true
54117
- }
54118
- };
54119
- return JSON.stringify(hook, null, 2);
54086
+ return `#!/usr/bin/env python3
54087
+ """
54088
+ GAL SDLC phase-gate hook.
54089
+ Installed by: gal enforce install --sdlc
54090
+ Fail-open: if GAL or the API is unavailable the tool call is allowed.
54091
+ API URL: ${apiUrl}
54092
+ """
54093
+ import json
54094
+ import subprocess
54095
+ import sys
54096
+
54097
+ PROTECTED_PREFIXES = (
54098
+ ".github/workflows/",
54099
+ ".claude/settings.json",
54100
+ )
54101
+
54102
+
54103
+ def read_hook_input():
54104
+ try:
54105
+ return json.load(sys.stdin)
54106
+ except Exception:
54107
+ return {}
54108
+
54109
+
54110
+ def current_branch() -> str:
54111
+ try:
54112
+ return subprocess.check_output(
54113
+ ["git", "rev-parse", "--abbrev-ref", "HEAD"],
54114
+ stderr=subprocess.DEVNULL,
54115
+ text=True,
54116
+ timeout=5,
54117
+ ).strip()
54118
+ except Exception:
54119
+ return ""
54120
+
54121
+
54122
+ def should_check(tool_name: str, tool_input: dict) -> bool:
54123
+ if tool_name == "Bash":
54124
+ command = str(tool_input.get("command", ""))
54125
+ return "git push" in command
54126
+
54127
+ if tool_name in {"Edit", "Write", "MultiEdit"}:
54128
+ file_path = str(tool_input.get("file_path", ""))
54129
+ return any(file_path.startswith(prefix) for prefix in PROTECTED_PREFIXES)
54130
+
54131
+ return False
54132
+
54133
+
54134
+ def query_gate(branch: str) -> dict:
54135
+ if not branch or branch in {"main", "master"}:
54136
+ return {"allowed": True}
54137
+
54138
+ try:
54139
+ result = subprocess.run(
54140
+ ["gal", "enforce", "sdlc", "gate", "--branch", branch, "--json"],
54141
+ capture_output=True,
54142
+ text=True,
54143
+ timeout=10,
54144
+ check=False,
54145
+ )
54146
+ except Exception:
54147
+ return {"allowed": True}
54148
+
54149
+ if result.returncode != 0:
54150
+ return {"allowed": True}
54151
+
54152
+ try:
54153
+ payload = json.loads(result.stdout or "{}")
54154
+ return payload if isinstance(payload, dict) else {"allowed": True}
54155
+ except Exception:
54156
+ return {"allowed": True}
54157
+
54158
+
54159
+ def main():
54160
+ hook_input = read_hook_input()
54161
+ tool_name = str(hook_input.get("tool_name", ""))
54162
+ tool_input = hook_input.get("tool_input", {}) or {}
54163
+ if not isinstance(tool_input, dict):
54164
+ tool_input = {}
54165
+
54166
+ if not should_check(tool_name, tool_input):
54167
+ print(json.dumps({"decision": "allow"}))
54168
+ return
54169
+
54170
+ branch = current_branch()
54171
+ gate = query_gate(branch)
54172
+ if gate.get("allowed", True):
54173
+ print(json.dumps({"decision": "allow"}))
54174
+ return
54175
+
54176
+ message = gate.get("message") or "Complete the required SDLC phase before continuing."
54177
+ print(json.dumps({
54178
+ "decision": "block",
54179
+ "reason": f"SDLC phase gate blocked this operation on branch '{branch}': {message}",
54180
+ }))
54181
+
54182
+
54183
+ if __name__ == "__main__":
54184
+ main()
54185
+ `;
54120
54186
  }
54121
54187
  var init_hook_generator = __esm({
54122
54188
  "src/enforcement/hook-generator.ts"() {
@@ -56832,7 +56898,7 @@ Installing hooks for: ${platforms.join(", ")}`));
56832
56898
  if (!options.dryRun) {
56833
56899
  await installSdlcAgentHook(basePath, sdlcApiUrl, options.force);
56834
56900
  }
56835
- spinner.succeed(`Installed SDLC agent hook \u2192 ${source_default.dim(".claude/hooks/sdlc-phase-gate.json")}`);
56901
+ spinner.succeed(`Installed SDLC agent hook \u2192 ${source_default.dim(".claude/hooks/sdlc-phase-gate.py")}`);
56836
56902
  } catch (err) {
56837
56903
  const msg = err instanceof Error ? err.message : String(err);
56838
56904
  spinner.warn(`SDLC agent hook: ${msg}`);
@@ -57435,6 +57501,48 @@ Installing hooks for: ${platforms.join(", ")}`));
57435
57501
  process.exit(1);
57436
57502
  }
57437
57503
  });
57504
+ sdlcCommand.command("gate").description("Evaluate the native SDLC gate for a tracked branch").requiredOption("--branch <branch>", "Git branch to evaluate").option("--json", "Output as JSON").action(async (options) => {
57505
+ const { authToken, orgName, apiUrl } = getAuthAndOrg2();
57506
+ const spinner = options.json ? null : ora(`Evaluating SDLC gate for ${options.branch}...`).start();
57507
+ try {
57508
+ const branch = options.branch.trim();
57509
+ const res = await fetchApi3(
57510
+ `${apiUrl}/organizations/${encodeURIComponent(orgName)}/enforcement/sdlc/gate?branch=${encodeURIComponent(branch)}`,
57511
+ authToken
57512
+ );
57513
+ if (!res.ok) {
57514
+ const data2 = await res.json().catch(() => ({}));
57515
+ throw new Error(data2.error || `HTTP ${res.status}`);
57516
+ }
57517
+ const data = await res.json();
57518
+ spinner?.stop();
57519
+ if (options.json) {
57520
+ console.log(JSON.stringify(data, null, 2));
57521
+ return;
57522
+ }
57523
+ const allowed = data.allowed !== false;
57524
+ const message = typeof data.message === "string" ? data.message : void 0;
57525
+ const source = typeof data.source === "string" ? data.source : "unknown";
57526
+ console.log(source_default.bold("\nNative SDLC Branch Gate\n"));
57527
+ console.log(` Organization: ${source_default.cyan(orgName)}`);
57528
+ console.log(` Branch: ${branch}`);
57529
+ console.log(` Allowed: ${allowed ? source_default.green("yes") : source_default.red("no")}`);
57530
+ console.log(` Source: ${source_default.dim(source)}`);
57531
+ if (message) {
57532
+ console.log(` Message: ${message}`);
57533
+ }
57534
+ console.log();
57535
+ } catch (error3) {
57536
+ spinner?.stop();
57537
+ const msg = error3 instanceof Error ? error3.message : String(error3);
57538
+ if (options.json) {
57539
+ console.log(JSON.stringify({ error: msg }));
57540
+ } else {
57541
+ console.error(source_default.red("Error evaluating SDLC branch gate:"), msg);
57542
+ }
57543
+ process.exit(1);
57544
+ }
57545
+ });
57438
57546
  sdlcCommand.command("set <level>").description("Set native SDLC enforcement mode (off, warn, block)").option("--reason <reason>", "Reason to include in the audit trail").option("--json", "Output as JSON").action(async (level, options) => {
57439
57547
  if (!["off", "warn", "block"].includes(level)) {
57440
57548
  console.error(source_default.red("Invalid level. Use one of: off, warn, block"));
@@ -57805,13 +57913,40 @@ async function installSdlcPrePushHook(basePath, apiUrl, orgName, force) {
57805
57913
  }
57806
57914
  async function installSdlcAgentHook(basePath, apiUrl, force) {
57807
57915
  const claudeHooksDir = (0, import_node_path3.join)(basePath, ".claude", "hooks");
57808
- const hookPath = (0, import_node_path3.join)(claudeHooksDir, "sdlc-phase-gate.json");
57916
+ const hookPath = (0, import_node_path3.join)(claudeHooksDir, "sdlc-phase-gate.py");
57809
57917
  if ((0, import_node_fs2.existsSync)(hookPath) && !force) {
57810
57918
  throw new Error("SDLC agent hook already installed. Use --force to overwrite.");
57811
57919
  }
57812
57920
  await (0, import_promises4.mkdir)(claudeHooksDir, { recursive: true });
57813
57921
  const hookContent = generateSdlcAgentHook(apiUrl);
57814
57922
  await (0, import_promises4.writeFile)(hookPath, hookContent, "utf-8");
57923
+ await (0, import_promises4.chmod)(hookPath, 493);
57924
+ const claudeDir = (0, import_node_path3.join)(basePath, ".claude");
57925
+ await (0, import_promises4.mkdir)(claudeDir, { recursive: true });
57926
+ const settingsPath = (0, import_node_path3.join)(claudeDir, "settings.json");
57927
+ let settings = {};
57928
+ try {
57929
+ const existingSettings = await (0, import_promises4.readFile)(settingsPath, "utf-8");
57930
+ settings = JSON.parse(existingSettings);
57931
+ } catch {
57932
+ settings = {};
57933
+ }
57934
+ const hooks = settings.hooks && typeof settings.hooks === "object" ? { ...settings.hooks } : {};
57935
+ const existingPreToolUse = Array.isArray(hooks.PreToolUse) ? [...hooks.PreToolUse] : [];
57936
+ const filteredPreToolUse = existingPreToolUse.filter((entry) => {
57937
+ if (!entry || typeof entry !== "object") {
57938
+ return true;
57939
+ }
57940
+ return entry.command !== "python3 .claude/hooks/sdlc-phase-gate.py";
57941
+ });
57942
+ filteredPreToolUse.push({
57943
+ type: "command",
57944
+ command: "python3 .claude/hooks/sdlc-phase-gate.py",
57945
+ timeout: 5e3
57946
+ });
57947
+ hooks.PreToolUse = filteredPreToolUse;
57948
+ settings.hooks = hooks;
57949
+ await (0, import_promises4.writeFile)(settingsPath, JSON.stringify(settings, null, 2), "utf-8");
57815
57950
  }
57816
57951
  var import_promises4, import_node_fs2, import_node_path3, import_node_crypto, defaultApiUrl7, PLATFORM_DIRS3, SCAN_PLATFORM_DIRS, ENFORCEMENT_HOOK_SUPPORT, GAL_PRECOMMIT_MARKER, GAL_PRECOMMIT_CONTENT, GAL_SDLC_PREPUSH_MARKER;
57817
57952
  var init_enforce = __esm({
@@ -70904,7 +71039,7 @@ var init_index = __esm({
70904
71039
  });
70905
71040
 
70906
71041
  // src/bootstrap.ts
70907
- var cliVersion10 = true ? "0.0.378" : "0.0.0-dev";
71042
+ var cliVersion10 = true ? "0.0.380" : "0.0.0-dev";
70908
71043
  var args = process.argv.slice(2);
70909
71044
  var requestedGlobalHelp = args.length === 1 && (args[0] === "--help" || args[0] === "-h");
70910
71045
  var requestedVersion = args.length === 1 && (args[0] === "--version" || args[0] === "-V");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scheduler-systems/gal-run",
3
- "version": "0.0.378",
3
+ "version": "0.0.380",
4
4
  "description": "GAL CLI - Command-line tool for managing AI agent configurations across your organization",
5
5
  "license": "Elastic-2.0",
6
6
  "private": false,