safe-push 0.2.1 → 0.4.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/.claude/settings.local.json +8 -0
- package/bun.lock +54 -0
- package/config.schema.json +47 -0
- package/dist/index.js +7418 -143
- package/package.json +10 -3
- package/scripts/generate-schema.ts +13 -0
- package/src/checker.ts +101 -49
- package/src/commands/check.ts +38 -11
- package/src/commands/config.ts +3 -0
- package/src/commands/push.ts +50 -21
- package/src/commands/utils.ts +7 -0
- package/src/config.ts +13 -1
- package/src/git.ts +142 -83
- package/src/index.ts +37 -2
- package/src/telemetry.ts +95 -0
- package/src/types.ts +27 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "safe-push",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Git push safety checker - blocks pushes to forbidden areas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -9,9 +9,15 @@
|
|
|
9
9
|
"scripts": {
|
|
10
10
|
"build": "bun build ./src/index.ts --outdir ./dist --target bun",
|
|
11
11
|
"typecheck": "tsc --noEmit",
|
|
12
|
-
"dev": "bun run ./src/index.ts"
|
|
12
|
+
"dev": "bun run ./src/index.ts",
|
|
13
|
+
"generate-schema": "bun run ./scripts/generate-schema.ts"
|
|
13
14
|
},
|
|
14
15
|
"dependencies": {
|
|
16
|
+
"@opentelemetry/api": "^1.9.0",
|
|
17
|
+
"@opentelemetry/exporter-trace-otlp-http": "^0.212.0",
|
|
18
|
+
"@opentelemetry/resources": "^2.5.1",
|
|
19
|
+
"@opentelemetry/sdk-trace-base": "^2.5.1",
|
|
20
|
+
"@opentelemetry/semantic-conventions": "^1.39.0",
|
|
15
21
|
"commander": "^12.1.0",
|
|
16
22
|
"jsonc-parser": "^3.3.1",
|
|
17
23
|
"zod": "^3.23.8"
|
|
@@ -19,6 +25,7 @@
|
|
|
19
25
|
"devDependencies": {
|
|
20
26
|
"@types/bun": "latest",
|
|
21
27
|
"@types/node": "^22.10.0",
|
|
22
|
-
"typescript": "^5.7.2"
|
|
28
|
+
"typescript": "^5.7.2",
|
|
29
|
+
"zod-to-json-schema": "^3.25.1"
|
|
23
30
|
}
|
|
24
31
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { zodToJsonSchema } from "zod-to-json-schema";
|
|
4
|
+
import { ConfigSchema } from "../src/types";
|
|
5
|
+
|
|
6
|
+
const jsonSchema = zodToJsonSchema(ConfigSchema, {
|
|
7
|
+
name: "SafePushConfig",
|
|
8
|
+
$refStrategy: "none",
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const outPath = path.join(import.meta.dirname, "..", "config.schema.json");
|
|
12
|
+
fs.writeFileSync(outPath, JSON.stringify(jsonSchema, null, 2) + "\n", "utf-8");
|
|
13
|
+
console.log(`Generated ${outPath}`);
|
package/src/checker.ts
CHANGED
|
@@ -1,11 +1,52 @@
|
|
|
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";
|
|
10
|
+
import { withSpan } from "./telemetry";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Visibility チェック結果
|
|
14
|
+
*/
|
|
15
|
+
export interface VisibilityCheckResult {
|
|
16
|
+
allowed: boolean;
|
|
17
|
+
reason: string;
|
|
18
|
+
visibility: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* リポジトリの visibility が許可リストに含まれるかチェック
|
|
23
|
+
* allowedVisibility が未設定または空配列の場合は null を返す(チェック不要)
|
|
24
|
+
*/
|
|
25
|
+
export async function checkVisibility(
|
|
26
|
+
allowedVisibility?: RepoVisibility[]
|
|
27
|
+
): Promise<VisibilityCheckResult | null> {
|
|
28
|
+
return withSpan("safe-push.check.visibility", async (span) => {
|
|
29
|
+
if (!allowedVisibility || allowedVisibility.length === 0) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const visibility = await getRepoVisibility();
|
|
34
|
+
const allowed = allowedVisibility.includes(visibility as RepoVisibility);
|
|
35
|
+
|
|
36
|
+
span.addEvent("visibility.result", {
|
|
37
|
+
value: visibility,
|
|
38
|
+
allowed,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
return {
|
|
42
|
+
allowed,
|
|
43
|
+
reason: allowed
|
|
44
|
+
? `Repository visibility "${visibility}" is allowed`
|
|
45
|
+
: `Repository visibility "${visibility}" is not in allowed list: [${allowedVisibility.join(", ")}]`,
|
|
46
|
+
visibility,
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
}
|
|
9
50
|
|
|
10
51
|
/**
|
|
11
52
|
* ファイルパスが禁止パターンにマッチするか判定
|
|
@@ -55,58 +96,69 @@ function findForbiddenFiles(
|
|
|
55
96
|
* (禁止エリア変更なし) AND (新規ブランチ OR 最終コミットが自分)
|
|
56
97
|
*/
|
|
57
98
|
export async function checkPush(config: Config): Promise<CheckResult> {
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
const forbiddenFiles = findForbiddenFiles(diffFiles, config.forbiddenPaths);
|
|
65
|
-
const hasForbiddenChanges = forbiddenFiles.length > 0;
|
|
66
|
-
const isOwnLastCommit =
|
|
67
|
-
authorEmail.toLowerCase() === localEmail.toLowerCase();
|
|
99
|
+
return withSpan("safe-push.check.push", async (span) => {
|
|
100
|
+
const currentBranch = await getCurrentBranch();
|
|
101
|
+
const newBranch = await isNewBranch();
|
|
102
|
+
const authorEmail = await getLastCommitAuthorEmail();
|
|
103
|
+
const localEmail = await getLocalEmail();
|
|
104
|
+
const diffFiles = await getDiffFiles();
|
|
68
105
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
isOwnLastCommit
|
|
72
|
-
|
|
73
|
-
forbiddenFiles,
|
|
74
|
-
currentBranch,
|
|
75
|
-
authorEmail,
|
|
76
|
-
localEmail,
|
|
77
|
-
};
|
|
106
|
+
const forbiddenFiles = findForbiddenFiles(diffFiles, config.forbiddenPaths);
|
|
107
|
+
const hasForbiddenChanges = forbiddenFiles.length > 0;
|
|
108
|
+
const isOwnLastCommit =
|
|
109
|
+
authorEmail.toLowerCase() === localEmail.toLowerCase();
|
|
78
110
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
111
|
+
const details = {
|
|
112
|
+
isNewBranch: newBranch,
|
|
113
|
+
isOwnLastCommit,
|
|
114
|
+
hasForbiddenChanges,
|
|
115
|
+
forbiddenFiles,
|
|
116
|
+
currentBranch,
|
|
117
|
+
authorEmail,
|
|
118
|
+
localEmail,
|
|
85
119
|
};
|
|
86
|
-
}
|
|
87
120
|
|
|
88
|
-
|
|
89
|
-
if (newBranch) {
|
|
90
|
-
return {
|
|
91
|
-
allowed: true,
|
|
92
|
-
reason: "New branch - no restrictions",
|
|
93
|
-
details,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
121
|
+
let result: CheckResult;
|
|
96
122
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
123
|
+
// 禁止エリアに変更がある場合は常にブロック
|
|
124
|
+
if (hasForbiddenChanges) {
|
|
125
|
+
result = {
|
|
126
|
+
allowed: false,
|
|
127
|
+
reason: `Forbidden files detected: ${forbiddenFiles.join(", ")}`,
|
|
128
|
+
details,
|
|
129
|
+
};
|
|
130
|
+
} else if (newBranch) {
|
|
131
|
+
// 新規ブランチの場合は許可
|
|
132
|
+
result = {
|
|
133
|
+
allowed: true,
|
|
134
|
+
reason: "New branch - no restrictions",
|
|
135
|
+
details,
|
|
136
|
+
};
|
|
137
|
+
} else if (isOwnLastCommit) {
|
|
138
|
+
// 最終コミットが自分の場合は許可
|
|
139
|
+
result = {
|
|
140
|
+
allowed: true,
|
|
141
|
+
reason: "Last commit is yours",
|
|
142
|
+
details,
|
|
143
|
+
};
|
|
144
|
+
} else {
|
|
145
|
+
// それ以外はブロック
|
|
146
|
+
result = {
|
|
147
|
+
allowed: false,
|
|
148
|
+
reason: `Last commit is by someone else (${authorEmail})`,
|
|
149
|
+
details,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
span.addEvent("check.result", {
|
|
154
|
+
allowed: result.allowed,
|
|
155
|
+
reason: result.reason,
|
|
156
|
+
isNewBranch: newBranch,
|
|
157
|
+
isOwnLastCommit,
|
|
158
|
+
hasForbiddenChanges,
|
|
159
|
+
forbiddenFileCount: forbiddenFiles.length,
|
|
160
|
+
});
|
|
105
161
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
allowed: false,
|
|
109
|
-
reason: `Last commit is by someone else (${authorEmail})`,
|
|
110
|
-
details,
|
|
111
|
-
};
|
|
162
|
+
return result;
|
|
163
|
+
});
|
|
112
164
|
}
|
package/src/commands/check.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
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
|
+
import { ExitError } from "../types";
|
|
7
|
+
import { withSpan } from "../telemetry";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* checkコマンドを作成
|
|
@@ -12,34 +14,59 @@ export function createCheckCommand(): Command {
|
|
|
12
14
|
.description("Check if push is allowed")
|
|
13
15
|
.option("--json", "Output result as JSON")
|
|
14
16
|
.action(async (options: { json?: boolean }) => {
|
|
15
|
-
|
|
17
|
+
await withSpan("safe-push.check", async (rootSpan) => {
|
|
16
18
|
// Gitリポジトリ内か確認
|
|
17
19
|
if (!(await isGitRepository())) {
|
|
18
20
|
printError("Not a git repository");
|
|
19
|
-
|
|
21
|
+
throw new ExitError(1);
|
|
20
22
|
}
|
|
21
23
|
|
|
22
24
|
// コミットが存在するか確認
|
|
23
25
|
if (!(await hasCommits())) {
|
|
24
26
|
printError("No commits found");
|
|
25
|
-
|
|
27
|
+
throw new ExitError(1);
|
|
26
28
|
}
|
|
27
29
|
|
|
28
30
|
const config = loadConfig();
|
|
31
|
+
|
|
32
|
+
rootSpan.addEvent("config.loaded", {
|
|
33
|
+
forbiddenPaths: JSON.stringify(config.forbiddenPaths),
|
|
34
|
+
onForbidden: config.onForbidden,
|
|
35
|
+
hasVisibilityRule: !!(config.allowedVisibility && config.allowedVisibility.length > 0),
|
|
36
|
+
});
|
|
37
|
+
|
|
29
38
|
const result = await checkPush(config);
|
|
30
39
|
|
|
40
|
+
// visibility チェック
|
|
41
|
+
if (config.allowedVisibility && config.allowedVisibility.length > 0) {
|
|
42
|
+
try {
|
|
43
|
+
const visibilityResult = await checkVisibility(config.allowedVisibility);
|
|
44
|
+
if (visibilityResult) {
|
|
45
|
+
result.details.repoVisibility = visibilityResult.visibility;
|
|
46
|
+
result.details.visibilityAllowed = visibilityResult.allowed;
|
|
47
|
+
if (!visibilityResult.allowed) {
|
|
48
|
+
result.allowed = false;
|
|
49
|
+
result.reason = visibilityResult.reason;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
} catch (error) {
|
|
53
|
+
if (error instanceof ExitError) throw error;
|
|
54
|
+
result.details.repoVisibility = "unknown";
|
|
55
|
+
result.details.visibilityAllowed = false;
|
|
56
|
+
result.allowed = false;
|
|
57
|
+
result.reason = `Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.`;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
31
61
|
if (options.json) {
|
|
32
62
|
printCheckResultJson(result);
|
|
33
63
|
} else {
|
|
34
64
|
printCheckResultHuman(result);
|
|
35
65
|
}
|
|
36
66
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
);
|
|
42
|
-
process.exit(1);
|
|
43
|
-
}
|
|
67
|
+
if (!result.allowed) {
|
|
68
|
+
throw new ExitError(1);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
44
71
|
});
|
|
45
72
|
}
|
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,
|
|
@@ -9,6 +9,8 @@ import {
|
|
|
9
9
|
printCheckResultHuman,
|
|
10
10
|
promptConfirm,
|
|
11
11
|
} from "./utils";
|
|
12
|
+
import { ExitError } from "../types";
|
|
13
|
+
import { withSpan } from "../telemetry";
|
|
12
14
|
|
|
13
15
|
/**
|
|
14
16
|
* pushコマンドを作成
|
|
@@ -21,37 +23,63 @@ export function createPushCommand(): Command {
|
|
|
21
23
|
.allowUnknownOption()
|
|
22
24
|
.action(async (options: { force?: boolean; dryRun?: boolean }, command: Command) => {
|
|
23
25
|
const gitArgs = command.args;
|
|
24
|
-
|
|
26
|
+
await withSpan("safe-push.push", async (rootSpan) => {
|
|
25
27
|
// Gitリポジトリ内か確認
|
|
26
28
|
if (!(await isGitRepository())) {
|
|
27
29
|
printError("Not a git repository");
|
|
28
|
-
|
|
30
|
+
throw new ExitError(1);
|
|
29
31
|
}
|
|
30
32
|
|
|
31
33
|
// コミットが存在するか確認
|
|
32
34
|
if (!(await hasCommits())) {
|
|
33
35
|
printError("No commits found");
|
|
34
|
-
|
|
36
|
+
throw new ExitError(1);
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
const config = loadConfig();
|
|
38
40
|
|
|
41
|
+
rootSpan.addEvent("config.loaded", {
|
|
42
|
+
forbiddenPaths: JSON.stringify(config.forbiddenPaths),
|
|
43
|
+
onForbidden: config.onForbidden,
|
|
44
|
+
hasVisibilityRule: !!(config.allowedVisibility && config.allowedVisibility.length > 0),
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// visibility チェック(--force でもバイパスできない)
|
|
48
|
+
if (config.allowedVisibility && config.allowedVisibility.length > 0) {
|
|
49
|
+
try {
|
|
50
|
+
const visibilityResult = await checkVisibility(config.allowedVisibility);
|
|
51
|
+
if (visibilityResult && !visibilityResult.allowed) {
|
|
52
|
+
printError(visibilityResult.reason);
|
|
53
|
+
throw new ExitError(1);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
if (error instanceof ExitError) throw error;
|
|
57
|
+
printError(
|
|
58
|
+
`Failed to check repository visibility. Ensure 'gh' CLI is installed and authenticated.\n ${error instanceof Error ? error.message : String(error)}`
|
|
59
|
+
);
|
|
60
|
+
throw new ExitError(1);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
39
64
|
// --forceオプションが指定されている場合はチェックをスキップ
|
|
40
65
|
if (options.force) {
|
|
41
66
|
printWarning("Safety checks bypassed with --force");
|
|
42
67
|
|
|
43
68
|
if (options.dryRun) {
|
|
44
69
|
printSuccess("Dry run: would push (checks bypassed)");
|
|
45
|
-
|
|
70
|
+
throw new ExitError(0);
|
|
46
71
|
}
|
|
47
72
|
|
|
48
73
|
const result = await execPush(gitArgs);
|
|
49
74
|
if (result.success) {
|
|
75
|
+
if (result.output) {
|
|
76
|
+
console.log(result.output);
|
|
77
|
+
}
|
|
50
78
|
printSuccess("Push successful");
|
|
51
|
-
|
|
79
|
+
return;
|
|
52
80
|
} else {
|
|
53
81
|
printError(`Push failed: ${result.output}`);
|
|
54
|
-
|
|
82
|
+
throw new ExitError(1);
|
|
55
83
|
}
|
|
56
84
|
}
|
|
57
85
|
|
|
@@ -72,45 +100,46 @@ export function createPushCommand(): Command {
|
|
|
72
100
|
if (confirmed) {
|
|
73
101
|
if (options.dryRun) {
|
|
74
102
|
printSuccess("Dry run: would push (user confirmed)");
|
|
75
|
-
|
|
103
|
+
throw new ExitError(0);
|
|
76
104
|
}
|
|
77
105
|
|
|
78
106
|
const result = await execPush(gitArgs);
|
|
79
107
|
if (result.success) {
|
|
108
|
+
if (result.output) {
|
|
109
|
+
console.log(result.output);
|
|
110
|
+
}
|
|
80
111
|
printSuccess("Push successful");
|
|
81
|
-
|
|
112
|
+
return;
|
|
82
113
|
} else {
|
|
83
114
|
printError(`Push failed: ${result.output}`);
|
|
84
|
-
|
|
115
|
+
throw new ExitError(1);
|
|
85
116
|
}
|
|
86
117
|
} else {
|
|
87
118
|
printError("Push cancelled by user");
|
|
88
|
-
|
|
119
|
+
throw new ExitError(1);
|
|
89
120
|
}
|
|
90
121
|
}
|
|
91
122
|
|
|
92
|
-
|
|
123
|
+
throw new ExitError(1);
|
|
93
124
|
}
|
|
94
125
|
|
|
95
126
|
// チェック通過、pushを実行
|
|
96
127
|
if (options.dryRun) {
|
|
97
128
|
printSuccess("Dry run: would push");
|
|
98
|
-
|
|
129
|
+
throw new ExitError(0);
|
|
99
130
|
}
|
|
100
131
|
|
|
101
132
|
const result = await execPush(gitArgs);
|
|
102
133
|
if (result.success) {
|
|
134
|
+
if (result.output) {
|
|
135
|
+
console.log(result.output);
|
|
136
|
+
}
|
|
103
137
|
printSuccess("Push successful");
|
|
104
|
-
|
|
138
|
+
return;
|
|
105
139
|
} else {
|
|
106
140
|
printError(`Push failed: ${result.output}`);
|
|
107
|
-
|
|
141
|
+
throw new ExitError(1);
|
|
108
142
|
}
|
|
109
|
-
}
|
|
110
|
-
printError(
|
|
111
|
-
`Push failed: ${error instanceof Error ? error.message : String(error)}`
|
|
112
|
-
);
|
|
113
|
-
process.exit(1);
|
|
114
|
-
}
|
|
143
|
+
});
|
|
115
144
|
});
|
|
116
145
|
}
|
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
|
@@ -4,6 +4,9 @@ import * as os from "node:os";
|
|
|
4
4
|
import * as jsonc from "jsonc-parser";
|
|
5
5
|
import { ConfigSchema, ConfigError, type Config } from "./types";
|
|
6
6
|
|
|
7
|
+
const CONFIG_SCHEMA_URL =
|
|
8
|
+
"https://raw.githubusercontent.com/shoppingjaws/safe-push/main/config.schema.json";
|
|
9
|
+
|
|
7
10
|
/**
|
|
8
11
|
* 設定ファイルのデフォルトパス
|
|
9
12
|
*/
|
|
@@ -88,11 +91,20 @@ export function saveConfig(config: Config, configPath?: string): void {
|
|
|
88
91
|
fs.mkdirSync(dir, { recursive: true });
|
|
89
92
|
}
|
|
90
93
|
|
|
94
|
+
const allowedVisibilitySection = config.allowedVisibility
|
|
95
|
+
? `,\n // 許可するリポジトリ visibility: "public" | "private" | "internal"\n "allowedVisibility": ${JSON.stringify(config.allowedVisibility)}`
|
|
96
|
+
: "";
|
|
97
|
+
|
|
98
|
+
const traceSection = config.trace
|
|
99
|
+
? `,\n // トレーシング: "otlp" | "console" (省略で無効)\n "trace": "${config.trace}"`
|
|
100
|
+
: "";
|
|
101
|
+
|
|
91
102
|
const content = `{
|
|
103
|
+
"$schema": "${CONFIG_SCHEMA_URL}",
|
|
92
104
|
// 禁止エリア(Globパターン)
|
|
93
105
|
"forbiddenPaths": ${JSON.stringify(config.forbiddenPaths, null, 4).replace(/\n/g, "\n ")},
|
|
94
106
|
// 禁止時の動作: "error" | "prompt"
|
|
95
|
-
"onForbidden": "${config.onForbidden}"
|
|
107
|
+
"onForbidden": "${config.onForbidden}"${allowedVisibilitySection}${traceSection}
|
|
96
108
|
}
|
|
97
109
|
`;
|
|
98
110
|
|