envstash-cli 0.10.0 → 0.11.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 +11 -1
- package/package.json +1 -1
- package/src/commands/misc.mjs +3 -1
- package/src/commands/push.mjs +40 -0
- package/src/commands/sync.mjs +12 -1
- package/src/index.mjs +2 -2
- package/src/project.mjs +27 -2
package/README.md
CHANGED
|
@@ -244,7 +244,7 @@ on the dashboard under **Shared Files**, not inside any project.
|
|
|
244
244
|
```json
|
|
245
245
|
{
|
|
246
246
|
"project": "demo-app",
|
|
247
|
-
"files": [".env
|
|
247
|
+
"files": [".env", ".env.production"]
|
|
248
248
|
}
|
|
249
249
|
```
|
|
250
250
|
|
|
@@ -259,6 +259,16 @@ and `.agents/` (see below). That list is the default, and
|
|
|
259
259
|
`.dist` are ignored, since those are meant to be committed and hold placeholder
|
|
260
260
|
values.
|
|
261
261
|
|
|
262
|
+
`.env.local` and `.env.*.local` are not discovered either. Every dotenv loader
|
|
263
|
+
that layers files puts `.local` on top — `.env`, then `.env.<mode>`, then
|
|
264
|
+
`.env.local`, then the real environment — so by name it is the override a
|
|
265
|
+
developer writes to differ from the shared values on this one checkout. A tool
|
|
266
|
+
that distributes the shared values fills the lower layers and leaves that one
|
|
267
|
+
alone; syncing it would leave no layer that can differ between machines.
|
|
268
|
+
`init`, `push`, and `sync` say when they skip one. To sync it anyway, list it in
|
|
269
|
+
`.envstash.json` or name it on the command line — that is taken as written. A
|
|
270
|
+
`.local` file already in the cloud keeps syncing until you `envs rm` it.
|
|
271
|
+
|
|
262
272
|
### Files in `.config/`
|
|
263
273
|
|
|
264
274
|
A service-account key or a cert is a secret that is not `KEY=value`, and it does
|
package/package.json
CHANGED
package/src/commands/misc.mjs
CHANGED
|
@@ -15,7 +15,7 @@ import {
|
|
|
15
15
|
import { requireHostToken, resolveHostFor, resolveHostSource } from '../session.mjs'
|
|
16
16
|
import { DEFAULT_SETTINGS, readSettings, settingsPath } from '../settings.mjs'
|
|
17
17
|
import { out, pad, pluralize, sanitize, style } from '../ui.mjs'
|
|
18
|
-
import { announceDiscoveryDirs } from './push.mjs'
|
|
18
|
+
import { announceDiscoveryDirs, announceLocalEnvFiles } from './push.mjs'
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* `envs version` — the version, and the environment it would act in.
|
|
@@ -266,6 +266,7 @@ export async function init(flags, args) {
|
|
|
266
266
|
const files = existing?.files?.length
|
|
267
267
|
? existing.files
|
|
268
268
|
: discoverTrackedFiles(repoRoot, readSettings().discover)
|
|
269
|
+
announceLocalEnvFiles(repoRoot, { discovering: !existing?.files?.length, covered: files })
|
|
269
270
|
const path = writeLink(repoRoot, {
|
|
270
271
|
project: name,
|
|
271
272
|
files,
|
|
@@ -332,6 +333,7 @@ export async function link(flags, args) {
|
|
|
332
333
|
const files = existing?.files?.length
|
|
333
334
|
? existing.files
|
|
334
335
|
: discoverTrackedFiles(repoRoot, readSettings().discover)
|
|
336
|
+
announceLocalEnvFiles(repoRoot, { discovering: !existing?.files?.length, covered: files })
|
|
335
337
|
const path = writeLink(repoRoot, { project: name, files, host: existing?.host ?? null })
|
|
336
338
|
|
|
337
339
|
out()
|
package/src/commands/push.mjs
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
ensureSecretDirIgnored,
|
|
14
14
|
ENV_FILES_TARGET,
|
|
15
15
|
lacksIgnoreRule,
|
|
16
|
+
LINK_FILE,
|
|
17
|
+
localEnvFiles,
|
|
16
18
|
SECRET_FILE_DIR,
|
|
17
19
|
} from '../project.mjs'
|
|
18
20
|
import { resolveRepoFile } from '../repo-path.mjs'
|
|
@@ -62,6 +64,39 @@ export function announceDiscoveryDirs(repoRoot, { discovering = true } = {}) {
|
|
|
62
64
|
}
|
|
63
65
|
}
|
|
64
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Say which `.local` files the dotenv scan left behind, and how to list one.
|
|
69
|
+
*
|
|
70
|
+
* The scan skips them by name: `.local` is the per-checkout override layer, not
|
|
71
|
+
* the shared values. A developer whose secrets live in `.env.local` would
|
|
72
|
+
* otherwise see it silently missing from the list a command just decided.
|
|
73
|
+
*
|
|
74
|
+
* `covered` is that list. A `.local` file already in the cloud — pushed by an
|
|
75
|
+
* older version, or by name — is still synced through the cloud listing until
|
|
76
|
+
* `envs rm`, and saying otherwise would be false. Only when discovery decided
|
|
77
|
+
* the list (`discovering`) and the scan is on is there anything to point out.
|
|
78
|
+
*/
|
|
79
|
+
export function announceLocalEnvFiles(repoRoot, { discovering = true, covered = [] } = {}) {
|
|
80
|
+
if (!discovering || !readSettings().discover.includes(ENV_FILES_TARGET)) {
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const left = localEnvFiles(repoRoot).filter(name => !covered.includes(name))
|
|
85
|
+
|
|
86
|
+
if (left.length === 0) {
|
|
87
|
+
return
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
out()
|
|
91
|
+
|
|
92
|
+
for (const name of left) {
|
|
93
|
+
out(
|
|
94
|
+
` ${style.dim('·')} ${style.bold(name)} is a local override, so it is not synced ` +
|
|
95
|
+
style.dim(`— list it in ${LINK_FILE} to sync it`)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
65
100
|
/**
|
|
66
101
|
* `envs push` — send local files up.
|
|
67
102
|
*
|
|
@@ -106,6 +141,11 @@ export async function push(flags, args) {
|
|
|
106
141
|
requested: args,
|
|
107
142
|
})
|
|
108
143
|
|
|
144
|
+
announceLocalEnvFiles(context.repoRoot, {
|
|
145
|
+
discovering: args.length === 0 && !context.link?.files?.length,
|
|
146
|
+
covered: names,
|
|
147
|
+
})
|
|
148
|
+
|
|
109
149
|
const localFiles = loadLocalFiles(context.repoRoot, names, cloudKinds(cloudFiles))
|
|
110
150
|
const presentNames = names.filter(name => localFiles.get(name) !== null)
|
|
111
151
|
|
package/src/commands/sync.mjs
CHANGED
|
@@ -15,7 +15,13 @@ import { resolveRepoFile } from '../repo-path.mjs'
|
|
|
15
15
|
import { compareProject } from '../status.mjs'
|
|
16
16
|
import { conflictExitCode, isBlocked, isChanging, planSync, PREFER, SYNC_ACTION } from '../sync.mjs'
|
|
17
17
|
import { CliError, out, pad, pluralize, sanitize, style } from '../ui.mjs'
|
|
18
|
-
import {
|
|
18
|
+
import {
|
|
19
|
+
announceDiscoveryDirs,
|
|
20
|
+
announceLocalEnvFiles,
|
|
21
|
+
confirmNewFiles,
|
|
22
|
+
fetchCloudFiles,
|
|
23
|
+
pushOneBlob,
|
|
24
|
+
} from './push.mjs'
|
|
19
25
|
|
|
20
26
|
/**
|
|
21
27
|
* `envs sync` — reconcile this repo with the cloud, in both directions at once.
|
|
@@ -70,6 +76,11 @@ export async function sync(flags, args) {
|
|
|
70
76
|
sink: 'write',
|
|
71
77
|
})
|
|
72
78
|
|
|
79
|
+
announceLocalEnvFiles(context.repoRoot, {
|
|
80
|
+
discovering: args.length === 0 && !context.link?.files?.length,
|
|
81
|
+
covered: names,
|
|
82
|
+
})
|
|
83
|
+
|
|
73
84
|
const localFiles = loadLocalFiles(context.repoRoot, names, cloudKinds(cloudFiles))
|
|
74
85
|
const comparison = compareProject({
|
|
75
86
|
cloudFiles: cloudFiles.filter(file => names.includes(file.name)),
|
package/src/index.mjs
CHANGED
|
@@ -444,8 +444,8 @@ const COMMAND_HELP = {
|
|
|
444
444
|
'',
|
|
445
445
|
' { "discover": [".env*", ".config", ".agents"] }',
|
|
446
446
|
'',
|
|
447
|
-
'".env*" is the dotenv files in the repo root
|
|
448
|
-
'whose git-ignored files are synced.',
|
|
447
|
+
'".env*" is the dotenv files in the repo root, minus templates and .local',
|
|
448
|
+
'overrides; anything else is a directory whose git-ignored files are synced.',
|
|
449
449
|
'',
|
|
450
450
|
' envs config',
|
|
451
451
|
' envs config --json',
|
package/src/project.mjs
CHANGED
|
@@ -57,6 +57,17 @@ function runGit(args, { cwd, input } = {}) {
|
|
|
57
57
|
*/
|
|
58
58
|
const TEMPLATE_SUFFIXES = ['.example', '.sample', '.template', '.dist']
|
|
59
59
|
|
|
60
|
+
/**
|
|
61
|
+
* The per-checkout override layer. Every dotenv loader that layers files agrees
|
|
62
|
+
* on the stack — `.env`, `.env.<mode>`, `.env.local`, `.env.<mode>.local`, then
|
|
63
|
+
* the real environment — and `.local` is, by name, what a developer writes to
|
|
64
|
+
* differ from the shared values on this one checkout. A tool that distributes
|
|
65
|
+
* the shared values fills the lower layers and leaves this one alone: syncing
|
|
66
|
+
* it would leave no layer that can differ between machines. Listing the file in
|
|
67
|
+
* the link file or naming it on the command line still syncs it.
|
|
68
|
+
*/
|
|
69
|
+
const LOCAL_SUFFIX = '.local'
|
|
70
|
+
|
|
60
71
|
/**
|
|
61
72
|
* Walk up from `startDir` looking for a repo root.
|
|
62
73
|
*
|
|
@@ -165,13 +176,14 @@ export function guessProjectName(repoRoot) {
|
|
|
165
176
|
}
|
|
166
177
|
|
|
167
178
|
/**
|
|
168
|
-
*
|
|
179
|
+
* The dotenv files in the repo root that could hold a real value: `.env` and
|
|
180
|
+
* `.env.*`, minus templates and type declarations, regular files only.
|
|
169
181
|
*
|
|
170
182
|
* Deliberately not recursive: walking a whole tree would wander into
|
|
171
183
|
* node_modules and vendored fixtures, and a monorepo's nested files are better
|
|
172
184
|
* declared explicitly in the link file than guessed at.
|
|
173
185
|
*/
|
|
174
|
-
|
|
186
|
+
function rootEnvFiles(repoRoot) {
|
|
175
187
|
let names
|
|
176
188
|
|
|
177
189
|
try {
|
|
@@ -196,6 +208,19 @@ export function discoverEnvFiles(repoRoot) {
|
|
|
196
208
|
.sort()
|
|
197
209
|
}
|
|
198
210
|
|
|
211
|
+
/** Discover `.env*` files in the repo root — the shared layers, not `.local`. */
|
|
212
|
+
export function discoverEnvFiles(repoRoot) {
|
|
213
|
+
return rootEnvFiles(repoRoot).filter(name => !name.endsWith(LOCAL_SUFFIX))
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* The `.local` files discovery left behind, so a command can say why they are
|
|
218
|
+
* missing from a list it just decided.
|
|
219
|
+
*/
|
|
220
|
+
export function localEnvFiles(repoRoot) {
|
|
221
|
+
return rootEnvFiles(repoRoot).filter(name => name.endsWith(LOCAL_SUFFIX))
|
|
222
|
+
}
|
|
223
|
+
|
|
199
224
|
/**
|
|
200
225
|
* Where credential *files* live, by convention: a service-account JSON, a cert, a
|
|
201
226
|
* private key — the things that are secrets but are not `KEY=value`.
|