@himorogy/enclave-env 0.2.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/README.md +283 -0
- package/dist/cli.js +238 -0
- package/package.json +41 -0
- package/scripts/check.sh +26 -0
- package/scripts/init-check-dev.sh +76 -0
- package/scripts/init-check-prod.sh +21 -0
- package/templates/devcontainer/Dockerfile +62 -0
- package/templates/devcontainer/dev/devcontainer.json +37 -0
- package/templates/devcontainer/prod/devcontainer.json +22 -0
- package/templates/prod-shell.sh +40 -0
package/README.md
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
# @himorogy/enclave-env
|
|
2
|
+
|
|
3
|
+
**LLM に本番用秘密情報を渡さない**ための env 管理 CLI ツールです。
|
|
4
|
+
|
|
5
|
+
[dotenvx](https://dotenvx.com/) による暗号化と実行時ガードを組み合わせ、LLM が動く devcontainer から prod キーを構造的に隔離します。
|
|
6
|
+
|
|
7
|
+
## このパッケージが提供するもの
|
|
8
|
+
|
|
9
|
+
| 種別 | 内容 |
|
|
10
|
+
|---|---|
|
|
11
|
+
| **CLI** | `encrypt` / `decrypt` — env ファイルの暗号化・復号 |
|
|
12
|
+
| **CLI** | `check` — 平文 `.env*` のコミットをブロック(pre-commit hook 用) |
|
|
13
|
+
| **シェルスクリプト** | `scripts/init-check-dev.sh` — dev コンテナ起動時のセキュリティチェック(`initializeCommand` 用) |
|
|
14
|
+
| **シェルスクリプト** | `scripts/init-check-prod.sh` — prod コンテナ起動時の相互排他チェック(`initializeCommand` 用) |
|
|
15
|
+
| **テンプレート** | `templates/` — prod-shell スクリプト・devcontainer 構成の参照実装 |
|
|
16
|
+
|
|
17
|
+
**このパッケージが担わないもの:** devcontainer.json の作成・管理、prod 環境の構築。これらはプロジェクト側の責務です。テンプレートはその参照実装として提供します。
|
|
18
|
+
|
|
19
|
+
# セキュリティ思想
|
|
20
|
+
|
|
21
|
+
## なぜ必要か
|
|
22
|
+
|
|
23
|
+
Claude Code などの LLM は devcontainer 内から bind mount 経由でワークスペース全体にアクセスできます。`.env.production` または復号キーを平文で置いておくと、LLM が意図せず本番秘密情報を参照・出力するリスクがあります。
|
|
24
|
+
prod用復号キーをワークスペース外に置くことで devcontainer への同期を防ぎ、 `.env.production` が平文の状態で devcontainer に同期されないように複数層でブロックします。
|
|
25
|
+
|
|
26
|
+
`.env.production` に対する単一変数の追加・更新は下記の通り `dotenvx set` で対応できます。
|
|
27
|
+
|
|
28
|
+
```sh
|
|
29
|
+
dotenvx set DATABASE_URL "postgres://..." -f .env.production
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
- `.env.production` に含まれる public key だけで暗号化するため private key 不要
|
|
33
|
+
- 平文ファイルが一切ディスクに書き出されない
|
|
34
|
+
- dev コンテナ内でも実行可能(ただしコマンドヒストリーを削除しないと、LLMがセットした値を読み取ることが可能である点に留意)
|
|
35
|
+
|
|
36
|
+
## prod キー隔離の弊害
|
|
37
|
+
|
|
38
|
+
prod の private key を dev コンテナの外に置くと、dev コンテナ内から `.env.production` を復号できなくなります。
|
|
39
|
+
これは意図した動作ですが、複数変数を一覧で確認しながら編集したい場合や、prod deploy・DB メンテナンスを伴う作業には対応できません。そうしたワークフローのために prod 環境を用意します。
|
|
40
|
+
|
|
41
|
+
## 守るべき原則
|
|
42
|
+
|
|
43
|
+
prod 環境の実現手段は問いませんが、以下の 2 つを守ることがこのツールの前提です:
|
|
44
|
+
|
|
45
|
+
| 原則 | 目的 |
|
|
46
|
+
|---|---|
|
|
47
|
+
| **prod の private key は常にワークスペース外に置く** | bind mount 経由で LLM に渡らない |
|
|
48
|
+
| **prod 操作は dev コンテナが停止した状態で行う** | 復号した平文を LLM から守る |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
# prod 環境の用意
|
|
53
|
+
|
|
54
|
+
## prod 環境の活用シナリオ
|
|
55
|
+
|
|
56
|
+
### 全復号して操作する
|
|
57
|
+
|
|
58
|
+
複数変数をまとめて確認・変更したい場合:
|
|
59
|
+
|
|
60
|
+
```sh
|
|
61
|
+
# 1. 復号
|
|
62
|
+
enclave-env decrypt --env prod
|
|
63
|
+
|
|
64
|
+
# 2. エディタで編集
|
|
65
|
+
vim .env.production
|
|
66
|
+
|
|
67
|
+
# 3. 再暗号化
|
|
68
|
+
enclave-env encrypt --env prod
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
`protected: true` の環境では、手順 1 が dev コンテナ稼働中にブロックされます。
|
|
72
|
+
|
|
73
|
+
### prod 環境の DB 整備や deploy
|
|
74
|
+
|
|
75
|
+
prod deploy・DB マイグレーションなど、prod の認証情報を使った操作も prod 環境内で完結できます。
|
|
76
|
+
|
|
77
|
+
```sh
|
|
78
|
+
pnpm deploy
|
|
79
|
+
pnpm db:migrate
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
## 運用上の注意点と 2 層の防御
|
|
83
|
+
|
|
84
|
+
全復号フロー中は `.env.production` が平文でディスクに存在します。この間に dev コンテナが起動すると、bind mount 経由で LLM が平文にアクセスできてしまいます。
|
|
85
|
+
|
|
86
|
+
enclave-env はこれを 2 つのレイヤーで防ぎます:
|
|
87
|
+
|
|
88
|
+
| レイヤー | タイミング | 担う側 | 実装 |
|
|
89
|
+
|---|---|---|---|
|
|
90
|
+
| 起動時 | devcontainer 起動 | **プロジェクト** が `initializeCommand` に登録 | `scripts/init-check-dev.sh` / `scripts/init-check-prod.sh` |
|
|
91
|
+
| 実行時 | `decrypt` 実行時 | **ライブラリ** が自動で実行 | `protected` フラグが付いた環境で dev コンテナの稼働を確認しブロック |
|
|
92
|
+
|
|
93
|
+
実行時ガードは `DEVCONTAINER=true`(コンテナ内からの実行)の場合はスキップされます。起動時ガードで保証済みのためです。
|
|
94
|
+
|
|
95
|
+
## 運用案
|
|
96
|
+
|
|
97
|
+
prod キーが使える実行環境の用意方法です。
|
|
98
|
+
|
|
99
|
+
### 選択肢 1:ホスト直実行
|
|
100
|
+
|
|
101
|
+
最もシンプル。dev コンテナを停止した状態でホストから実行します。
|
|
102
|
+
|
|
103
|
+
```sh
|
|
104
|
+
# DOTENV_PRIVATE_KEY_PRODUCTION が使える状態で
|
|
105
|
+
pnpm decrypt-env:prod
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
ホストに Node.js と `enclave-env` のインストールが必要です。
|
|
109
|
+
|
|
110
|
+
### 選択肢 2:prod-shell スクリプト(推奨)
|
|
111
|
+
|
|
112
|
+
Docker だけで prod 操作環境を再現するスクリプトを用意します。相互排他チェック・コンテナ起動・prod キー注入を 1 コマンドで行います。
|
|
113
|
+
|
|
114
|
+
```sh
|
|
115
|
+
sh scripts/prod-shell.sh
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
コンテナ内で `enclave-env`・`dotenvx`・各種デプロイツールが使えます。macOS / Linux 間の環境差が出ないため、CI/CD 安定前の prod deploy や DB メンテナンスも同じコンテナ内で完結します。
|
|
119
|
+
|
|
120
|
+
テンプレート:[`templates/prod-shell.sh`](./templates/prod-shell.sh)
|
|
121
|
+
|
|
122
|
+
### 選択肢 3:2 層 devcontainer
|
|
123
|
+
|
|
124
|
+
VS Code の「Reopen in Container」で prod devcontainer に切り替える構成です。再現性が最も高く、チームでの運用に向いています。
|
|
125
|
+
|
|
126
|
+
設計の核心は **Dockerfile を dev / prod で共有し、Claude Code のインストールを `dev/devcontainer.json` の `postCreateCommand` で行う**点です。同じ Dockerfile から prod 環境をビルドしても Claude Code が含まれない状態が構造的に保証されます。
|
|
127
|
+
|
|
128
|
+
```
|
|
129
|
+
.devcontainer/
|
|
130
|
+
├── Dockerfile # 共有ベース(Claude Code なし)
|
|
131
|
+
├── dev/
|
|
132
|
+
│ └── devcontainer.json # postCreateCommand で Claude Code をインストール
|
|
133
|
+
│ # initializeCommand: init-check-dev.sh
|
|
134
|
+
└── prod/
|
|
135
|
+
└── devcontainer.json # Claude Code 関連の設定を含まない
|
|
136
|
+
# initializeCommand: init-check-prod.sh
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
テンプレート:[`templates/devcontainer/`](./templates/devcontainer/)
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
# インストール
|
|
144
|
+
|
|
145
|
+
```sh
|
|
146
|
+
pnpm add -D @himorogy/enclave-env @dotenvx/dotenvx simple-git-hooks
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
> `@dotenvx/dotenvx` は peer dependency です。プロジェクトに直接インストールしてください。
|
|
150
|
+
|
|
151
|
+
# セットアップ
|
|
152
|
+
|
|
153
|
+
## 1. `enclave-env` を作成する
|
|
154
|
+
|
|
155
|
+
プロジェクトルートに設定ファイルを置きます。シェルスクリプトから直接 `source` できる dotenv 形式です。
|
|
156
|
+
|
|
157
|
+
```sh
|
|
158
|
+
MODE=single
|
|
159
|
+
|
|
160
|
+
ENV_LOCAL_FILE=.env
|
|
161
|
+
ENV_PROD_FILE=.env.production
|
|
162
|
+
ENV_PROD_PROTECTED=true
|
|
163
|
+
|
|
164
|
+
DEV_CONTAINER_NAME=your-project-dev-devcontainer
|
|
165
|
+
PROD_CONTAINER_NAME=your-project-prod-devcontainer # optional: prod-shell や 2層 devcontainer で使用
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
## 2. `package.json` にスクリプトと git hook を追加する
|
|
169
|
+
|
|
170
|
+
```json
|
|
171
|
+
{
|
|
172
|
+
"scripts": {
|
|
173
|
+
"decrypt-env": "enclave-env decrypt --env local",
|
|
174
|
+
"encrypt-env": "enclave-env encrypt --env local",
|
|
175
|
+
"decrypt-env:prod": "enclave-env decrypt --env prod",
|
|
176
|
+
"encrypt-env:prod": "enclave-env encrypt --env prod",
|
|
177
|
+
"prepare": "simple-git-hooks"
|
|
178
|
+
},
|
|
179
|
+
"simple-git-hooks": {
|
|
180
|
+
"pre-commit": "sh node_modules/@himorogy/enclave-env/scripts/check.sh"
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
`pnpm install` 実行時に `prepare` が走り、フックが自動登録されます。
|
|
186
|
+
|
|
187
|
+
## 3. `.env` を dotenvx で暗号化する
|
|
188
|
+
|
|
189
|
+
```sh
|
|
190
|
+
pnpm encrypt-env
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
生成された `DOTENV_PUBLIC_KEY` はリポジトリにコミットします。`.env.keys` はコミットしないでください。
|
|
194
|
+
|
|
195
|
+
# CLI リファレンス
|
|
196
|
+
|
|
197
|
+
```
|
|
198
|
+
enclave-env encrypt --env <environment> # 指定環境の env ファイルを in-place 暗号化
|
|
199
|
+
enclave-env decrypt --env <environment> # 指定環境の env ファイルを in-place 復号
|
|
200
|
+
enclave-env check # ステージ済み .env* ファイルの暗号化確認(git pre-commit hook 用)
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
`<environment>` は `enclave-env` の `ENV_<NAME>_FILE` キーの `<NAME>` を小文字にしたものと一致させてください。
|
|
204
|
+
|
|
205
|
+
devcontainer の `initializeCommand` 用チェックはシェルスクリプトで提供しています:
|
|
206
|
+
|
|
207
|
+
```
|
|
208
|
+
scripts/init-check-dev.sh # dev コンテナ起動時(enclave-env を source して実行)
|
|
209
|
+
scripts/init-check-prod.sh # prod コンテナ起動時(enclave-env を source して実行)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
# `enclave-env` 設定リファレンス
|
|
213
|
+
|
|
214
|
+
| キー | 説明 |
|
|
215
|
+
|---|---|
|
|
216
|
+
| `MODE` | `single` のみ実装済み |
|
|
217
|
+
| `ENV_<NAME>_FILE` | 環境 `<name>` の対象 env ファイルパス |
|
|
218
|
+
| `ENV_<NAME>_PROTECTED` | `true` の場合、`decrypt` 前に dev コンテナ稼働チェックを実行 |
|
|
219
|
+
| `DEV_CONTAINER_NAME` | `protected` チェックおよび `init-check-prod.sh` の対象コンテナ名 |
|
|
220
|
+
| `PROD_CONTAINER_NAME` | 設定すると 2 層 devcontainer モードで動作(`init-check-dev.sh` が起動チェックを実施) |
|
|
221
|
+
|
|
222
|
+
`<NAME>` は大文字・数字・アンダースコアで構成し、`--env` オプションでは小文字で参照します。例:`ENV_PROD_FILE` → `--env prod`
|
|
223
|
+
|
|
224
|
+
# キー管理の推奨構成
|
|
225
|
+
|
|
226
|
+
| 環境 | キーの保管場所 |
|
|
227
|
+
|---|---|
|
|
228
|
+
| local / dev | `.devcontainer/dev/.env.container`(gitignore 対象) |
|
|
229
|
+
| prod | `~/.config/<your-project>/.env.container`(ワークスペース外) |
|
|
230
|
+
|
|
231
|
+
prod キーをワークスペース外に置くことで、bind mount 経由で LLM に渡るリスクをなくします。
|
|
232
|
+
|
|
233
|
+
# Git 管理ポリシー(推奨)
|
|
234
|
+
|
|
235
|
+
| ファイル | Git 管理 |
|
|
236
|
+
|---|---|
|
|
237
|
+
| `.env`(local、暗号化済) | プロジェクトによる(未暗号化なら ❌ 推奨) |
|
|
238
|
+
| `.env.production`(暗号化済) | ✅ |
|
|
239
|
+
| `.env.keys*` | ❌ 必ず gitignore |
|
|
240
|
+
| `enclave-env` | ✅ |
|
|
241
|
+
|
|
242
|
+
# 機能一覧とテスト状況
|
|
243
|
+
|
|
244
|
+
| 機能 | 種別 | テスト |
|
|
245
|
+
|---|---|---|
|
|
246
|
+
| `enclave-env` ファイルの解析(`loadConfig`) | TypeScript | ✅ |
|
|
247
|
+
| env ファイルパスの解決(`resolveEnvFile`) | TypeScript | ✅ |
|
|
248
|
+
| ステージファイルの暗号化確認(`scripts/check.sh`) | シェル | ✅ |
|
|
249
|
+
| dev コンテナ起動時セキュリティチェック(`scripts/init-check-dev.sh`) | シェル | ✅ |
|
|
250
|
+
| prod コンテナ起動時相互排他チェック(`scripts/init-check-prod.sh`) | シェル | — |
|
|
251
|
+
| env ファイルの暗号化(`encrypt`) | TypeScript | — (dotenvx 依存) |
|
|
252
|
+
| env ファイルの復号(`decrypt`) | TypeScript | — (dotenvx 依存) |
|
|
253
|
+
|
|
254
|
+
```sh
|
|
255
|
+
pnpm test # 全テスト
|
|
256
|
+
pnpm test:ts # TypeScript のみ
|
|
257
|
+
pnpm test:sh # シェルスクリプトのみ
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
# 開発・リリース
|
|
261
|
+
|
|
262
|
+
## CI/CD 構成
|
|
263
|
+
|
|
264
|
+
| ワークフロー | トリガー | 内容 |
|
|
265
|
+
|---|---|---|
|
|
266
|
+
| **CI** | PR・push | lint + test |
|
|
267
|
+
| **Registry Monitor** | 定期実行 | npm registry 監視 → Slack 通知 |
|
|
268
|
+
|
|
269
|
+
## リリース手順
|
|
270
|
+
|
|
271
|
+
```sh
|
|
272
|
+
# 1. 変更を記述(対話形式で bump type と説明を入力)
|
|
273
|
+
pnpm changeset add
|
|
274
|
+
git commit -m "chore: add changeset"
|
|
275
|
+
git push
|
|
276
|
+
|
|
277
|
+
# 2. バージョン更新・git tag・publish を一括実行
|
|
278
|
+
pnpm release
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
# ライセンス
|
|
282
|
+
|
|
283
|
+
MIT
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
|
|
6
|
+
// package.json
|
|
7
|
+
var package_default = {
|
|
8
|
+
name: "@himorogy/enclave-env",
|
|
9
|
+
version: "0.2.0",
|
|
10
|
+
description: "Encrypted env management that keeps secrets out of LLM reach",
|
|
11
|
+
packageManager: "pnpm@10.33.2",
|
|
12
|
+
type: "module",
|
|
13
|
+
bin: {
|
|
14
|
+
"enclave-env": "dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
scripts: {
|
|
17
|
+
build: "tsup",
|
|
18
|
+
prepare: "pnpm run build",
|
|
19
|
+
typecheck: "tsc --noEmit",
|
|
20
|
+
format: "biome check --write .",
|
|
21
|
+
lint: "biome ci .",
|
|
22
|
+
test: "pnpm test:ts && pnpm test:sh",
|
|
23
|
+
"test:ts": "tsc -p tsconfig.test.json && node --test dist/test/config.test.js",
|
|
24
|
+
"test:sh": "sh tests/check.test.sh && sh tests/init-check-dev.test.sh",
|
|
25
|
+
release: "sh release.sh"
|
|
26
|
+
},
|
|
27
|
+
dependencies: {
|
|
28
|
+
"@dotenvx/dotenvx": "^1.63.0",
|
|
29
|
+
commander: "^14.0.3"
|
|
30
|
+
},
|
|
31
|
+
devDependencies: {
|
|
32
|
+
"@biomejs/biome": "^2.4.13",
|
|
33
|
+
"@changesets/cli": "^2.31.0",
|
|
34
|
+
"@types/node": "^25.6.0",
|
|
35
|
+
tsup: "^8.5.1",
|
|
36
|
+
typescript: "^6.0.3"
|
|
37
|
+
},
|
|
38
|
+
files: [
|
|
39
|
+
"dist",
|
|
40
|
+
"scripts",
|
|
41
|
+
"templates",
|
|
42
|
+
"README.md"
|
|
43
|
+
],
|
|
44
|
+
publishConfig: {
|
|
45
|
+
access: "public"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
// src/commands/check.ts
|
|
50
|
+
import { execSync } from "child_process";
|
|
51
|
+
import { existsSync, readFileSync } from "fs";
|
|
52
|
+
function check() {
|
|
53
|
+
let stagedOutput;
|
|
54
|
+
try {
|
|
55
|
+
stagedOutput = execSync("git diff --cached --name-only", {
|
|
56
|
+
encoding: "utf-8"
|
|
57
|
+
});
|
|
58
|
+
} catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const staged = stagedOutput.split("\n").filter((f) => f && /(^|\/)\.env/.test(f));
|
|
62
|
+
if (staged.length === 0) {
|
|
63
|
+
process.exit(0);
|
|
64
|
+
}
|
|
65
|
+
let failed = false;
|
|
66
|
+
for (const file of staged) {
|
|
67
|
+
if (file.includes(".env.keys")) {
|
|
68
|
+
console.error(
|
|
69
|
+
`\u274C pre-commit: ERROR: ${file} contains private keys and must not be committed`
|
|
70
|
+
);
|
|
71
|
+
failed = true;
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (existsSync(file) && !readFileSync(file, "utf-8").includes("DOTENV_PUBLIC_KEY")) {
|
|
75
|
+
console.error(`\u274C pre-commit: ERROR: ${file} is not encrypted`);
|
|
76
|
+
console.error(
|
|
77
|
+
" Run 'enclave-env encrypt --env <environment>' before committing"
|
|
78
|
+
);
|
|
79
|
+
failed = true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
if (failed) {
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
console.log("\u2705 pre-commit: all staged .env* files are encrypted");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/commands/decrypt.ts
|
|
89
|
+
import { execSync as execSync3 } from "child_process";
|
|
90
|
+
|
|
91
|
+
// src/config.ts
|
|
92
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
93
|
+
import { resolve } from "path";
|
|
94
|
+
function loadConfig(cwd = process.cwd()) {
|
|
95
|
+
const configPath = resolve(cwd, "enclave-env");
|
|
96
|
+
let raw;
|
|
97
|
+
try {
|
|
98
|
+
raw = readFileSync2(configPath, "utf-8");
|
|
99
|
+
} catch {
|
|
100
|
+
throw new Error(
|
|
101
|
+
"enclave-env not found. Run from project root or create enclave-env."
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
const vars = {};
|
|
105
|
+
for (const line of raw.split("\n")) {
|
|
106
|
+
const trimmed = line.trim();
|
|
107
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
108
|
+
const eqIdx = trimmed.indexOf("=");
|
|
109
|
+
if (eqIdx < 0) continue;
|
|
110
|
+
vars[trimmed.slice(0, eqIdx).trim()] = trimmed.slice(eqIdx + 1).trim();
|
|
111
|
+
}
|
|
112
|
+
const mode = vars.MODE ?? "single";
|
|
113
|
+
const environments = {};
|
|
114
|
+
for (const [key, value] of Object.entries(vars)) {
|
|
115
|
+
const fileMatch = key.match(/^ENV_([A-Z0-9]+)_FILE$/);
|
|
116
|
+
if (fileMatch) {
|
|
117
|
+
const name = fileMatch[1].toLowerCase();
|
|
118
|
+
environments[name] ??= {};
|
|
119
|
+
environments[name].file = value;
|
|
120
|
+
}
|
|
121
|
+
const protectedMatch = key.match(/^ENV_([A-Z0-9]+)_PROTECTED$/);
|
|
122
|
+
if (protectedMatch) {
|
|
123
|
+
const name = protectedMatch[1].toLowerCase();
|
|
124
|
+
environments[name] ??= {};
|
|
125
|
+
environments[name].protected = value === "true";
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const security = {};
|
|
129
|
+
if (vars.DEV_CONTAINER_NAME)
|
|
130
|
+
security.devContainerName = vars.DEV_CONTAINER_NAME;
|
|
131
|
+
if (vars.PROD_CONTAINER_NAME)
|
|
132
|
+
security.prodContainerName = vars.PROD_CONTAINER_NAME;
|
|
133
|
+
return {
|
|
134
|
+
mode,
|
|
135
|
+
environments,
|
|
136
|
+
...Object.keys(security).length > 0 ? { security } : {}
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
function resolveEnvFile(config, env, cwd = process.cwd()) {
|
|
140
|
+
const envConfig = config.environments[env];
|
|
141
|
+
if (!envConfig) {
|
|
142
|
+
const available = Object.keys(config.environments).join(", ");
|
|
143
|
+
throw new Error(
|
|
144
|
+
`Environment "${env}" not found in enclave-env. Available: ${available}`
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
if (!envConfig.file) {
|
|
148
|
+
throw new Error(
|
|
149
|
+
`Environment "${env}" has no FILE setting. Check enclave-env.`
|
|
150
|
+
);
|
|
151
|
+
}
|
|
152
|
+
return resolve(cwd, envConfig.file);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// src/security.ts
|
|
156
|
+
import { execSync as execSync2 } from "child_process";
|
|
157
|
+
function checkContainerNotRunning(containerName, errorMessage) {
|
|
158
|
+
try {
|
|
159
|
+
const result = execSync2(
|
|
160
|
+
`docker ps --filter "name=${containerName}" --format "{{.Names}}"`,
|
|
161
|
+
{ encoding: "utf-8" }
|
|
162
|
+
).trim();
|
|
163
|
+
if (result) {
|
|
164
|
+
console.error(`\u274C ERROR: ${errorMessage}`);
|
|
165
|
+
process.exit(1);
|
|
166
|
+
}
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
function checkDevContainerNotRunning(containerName) {
|
|
171
|
+
if (process.env.DEVCONTAINER) return;
|
|
172
|
+
checkContainerNotRunning(
|
|
173
|
+
containerName,
|
|
174
|
+
`Dev container is running (${containerName}). Stop it before running prod operations to prevent secrets from syncing via bind mount.`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// src/commands/decrypt.ts
|
|
179
|
+
function decryptEnv(config, env) {
|
|
180
|
+
if (config.mode !== "single") {
|
|
181
|
+
throw new Error(`Mode "${config.mode}" is not yet supported`);
|
|
182
|
+
}
|
|
183
|
+
const envConfig = config.environments[env];
|
|
184
|
+
if (envConfig?.protected && config.security?.devContainerName) {
|
|
185
|
+
checkDevContainerNotRunning(config.security.devContainerName);
|
|
186
|
+
}
|
|
187
|
+
const filePath = resolveEnvFile(config, env);
|
|
188
|
+
execSync3(`dotenvx decrypt -f "${filePath}"`, { stdio: "inherit" });
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// src/commands/encrypt.ts
|
|
192
|
+
import { execSync as execSync4 } from "child_process";
|
|
193
|
+
function encryptEnv(config, env) {
|
|
194
|
+
if (config.mode !== "single") {
|
|
195
|
+
throw new Error(`Mode "${config.mode}" is not yet supported`);
|
|
196
|
+
}
|
|
197
|
+
const filePath = resolveEnvFile(config, env);
|
|
198
|
+
execSync4(`dotenvx encrypt -f "${filePath}"`, { stdio: "inherit" });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// src/cli.ts
|
|
202
|
+
var program = new Command();
|
|
203
|
+
program.name("enclave-env").description("Encrypted env management that keeps secrets out of LLM reach").version(package_default.version);
|
|
204
|
+
program.command("encrypt").description("Encrypt an env file in-place").requiredOption(
|
|
205
|
+
"--env <environment>",
|
|
206
|
+
"Target environment (e.g. local, prod)"
|
|
207
|
+
).action((options) => {
|
|
208
|
+
try {
|
|
209
|
+
const config = loadConfig();
|
|
210
|
+
encryptEnv(config, options.env);
|
|
211
|
+
} catch (err) {
|
|
212
|
+
console.error(`\u274C ${err instanceof Error ? err.message : err}`);
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
program.command("decrypt").description("Decrypt an env file in-place").requiredOption(
|
|
217
|
+
"--env <environment>",
|
|
218
|
+
"Target environment (e.g. local, prod)"
|
|
219
|
+
).action((options) => {
|
|
220
|
+
try {
|
|
221
|
+
const config = loadConfig();
|
|
222
|
+
decryptEnv(config, options.env);
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.error(`\u274C ${err instanceof Error ? err.message : err}`);
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
program.command("check").description(
|
|
229
|
+
"Check staged .env files are encrypted (use as git pre-commit hook)"
|
|
230
|
+
).action(() => {
|
|
231
|
+
try {
|
|
232
|
+
check();
|
|
233
|
+
} catch (err) {
|
|
234
|
+
console.error(`\u274C ${err instanceof Error ? err.message : err}`);
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@himorogy/enclave-env",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Encrypted env management that keeps secrets out of LLM reach",
|
|
5
|
+
"packageManager": "pnpm@10.33.2",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"enclave-env": "dist/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"scripts": {
|
|
11
|
+
"build": "tsup",
|
|
12
|
+
"prepare": "pnpm run build",
|
|
13
|
+
"typecheck": "tsc --noEmit",
|
|
14
|
+
"format": "biome check --write .",
|
|
15
|
+
"lint": "biome ci .",
|
|
16
|
+
"test": "pnpm test:ts && pnpm test:sh",
|
|
17
|
+
"test:ts": "tsc -p tsconfig.test.json && node --test dist/test/config.test.js",
|
|
18
|
+
"test:sh": "sh tests/check.test.sh && sh tests/init-check-dev.test.sh",
|
|
19
|
+
"release": "sh release.sh"
|
|
20
|
+
},
|
|
21
|
+
"dependencies": {
|
|
22
|
+
"@dotenvx/dotenvx": "^1.63.0",
|
|
23
|
+
"commander": "^14.0.3"
|
|
24
|
+
},
|
|
25
|
+
"devDependencies": {
|
|
26
|
+
"@biomejs/biome": "^2.4.13",
|
|
27
|
+
"@changesets/cli": "^2.31.0",
|
|
28
|
+
"@types/node": "^25.6.0",
|
|
29
|
+
"tsup": "^8.5.1",
|
|
30
|
+
"typescript": "^6.0.3"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"scripts",
|
|
35
|
+
"templates",
|
|
36
|
+
"README.md"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"access": "public"
|
|
40
|
+
}
|
|
41
|
+
}
|
package/scripts/check.sh
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
#
|
|
3
|
+
# Pre-commit hook: checks that staged .env* and secret.env.* files contain only encrypted values.
|
|
4
|
+
|
|
5
|
+
for file in $(git diff --cached --name-only | grep -E '(^|/)\.env|(^|/)secret\.env\.'); do
|
|
6
|
+
if [ ! -f "$file" ]; then
|
|
7
|
+
continue
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
case "$file" in
|
|
11
|
+
*.env.container.example) continue ;;
|
|
12
|
+
esac
|
|
13
|
+
|
|
14
|
+
while IFS= read -r line; do
|
|
15
|
+
case "$line" in
|
|
16
|
+
''|'#'*|DOTENV_PUBLIC_KEY*) continue ;;
|
|
17
|
+
esac
|
|
18
|
+
|
|
19
|
+
if ! echo "$line" | grep -qE '^[A-Z0-9_]+=encrypted:'; then
|
|
20
|
+
echo "❌ Error: $file contains unencrypted value:"
|
|
21
|
+
echo " $line"
|
|
22
|
+
echo " Run: pnpm dotenvx encrypt"
|
|
23
|
+
exit 1
|
|
24
|
+
fi
|
|
25
|
+
done < "$file"
|
|
26
|
+
done
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# dev コンテナ起動時のセキュリティチェック(devcontainer.json の initializeCommand 用)
|
|
3
|
+
# ホスト側で実行されるため、exit 1 でコンテナ起動を中断できる。
|
|
4
|
+
|
|
5
|
+
if [ ! -f "./enclave-env" ]; then
|
|
6
|
+
echo "⚠️ enclave-env not found, skipping checks"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
. ./enclave-env
|
|
12
|
+
|
|
13
|
+
FAIL_FILE=$(mktemp)
|
|
14
|
+
|
|
15
|
+
# Check 0: prod コンテナが稼働していないか確認(2層 devcontainer モード時)
|
|
16
|
+
if [ -n "$PROD_CONTAINER_NAME" ]; then
|
|
17
|
+
if docker ps --filter "name=$PROD_CONTAINER_NAME" --format "{{.Names}}" 2>/dev/null | grep -q .; then
|
|
18
|
+
echo "❌ ERROR: Prod container is already running ($PROD_CONTAINER_NAME)."
|
|
19
|
+
echo " Stop it before starting the dev container."
|
|
20
|
+
exit 1
|
|
21
|
+
fi
|
|
22
|
+
fi
|
|
23
|
+
|
|
24
|
+
# Check 1: .env.production / secret.env.* must contain only encrypted values
|
|
25
|
+
find . -type f \
|
|
26
|
+
-not -path "*/node_modules/*" \
|
|
27
|
+
-not -path "*/.git/*" \
|
|
28
|
+
\( -name ".env.production" -o -name "secret.env.*" \) \
|
|
29
|
+
| while read -r FILE; do
|
|
30
|
+
FILE_HEADER_SHOWN=0
|
|
31
|
+
while IFS= read -r line; do
|
|
32
|
+
case "$line" in
|
|
33
|
+
''|'#'*|DOTENV_PUBLIC_KEY*) continue ;;
|
|
34
|
+
esac
|
|
35
|
+
if ! echo "$line" | grep -qE '^[A-Z0-9_]+=encrypted:'; then
|
|
36
|
+
if [ "$FILE_HEADER_SHOWN" = "0" ]; then
|
|
37
|
+
echo "❌ ERROR: $FILE contains unencrypted values:"
|
|
38
|
+
FILE_HEADER_SHOWN=1
|
|
39
|
+
fi
|
|
40
|
+
echo " $line"
|
|
41
|
+
echo "FAILED" > "$FAIL_FILE"
|
|
42
|
+
fi
|
|
43
|
+
done < "$FILE"
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
# Check 2: .env.container must not contain DOTENV_PRIVATE_KEY_PRODUCTION
|
|
47
|
+
find . -type f \
|
|
48
|
+
-not -path "*/node_modules/*" \
|
|
49
|
+
-not -path "*/.git/*" \
|
|
50
|
+
-name ".env.container" \
|
|
51
|
+
| while read -r FILE; do
|
|
52
|
+
if grep -q "DOTENV_PRIVATE_KEY_PRODUCTION" "$FILE" 2>/dev/null; then
|
|
53
|
+
echo "❌ ERROR: DOTENV_PRIVATE_KEY_PRODUCTION found in $FILE"
|
|
54
|
+
echo " Production keys must be placed outside the workspace."
|
|
55
|
+
echo "FAILED" > "$FAIL_FILE"
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
|
|
59
|
+
# Check 3: .env.keys* must not exist inside the workspace
|
|
60
|
+
find . -type f \
|
|
61
|
+
-not -path "*/node_modules/*" \
|
|
62
|
+
-not -path "*/.git/*" \
|
|
63
|
+
-name ".env.keys*" \
|
|
64
|
+
| while read -r FILE; do
|
|
65
|
+
echo "❌ ERROR: Key file found inside the workspace: $FILE"
|
|
66
|
+
echo " Move it outside the workspace (e.g. ~/.config/<your-project>/)."
|
|
67
|
+
echo "FAILED" > "$FAIL_FILE"
|
|
68
|
+
done
|
|
69
|
+
|
|
70
|
+
if [ -s "$FAIL_FILE" ]; then
|
|
71
|
+
rm -f "$FAIL_FILE"
|
|
72
|
+
exit 1
|
|
73
|
+
fi
|
|
74
|
+
rm -f "$FAIL_FILE"
|
|
75
|
+
|
|
76
|
+
echo "✅ enclave-env - security checks passed"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# prod コンテナ起動時の相互排他チェック(devcontainer.json の initializeCommand 用)
|
|
3
|
+
# ホスト側で実行されるため、exit 1 でコンテナ起動を中断できる。
|
|
4
|
+
|
|
5
|
+
if [ ! -f "./enclave-env" ]; then
|
|
6
|
+
echo "⚠️ enclave-env not found, skipping check"
|
|
7
|
+
exit 0
|
|
8
|
+
fi
|
|
9
|
+
|
|
10
|
+
# shellcheck disable=SC1091
|
|
11
|
+
. ./enclave-env
|
|
12
|
+
|
|
13
|
+
if [ -n "$DEV_CONTAINER_NAME" ]; then
|
|
14
|
+
if docker ps --filter "name=$DEV_CONTAINER_NAME" --format "{{.Names}}" 2>/dev/null | grep -q .; then
|
|
15
|
+
echo "❌ ERROR: Dev container is already running ($DEV_CONTAINER_NAME)."
|
|
16
|
+
echo " Stop it before starting the prod container."
|
|
17
|
+
exit 1
|
|
18
|
+
fi
|
|
19
|
+
fi
|
|
20
|
+
|
|
21
|
+
echo "✅ enclave-env - mutual exclusion check passed"
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
FROM node:24
|
|
2
|
+
|
|
3
|
+
ARG TZ
|
|
4
|
+
ENV TZ="$TZ"
|
|
5
|
+
|
|
6
|
+
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
7
|
+
less \
|
|
8
|
+
git \
|
|
9
|
+
procps \
|
|
10
|
+
sudo \
|
|
11
|
+
fzf \
|
|
12
|
+
zsh \
|
|
13
|
+
man-db \
|
|
14
|
+
unzip \
|
|
15
|
+
gnupg2 \
|
|
16
|
+
gh \
|
|
17
|
+
iptables \
|
|
18
|
+
ipset \
|
|
19
|
+
iproute2 \
|
|
20
|
+
dnsutils \
|
|
21
|
+
jq \
|
|
22
|
+
nano \
|
|
23
|
+
vim \
|
|
24
|
+
&& apt-get clean && rm -rf /var/lib/apt/lists/*
|
|
25
|
+
|
|
26
|
+
ENV PNPM_HOME="/usr/local/share/pnpm"
|
|
27
|
+
ENV PATH="${PNPM_HOME}:${PATH}"
|
|
28
|
+
RUN mkdir -p "${PNPM_HOME}" && \
|
|
29
|
+
chown -R node:node /usr/local/share
|
|
30
|
+
|
|
31
|
+
ARG USERNAME=node
|
|
32
|
+
|
|
33
|
+
RUN SNIPPET="export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
|
34
|
+
&& mkdir /commandhistory \
|
|
35
|
+
&& touch /commandhistory/.bash_history \
|
|
36
|
+
&& chown -R $USERNAME /commandhistory
|
|
37
|
+
|
|
38
|
+
ENV DEVCONTAINER=true
|
|
39
|
+
|
|
40
|
+
RUN mkdir -p /workspace /home/node/.claude && \
|
|
41
|
+
chown -R node:node /workspace /home/node/.claude
|
|
42
|
+
|
|
43
|
+
WORKDIR /workspace
|
|
44
|
+
|
|
45
|
+
USER node
|
|
46
|
+
|
|
47
|
+
ENV NPM_CONFIG_PREFIX=/usr/local/share/npm-global
|
|
48
|
+
ENV PATH=$PATH:/usr/local/share/npm-global/bin
|
|
49
|
+
RUN npm install -g pnpm
|
|
50
|
+
|
|
51
|
+
ENV SHELL=/bin/zsh
|
|
52
|
+
ENV EDITOR=vim
|
|
53
|
+
ENV VISUAL=vim
|
|
54
|
+
|
|
55
|
+
ARG ZSH_IN_DOCKER_VERSION=1.2.0
|
|
56
|
+
RUN sh -c "$(wget -O- https://github.com/deluan/zsh-in-docker/releases/download/v${ZSH_IN_DOCKER_VERSION}/zsh-in-docker.sh)" -- \
|
|
57
|
+
-p git \
|
|
58
|
+
-p fzf \
|
|
59
|
+
-a "source /usr/share/doc/fzf/examples/key-bindings.zsh" \
|
|
60
|
+
-a "source /usr/share/doc/fzf/examples/completion.zsh" \
|
|
61
|
+
-a "export PROMPT_COMMAND='history -a' && export HISTFILE=/commandhistory/.bash_history" \
|
|
62
|
+
-x
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<your-project>",
|
|
3
|
+
"build": {
|
|
4
|
+
"dockerfile": "../Dockerfile",
|
|
5
|
+
"args": {
|
|
6
|
+
"TZ": "${localEnv:TZ}"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"runArgs": [
|
|
10
|
+
"--name=<your-project>-dev-devcontainer",
|
|
11
|
+
"--env-file",
|
|
12
|
+
"${localWorkspaceFolder}/.devcontainer/dev/.env.container"
|
|
13
|
+
],
|
|
14
|
+
"customizations": {
|
|
15
|
+
"vscode": {
|
|
16
|
+
"extensions": ["anthropic.claude-code", "biomejs.biome"],
|
|
17
|
+
"settings": {
|
|
18
|
+
"terminal.integrated.defaultProfile.linux": "zsh"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"remoteUser": "node",
|
|
23
|
+
"mounts": [
|
|
24
|
+
// Persist shell history across rebuilds
|
|
25
|
+
"source=<your-project>-bashhistory-${devcontainerId},target=/commandhistory,type=volume",
|
|
26
|
+
// Persist Claude Code config and auth across rebuilds
|
|
27
|
+
"source=<your-project>-claude-config-${devcontainerId},target=/home/node/.claude,type=volume"
|
|
28
|
+
],
|
|
29
|
+
"containerEnv": {
|
|
30
|
+
"NODE_OPTIONS": "--max-old-space-size=2048"
|
|
31
|
+
},
|
|
32
|
+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
|
33
|
+
"workspaceFolder": "/workspace",
|
|
34
|
+
"initializeCommand": "sh node_modules/@himorogy/enclave-env/scripts/init-check-dev.sh",
|
|
35
|
+
// Install Claude Code here (not in Dockerfile) to exclude it from the prod environment
|
|
36
|
+
"postCreateCommand": "curl -fsSL https://claude.ai/install.sh | bash"
|
|
37
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "<your-project> prod",
|
|
3
|
+
"build": {
|
|
4
|
+
"dockerfile": "../Dockerfile",
|
|
5
|
+
"args": {
|
|
6
|
+
"TZ": "${localEnv:TZ}"
|
|
7
|
+
}
|
|
8
|
+
},
|
|
9
|
+
"runArgs": [
|
|
10
|
+
"--name=<your-project>-prod-devcontainer",
|
|
11
|
+
"--env-file",
|
|
12
|
+
// Prod key is stored outside the workspace to prevent LLM access via bind mount
|
|
13
|
+
"${localEnv:HOME}/.config/<your-project>/.env.container"
|
|
14
|
+
],
|
|
15
|
+
"remoteUser": "node",
|
|
16
|
+
"containerEnv": {
|
|
17
|
+
"NODE_OPTIONS": "--max-old-space-size=2048"
|
|
18
|
+
},
|
|
19
|
+
"workspaceMount": "source=${localWorkspaceFolder},target=/workspace,type=bind,consistency=delegated",
|
|
20
|
+
"workspaceFolder": "/workspace",
|
|
21
|
+
"initializeCommand": "sh node_modules/@himorogy/enclave-env/scripts/init-check-prod.sh"
|
|
22
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#!/bin/sh
|
|
2
|
+
# scripts/prod-shell.sh
|
|
3
|
+
#
|
|
4
|
+
# Starts a prod shell with prod keys loaded from outside the workspace.
|
|
5
|
+
# Reads container names from enclave-env and performs mutual exclusion check.
|
|
6
|
+
#
|
|
7
|
+
# Setup:
|
|
8
|
+
# 1. Set DEV_CONTAINER_NAME (and optionally PROD_CONTAINER_NAME) in enclave-env
|
|
9
|
+
# 2. Place prod keys at PROD_KEY_FILE (gitignored, outside workspace)
|
|
10
|
+
# 3. Copy this file to scripts/prod-shell.sh in your project
|
|
11
|
+
|
|
12
|
+
if [ ! -f "./enclave-env" ]; then
|
|
13
|
+
echo "❌ enclave-env not found. Run from project root."
|
|
14
|
+
exit 1
|
|
15
|
+
fi
|
|
16
|
+
|
|
17
|
+
# shellcheck disable=SC1091
|
|
18
|
+
. ./enclave-env
|
|
19
|
+
|
|
20
|
+
PROD_KEY_FILE="${HOME}/.config/<your-project>/.env.container"
|
|
21
|
+
IMAGE="<your-project>-devcontainer"
|
|
22
|
+
|
|
23
|
+
if docker ps --filter "name=${DEV_CONTAINER_NAME}" --format "{{.Names}}" | grep -q .; then
|
|
24
|
+
echo "❌ Dev container '${DEV_CONTAINER_NAME}' is running."
|
|
25
|
+
echo " Stop it first before entering the prod shell."
|
|
26
|
+
exit 1
|
|
27
|
+
fi
|
|
28
|
+
|
|
29
|
+
if [ ! -f "${PROD_KEY_FILE}" ]; then
|
|
30
|
+
echo "❌ Prod key file not found: ${PROD_KEY_FILE}"
|
|
31
|
+
exit 1
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
docker run --rm -it \
|
|
35
|
+
--name "${PROD_CONTAINER_NAME:-<your-project>-prod-shell}" \
|
|
36
|
+
-v "$(pwd):/workspace" \
|
|
37
|
+
-w /workspace \
|
|
38
|
+
--env-file "${PROD_KEY_FILE}" \
|
|
39
|
+
"${IMAGE}" \
|
|
40
|
+
bash
|