safe-push 0.2.1 → 0.3.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/dist/index.js CHANGED
@@ -6704,9 +6704,11 @@ var coerce = {
6704
6704
  var NEVER = INVALID;
6705
6705
  // src/types.ts
6706
6706
  var OnForbiddenSchema = exports_external.enum(["error", "prompt"]);
6707
+ var RepoVisibilitySchema = exports_external.enum(["public", "private", "internal"]);
6707
6708
  var ConfigSchema = exports_external.object({
6708
6709
  forbiddenPaths: exports_external.array(exports_external.string()).default([".github/"]),
6709
- onForbidden: OnForbiddenSchema.default("error")
6710
+ onForbidden: OnForbiddenSchema.default("error"),
6711
+ allowedVisibility: exports_external.array(RepoVisibilitySchema).optional()
6710
6712
  });
6711
6713
 
6712
6714
  class GitError extends Error {
@@ -6775,12 +6777,15 @@ function saveConfig(config, configPath) {
6775
6777
  if (!fs.existsSync(dir)) {
6776
6778
  fs.mkdirSync(dir, { recursive: true });
6777
6779
  }
6780
+ const allowedVisibilitySection = config.allowedVisibility ? `,
6781
+ // \u8A31\u53EF\u3059\u308B\u30EA\u30DD\u30B8\u30C8\u30EA visibility: "public" | "private" | "internal"
6782
+ "allowedVisibility": ${JSON.stringify(config.allowedVisibility)}` : "";
6778
6783
  const content = `{
6779
6784
  // \u7981\u6B62\u30A8\u30EA\u30A2\uFF08Glob\u30D1\u30BF\u30FC\u30F3\uFF09
6780
6785
  "forbiddenPaths": ${JSON.stringify(config.forbiddenPaths, null, 4).replace(/\n/g, `
6781
6786
  `)},
6782
6787
  // \u7981\u6B62\u6642\u306E\u52D5\u4F5C: "error" | "prompt"
6783
- "onForbidden": "${config.onForbidden}"
6788
+ "onForbidden": "${config.onForbidden}"${allowedVisibilitySection}
6784
6789
  }
6785
6790
  `;
6786
6791
  fs.writeFileSync(filePath, content, "utf-8");
@@ -6888,6 +6893,20 @@ async function execPush(args = [], remote = "origin") {
6888
6893
  };
6889
6894
  }
6890
6895
  }
6896
+ async function getRepoVisibility() {
6897
+ const command = "gh repo view --json visibility --jq '.visibility'";
6898
+ try {
6899
+ const result = await $`gh repo view --json visibility --jq .visibility`.quiet();
6900
+ return result.stdout.toString().trim().toLowerCase();
6901
+ } catch (error) {
6902
+ if (error && typeof error === "object" && "exitCode" in error) {
6903
+ const exitCode = error.exitCode;
6904
+ const stderr = "stderr" in error ? String(error.stderr) : "";
6905
+ throw new GitError(`Failed to get repository visibility: ${stderr || command}`, command, exitCode);
6906
+ }
6907
+ throw new GitError(`Failed to get repository visibility: ${command}`, command, null);
6908
+ }
6909
+ }
6891
6910
  async function isGitRepository() {
6892
6911
  try {
6893
6912
  await execGit(["rev-parse", "--git-dir"]);
@@ -6906,6 +6925,18 @@ async function hasCommits() {
6906
6925
  }
6907
6926
 
6908
6927
  // src/checker.ts
6928
+ async function checkVisibility(allowedVisibility) {
6929
+ if (!allowedVisibility || allowedVisibility.length === 0) {
6930
+ return null;
6931
+ }
6932
+ const visibility = await getRepoVisibility();
6933
+ const allowed = allowedVisibility.includes(visibility);
6934
+ return {
6935
+ allowed,
6936
+ reason: allowed ? `Repository visibility "${visibility}" is allowed` : `Repository visibility "${visibility}" is not in allowed list: [${allowedVisibility.join(", ")}]`,
6937
+ visibility
6938
+ };
6939
+ }
6909
6940
  function matchesForbiddenPath(filePath, forbiddenPaths) {
6910
6941
  for (const pattern of forbiddenPaths) {
6911
6942
  if (pattern.endsWith("/")) {
@@ -7004,6 +7035,10 @@ function printCheckResultHuman(result) {
7004
7035
  console.log(` Local user email: ${details.localEmail}`);
7005
7036
  console.log(` Own last commit: ${details.isOwnLastCommit ? "Yes" : "No"}`);
7006
7037
  console.log(` Forbidden changes: ${details.hasForbiddenChanges ? "Yes" : "No"}`);
7038
+ if (details.repoVisibility !== undefined) {
7039
+ console.log(` Repo visibility: ${details.repoVisibility}`);
7040
+ console.log(` Visibility allowed: ${details.visibilityAllowed ? "Yes" : "No"}`);
7041
+ }
7007
7042
  if (details.forbiddenFiles.length > 0) {
7008
7043
  console.log("");
7009
7044
  console.log("Forbidden files changed:");
@@ -7037,6 +7072,24 @@ function createCheckCommand() {
7037
7072
  }
7038
7073
  const config = loadConfig();
7039
7074
  const result = await checkPush(config);
7075
+ if (config.allowedVisibility && config.allowedVisibility.length > 0) {
7076
+ try {
7077
+ const visibilityResult = await checkVisibility(config.allowedVisibility);
7078
+ if (visibilityResult) {
7079
+ result.details.repoVisibility = visibilityResult.visibility;
7080
+ result.details.visibilityAllowed = visibilityResult.allowed;
7081
+ if (!visibilityResult.allowed) {
7082
+ result.allowed = false;
7083
+ result.reason = visibilityResult.reason;
7084
+ }
7085
+ }
7086
+ } catch (error) {
7087
+ result.details.repoVisibility = "unknown";
7088
+ result.details.visibilityAllowed = false;
7089
+ result.allowed = false;
7090
+ result.reason = `Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.`;
7091
+ }
7092
+ }
7040
7093
  if (options.json) {
7041
7094
  printCheckResultJson(result);
7042
7095
  } else {
@@ -7064,6 +7117,19 @@ function createPushCommand() {
7064
7117
  process.exit(1);
7065
7118
  }
7066
7119
  const config = loadConfig();
7120
+ if (config.allowedVisibility && config.allowedVisibility.length > 0) {
7121
+ try {
7122
+ const visibilityResult = await checkVisibility(config.allowedVisibility);
7123
+ if (visibilityResult && !visibilityResult.allowed) {
7124
+ printError(visibilityResult.reason);
7125
+ process.exit(1);
7126
+ }
7127
+ } catch (error) {
7128
+ printError(`Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.
7129
+ ${error instanceof Error ? error.message : String(error)}`);
7130
+ process.exit(1);
7131
+ }
7132
+ }
7067
7133
  if (options.force) {
7068
7134
  printWarning("Safety checks bypassed with --force");
7069
7135
  if (options.dryRun) {
@@ -7160,6 +7226,7 @@ function createConfigCommand() {
7160
7226
  console.log("Settings:");
7161
7227
  console.log(` forbiddenPaths: ${JSON.stringify(configData.forbiddenPaths)}`);
7162
7228
  console.log(` onForbidden: ${configData.onForbidden}`);
7229
+ console.log(` allowedVisibility: ${configData.allowedVisibility ? JSON.stringify(configData.allowedVisibility) : "(not set - all visibilities allowed)"}`);
7163
7230
  console.log("");
7164
7231
  }
7165
7232
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safe-push",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Git push safety checker - blocks pushes to forbidden areas",
5
5
  "type": "module",
6
6
  "bin": {
package/src/checker.ts CHANGED
@@ -1,12 +1,45 @@
1
- import type { Config, CheckResult } from "./types";
1
+ import type { Config, CheckResult, RepoVisibility } from "./types";
2
2
  import {
3
3
  getCurrentBranch,
4
4
  isNewBranch,
5
5
  getLastCommitAuthorEmail,
6
6
  getLocalEmail,
7
7
  getDiffFiles,
8
+ getRepoVisibility,
8
9
  } from "./git";
9
10
 
11
+ /**
12
+ * Visibility チェック結果
13
+ */
14
+ export interface VisibilityCheckResult {
15
+ allowed: boolean;
16
+ reason: string;
17
+ visibility: string;
18
+ }
19
+
20
+ /**
21
+ * リポジトリの visibility が許可リストに含まれるかチェック
22
+ * allowedVisibility が未設定または空配列の場合は null を返す(チェック不要)
23
+ */
24
+ export async function checkVisibility(
25
+ allowedVisibility?: RepoVisibility[]
26
+ ): Promise<VisibilityCheckResult | null> {
27
+ if (!allowedVisibility || allowedVisibility.length === 0) {
28
+ return null;
29
+ }
30
+
31
+ const visibility = await getRepoVisibility();
32
+ const allowed = allowedVisibility.includes(visibility as RepoVisibility);
33
+
34
+ return {
35
+ allowed,
36
+ reason: allowed
37
+ ? `Repository visibility "${visibility}" is allowed`
38
+ : `Repository visibility "${visibility}" is not in allowed list: [${allowedVisibility.join(", ")}]`,
39
+ visibility,
40
+ };
41
+ }
42
+
10
43
  /**
11
44
  * ファイルパスが禁止パターンにマッチするか判定
12
45
  */
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { loadConfig } from "../config";
3
- import { checkPush } from "../checker";
3
+ import { checkPush, checkVisibility } from "../checker";
4
4
  import { isGitRepository, hasCommits } from "../git";
5
5
  import { printError, printCheckResultJson, printCheckResultHuman } from "./utils";
6
6
 
@@ -28,6 +28,26 @@ export function createCheckCommand(): Command {
28
28
  const config = loadConfig();
29
29
  const result = await checkPush(config);
30
30
 
31
+ // visibility チェック
32
+ if (config.allowedVisibility && config.allowedVisibility.length > 0) {
33
+ try {
34
+ const visibilityResult = await checkVisibility(config.allowedVisibility);
35
+ if (visibilityResult) {
36
+ result.details.repoVisibility = visibilityResult.visibility;
37
+ result.details.visibilityAllowed = visibilityResult.allowed;
38
+ if (!visibilityResult.allowed) {
39
+ result.allowed = false;
40
+ result.reason = visibilityResult.reason;
41
+ }
42
+ }
43
+ } catch (error) {
44
+ result.details.repoVisibility = "unknown";
45
+ result.details.visibilityAllowed = false;
46
+ result.allowed = false;
47
+ result.reason = `Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.`;
48
+ }
49
+ }
50
+
31
51
  if (options.json) {
32
52
  printCheckResultJson(result);
33
53
  } else {
@@ -68,6 +68,9 @@ export function createConfigCommand(): Command {
68
68
  ` forbiddenPaths: ${JSON.stringify(configData.forbiddenPaths)}`
69
69
  );
70
70
  console.log(` onForbidden: ${configData.onForbidden}`);
71
+ console.log(
72
+ ` allowedVisibility: ${configData.allowedVisibility ? JSON.stringify(configData.allowedVisibility) : "(not set - all visibilities allowed)"}`
73
+ );
71
74
  console.log("");
72
75
  }
73
76
  } catch (error) {
@@ -1,6 +1,6 @@
1
1
  import { Command } from "commander";
2
2
  import { loadConfig } from "../config";
3
- import { checkPush } from "../checker";
3
+ import { checkPush, checkVisibility } from "../checker";
4
4
  import { isGitRepository, hasCommits, execPush } from "../git";
5
5
  import {
6
6
  printError,
@@ -36,6 +36,22 @@ export function createPushCommand(): Command {
36
36
 
37
37
  const config = loadConfig();
38
38
 
39
+ // visibility チェック(--force でもバイパスできない)
40
+ if (config.allowedVisibility && config.allowedVisibility.length > 0) {
41
+ try {
42
+ const visibilityResult = await checkVisibility(config.allowedVisibility);
43
+ if (visibilityResult && !visibilityResult.allowed) {
44
+ printError(visibilityResult.reason);
45
+ process.exit(1);
46
+ }
47
+ } catch (error) {
48
+ printError(
49
+ `Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.\n ${error instanceof Error ? error.message : String(error)}`
50
+ );
51
+ process.exit(1);
52
+ }
53
+ }
54
+
39
55
  // --forceオプションが指定されている場合はチェックをスキップ
40
56
  if (options.force) {
41
57
  printWarning("Safety checks bypassed with --force");
@@ -60,6 +60,13 @@ export function printCheckResultHuman(result: CheckResult): void {
60
60
  ` Forbidden changes: ${details.hasForbiddenChanges ? "Yes" : "No"}`
61
61
  );
62
62
 
63
+ if (details.repoVisibility !== undefined) {
64
+ console.log(` Repo visibility: ${details.repoVisibility}`);
65
+ console.log(
66
+ ` Visibility allowed: ${details.visibilityAllowed ? "Yes" : "No"}`
67
+ );
68
+ }
69
+
63
70
  if (details.forbiddenFiles.length > 0) {
64
71
  console.log("");
65
72
  console.log("Forbidden files changed:");
package/src/config.ts CHANGED
@@ -88,11 +88,15 @@ export function saveConfig(config: Config, configPath?: string): void {
88
88
  fs.mkdirSync(dir, { recursive: true });
89
89
  }
90
90
 
91
+ const allowedVisibilitySection = config.allowedVisibility
92
+ ? `,\n // 許可するリポジトリ visibility: "public" | "private" | "internal"\n "allowedVisibility": ${JSON.stringify(config.allowedVisibility)}`
93
+ : "";
94
+
91
95
  const content = `{
92
96
  // 禁止エリア(Globパターン)
93
97
  "forbiddenPaths": ${JSON.stringify(config.forbiddenPaths, null, 4).replace(/\n/g, "\n ")},
94
98
  // 禁止時の動作: "error" | "prompt"
95
- "onForbidden": "${config.onForbidden}"
99
+ "onForbidden": "${config.onForbidden}"${allowedVisibilitySection}
96
100
  }
97
101
  `;
98
102
 
package/src/git.ts CHANGED
@@ -152,6 +152,29 @@ export async function execPush(
152
152
  }
153
153
  }
154
154
 
155
+ /**
156
+ * リポジトリの visibility を取得(gh CLI を使用)
157
+ */
158
+ export async function getRepoVisibility(): Promise<string> {
159
+ const command = "gh repo view --json visibility --jq '.visibility'";
160
+ try {
161
+ const result = await $`gh repo view --json visibility --jq .visibility`.quiet();
162
+ return result.stdout.toString().trim().toLowerCase();
163
+ } catch (error) {
164
+ if (error && typeof error === "object" && "exitCode" in error) {
165
+ const exitCode = (error as { exitCode: number }).exitCode;
166
+ const stderr =
167
+ "stderr" in error ? String((error as { stderr: unknown }).stderr) : "";
168
+ throw new GitError(
169
+ `Failed to get repository visibility: ${stderr || command}`,
170
+ command,
171
+ exitCode
172
+ );
173
+ }
174
+ throw new GitError(`Failed to get repository visibility: ${command}`, command, null);
175
+ }
176
+ }
177
+
155
178
  /**
156
179
  * Gitリポジトリ内かどうかを確認
157
180
  */
package/src/types.ts CHANGED
@@ -6,12 +6,19 @@ import { z } from "zod";
6
6
  export const OnForbiddenSchema = z.enum(["error", "prompt"]);
7
7
  export type OnForbidden = z.infer<typeof OnForbiddenSchema>;
8
8
 
9
+ /**
10
+ * リポジトリの visibility
11
+ */
12
+ export const RepoVisibilitySchema = z.enum(["public", "private", "internal"]);
13
+ export type RepoVisibility = z.infer<typeof RepoVisibilitySchema>;
14
+
9
15
  /**
10
16
  * 設定ファイルのスキーマ
11
17
  */
12
18
  export const ConfigSchema = z.object({
13
19
  forbiddenPaths: z.array(z.string()).default([".github/"]),
14
20
  onForbidden: OnForbiddenSchema.default("error"),
21
+ allowedVisibility: z.array(RepoVisibilitySchema).optional(),
15
22
  });
16
23
  export type Config = z.infer<typeof ConfigSchema>;
17
24
 
@@ -29,6 +36,8 @@ export interface CheckResult {
29
36
  currentBranch: string;
30
37
  authorEmail: string;
31
38
  localEmail: string;
39
+ repoVisibility?: string;
40
+ visibilityAllowed?: boolean;
32
41
  };
33
42
  }
34
43