envstash-cli 0.8.0 → 0.9.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 +33 -13
- package/package.json +1 -1
- package/src/commands/push.mjs +20 -9
- package/src/commands/sync.mjs +3 -3
- package/src/index.mjs +8 -5
- package/src/settings.mjs +78 -0
package/README.md
CHANGED
|
@@ -119,11 +119,10 @@ definition and therefore has no undo:
|
|
|
119
119
|
file are left alone, so pushing a partial `.env` cannot destroy a teammate's
|
|
120
120
|
key. Use `--prune` when you do want the cloud to mirror local exactly.
|
|
121
121
|
- **`push` lists new files before uploading them.** Any file that is not in the
|
|
122
|
-
cloud yet is shown, with its size, before the first upload
|
|
122
|
+
cloud yet is shown, with its size, before the first upload, and files that
|
|
123
123
|
discovery found — not named on the command line and not listed in
|
|
124
|
-
`.envstash.json` —
|
|
125
|
-
|
|
126
|
-
cloud, later pushes of it go through without asking.
|
|
124
|
+
`.envstash.json` — are marked `(discovered)`. They are then uploaded. To be
|
|
125
|
+
asked first, turn on [`confirmNewFiles`](#settings).
|
|
127
126
|
- **`diff` masks values**, showing at most the first 4 characters and the
|
|
128
127
|
length, and nothing at all of a value 12 characters or shorter. Use
|
|
129
128
|
`--show-values` to reveal them.
|
|
@@ -321,15 +320,15 @@ kind wins — a file cannot silently change identity between dotenv and opaque.
|
|
|
321
320
|
|
|
322
321
|
## Configuration
|
|
323
322
|
|
|
324
|
-
| Variable | Purpose
|
|
325
|
-
| --------------------- |
|
|
326
|
-
| `ENVSTASH_TOKEN` | Use this token instead of the stored one — for CI
|
|
327
|
-
| `ENVSTASH_HOST` | Target another server (default `https://www.envstash.co`)
|
|
328
|
-
| `ENVSTASH_PORT` | Target a dev server on localhost at this port
|
|
329
|
-
| `ENVSTASH_CONFIG_DIR` | Where credentials live (default `~/.envstash`)
|
|
330
|
-
| `ENVSTASH_DEV` | Look for a local dev server to log in to
|
|
331
|
-
| `ENVSTASH_DEBUG` | Print stack traces on failure
|
|
332
|
-
| `NO_COLOR` | Disable colour (also off automatically when piped)
|
|
323
|
+
| Variable | Purpose |
|
|
324
|
+
| --------------------- | ----------------------------------------------------------- |
|
|
325
|
+
| `ENVSTASH_TOKEN` | Use this token instead of the stored one — for CI |
|
|
326
|
+
| `ENVSTASH_HOST` | Target another server (default `https://www.envstash.co`) |
|
|
327
|
+
| `ENVSTASH_PORT` | Target a dev server on localhost at this port |
|
|
328
|
+
| `ENVSTASH_CONFIG_DIR` | Where credentials and settings live (default `~/.envstash`) |
|
|
329
|
+
| `ENVSTASH_DEV` | Look for a local dev server to log in to |
|
|
330
|
+
| `ENVSTASH_DEBUG` | Print stack traces on failure |
|
|
331
|
+
| `NO_COLOR` | Disable colour (also off automatically when piped) |
|
|
333
332
|
|
|
334
333
|
In CI, set `ENVSTASH_TOKEN` from your platform's secret store **and set
|
|
335
334
|
`ENVSTASH_HOST`**, then run `envs pull` — no interactive login required.
|
|
@@ -342,6 +341,27 @@ checkout, and without this rule a repo could name a server and be handed your
|
|
|
342
341
|
credential. If a repo pins a host you have not used, the CLI says so and tells
|
|
343
342
|
you the `--host` to pass if you meant it.
|
|
344
343
|
|
|
344
|
+
### Settings
|
|
345
|
+
|
|
346
|
+
Preferences for this machine live in `~/.envstash/settings.json` — a separate
|
|
347
|
+
file from `config.json`, which holds your tokens and is not meant to be edited
|
|
348
|
+
by hand. The file is optional; create it to change a default.
|
|
349
|
+
|
|
350
|
+
```json
|
|
351
|
+
{
|
|
352
|
+
"confirmNewFiles": true
|
|
353
|
+
}
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
| Setting | Default | Purpose |
|
|
357
|
+
| ----------------- | ------- | ---------------------------------------------------------------------------------- |
|
|
358
|
+
| `confirmNewFiles` | `false` | Ask before `push` or `sync` uploads a file that discovery found for the first time |
|
|
359
|
+
|
|
360
|
+
With `confirmNewFiles` on, `push` and `sync` ask on a terminal, and anywhere
|
|
361
|
+
else (CI, a pipe) they stop unless you pass `--yes` (`-y`). Files you named on
|
|
362
|
+
the command line, files listed in `.envstash.json`, and files already in the
|
|
363
|
+
cloud are never asked about.
|
|
364
|
+
|
|
345
365
|
### Working against a local server
|
|
346
366
|
|
|
347
367
|
Every command takes `--port` as shorthand for a server on this machine, so
|
package/package.json
CHANGED
package/src/commands/push.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import {
|
|
|
11
11
|
} from '../context.mjs'
|
|
12
12
|
import { ensureSecretDirIgnored, SECRET_FILE_DIR } from '../project.mjs'
|
|
13
13
|
import { resolveRepoFile } from '../repo-path.mjs'
|
|
14
|
+
import { readSettings } from '../settings.mjs'
|
|
14
15
|
import { compareProject, FILE_STATE } from '../status.mjs'
|
|
15
16
|
import { CliError, confirm, isInteractive, out, pad, pluralize, style } from '../ui.mjs'
|
|
16
17
|
|
|
@@ -183,18 +184,24 @@ export async function push(flags, args) {
|
|
|
183
184
|
}
|
|
184
185
|
|
|
185
186
|
/**
|
|
186
|
-
* Show
|
|
187
|
-
*
|
|
187
|
+
* Show every file this push would create in the cloud, before the first byte
|
|
188
|
+
* goes up — and, when the developer has asked to be asked, get a yes for the
|
|
189
|
+
* ones discovery found.
|
|
188
190
|
*
|
|
189
191
|
* Discovery sweeps root `.env*` files and everything git-ignored under
|
|
190
|
-
* `.config/`, recursively, and used to upload the lot and only then list it.
|
|
191
|
-
*
|
|
192
|
-
* developer saw its name. A file already in the cloud, named on the command
|
|
193
|
-
* line, or listed in `.envstash.json` was chosen by someone; a file discovery
|
|
194
|
-
* found for the first time was not, so that is the set that needs a yes.
|
|
192
|
+
* `.config/`, recursively, and used to upload the lot and only then list it.
|
|
193
|
+
* The list now always comes first, marking which files were discovered.
|
|
195
194
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
195
|
+
* Uploading them is the default: a developer who runs `envs push` in their own
|
|
196
|
+
* repo means "send my files up", and a question on every new file trained the
|
|
197
|
+
* answer to be `y`. Setting `"confirmNewFiles": true` in
|
|
198
|
+
* `~/.envstash/settings.json` restores the gate. A file already in the cloud,
|
|
199
|
+
* named on the command line, or listed in `.envstash.json` was chosen by
|
|
200
|
+
* someone; a file discovery found for the first time was not, so that is the
|
|
201
|
+
* set the gate asks about.
|
|
202
|
+
*
|
|
203
|
+
* With the gate on: asked on a terminal; refused without `--yes` anywhere
|
|
204
|
+
* else, since a CI job cannot answer.
|
|
198
205
|
*
|
|
199
206
|
* `envs sync` uploads new files too, and runs the same gate — `command` only
|
|
200
207
|
* changes which command name the messages tell the developer to re-run.
|
|
@@ -224,6 +231,10 @@ export async function confirmNewFiles(flags, context, args, comparison, { comman
|
|
|
224
231
|
return
|
|
225
232
|
}
|
|
226
233
|
|
|
234
|
+
if (!readSettings().confirmNewFiles) {
|
|
235
|
+
return
|
|
236
|
+
}
|
|
237
|
+
|
|
227
238
|
const count = pluralize(discovered.length, 'discovered file')
|
|
228
239
|
|
|
229
240
|
if (isInteractive()) {
|
package/src/commands/sync.mjs
CHANGED
|
@@ -84,9 +84,9 @@ export async function sync(flags, args) {
|
|
|
84
84
|
const plans = planSync(comparison, { prefer })
|
|
85
85
|
const byName = new Map(comparison.map(file => [file.name, file]))
|
|
86
86
|
|
|
87
|
-
// The same
|
|
88
|
-
//
|
|
89
|
-
//
|
|
87
|
+
// The same listing — and, when `confirmNewFiles` is set, the same gate —
|
|
88
|
+
// `push` puts in front of a file discovery found on its own. Sync uploads
|
|
89
|
+
// new files too.
|
|
90
90
|
await confirmNewFiles(flags, context, args, comparison, { command: 'sync' })
|
|
91
91
|
|
|
92
92
|
out()
|
package/src/index.mjs
CHANGED
|
@@ -288,7 +288,9 @@ function printHelp(topic) {
|
|
|
288
288
|
)
|
|
289
289
|
out(` --prefer <side> ${style.dim('Resolve conflicts as "local" or "cloud" (sync)')}`)
|
|
290
290
|
out(` --dry-run ${style.dim('Show what would happen (pull, push, sync, remove)')}`)
|
|
291
|
-
out(
|
|
291
|
+
out(
|
|
292
|
+
` -y, --yes ${style.dim('Skip the new-file question, if you turned it on (push, sync)')}`
|
|
293
|
+
)
|
|
292
294
|
out(
|
|
293
295
|
` --shared ${style.dim('Target your account-wide shared files (pull, push, sync, remove)')}`
|
|
294
296
|
)
|
|
@@ -378,9 +380,10 @@ const COMMAND_HELP = {
|
|
|
378
380
|
'Keys in the cloud that are absent locally are left alone unless --prune is',
|
|
379
381
|
'given, so pushing a partial .env cannot delete a teammate’s key.',
|
|
380
382
|
'',
|
|
381
|
-
'Files that would be new in the cloud are listed before anything uploads
|
|
382
|
-
'
|
|
383
|
-
'
|
|
383
|
+
'Files that would be new in the cloud are listed before anything uploads,',
|
|
384
|
+
'then uploaded. To be asked first about ones found by discovery — not',
|
|
385
|
+
'named, not in .envstash.json — put { "confirmNewFiles": true } in',
|
|
386
|
+
'~/.envstash/settings.json; --yes (-y) then answers for one run.',
|
|
384
387
|
'',
|
|
385
388
|
'--shared sends the named files to your account-wide shared files — for',
|
|
386
389
|
'credentials that follow you across projects, like a global .npmrc. Pull',
|
|
@@ -388,7 +391,7 @@ const COMMAND_HELP = {
|
|
|
388
391
|
'',
|
|
389
392
|
' envs push',
|
|
390
393
|
' envs push --dry-run',
|
|
391
|
-
' envs push --yes
|
|
394
|
+
' envs push --yes Skip the new-file question, if you turned it on',
|
|
392
395
|
' envs push --prune',
|
|
393
396
|
' envs push --shared .npmrc',
|
|
394
397
|
],
|
package/src/settings.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs'
|
|
2
|
+
import { join } from 'node:path'
|
|
3
|
+
|
|
4
|
+
import { configDir } from './config.mjs'
|
|
5
|
+
import { CliError } from './ui.mjs'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Preferences for this machine, next to — and deliberately not inside — the
|
|
9
|
+
* credential file. `config.json` holds every token and is rewritten by login
|
|
10
|
+
* and logout; a file meant to be edited by hand should not be one where a
|
|
11
|
+
* stray comma signs the developer out of everything.
|
|
12
|
+
*
|
|
13
|
+
* Per machine rather than per repo: `.envstash.json` travels with a clone, and
|
|
14
|
+
* how much a developer wants to be asked is theirs to decide, not the repo's.
|
|
15
|
+
*
|
|
16
|
+
* {
|
|
17
|
+
* "confirmNewFiles": true
|
|
18
|
+
* }
|
|
19
|
+
*/
|
|
20
|
+
const DEFAULTS = {
|
|
21
|
+
/** Ask before uploading a file that discovery found for the first time. */
|
|
22
|
+
confirmNewFiles: false,
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function settingsPath() {
|
|
26
|
+
return join(configDir(), 'settings.json')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* The settings, with defaults for whatever the file does not say — or for a
|
|
31
|
+
* file that is not there, which is the normal case.
|
|
32
|
+
*
|
|
33
|
+
* A value of the wrong type is refused rather than coerced: `"false"` is a
|
|
34
|
+
* truthy string, and reading it as "yes, ask me" or as "no, don't" would both
|
|
35
|
+
* be a guess about a safety preference.
|
|
36
|
+
*/
|
|
37
|
+
export function readSettings() {
|
|
38
|
+
const path = settingsPath()
|
|
39
|
+
|
|
40
|
+
if (!existsSync(path)) {
|
|
41
|
+
return { ...DEFAULTS }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
let parsed
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
parsed = JSON.parse(readFileSync(path, 'utf8'))
|
|
48
|
+
} catch {
|
|
49
|
+
throw new CliError(`Could not read ${path} — it is not valid JSON.`, {
|
|
50
|
+
hint: 'Fix the file, or delete it to go back to the defaults.',
|
|
51
|
+
})
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
55
|
+
throw new CliError(`${path} must hold a JSON object.`, {
|
|
56
|
+
hint: 'Example: { "confirmNewFiles": true }',
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const settings = { ...DEFAULTS }
|
|
61
|
+
|
|
62
|
+
for (const [key, fallback] of Object.entries(DEFAULTS)) {
|
|
63
|
+
if (!(key in parsed)) {
|
|
64
|
+
continue
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (typeof parsed[key] !== typeof fallback) {
|
|
68
|
+
throw new CliError(
|
|
69
|
+
`"${key}" in ${path} must be ${typeof fallback === 'boolean' ? 'true or false' : `a ${typeof fallback}`}.`,
|
|
70
|
+
{ hint: `Example: { "${key}": ${JSON.stringify(!fallback)} }` }
|
|
71
|
+
)
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
settings[key] = parsed[key]
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
return settings
|
|
78
|
+
}
|