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 +69 -2
- package/package.json +1 -1
- package/src/checker.ts +34 -1
- package/src/commands/check.ts +21 -1
- package/src/commands/config.ts +3 -0
- package/src/commands/push.ts +17 -1
- package/src/commands/utils.ts +7 -0
- package/src/config.ts +5 -1
- package/src/git.ts +23 -0
- package/src/types.ts +9 -0
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
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
|
*/
|
package/src/commands/check.ts
CHANGED
|
@@ -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 {
|
package/src/commands/config.ts
CHANGED
|
@@ -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) {
|
package/src/commands/push.ts
CHANGED
|
@@ -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");
|
package/src/commands/utils.ts
CHANGED
|
@@ -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
|
|