safe-push 0.2.0 → 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");
@@ -6859,15 +6864,21 @@ async function getDiffFiles(remote = "origin") {
6859
6864
  `).filter(Boolean);
6860
6865
  }
6861
6866
  async function execPush(args = [], remote = "origin") {
6862
- const branch = await getCurrentBranch();
6863
- const isNew = await isNewBranch(remote);
6864
- const pushArgs = ["push"];
6865
- if (isNew) {
6866
- pushArgs.push("-u");
6867
- }
6868
- pushArgs.push(remote, branch);
6869
- if (args.length > 0) {
6870
- pushArgs.push(...args);
6867
+ let pushArgs;
6868
+ const hasUserRefspec = args.some((arg) => !arg.startsWith("-"));
6869
+ if (hasUserRefspec) {
6870
+ pushArgs = ["push", ...args];
6871
+ } else {
6872
+ const branch = await getCurrentBranch();
6873
+ const isNew = await isNewBranch(remote);
6874
+ pushArgs = ["push"];
6875
+ const hasSetUpstream = args.some((a) => a === "-u" || a === "--set-upstream");
6876
+ if (isNew || hasSetUpstream) {
6877
+ pushArgs.push("-u");
6878
+ }
6879
+ pushArgs.push(remote, branch);
6880
+ const remainingFlags = args.filter((a) => a !== "-u" && a !== "--set-upstream");
6881
+ pushArgs.push(...remainingFlags);
6871
6882
  }
6872
6883
  try {
6873
6884
  const output = await execGit(pushArgs);
@@ -6882,6 +6893,20 @@ async function execPush(args = [], remote = "origin") {
6882
6893
  };
6883
6894
  }
6884
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
+ }
6885
6910
  async function isGitRepository() {
6886
6911
  try {
6887
6912
  await execGit(["rev-parse", "--git-dir"]);
@@ -6900,6 +6925,18 @@ async function hasCommits() {
6900
6925
  }
6901
6926
 
6902
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
+ }
6903
6940
  function matchesForbiddenPath(filePath, forbiddenPaths) {
6904
6941
  for (const pattern of forbiddenPaths) {
6905
6942
  if (pattern.endsWith("/")) {
@@ -6998,6 +7035,10 @@ function printCheckResultHuman(result) {
6998
7035
  console.log(` Local user email: ${details.localEmail}`);
6999
7036
  console.log(` Own last commit: ${details.isOwnLastCommit ? "Yes" : "No"}`);
7000
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
+ }
7001
7042
  if (details.forbiddenFiles.length > 0) {
7002
7043
  console.log("");
7003
7044
  console.log("Forbidden files changed:");
@@ -7031,6 +7072,24 @@ function createCheckCommand() {
7031
7072
  }
7032
7073
  const config = loadConfig();
7033
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
+ }
7034
7093
  if (options.json) {
7035
7094
  printCheckResultJson(result);
7036
7095
  } else {
@@ -7058,6 +7117,19 @@ function createPushCommand() {
7058
7117
  process.exit(1);
7059
7118
  }
7060
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
+ }
7061
7133
  if (options.force) {
7062
7134
  printWarning("Safety checks bypassed with --force");
7063
7135
  if (options.dryRun) {
@@ -7154,6 +7226,7 @@ function createConfigCommand() {
7154
7226
  console.log("Settings:");
7155
7227
  console.log(` forbiddenPaths: ${JSON.stringify(configData.forbiddenPaths)}`);
7156
7228
  console.log(` onForbidden: ${configData.onForbidden}`);
7229
+ console.log(` allowedVisibility: ${configData.allowedVisibility ? JSON.stringify(configData.allowedVisibility) : "(not set - all visibilities allowed)"}`);
7157
7230
  console.log("");
7158
7231
  }
7159
7232
  } catch (error) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "safe-push",
3
- "version": "0.2.0",
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
@@ -106,21 +106,36 @@ export async function execPush(
106
106
  args: string[] = [],
107
107
  remote = "origin"
108
108
  ): Promise<{ success: boolean; output: string }> {
109
- const branch = await getCurrentBranch();
110
- const isNew = await isNewBranch(remote);
109
+ let pushArgs: string[];
111
110
 
112
- const pushArgs = ["push"];
111
+ // ユーザーがremote/refspecを明示的に指定したか判定
112
+ const hasUserRefspec = args.some((arg) => !arg.startsWith("-"));
113
113
 
114
- // 新規ブランチの場合は-uオプションを追加
115
- if (isNew) {
116
- pushArgs.push("-u");
117
- }
114
+ if (hasUserRefspec) {
115
+ // ユーザー指定のremote/refspecをそのまま使用
116
+ pushArgs = ["push", ...args];
117
+ } else {
118
+ // 自動でremote/branchを決定
119
+ const branch = await getCurrentBranch();
120
+ const isNew = await isNewBranch(remote);
121
+
122
+ pushArgs = ["push"];
123
+
124
+ // 新規ブランチ、またはユーザーが-uを指定した場合に追加
125
+ const hasSetUpstream = args.some(
126
+ (a) => a === "-u" || a === "--set-upstream"
127
+ );
128
+ if (isNew || hasSetUpstream) {
129
+ pushArgs.push("-u");
130
+ }
118
131
 
119
- pushArgs.push(remote, branch);
132
+ pushArgs.push(remote, branch);
120
133
 
121
- // 追加の引数がある場合
122
- if (args.length > 0) {
123
- pushArgs.push(...args);
134
+ // -u/--set-upstream以外のフラグを追加
135
+ const remainingFlags = args.filter(
136
+ (a) => a !== "-u" && a !== "--set-upstream"
137
+ );
138
+ pushArgs.push(...remainingFlags);
124
139
  }
125
140
 
126
141
  try {
@@ -137,6 +152,29 @@ export async function execPush(
137
152
  }
138
153
  }
139
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
+
140
178
  /**
141
179
  * Gitリポジトリ内かどうかを確認
142
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