envstash-cli 0.4.3 → 0.4.4
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/package.json +1 -1
- package/src/commands/misc.mjs +8 -0
- package/src/commands/push.mjs +22 -0
- package/src/project.mjs +68 -0
package/package.json
CHANGED
package/src/commands/misc.mjs
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
} from '../project.mjs'
|
|
14
14
|
import { requireHostToken, resolveHostFor, resolveHostSource } from '../session.mjs'
|
|
15
15
|
import { out, pad, pluralize, style } from '../ui.mjs'
|
|
16
|
+
import { announceIgnoredSecretDir } from './push.mjs'
|
|
16
17
|
|
|
17
18
|
/**
|
|
18
19
|
* `envs version` — the version, and the environment it would act in.
|
|
@@ -140,6 +141,10 @@ export async function init(flags, args) {
|
|
|
140
141
|
|
|
141
142
|
await api.createProject(host, token, name)
|
|
142
143
|
|
|
144
|
+
// Before discovery, so a not-yet-ignored .config/ lands in the link file
|
|
145
|
+
// this init writes rather than being silently absent from it.
|
|
146
|
+
announceIgnoredSecretDir(repoRoot)
|
|
147
|
+
|
|
143
148
|
const files = existing?.files?.length ? existing.files : discoverTrackedFiles(repoRoot)
|
|
144
149
|
const path = writeLink(repoRoot, {
|
|
145
150
|
project: name,
|
|
@@ -201,6 +206,9 @@ export async function link(flags, args) {
|
|
|
201
206
|
// that points at nothing.
|
|
202
207
|
await api.projectStatus(host, token, name)
|
|
203
208
|
|
|
209
|
+
// Same moment as in init: discovery is about to decide the link file's list.
|
|
210
|
+
announceIgnoredSecretDir(repoRoot)
|
|
211
|
+
|
|
204
212
|
const files = existing?.files?.length ? existing.files : discoverTrackedFiles(repoRoot)
|
|
205
213
|
const path = writeLink(repoRoot, { project: name, files, host: existing?.host ?? null })
|
|
206
214
|
|
package/src/commands/push.mjs
CHANGED
|
@@ -1,9 +1,23 @@
|
|
|
1
1
|
import { api, ApiError } from '../api.mjs'
|
|
2
2
|
import { formatSize, MAX_BLOB_BYTES } from '../blob.mjs'
|
|
3
3
|
import { cloudKinds, loadLocalFiles, resolveContext, resolveFileNames } from '../context.mjs'
|
|
4
|
+
import { ensureSecretDirIgnored, SECRET_FILE_DIR } from '../project.mjs'
|
|
4
5
|
import { compareProject, FILE_STATE } from '../status.mjs'
|
|
5
6
|
import { CliError, out, pluralize, style } from '../ui.mjs'
|
|
6
7
|
|
|
8
|
+
/** Say that .gitignore was edited — quietly rewriting a user's file is worse. */
|
|
9
|
+
export function announceIgnoredSecretDir(repoRoot) {
|
|
10
|
+
if (!ensureSecretDirIgnored(repoRoot)) {
|
|
11
|
+
return
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
out()
|
|
15
|
+
out(
|
|
16
|
+
` ${style.green('+')} Added ${style.bold(`${SECRET_FILE_DIR}/`)} to .gitignore ` +
|
|
17
|
+
style.dim('— secrets sync through EnvStash, not git')
|
|
18
|
+
)
|
|
19
|
+
}
|
|
20
|
+
|
|
7
21
|
/**
|
|
8
22
|
* `envs push` — send local files up.
|
|
9
23
|
*
|
|
@@ -14,6 +28,14 @@ import { CliError, out, pluralize, style } from '../ui.mjs'
|
|
|
14
28
|
*/
|
|
15
29
|
export async function push(flags, args) {
|
|
16
30
|
const context = resolveContext(flags)
|
|
31
|
+
|
|
32
|
+
// Before discovery, so files in a not-yet-ignored .config/ are picked up by
|
|
33
|
+
// this very push rather than the next one. Not under --dry-run: a preview
|
|
34
|
+
// that edits files is not a preview.
|
|
35
|
+
if (!flags['dry-run']) {
|
|
36
|
+
announceIgnoredSecretDir(context.repoRoot)
|
|
37
|
+
}
|
|
38
|
+
|
|
17
39
|
const cloudFiles = await fetchCloudFiles(context)
|
|
18
40
|
|
|
19
41
|
const names = resolveFileNames({
|
package/src/project.mjs
CHANGED
|
@@ -280,6 +280,74 @@ export function discoverSecretFiles(repoRoot) {
|
|
|
280
280
|
return candidates.filter(name => ignored.has(name)).sort()
|
|
281
281
|
}
|
|
282
282
|
|
|
283
|
+
/**
|
|
284
|
+
* Make sure `.config/` is git-ignored before discovery asks about it.
|
|
285
|
+
*
|
|
286
|
+
* Being ignored is discovery's signal for "secret", which makes an unignored
|
|
287
|
+
* `.config/` silently invisible to `envs push` — a real footgun: the developer
|
|
288
|
+
* sees a directory full of credentials and a push that ignores them, with
|
|
289
|
+
* nothing saying why. Rather than documenting that, push and init close it:
|
|
290
|
+
* when the directory has contents, nothing in it is tracked by git, and it is
|
|
291
|
+
* not already ignored, `.config/` is added to .gitignore. A `.config/` with
|
|
292
|
+
* tracked files is a repo that commits configuration there on purpose, and is
|
|
293
|
+
* left alone.
|
|
294
|
+
*
|
|
295
|
+
* Returns true when .gitignore was changed — the caller must say so out loud,
|
|
296
|
+
* because a CLI editing a file it does not own should never do it silently.
|
|
297
|
+
*/
|
|
298
|
+
export function ensureSecretDirIgnored(repoRoot) {
|
|
299
|
+
let entries
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
entries = readdirSync(join(repoRoot, SECRET_FILE_DIR))
|
|
303
|
+
} catch {
|
|
304
|
+
return false
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
if (entries.length === 0) {
|
|
308
|
+
return false
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Also the git-and-repo probe: outside a repository (or without git) this
|
|
312
|
+
// throws, and an ignore rule would mean nothing there anyway.
|
|
313
|
+
let tracked
|
|
314
|
+
|
|
315
|
+
try {
|
|
316
|
+
tracked = runGit(['ls-files', '--', SECRET_FILE_DIR], { cwd: repoRoot }).trim()
|
|
317
|
+
} catch {
|
|
318
|
+
return false
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
if (tracked !== '') {
|
|
322
|
+
return false
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// A synthetic name, so one question covers the directory: any rule broad
|
|
326
|
+
// enough to ignore this probe ignores everything discovery would find.
|
|
327
|
+
const probe = `${SECRET_FILE_DIR}/envstash-ignore-probe`
|
|
328
|
+
|
|
329
|
+
if (gitIgnoredNames(repoRoot, [probe]).has(probe)) {
|
|
330
|
+
return false
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const path = join(repoRoot, '.gitignore')
|
|
334
|
+
let existing = ''
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
existing = readFileSync(path, 'utf8')
|
|
338
|
+
} catch {
|
|
339
|
+
// No .gitignore yet — the addition below becomes the whole file.
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
const addition = `# secret files — synced through EnvStash, not git\n${SECRET_FILE_DIR}/\n`
|
|
343
|
+
const body =
|
|
344
|
+
existing === '' ? addition : `${existing}${existing.endsWith('\n') ? '' : '\n'}\n${addition}`
|
|
345
|
+
|
|
346
|
+
writeFileSync(path, body)
|
|
347
|
+
|
|
348
|
+
return true
|
|
349
|
+
}
|
|
350
|
+
|
|
283
351
|
/** Everything auto-discovery offers to track: root dotenv files, then `.config/` files. */
|
|
284
352
|
export function discoverTrackedFiles(repoRoot) {
|
|
285
353
|
return [...discoverEnvFiles(repoRoot), ...discoverSecretFiles(repoRoot)]
|