@jondotsoy/envs 0.0.3 → 0.0.5
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 +103 -0
- package/SECURITY.md +46 -0
- package/envs.js +275 -1
- package/package.json +1 -1
package/README.md
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# @jondotsoy/envs
|
|
2
|
+
|
|
3
|
+
CLI para mantener en un solo archivo (`.envs/values.yml`) los valores del `.env` de cada worktree de git. Edita todos los valores juntos y distribúyelos a cada worktree, o recógelos desde los `.env` existentes.
|
|
4
|
+
|
|
5
|
+
Requiere [Bun](https://bun.com) y git.
|
|
6
|
+
|
|
7
|
+
## Instalación
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
bun add -g @jondotsoy/envs
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
O sin instalar:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
bunx @jondotsoy/envs help
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Uso
|
|
20
|
+
|
|
21
|
+
Ejecuta los comandos dentro del repo o de cualquiera de sus worktrees. Dentro de un worktree, `.envs/` siempre vive en el repo principal.
|
|
22
|
+
|
|
23
|
+
| Comando | Qué hace |
|
|
24
|
+
| --- | --- |
|
|
25
|
+
| `envs init` | Crea `.envs/`, `.envs/.gitignore` (con `*`) y un `.envs/values.yml` vacío. No sobrescribe archivos existentes. |
|
|
26
|
+
| `envs pull` | Lee el `.env` del repo principal y de cada worktree y lo escribe en `values.yml`. Las variables que ya no están en un `.env` se quitan de `values.yml` para ese worktree. |
|
|
27
|
+
| `envs push` | Escribe `values.yml` en el `.env` de cada worktree. Actualiza las variables existentes en su línea, agrega las nuevas al final y conserva comentarios y variables que no están en `values.yml`. |
|
|
28
|
+
| `envs lint` | Revisa la seguridad y muestra un warning por cada problema. Termina con código 1 si hay alguno (útil en CI). Ver abajo. |
|
|
29
|
+
| `envs edit` | Ejecuta `pull`, abre `values.yml` con `code -w` y, al cerrar el editor, ejecuta `push`. Si el editor falla, no hace `push`. |
|
|
30
|
+
| `envs help` | Muestra la ayuda. |
|
|
31
|
+
|
|
32
|
+
Los worktrees se identifican por el nombre de su rama (o el nombre de su carpeta si están en HEAD desacoplado). El repo principal usa el nombre de su rama, por ejemplo `main`.
|
|
33
|
+
|
|
34
|
+
### `envs lint`
|
|
35
|
+
|
|
36
|
+
Nunca imprime los valores, solo la variable y su ubicación (`envs.DB_PASSWORD.main`).
|
|
37
|
+
|
|
38
|
+
| Regla | Detecta |
|
|
39
|
+
| --- | --- |
|
|
40
|
+
| `tracked-values`, `unignored-values` | `values.yml` está versionado en git o no está ignorado. |
|
|
41
|
+
| `tracked-dotenv`, `unignored-dotenv` | El `.env` de un worktree está versionado o no está ignorado. |
|
|
42
|
+
| `open-permissions` | `values.yml` es legible por otros usuarios (usa `chmod 600`). |
|
|
43
|
+
| `executable-files` | `values.yml` o un `.env` tiene permiso de ejecución (usa `chmod -x`). |
|
|
44
|
+
| `writable-files` | `values.yml` o un `.env` es escribible por el grupo u otros usuarios (usa `chmod go-w`). |
|
|
45
|
+
| `weak-secret` | Variable sensible (`PASSWORD`, `TOKEN`, `SECRET`, `API_KEY`…) vacía o con un valor típico (`changeme`, `admin`…). |
|
|
46
|
+
| `shared-secret` | Variable sensible en `defaults`, que se escribe en todos los worktrees. |
|
|
47
|
+
| `secret-pattern` | Valor con formato de secreto conocido (AWS, GitHub, Slack, `sk-…`, clave privada). |
|
|
48
|
+
| `url-credentials`, `insecure-url` | URL remota con contraseña embebida, o con `http://`, `ws://` o `ftp://`. |
|
|
49
|
+
|
|
50
|
+
## Formato de `values.yml`
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
defaults:
|
|
54
|
+
LOG_LEVEL: info
|
|
55
|
+
|
|
56
|
+
envs:
|
|
57
|
+
DATABASE_URL:
|
|
58
|
+
main: postgres://localhost/main
|
|
59
|
+
feature-x: postgres://localhost/feature_x
|
|
60
|
+
PORT:
|
|
61
|
+
main: "3000"
|
|
62
|
+
feature-x: "3001"
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
- `defaults`: valor por defecto de cada variable. `push` lo escribe en el `.env` de todos los worktrees.
|
|
66
|
+
- `envs.<VARIABLE>.<worktree>`: valor de la variable en ese worktree. Sobrescribe al valor de `defaults`.
|
|
67
|
+
- Los valores pueden ser strings, números o booleanos (`PORT: 3000`, `DEBUG: true`); `push` los escribe en el `.env` como texto (`PORT=3000`, `DEBUG=true`). `pull` convierte `true`/`false` y los números canónicos (`3000`, `0.5`) a booleanos y números; el resto (`007`, `1e3`) queda como string.
|
|
68
|
+
|
|
69
|
+
El schema (JSON Schema) está en [`schema/values.schema.json`](schema/values.schema.json). Para que VS Code valide el archivo con la extensión YAML, agrega al inicio de `values.yml`:
|
|
70
|
+
|
|
71
|
+
```yaml
|
|
72
|
+
# yaml-language-server: $schema=../schema/values.schema.json
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
## Desarrollo
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
bun install
|
|
79
|
+
bun test # ejecuta los tests
|
|
80
|
+
bun test --coverage # con cobertura
|
|
81
|
+
bun run build # genera dist/ (envs.js y package.json)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
- `src/envs.ts`: lógica de los comandos. `src/bin/envs.ts`: punto de entrada del CLI.
|
|
85
|
+
- `test/fixtures/workspace.ts`: fixture `createWorkspace({ main, worktrees, files })`, que crea un repo git temporal con worktrees, cada uno en su propia carpeta temporal.
|
|
86
|
+
|
|
87
|
+
## Publicación
|
|
88
|
+
|
|
89
|
+
El workflow **Publish** (Actions → Publish → Run workflow) sube la versión, hace el build y publica `dist/` en npm por OIDC (trusted publishing). Inputs:
|
|
90
|
+
|
|
91
|
+
- `bump`: `major`, `minor`, `patch` o `prerelease`.
|
|
92
|
+
- `tag`: dist-tag de npm (`latest`, `default`, `alpha` o `demo`). Con un tag distinto de `latest` la versión es una prerelease (`0.0.2-alpha.0`).
|
|
93
|
+
- `provenance`: publica con provenance. npm solo lo acepta si el repositorio es público.
|
|
94
|
+
|
|
95
|
+
Luego del publish crea el commit y el tag de la versión, y un release en GitHub con el enlace a la versión en npm.
|
|
96
|
+
|
|
97
|
+
## Seguridad
|
|
98
|
+
|
|
99
|
+
Para reportar una vulnerabilidad, mira [SECURITY.md](SECURITY.md).
|
|
100
|
+
|
|
101
|
+
## Licencia
|
|
102
|
+
|
|
103
|
+
[MIT](LICENSE)
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Política de seguridad
|
|
2
|
+
|
|
3
|
+
## Versiones soportadas
|
|
4
|
+
|
|
5
|
+
Solo se mantiene la última versión publicada en npm de `@jondotsoy/envs`. Las correcciones de seguridad se publican en una versión nueva.
|
|
6
|
+
|
|
7
|
+
## Reportar una vulnerabilidad
|
|
8
|
+
|
|
9
|
+
No abras un issue público para reportar una vulnerabilidad. Repórtala de forma privada por uno de estos medios:
|
|
10
|
+
|
|
11
|
+
- Con el reporte privado de vulnerabilidades de GitHub: pestaña **Security** del repositorio, **Report a vulnerability**.
|
|
12
|
+
- Por correo a [hi@jon.soy](mailto:hi@jon.soy).
|
|
13
|
+
|
|
14
|
+
Incluye, si puedes:
|
|
15
|
+
|
|
16
|
+
- la versión de `@jondotsoy/envs` y de Bun que usas;
|
|
17
|
+
- una descripción del problema y su impacto;
|
|
18
|
+
- los pasos para reproducirlo.
|
|
19
|
+
|
|
20
|
+
Te responderé lo antes posible y coordinaremos la corrección y la divulgación del problema.
|
|
21
|
+
|
|
22
|
+
## Archivos que modifica
|
|
23
|
+
|
|
24
|
+
`envs` solo lee y escribe los archivos de esta tabla. `<raíz>` es el repo principal (dentro de un worktree, el repo padre) y `<worktree>` es la carpeta de cada worktree que lista `git worktree list`, incluido el repo principal, aunque esté fuera de `<raíz>`.
|
|
25
|
+
|
|
26
|
+
| Comando | Lee | Escribe |
|
|
27
|
+
| --- | --- | --- |
|
|
28
|
+
| `envs init` | — | Crea `<raíz>/.envs/`, `<raíz>/.envs/.gitignore` y `<raíz>/.envs/values.yml`, solo si no existen. Nunca sobrescribe. |
|
|
29
|
+
| `envs pull` | `<worktree>/.env` de cada worktree | Reescribe `<raíz>/.envs/values.yml`. El archivo se serializa de nuevo, así que se pierden los comentarios del YAML. |
|
|
30
|
+
| `envs push` | `<raíz>/.envs/values.yml` | Crea o actualiza `<worktree>/.env` de cada worktree que tenga valores. Actualiza las variables existentes en su línea, agrega las nuevas al final y conserva el resto del archivo. Si hay `defaults`, esto incluye a todos los worktrees. |
|
|
31
|
+
| `envs edit` | Lo mismo que `pull` y `push` | Lo mismo que `pull` y `push`. Además ejecuta `code -w <raíz>/.envs/values.yml`. |
|
|
32
|
+
|
|
33
|
+
Además:
|
|
34
|
+
|
|
35
|
+
- Ejecuta `git rev-parse` y `git worktree list` para ubicar la raíz y los worktrees. Son comandos de solo lectura.
|
|
36
|
+
- `envs edit` ejecuta el programa `code` que encuentre en el `PATH`.
|
|
37
|
+
- No hace conexiones de red ni modifica otros archivos del proyecto, ni siquiera el `.gitignore` de tu proyecto.
|
|
38
|
+
|
|
39
|
+
## Manejo de secretos
|
|
40
|
+
|
|
41
|
+
`envs` trabaja con valores que suelen ser secretos:
|
|
42
|
+
|
|
43
|
+
- `.envs/values.yml` guarda los valores de todos los worktrees en texto plano.
|
|
44
|
+
- `envs init` crea `.envs/.gitignore` con `*`, para que la carpeta `.envs/` no se suba a git. No quites ese archivo.
|
|
45
|
+
- `envs push` escribe los valores en los archivos `.env` de cada worktree. Asegúrate de que `.env` esté en el `.gitignore` de tu proyecto.
|
|
46
|
+
- No compartas `values.yml` ni lo pegues en issues o logs.
|
package/envs.js
CHANGED
|
@@ -4,13 +4,228 @@
|
|
|
4
4
|
// src/envs.ts
|
|
5
5
|
import { mkdir } from "fs/promises";
|
|
6
6
|
import { basename, dirname, join } from "path";
|
|
7
|
+
import { stat } from "fs/promises";
|
|
7
8
|
var {$ } = globalThis.Bun;
|
|
9
|
+
|
|
10
|
+
// src/lint-rules.ts
|
|
11
|
+
var SENSITIVE_KEY = /(^|_)(SECRET|PASSWORD|PASSWD|PWD|TOKEN|API_?KEY|PRIVATE_?KEY|ACCESS_?KEY|CREDENTIALS?|AUTH|SALT|SIGNING)(_|$)/i;
|
|
12
|
+
var WEAK_VALUES = new Set([
|
|
13
|
+
"",
|
|
14
|
+
"changeme",
|
|
15
|
+
"change-me",
|
|
16
|
+
"change_me",
|
|
17
|
+
"password",
|
|
18
|
+
"passw0rd",
|
|
19
|
+
"secret",
|
|
20
|
+
"admin",
|
|
21
|
+
"root",
|
|
22
|
+
"test",
|
|
23
|
+
"default",
|
|
24
|
+
"example",
|
|
25
|
+
"123456",
|
|
26
|
+
"12345678",
|
|
27
|
+
"qwerty",
|
|
28
|
+
"todo",
|
|
29
|
+
"xxx"
|
|
30
|
+
]);
|
|
31
|
+
var SECRET_PATTERNS = [
|
|
32
|
+
[/AKIA[0-9A-Z]{16}/, "an AWS access key id"],
|
|
33
|
+
[/gh[pousr]_[A-Za-z0-9]{36,}/, "a GitHub token"],
|
|
34
|
+
[/sk-[A-Za-z0-9_-]{20,}/, "an API secret key"],
|
|
35
|
+
[/xox[abprs]-[A-Za-z0-9-]{10,}/, "a Slack token"],
|
|
36
|
+
[/-----BEGIN [A-Z ]*PRIVATE KEY-----/, "a private key"]
|
|
37
|
+
];
|
|
38
|
+
function isSensitiveKey(key) {
|
|
39
|
+
return SENSITIVE_KEY.test(key);
|
|
40
|
+
}
|
|
41
|
+
function entries({ values }) {
|
|
42
|
+
const result = [];
|
|
43
|
+
for (const [key, value] of Object.entries(values.defaults ?? {})) {
|
|
44
|
+
result.push({ key, text: String(value), location: `defaults.${key}`, shared: true });
|
|
45
|
+
}
|
|
46
|
+
for (const [key, byWorktree] of Object.entries(values.envs ?? {})) {
|
|
47
|
+
for (const [worktree, value] of Object.entries(byWorktree ?? {})) {
|
|
48
|
+
result.push({
|
|
49
|
+
key,
|
|
50
|
+
text: String(value),
|
|
51
|
+
location: `envs.${key}.${worktree}`,
|
|
52
|
+
shared: false
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
function parseUrl(text) {
|
|
59
|
+
const match = text.match(/^([a-z][a-z0-9+.-]*):\/\/([^/\s]*)/i);
|
|
60
|
+
if (!match)
|
|
61
|
+
return;
|
|
62
|
+
const scheme = match[1].toLowerCase();
|
|
63
|
+
const authority = match[2];
|
|
64
|
+
const hostname = authority.slice(authority.lastIndexOf("@") + 1).replace(/:\d+$/, "").replace(/^\[|\]$/g, "");
|
|
65
|
+
return {
|
|
66
|
+
scheme,
|
|
67
|
+
hasPassword: /^[^@]*:[^@]+@/.test(authority),
|
|
68
|
+
local: /^(localhost|127\.\d+\.\d+\.\d+|::1|0\.0\.0\.0)$/.test(hostname)
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
var valuesFile = ".envs/values.yml";
|
|
72
|
+
function envFiles(ctx) {
|
|
73
|
+
return [
|
|
74
|
+
{ location: valuesFile, mode: ctx.valuesMode },
|
|
75
|
+
...ctx.dotenvs.map((d) => ({ location: `${d.name}/.env`, mode: d.mode }))
|
|
76
|
+
];
|
|
77
|
+
}
|
|
78
|
+
var rules = [
|
|
79
|
+
{
|
|
80
|
+
id: "tracked-values",
|
|
81
|
+
description: "values.yml is tracked by git.",
|
|
82
|
+
async check(ctx) {
|
|
83
|
+
if (!await ctx.isTracked(ctx.root, valuesFile))
|
|
84
|
+
return [];
|
|
85
|
+
return [
|
|
86
|
+
{
|
|
87
|
+
location: valuesFile,
|
|
88
|
+
message: "values.yml is tracked by git, so its secrets are in the history. Run `git rm --cached .envs/values.yml`."
|
|
89
|
+
}
|
|
90
|
+
];
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
id: "unignored-values",
|
|
95
|
+
description: "values.yml is neither tracked nor ignored by git.",
|
|
96
|
+
async check(ctx) {
|
|
97
|
+
if (await ctx.isTracked(ctx.root, valuesFile))
|
|
98
|
+
return [];
|
|
99
|
+
if (await ctx.isIgnored(ctx.root, valuesFile))
|
|
100
|
+
return [];
|
|
101
|
+
return [
|
|
102
|
+
{
|
|
103
|
+
location: valuesFile,
|
|
104
|
+
message: "values.yml is not ignored by git and could be committed by accident. Add `*` to .envs/.gitignore."
|
|
105
|
+
}
|
|
106
|
+
];
|
|
107
|
+
}
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
id: "open-permissions",
|
|
111
|
+
description: "values.yml is readable by other users.",
|
|
112
|
+
check({ valuesMode }) {
|
|
113
|
+
if (valuesMode === undefined || (valuesMode & 63) === 0)
|
|
114
|
+
return [];
|
|
115
|
+
return [
|
|
116
|
+
{
|
|
117
|
+
location: valuesFile,
|
|
118
|
+
message: `values.yml is readable by other users (mode ${(valuesMode & 511).toString(8)}). Run \`chmod 600 .envs/values.yml\`.`
|
|
119
|
+
}
|
|
120
|
+
];
|
|
121
|
+
}
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: "executable-files",
|
|
125
|
+
description: "values.yml or a worktree's .env has execute permission.",
|
|
126
|
+
check: (ctx) => envFiles(ctx).filter((f) => f.mode !== undefined && (f.mode & 73) !== 0).map((f) => ({
|
|
127
|
+
location: f.location,
|
|
128
|
+
message: `The file is executable (mode ${(f.mode & 511).toString(8)}); env files are data and must not be. Run \`chmod -x\` on it.`
|
|
129
|
+
}))
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: "writable-files",
|
|
133
|
+
description: "values.yml or a worktree's .env is writable by other users.",
|
|
134
|
+
check: (ctx) => envFiles(ctx).filter((f) => f.mode !== undefined && (f.mode & 18) !== 0).map((f) => ({
|
|
135
|
+
location: f.location,
|
|
136
|
+
message: `The file is writable by group or other users (mode ${(f.mode & 511).toString(8)}), who could change your environment. Run \`chmod go-w\` on it.`
|
|
137
|
+
}))
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
id: "tracked-dotenv",
|
|
141
|
+
description: "A worktree's .env is tracked by git.",
|
|
142
|
+
async check(ctx) {
|
|
143
|
+
const found = [];
|
|
144
|
+
for (const { name, path } of ctx.dotenvs) {
|
|
145
|
+
if (!await ctx.isTracked(path, ".env"))
|
|
146
|
+
continue;
|
|
147
|
+
found.push({
|
|
148
|
+
location: `${name}/.env`,
|
|
149
|
+
message: "The .env is tracked by git. Run `git rm --cached .env` and add it to .gitignore."
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
return found;
|
|
153
|
+
}
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
id: "unignored-dotenv",
|
|
157
|
+
description: "A worktree's .env is neither tracked nor ignored by git.",
|
|
158
|
+
async check(ctx) {
|
|
159
|
+
const found = [];
|
|
160
|
+
for (const { name, path } of ctx.dotenvs) {
|
|
161
|
+
if (await ctx.isTracked(path, ".env"))
|
|
162
|
+
continue;
|
|
163
|
+
if (await ctx.isIgnored(path, ".env"))
|
|
164
|
+
continue;
|
|
165
|
+
found.push({
|
|
166
|
+
location: `${name}/.env`,
|
|
167
|
+
message: "The .env is not ignored by git. Add `.env` to .gitignore."
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return found;
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
id: "weak-secret",
|
|
175
|
+
description: "A sensitive variable is empty or has an easily guessed value.",
|
|
176
|
+
check: (ctx) => entries(ctx).filter((e) => isSensitiveKey(e.key) && WEAK_VALUES.has(e.text.trim().toLowerCase())).map((e) => ({
|
|
177
|
+
location: e.location,
|
|
178
|
+
message: `${e.key} has an empty or easily guessed value.`
|
|
179
|
+
}))
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
id: "shared-secret",
|
|
183
|
+
description: "A sensitive variable is in defaults, so it goes to every worktree.",
|
|
184
|
+
check: (ctx) => entries(ctx).filter((e) => e.shared && isSensitiveKey(e.key)).map((e) => ({
|
|
185
|
+
location: e.location,
|
|
186
|
+
message: `${e.key} looks sensitive but is written to every worktree; set it per worktree under envs instead.`
|
|
187
|
+
}))
|
|
188
|
+
},
|
|
189
|
+
{
|
|
190
|
+
id: "secret-pattern",
|
|
191
|
+
description: "A value has the format of a well-known secret.",
|
|
192
|
+
check: (ctx) => entries(ctx).flatMap((e) => SECRET_PATTERNS.filter(([pattern]) => pattern.test(e.text)).map(([, name]) => ({
|
|
193
|
+
location: e.location,
|
|
194
|
+
message: `${e.key} looks like ${name}.`
|
|
195
|
+
})))
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
id: "url-credentials",
|
|
199
|
+
description: "A remote URL embeds a password.",
|
|
200
|
+
check: (ctx) => entries(ctx).filter((e) => {
|
|
201
|
+
const url = parseUrl(e.text);
|
|
202
|
+
return url?.hasPassword && !url.local;
|
|
203
|
+
}).map((e) => ({
|
|
204
|
+
location: e.location,
|
|
205
|
+
message: `${e.key} embeds a password in its URL.`
|
|
206
|
+
}))
|
|
207
|
+
},
|
|
208
|
+
{
|
|
209
|
+
id: "insecure-url",
|
|
210
|
+
description: "A remote URL uses an unencrypted protocol.",
|
|
211
|
+
check: (ctx) => entries(ctx).filter((e) => {
|
|
212
|
+
const url = parseUrl(e.text);
|
|
213
|
+
return url && !url.local && ["http", "ws", "ftp"].includes(url.scheme);
|
|
214
|
+
}).map((e) => ({
|
|
215
|
+
location: e.location,
|
|
216
|
+
message: `${e.key} uses ${parseUrl(e.text).scheme}:// instead of an encrypted protocol.`
|
|
217
|
+
}))
|
|
218
|
+
}
|
|
219
|
+
];
|
|
220
|
+
|
|
221
|
+
// src/envs.ts
|
|
8
222
|
var HELP = `Usage: envs <command>
|
|
9
223
|
|
|
10
224
|
Commands:
|
|
11
225
|
init Create the .envs/ directory (.gitignore, values.yml)
|
|
12
226
|
pull Read each worktree's .env and write it into .envs/values.yml
|
|
13
227
|
push Write defaults and .envs/values.yml into the .env of each worktree
|
|
228
|
+
lint Warn about insecure values and unprotected secret files
|
|
14
229
|
edit Pull, open .envs/values.yml in VS Code (code -w), then push
|
|
15
230
|
help Show this help message
|
|
16
231
|
`;
|
|
@@ -67,6 +282,18 @@ function parseDotenv(text) {
|
|
|
67
282
|
}
|
|
68
283
|
return result;
|
|
69
284
|
}
|
|
285
|
+
function parseValue(value) {
|
|
286
|
+
if (value === "true")
|
|
287
|
+
return true;
|
|
288
|
+
if (value === "false")
|
|
289
|
+
return false;
|
|
290
|
+
if (/^-?\d+(\.\d+)?$/.test(value)) {
|
|
291
|
+
const number = Number(value);
|
|
292
|
+
if (String(number) === value)
|
|
293
|
+
return number;
|
|
294
|
+
}
|
|
295
|
+
return value;
|
|
296
|
+
}
|
|
70
297
|
function formatDotenvValue(value) {
|
|
71
298
|
if (value !== "" && !/[\s#"'\\$]/.test(value))
|
|
72
299
|
return value;
|
|
@@ -155,7 +382,7 @@ async function pull(ctx) {
|
|
|
155
382
|
delete byWorktree[name];
|
|
156
383
|
}
|
|
157
384
|
for (const [key, value] of Object.entries(parsed)) {
|
|
158
|
-
(envs[key] ??= {})[name] = value;
|
|
385
|
+
(envs[key] ??= {})[name] = parseValue(value);
|
|
159
386
|
}
|
|
160
387
|
ctx.log(`Pulled ${name}`);
|
|
161
388
|
}
|
|
@@ -166,6 +393,51 @@ async function pull(ctx) {
|
|
|
166
393
|
await Bun.write(valuesPath, Bun.YAML.stringify(values, null, 2));
|
|
167
394
|
return 0;
|
|
168
395
|
}
|
|
396
|
+
async function isIgnored(dir, file) {
|
|
397
|
+
const result = await $`git check-ignore -q ${file}`.cwd(dir).nothrow().quiet();
|
|
398
|
+
return result.exitCode === 0;
|
|
399
|
+
}
|
|
400
|
+
async function isTracked(dir, file) {
|
|
401
|
+
const result = await $`git ls-files --error-unmatch ${file}`.cwd(dir).nothrow().quiet();
|
|
402
|
+
return result.exitCode === 0;
|
|
403
|
+
}
|
|
404
|
+
async function lint(ctx) {
|
|
405
|
+
const read = await readValues(ctx);
|
|
406
|
+
if (!read)
|
|
407
|
+
return 1;
|
|
408
|
+
const root = await findRoot(ctx.cwd);
|
|
409
|
+
const worktrees = await listWorktrees(ctx.cwd);
|
|
410
|
+
const modeOf = async (path) => process.platform === "win32" ? undefined : (await stat(path)).mode;
|
|
411
|
+
const dotenvs = [];
|
|
412
|
+
for (const worktree of worktrees) {
|
|
413
|
+
const path = join(worktree.path, ".env");
|
|
414
|
+
if (await Bun.file(path).exists()) {
|
|
415
|
+
dotenvs.push({ ...worktree, mode: await modeOf(path) });
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
const lintContext = {
|
|
419
|
+
values: read.values,
|
|
420
|
+
root,
|
|
421
|
+
valuesMode: await modeOf(read.path),
|
|
422
|
+
dotenvs,
|
|
423
|
+
isTracked,
|
|
424
|
+
isIgnored
|
|
425
|
+
};
|
|
426
|
+
let count = 0;
|
|
427
|
+
for (const rule of rules) {
|
|
428
|
+
for (const { location, message } of await rule.check(lintContext)) {
|
|
429
|
+
ctx.error(`warning [${rule.id}] ${location}: ${message}`);
|
|
430
|
+
count++;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
if (count === 0) {
|
|
434
|
+
ctx.log("No security problems found.");
|
|
435
|
+
return 0;
|
|
436
|
+
}
|
|
437
|
+
ctx.error(`
|
|
438
|
+
${count} warning(s) found.`);
|
|
439
|
+
return 1;
|
|
440
|
+
}
|
|
169
441
|
async function edit(ctx) {
|
|
170
442
|
const pulled = await pull(ctx);
|
|
171
443
|
if (pulled !== 0)
|
|
@@ -190,6 +462,8 @@ async function run(args, ctx = defaultContext()) {
|
|
|
190
462
|
return await push(ctx);
|
|
191
463
|
case "pull":
|
|
192
464
|
return await pull(ctx);
|
|
465
|
+
case "lint":
|
|
466
|
+
return await lint(ctx);
|
|
193
467
|
case "edit":
|
|
194
468
|
return await edit(ctx);
|
|
195
469
|
default:
|