@yhonda/gcloud-secrets 2.0.10 → 2.0.12
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/README.md +84 -36
- package/cli.js +256 -25
- package/package.json +1 -1
- package/skills/secrets.md +25 -0
package/README.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
# @yhonda/gcloud-secrets
|
|
1
|
+
# @yhonda/gcloud-secrets
|
|
2
2
|
|
|
3
3
|
複数の GCP プロジェクトの `.env` / `.dev.vars` を1つの Secret Manager で一元管理する CLI ツール。
|
|
4
4
|
|
|
@@ -8,11 +8,14 @@ Claude Code のスキルとしても利用可能。
|
|
|
8
8
|
|
|
9
9
|
```
|
|
10
10
|
Secret Manager (中央プロジェクト)
|
|
11
|
-
├── project-a/
|
|
11
|
+
├── project-a/ [dev]
|
|
12
12
|
│ ├── DATABASE_URL
|
|
13
13
|
│ ├── API_KEY
|
|
14
14
|
│ └── CLOUDFLARE_SECRET
|
|
15
|
-
├── project-
|
|
15
|
+
├── project-a/ [prod]
|
|
16
|
+
│ ├── DATABASE_URL
|
|
17
|
+
│ └── API_KEY
|
|
18
|
+
├── project-b/ [dev]
|
|
16
19
|
│ ├── DATABASE_URL
|
|
17
20
|
│ └── STRIPE_KEY
|
|
18
21
|
└── project-c/
|
|
@@ -22,13 +25,7 @@ Secret Manager (中央プロジェクト)
|
|
|
22
25
|
## インストール
|
|
23
26
|
|
|
24
27
|
```bash
|
|
25
|
-
|
|
26
|
-
mkdir -p ~/bin && cd ~/bin
|
|
27
|
-
npm install @yhonda/gcloud-secrets-mcp
|
|
28
|
-
ln -sf ~/bin/node_modules/.bin/gcloud-secrets-mcp ~/bin/gcloud-secrets-mcp
|
|
29
|
-
|
|
30
|
-
# PATH に追加 (~/.bashrc または ~/.zshrc)
|
|
31
|
-
echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
|
|
28
|
+
npm install -g @yhonda/gcloud-secrets
|
|
32
29
|
```
|
|
33
30
|
|
|
34
31
|
### 前提条件
|
|
@@ -39,58 +36,100 @@ echo 'export PATH="$HOME/bin:$PATH"' >> ~/.bashrc
|
|
|
39
36
|
## 初期設定
|
|
40
37
|
|
|
41
38
|
```bash
|
|
42
|
-
gcloud-secrets
|
|
39
|
+
gcloud-secrets init <project-id> [--env <default-env>]
|
|
43
40
|
```
|
|
44
41
|
|
|
45
42
|
設定は `~/.secrets-manager.conf` に保存されます。
|
|
46
43
|
|
|
47
|
-
## CLI
|
|
44
|
+
## CLI コマンド
|
|
45
|
+
|
|
46
|
+
### 基本操作
|
|
48
47
|
|
|
49
48
|
```bash
|
|
50
|
-
#
|
|
51
|
-
gcloud-secrets
|
|
49
|
+
# フォルダ一覧(環境ごとにグループ化)
|
|
50
|
+
gcloud-secrets list
|
|
52
51
|
|
|
53
52
|
# フォルダ内のシークレット一覧
|
|
54
|
-
gcloud-secrets
|
|
53
|
+
gcloud-secrets list my-project --env dev
|
|
55
54
|
|
|
56
|
-
# シークレットを取得(.env形式で標準出力)
|
|
57
|
-
gcloud-secrets
|
|
55
|
+
# シークレットを取得(.env 形式で標準出力)
|
|
56
|
+
gcloud-secrets pull my-project --env prod
|
|
58
57
|
|
|
59
58
|
# シークレットをアップロード
|
|
60
|
-
gcloud-secrets
|
|
59
|
+
gcloud-secrets push my-project .env --env dev
|
|
61
60
|
```
|
|
62
61
|
|
|
63
|
-
|
|
62
|
+
### スキャン & 検索
|
|
64
63
|
|
|
65
|
-
|
|
64
|
+
```bash
|
|
65
|
+
# 全リポジトリの .env 同期状況をスキャン
|
|
66
|
+
gcloud-secrets scan
|
|
67
|
+
|
|
68
|
+
# 指定パス以下をスキャン(特定環境のみ)
|
|
69
|
+
gcloud-secrets scan ~/projects --env dev
|
|
66
70
|
|
|
67
|
-
|
|
68
|
-
|
|
71
|
+
# 値から逆引き検索
|
|
72
|
+
gcloud-secrets search "api-key-12345"
|
|
73
|
+
```
|
|
69
74
|
|
|
70
|
-
|
|
75
|
+
### 自動同期 (pre-commit hook)
|
|
71
76
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
77
|
+
```bash
|
|
78
|
+
# グローバル git hook をインストール(全リポジトリ対象)
|
|
79
|
+
gcloud-secrets hook install
|
|
80
|
+
|
|
81
|
+
# アンインストール
|
|
82
|
+
gcloud-secrets hook uninstall
|
|
83
|
+
|
|
84
|
+
# 手動実行
|
|
85
|
+
gcloud-secrets pre-commit
|
|
76
86
|
```
|
|
77
87
|
|
|
78
|
-
|
|
88
|
+
`hook install` すると、全リポジトリで `git commit` のたびに `.env` が自動で Secret Manager に同期されます。
|
|
79
89
|
|
|
80
|
-
|
|
90
|
+
**高速化の仕組み:**
|
|
91
|
+
- キャッシュ (`~/.secrets-manager-cache.json`) で .env の変更を検知
|
|
92
|
+
- 変更なし → **0 API コール**(即座に終了)
|
|
93
|
+
- 変更あり → フィルタ付き API + 並列取得で高速チェック&自動 push
|
|
94
|
+
- 常に exit 0(commit をブロックしない)
|
|
95
|
+
- 既存の `.husky/` や `.git/hooks/` と互換性あり
|
|
81
96
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
97
|
+
## 環境 (Environment)
|
|
98
|
+
|
|
99
|
+
`--env` または `-e` で環境を指定できます:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
gcloud-secrets push --env dev # dev 環境にアップロード
|
|
103
|
+
gcloud-secrets pull -e prod # prod 環境から取得
|
|
104
|
+
gcloud-secrets scan --env staging # staging のみスキャン
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
デフォルト環境は `~/.secrets-manager.conf` の `DEFAULT_ENVIRONMENT` で設定。
|
|
85
108
|
|
|
86
109
|
## コマンド一覧
|
|
87
110
|
|
|
88
111
|
| コマンド | 説明 |
|
|
89
112
|
|---------|------|
|
|
90
|
-
| `init <project-id
|
|
91
|
-
| `list [folder]` | 一覧表示 |
|
|
92
|
-
| `pull [folder]` | シークレットを取得 |
|
|
93
|
-
| `push [folder] [file]` | アップロード |
|
|
113
|
+
| `init <project-id> [--env <default>]` | 中央プロジェクトを設定 |
|
|
114
|
+
| `list [folder] [--env <env>]` | 一覧表示 |
|
|
115
|
+
| `pull [folder] [--env <env>]` | シークレットを取得 |
|
|
116
|
+
| `push [folder] [file] [--env <env>]` | アップロード |
|
|
117
|
+
| `scan [basePath] [--env <env>]` | Git リポジトリの同期状況をスキャン |
|
|
118
|
+
| `search <keyword> [--env <env>]` | 値から逆引き検索 |
|
|
119
|
+
| `pre-commit` | .env 自動同期(git hook 用) |
|
|
120
|
+
| `hook install` | グローバル git hook をインストール |
|
|
121
|
+
| `hook uninstall` | グローバル git hook をアンインストール |
|
|
122
|
+
|
|
123
|
+
## フォルダ名の正規化
|
|
124
|
+
|
|
125
|
+
ディレクトリ名は自動で kebab-case に変換されます:
|
|
126
|
+
|
|
127
|
+
- `gcloudSec` → `gcloud-sec`
|
|
128
|
+
- `myAppTest` → `my-app-test`
|
|
129
|
+
|
|
130
|
+
## シークレット名の形式
|
|
131
|
+
|
|
132
|
+
`{folder}_{env}_{KEY}` (例: `gcloud-sec_dev_DATABASE_URL`)
|
|
94
133
|
|
|
95
134
|
## 設定
|
|
96
135
|
|
|
@@ -102,8 +141,17 @@ export SECRETS_CENTRAL_PROJECT=your-project-id
|
|
|
102
141
|
|
|
103
142
|
# または設定ファイル (~/.secrets-manager.conf)
|
|
104
143
|
SECRETS_CENTRAL_PROJECT=your-project-id
|
|
144
|
+
DEFAULT_ENVIRONMENT=dev
|
|
105
145
|
```
|
|
106
146
|
|
|
147
|
+
## Claude Code スキル
|
|
148
|
+
|
|
149
|
+
インストール時に `~/.claude/skills/secrets.md` が自動作成され、`/secrets` コマンドが使えます:
|
|
150
|
+
|
|
151
|
+
- 「このプロジェクトの .env を Secret Manager にアップロードして」
|
|
152
|
+
- 「dev 環境のシークレットを確認して」
|
|
153
|
+
- 「全リポジトリの同期状況をスキャンして」
|
|
154
|
+
|
|
107
155
|
## ライセンス
|
|
108
156
|
|
|
109
157
|
MIT
|
package/cli.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
import { SecretManagerServiceClient } from "@google-cloud/secret-manager";
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync, readdirSync, lstatSync } from "fs";
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, readdirSync, lstatSync, mkdirSync, chmodSync, rmSync } from "fs";
|
|
5
|
+
import { createHash } from "crypto";
|
|
5
6
|
import { basename, join, dirname, resolve } from "path";
|
|
6
7
|
import { homedir } from "os";
|
|
7
8
|
import { execSync } from "child_process";
|
|
@@ -146,6 +147,27 @@ function compareValues(a, b) {
|
|
|
146
147
|
return a.trim().replace(/\r\n/g, '\n') === b.trim().replace(/\r\n/g, '\n');
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
// キャッシュ管理
|
|
151
|
+
function getCachePath() {
|
|
152
|
+
return join(homedir(), '.secrets-manager-cache.json');
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function readCache() {
|
|
156
|
+
const cachePath = getCachePath();
|
|
157
|
+
if (existsSync(cachePath)) {
|
|
158
|
+
try { return JSON.parse(readFileSync(cachePath, 'utf-8')); } catch { return {}; }
|
|
159
|
+
}
|
|
160
|
+
return {};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function writeCache(cache) {
|
|
164
|
+
writeFileSync(getCachePath(), JSON.stringify(cache, null, 2));
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function hashContent(content) {
|
|
168
|
+
return createHash('md5').update(content).digest('hex');
|
|
169
|
+
}
|
|
170
|
+
|
|
149
171
|
// CLI モード
|
|
150
172
|
async function runCli(args) {
|
|
151
173
|
const parsed = parseArgs(args);
|
|
@@ -153,7 +175,7 @@ async function runCli(args) {
|
|
|
153
175
|
const config = getConfig();
|
|
154
176
|
const targetEnv = parsed.env || config.defaultEnvironment;
|
|
155
177
|
|
|
156
|
-
if (!config.centralProject && command !== "init") {
|
|
178
|
+
if (!config.centralProject && command !== "init" && command !== "pre-commit" && command !== "hook") {
|
|
157
179
|
console.error("エラー: 先に init を実行してください");
|
|
158
180
|
process.exit(1);
|
|
159
181
|
}
|
|
@@ -408,6 +430,128 @@ async function runCli(args) {
|
|
|
408
430
|
break;
|
|
409
431
|
}
|
|
410
432
|
|
|
433
|
+
case "pre-commit": {
|
|
434
|
+
// config なし → サイレント exit
|
|
435
|
+
if (!config.centralProject) process.exit(0);
|
|
436
|
+
|
|
437
|
+
const cwd = process.cwd();
|
|
438
|
+
const folder = normalizeFolder(basename(resolve(cwd)));
|
|
439
|
+
const envFiles = findEnvFiles(cwd);
|
|
440
|
+
if (envFiles.length === 0) process.exit(0);
|
|
441
|
+
|
|
442
|
+
const cache = readCache();
|
|
443
|
+
const parent = `projects/${config.centralProject}`;
|
|
444
|
+
let totalPushed = 0;
|
|
445
|
+
|
|
446
|
+
for (const envFile of envFiles) {
|
|
447
|
+
let content;
|
|
448
|
+
try { content = readFileSync(envFile.path, 'utf-8'); } catch { continue; }
|
|
449
|
+
if (!content.trim()) continue;
|
|
450
|
+
|
|
451
|
+
const currentHash = hashContent(content);
|
|
452
|
+
const cacheKey = envFile.path;
|
|
453
|
+
|
|
454
|
+
// キャッシュヒット → スキップ (0 API コール)
|
|
455
|
+
if (cache[cacheKey] && cache[cacheKey].hash === currentHash) {
|
|
456
|
+
console.log(`✓ ${envFile.filename} synced`);
|
|
457
|
+
continue;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const localEntries = parseEnvFile(content);
|
|
461
|
+
if (localEntries.length === 0) continue;
|
|
462
|
+
|
|
463
|
+
const labels = { folder, environment: targetEnv };
|
|
464
|
+
|
|
465
|
+
try {
|
|
466
|
+
// フィルタ付き listSecrets (1 API コール、ラベル込み)
|
|
467
|
+
const filter = `labels.folder=${folder} AND labels.environment=${targetEnv}`;
|
|
468
|
+
const [remoteSecrets] = await client.listSecrets({ parent, filter });
|
|
469
|
+
|
|
470
|
+
// リモートキー → シークレット名マップ
|
|
471
|
+
const remoteMap = new Map();
|
|
472
|
+
for (const secret of remoteSecrets) {
|
|
473
|
+
const { key } = getKeyFromSecret(secret.name.split('/').pop(), folder);
|
|
474
|
+
remoteMap.set(key, secret.name);
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
// リモート値を並列取得
|
|
478
|
+
const remoteValues = new Map();
|
|
479
|
+
const keysToCompare = localEntries.filter(e => remoteMap.has(e.key));
|
|
480
|
+
if (keysToCompare.length > 0) {
|
|
481
|
+
const results = await Promise.all(
|
|
482
|
+
keysToCompare.map(async (entry) => {
|
|
483
|
+
try {
|
|
484
|
+
const [version] = await client.accessSecretVersion({
|
|
485
|
+
name: `${remoteMap.get(entry.key)}/versions/latest`,
|
|
486
|
+
});
|
|
487
|
+
return { key: entry.key, value: version.payload.data.toString('utf-8') };
|
|
488
|
+
} catch { return { key: entry.key, value: null }; }
|
|
489
|
+
})
|
|
490
|
+
);
|
|
491
|
+
for (const r of results) {
|
|
492
|
+
if (r.value !== null) remoteValues.set(r.key, r.value);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// 新規/変更キーを特定
|
|
497
|
+
const toPush = [];
|
|
498
|
+
for (const entry of localEntries) {
|
|
499
|
+
if (!remoteMap.has(entry.key)) {
|
|
500
|
+
toPush.push(entry);
|
|
501
|
+
} else if (!remoteValues.has(entry.key) || !compareValues(entry.value, remoteValues.get(entry.key))) {
|
|
502
|
+
toPush.push(entry);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
// push
|
|
507
|
+
if (toPush.length > 0) {
|
|
508
|
+
for (const entry of toPush) {
|
|
509
|
+
const secretId = makeSecretName(folder, entry.key, targetEnv);
|
|
510
|
+
const secretName = `${parent}/secrets/${secretId}`;
|
|
511
|
+
try {
|
|
512
|
+
await client.getSecret({ name: secretName });
|
|
513
|
+
await client.updateSecret({
|
|
514
|
+
secret: { name: secretName, labels },
|
|
515
|
+
updateMask: { paths: ['labels'] }
|
|
516
|
+
});
|
|
517
|
+
await client.addSecretVersion({
|
|
518
|
+
parent: secretName,
|
|
519
|
+
payload: { data: Buffer.from(entry.value) },
|
|
520
|
+
});
|
|
521
|
+
} catch {
|
|
522
|
+
await client.createSecret({
|
|
523
|
+
parent,
|
|
524
|
+
secretId,
|
|
525
|
+
secret: { replication: { automatic: {} }, labels },
|
|
526
|
+
});
|
|
527
|
+
await client.addSecretVersion({
|
|
528
|
+
parent: secretName,
|
|
529
|
+
payload: { data: Buffer.from(entry.value) },
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
console.log(`↑ ${envFile.filename}: ${toPush.length} secrets pushed (${folder}/${targetEnv})`);
|
|
534
|
+
totalPushed += toPush.length;
|
|
535
|
+
} else {
|
|
536
|
+
console.log(`✓ ${envFile.filename} synced`);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// キャッシュ更新
|
|
540
|
+
cache[cacheKey] = {
|
|
541
|
+
hash: currentHash,
|
|
542
|
+
folder,
|
|
543
|
+
env: targetEnv,
|
|
544
|
+
syncedAt: new Date().toISOString()
|
|
545
|
+
};
|
|
546
|
+
} catch (error) {
|
|
547
|
+
console.log(`⚠ ${envFile.filename}: sync skipped (${error.message})`);
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
try { writeCache(cache); } catch { }
|
|
552
|
+
process.exit(0);
|
|
553
|
+
}
|
|
554
|
+
|
|
411
555
|
case "search": {
|
|
412
556
|
const keyword = parsed.positional[1];
|
|
413
557
|
if (!keyword) {
|
|
@@ -423,32 +567,34 @@ async function runCli(args) {
|
|
|
423
567
|
if (filterEnv) console.log(` 環境: ${filterEnv}`);
|
|
424
568
|
console.log(`\nScanning ${secrets.length} secrets...\n`);
|
|
425
569
|
|
|
426
|
-
const
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
const env = secretData.labels?.environment || "(default)";
|
|
570
|
+
const results = await Promise.all(
|
|
571
|
+
secrets.map(async (secret) => {
|
|
572
|
+
try {
|
|
573
|
+
const [secretData] = await client.getSecret({ name: secret.name });
|
|
574
|
+
const folder = secretData.labels?.folder;
|
|
575
|
+
const env = secretData.labels?.environment || "(default)";
|
|
433
576
|
|
|
434
|
-
|
|
435
|
-
|
|
577
|
+
// 環境フィルタ
|
|
578
|
+
if (filterEnv && secretData.labels?.environment !== filterEnv) return null;
|
|
436
579
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
580
|
+
// 値を取得してキーワード検索
|
|
581
|
+
const [version] = await client.accessSecretVersion({
|
|
582
|
+
name: `${secret.name}/versions/latest`,
|
|
583
|
+
});
|
|
584
|
+
const value = version.payload.data.toString("utf-8");
|
|
585
|
+
if (value.includes(keyword)) {
|
|
586
|
+
const { key } = getKeyFromSecret(secret.name.split("/").pop(), folder);
|
|
587
|
+
return { folder, env, key };
|
|
588
|
+
}
|
|
589
|
+
} catch {
|
|
590
|
+
// バージョンがない場合はスキップ
|
|
447
591
|
}
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
592
|
+
return null;
|
|
593
|
+
})
|
|
594
|
+
);
|
|
595
|
+
|
|
596
|
+
const matches = results.filter((r) => r !== null);
|
|
597
|
+
const folders = new Set(matches.map((m) => m.folder));
|
|
452
598
|
|
|
453
599
|
if (matches.length === 0) {
|
|
454
600
|
console.log("No matches found");
|
|
@@ -461,6 +607,88 @@ async function runCli(args) {
|
|
|
461
607
|
break;
|
|
462
608
|
}
|
|
463
609
|
|
|
610
|
+
case "hook": {
|
|
611
|
+
const subcommand = parsed.positional[1];
|
|
612
|
+
|
|
613
|
+
if (subcommand === "install") {
|
|
614
|
+
const hooksDir = join(homedir(), '.git-hooks');
|
|
615
|
+
if (!existsSync(hooksDir)) mkdirSync(hooksDir, { recursive: true });
|
|
616
|
+
|
|
617
|
+
const hookTypes = [
|
|
618
|
+
'applypatch-msg', 'pre-applypatch', 'post-applypatch',
|
|
619
|
+
'pre-commit', 'prepare-commit-msg', 'commit-msg', 'post-commit',
|
|
620
|
+
'pre-rebase', 'post-checkout', 'post-merge',
|
|
621
|
+
'pre-push', 'pre-auto-gc', 'post-rewrite'
|
|
622
|
+
];
|
|
623
|
+
|
|
624
|
+
for (const hookType of hookTypes) {
|
|
625
|
+
const hookPath = join(hooksDir, hookType);
|
|
626
|
+
let extraLogic = '';
|
|
627
|
+
|
|
628
|
+
if (hookType === 'pre-commit') {
|
|
629
|
+
extraLogic = `
|
|
630
|
+
# gcloud-secrets: auto-sync .env to Secret Manager
|
|
631
|
+
if command -v gcloud-secrets >/dev/null 2>&1; then
|
|
632
|
+
gcloud-secrets pre-commit
|
|
633
|
+
fi
|
|
634
|
+
`;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const hookScript = `#!/bin/sh
|
|
638
|
+
# Global git hook: ${hookType}
|
|
639
|
+
# Installed by gcloud-secrets
|
|
640
|
+
${extraLogic}
|
|
641
|
+
# Forward to .husky/${hookType} if it exists
|
|
642
|
+
if [ -f "$(pwd)/.husky/${hookType}" ]; then
|
|
643
|
+
"$(pwd)/.husky/${hookType}" "$@"
|
|
644
|
+
exit_code=$?
|
|
645
|
+
if [ $exit_code -ne 0 ]; then
|
|
646
|
+
exit $exit_code
|
|
647
|
+
fi
|
|
648
|
+
fi
|
|
649
|
+
|
|
650
|
+
# Forward to .git/hooks/${hookType} if it exists
|
|
651
|
+
GIT_DIR_HOOKS="$(git rev-parse --git-dir 2>/dev/null)/hooks/${hookType}"
|
|
652
|
+
if [ -f "$GIT_DIR_HOOKS" ] && [ -x "$GIT_DIR_HOOKS" ]; then
|
|
653
|
+
"$GIT_DIR_HOOKS" "$@"
|
|
654
|
+
exit $?
|
|
655
|
+
fi
|
|
656
|
+
|
|
657
|
+
exit 0
|
|
658
|
+
`;
|
|
659
|
+
writeFileSync(hookPath, hookScript);
|
|
660
|
+
chmodSync(hookPath, '755');
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
execSync('git config --global core.hooksPath ~/.git-hooks');
|
|
664
|
+
console.log(`グローバル git hooks をインストールしました:
|
|
665
|
+
フックディレクトリ: ${hooksDir}
|
|
666
|
+
対象: pre-commit (gcloud-secrets auto-sync)
|
|
667
|
+
互換性: .husky/ と .git/hooks/ にフォワード
|
|
668
|
+
|
|
669
|
+
全リポジトリの git commit で .env が自動同期されます。`);
|
|
670
|
+
|
|
671
|
+
} else if (subcommand === "uninstall") {
|
|
672
|
+
try { execSync('git config --global --unset core.hooksPath', { stdio: 'ignore' }); } catch { }
|
|
673
|
+
const hooksDir = join(homedir(), '.git-hooks');
|
|
674
|
+
if (existsSync(hooksDir)) {
|
|
675
|
+
try {
|
|
676
|
+
rmSync(hooksDir, { recursive: true, force: true });
|
|
677
|
+
} catch (error) {
|
|
678
|
+
console.log(`⚠ ${hooksDir} の削除に失敗: ${error.message}`);
|
|
679
|
+
console.log(`手動で削除してください: rm -rf ${hooksDir}`);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
console.log(`グローバル git hooks をアンインストールしました。`);
|
|
683
|
+
|
|
684
|
+
} else {
|
|
685
|
+
console.log(`使い方:
|
|
686
|
+
gcloud-secrets hook install グローバル pre-commit hook をインストール
|
|
687
|
+
gcloud-secrets hook uninstall グローバル pre-commit hook をアンインストール`);
|
|
688
|
+
}
|
|
689
|
+
break;
|
|
690
|
+
}
|
|
691
|
+
|
|
464
692
|
default:
|
|
465
693
|
console.log(`gcloud-secrets - GCP Secret Manager CLI
|
|
466
694
|
|
|
@@ -471,6 +699,9 @@ async function runCli(args) {
|
|
|
471
699
|
gcloud-secrets push [folder] [file] [--env <env>] シークレットをアップロード
|
|
472
700
|
gcloud-secrets scan [basePath] [--env <env>] Git リポジトリの .env 同期状況をスキャン
|
|
473
701
|
gcloud-secrets search <keyword> [--env <env>] 値から逆引き検索
|
|
702
|
+
gcloud-secrets pre-commit .env 自動同期 (git hook 用)
|
|
703
|
+
gcloud-secrets hook install グローバル git hook インストール
|
|
704
|
+
gcloud-secrets hook uninstall グローバル git hook アンインストール
|
|
474
705
|
|
|
475
706
|
オプション:
|
|
476
707
|
--env, -e <env> 環境を指定 (dev, staging, prod など)
|
package/package.json
CHANGED
package/skills/secrets.md
CHANGED
|
@@ -92,6 +92,25 @@ Found 3 matches in 2 folders
|
|
|
92
92
|
未登録: 1
|
|
93
93
|
```
|
|
94
94
|
|
|
95
|
+
### .env 自動同期 (pre-commit)
|
|
96
|
+
```bash
|
|
97
|
+
# カレントディレクトリの .env を Secret Manager に自動同期
|
|
98
|
+
gcloud-secrets pre-commit
|
|
99
|
+
```
|
|
100
|
+
git hook 用の高速コマンド。キャッシュで .env の変更を検知し、変更がなければ API コール 0 で即座に終了。
|
|
101
|
+
変更があれば `listSecrets` のフィルタ + 並列取得で高速にチェックし、新規/差分のある secret を自動 push。
|
|
102
|
+
|
|
103
|
+
### グローバル git hook (hook)
|
|
104
|
+
```bash
|
|
105
|
+
# グローバル pre-commit hook をインストール
|
|
106
|
+
gcloud-secrets hook install
|
|
107
|
+
|
|
108
|
+
# アンインストール
|
|
109
|
+
gcloud-secrets hook uninstall
|
|
110
|
+
```
|
|
111
|
+
`hook install` で全リポジトリの `git commit` 時に `pre-commit` が自動実行されます。
|
|
112
|
+
既存の `.husky/` や `.git/hooks/` のフックにもフォワードするので互換性があります。
|
|
113
|
+
|
|
95
114
|
## 環境 (Environment) オプション
|
|
96
115
|
|
|
97
116
|
`--env` または `-e` で環境を指定できます:
|
|
@@ -122,4 +141,10 @@ gcloud-secrets scan ~/ --env dev
|
|
|
122
141
|
|
|
123
142
|
# 6. 特定の値がどこで使われているか検索
|
|
124
143
|
gcloud-secrets search "line-client-id-xxx"
|
|
144
|
+
|
|
145
|
+
# 7. グローバル git hook をインストール (全リポジトリで自動同期)
|
|
146
|
+
gcloud-secrets hook install
|
|
147
|
+
|
|
148
|
+
# 8. 手動で pre-commit を実行
|
|
149
|
+
gcloud-secrets pre-commit
|
|
125
150
|
```
|