envstash-cli 0.1.1 → 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 +36 -9
- package/package.json +1 -1
- package/src/dev-host.mjs +50 -0
- package/src/index.mjs +11 -1
- package/src/manifest.mjs +5 -1
- package/src/session.mjs +60 -12
package/README.md
CHANGED
|
@@ -141,8 +141,9 @@ Monorepos can track nested paths by listing them explicitly:
|
|
|
141
141
|
| --------------------- | --------------------------------------------------------- |
|
|
142
142
|
| `ENVSTASH_TOKEN` | Use this token instead of the stored one — for CI |
|
|
143
143
|
| `ENVSTASH_HOST` | Target another server (default `https://www.envstash.co`) |
|
|
144
|
+
| `ENVSTASH_PORT` | Target a dev server on localhost at this port |
|
|
144
145
|
| `ENVSTASH_CONFIG_DIR` | Where credentials live (default `~/.envstash`) |
|
|
145
|
-
| `ENVSTASH_DEV` |
|
|
146
|
+
| `ENVSTASH_DEV` | Look for a local dev server to log in to |
|
|
146
147
|
| `ENVSTASH_DEBUG` | Print stack traces on failure |
|
|
147
148
|
| `NO_COLOR` | Disable colour (also off automatically when piped) |
|
|
148
149
|
|
|
@@ -157,16 +158,42 @@ checkout, and without this rule a repo could name a server and be handed your
|
|
|
157
158
|
credential. If a repo pins a host you have not used, the CLI says so and tells
|
|
158
159
|
you the `--host` to pass if you meant it.
|
|
159
160
|
|
|
160
|
-
###
|
|
161
|
+
### Working against a local server
|
|
161
162
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
link it opens points at the server you are running rather than production.
|
|
165
|
-
Production is the fallback when none answer, and an explicit `--host` always
|
|
166
|
-
wins over detection.
|
|
163
|
+
Every command takes `--port` as shorthand for a server on this machine, so
|
|
164
|
+
`--port 3001` says what `--host http://localhost:3001` says:
|
|
167
165
|
|
|
168
|
-
|
|
169
|
-
|
|
166
|
+
```bash
|
|
167
|
+
envs status --port 3001
|
|
168
|
+
envs pull --port 3001
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
`ENVSTASH_PORT=3001` does the same for a whole shell. Passing `--host` and
|
|
172
|
+
`--port` together is refused rather than resolved — they are two ways of naming
|
|
173
|
+
one server, and picking one silently is how a command ends up talking to the
|
|
174
|
+
server you meant to exclude.
|
|
175
|
+
|
|
176
|
+
`envs login` is deliberately not affected by any of this. **It signs in to
|
|
177
|
+
`https://www.envstash.co` unless you say otherwise**, even if your last login
|
|
178
|
+
was to a dev server: where the previous login went is not an instruction about
|
|
179
|
+
where the next one should go.
|
|
180
|
+
|
|
181
|
+
To sign in to a local server, name it — `envs login --port 3001` — or let the
|
|
182
|
+
CLI find it:
|
|
183
|
+
|
|
184
|
+
```bash
|
|
185
|
+
envs login --dev
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
`--dev` (or `ENVSTASH_DEV=1`) looks for an EnvStash dev server on ports 3000,
|
|
189
|
+
3001, 3002 and 3003 and logs in to the first one that answers. Production is the
|
|
190
|
+
fallback when none answer, and an explicit `--host` or `--port` always wins over
|
|
191
|
+
detection.
|
|
192
|
+
|
|
193
|
+
A port is only accepted by detection if it responds as an EnvStash server, so an
|
|
194
|
+
unrelated process holding 3000 is skipped rather than logged in to. `--port`
|
|
195
|
+
does not probe: naming a port is you being explicit, and that is not
|
|
196
|
+
second-guessed.
|
|
170
197
|
|
|
171
198
|
## Notes
|
|
172
199
|
|
package/package.json
CHANGED
package/src/dev-host.mjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* mean "dev".
|
|
9
9
|
*/
|
|
10
10
|
import { DEFAULT_HOST } from './config.mjs'
|
|
11
|
+
import { CliError } from './ui.mjs'
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* The ports `scripts/dev-with-port.mjs` hands out, which are in turn the ports
|
|
@@ -82,3 +83,52 @@ export function isDevMode(flags) {
|
|
|
82
83
|
|
|
83
84
|
/** The host used when dev mode finds nothing — production, the last resort. */
|
|
84
85
|
export const FALLBACK_HOST = DEFAULT_HOST
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* The host a `--port` (or `ENVSTASH_PORT`) asked for, or null when neither did.
|
|
89
|
+
*
|
|
90
|
+
* A port with no host can only mean a server on this machine, so it expands to
|
|
91
|
+
* the same `http://localhost:<port>` shape detection produces. It exists because
|
|
92
|
+
* the alternative for every non-login command was spelling out
|
|
93
|
+
* `--host http://localhost:3001` — long enough that people export ENVSTASH_HOST
|
|
94
|
+
* for the shell instead, and then forget it is set when they go back to
|
|
95
|
+
* production.
|
|
96
|
+
*
|
|
97
|
+
* Unlike detection this does not probe: naming a port is the developer being
|
|
98
|
+
* explicit, and second-guessing an explicit answer is how `--host` would behave
|
|
99
|
+
* if it checked too.
|
|
100
|
+
*/
|
|
101
|
+
export function resolvePortHost(flags) {
|
|
102
|
+
// `hasOwn` rather than a truthiness check: `--port` with nothing after it
|
|
103
|
+
// parses to undefined, and that is a mistake worth naming rather than a
|
|
104
|
+
// reason to fall through to the environment.
|
|
105
|
+
const fromFlag = Boolean(flags) && Object.hasOwn(flags, 'port')
|
|
106
|
+
const raw = fromFlag ? flags.port : process.env.ENVSTASH_PORT
|
|
107
|
+
|
|
108
|
+
if (!fromFlag && !raw) {
|
|
109
|
+
return null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return { host: `http://localhost:${parsePort(raw, fromFlag)}`, source: fromFlag ? 'flag' : 'env' }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* A port is an integer in 1–65535 and nothing else.
|
|
117
|
+
*
|
|
118
|
+
* Refusing the rest matters more here than it looks: every value that slips
|
|
119
|
+
* through becomes a host, and a host that is quietly wrong fails later, as a
|
|
120
|
+
* connection error against a server the developer never meant to name.
|
|
121
|
+
*/
|
|
122
|
+
function parsePort(value, fromFlag) {
|
|
123
|
+
const text = String(value ?? '').trim()
|
|
124
|
+
const port = Number(text)
|
|
125
|
+
|
|
126
|
+
if (!text || !Number.isInteger(port) || port < 1 || port > 65535) {
|
|
127
|
+
throw new CliError(
|
|
128
|
+
`${fromFlag ? '--port' : 'ENVSTASH_PORT'} needs a port number${text ? `, not "${text}"` : ''}.`,
|
|
129
|
+
{ hint: 'For example: --port 3001' }
|
|
130
|
+
)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return port
|
|
134
|
+
}
|
package/src/index.mjs
CHANGED
|
@@ -16,7 +16,7 @@ import { CliError, errOut, out, style } from './ui.mjs'
|
|
|
16
16
|
* Flags that take a value. Everything else is a boolean, which is what lets
|
|
17
17
|
* `--force` and `--project my-app` be parsed without a schema per command.
|
|
18
18
|
*/
|
|
19
|
-
const VALUE_FLAGS = new Set(['host', 'project', 'label'])
|
|
19
|
+
const VALUE_FLAGS = new Set(['host', 'port', 'project', 'label'])
|
|
20
20
|
|
|
21
21
|
/** The implementation of each command, by name. */
|
|
22
22
|
const HANDLERS = { login, logout, whoami, init, link, list, status, diff, pull, push }
|
|
@@ -197,11 +197,16 @@ function printHelp(topic) {
|
|
|
197
197
|
out(` ${style.bold('Common options')}`)
|
|
198
198
|
out(` --project <name> ${style.dim('Target a project other than the linked one')}`)
|
|
199
199
|
out(` --host <url> ${style.dim('Target another EnvStash server')}`)
|
|
200
|
+
out(` --port <n> ${style.dim('Target a dev server on localhost at this port')}`)
|
|
200
201
|
out(` --json ${style.dim('Machine-readable output (status, list, whoami)')}`)
|
|
201
202
|
out(` --exit-zero ${style.dim('Always exit 0, even when out of sync (status, diff)')}`)
|
|
202
203
|
out(` --dry-run ${style.dim('Show what would happen (pull, push)')}`)
|
|
203
204
|
out(` --dev ${style.dim('Log in to a local dev server if one is running')}`)
|
|
204
205
|
out()
|
|
206
|
+
out(
|
|
207
|
+
` ${style.dim('login always signs in to')} ${DEFAULT_HOST} ${style.dim('unless one of those says otherwise.')}`
|
|
208
|
+
)
|
|
209
|
+
out()
|
|
205
210
|
out(` ${style.bold('Environment')}`)
|
|
206
211
|
|
|
207
212
|
const envPad =
|
|
@@ -265,9 +270,14 @@ const COMMAND_HELP = {
|
|
|
265
270
|
login: [
|
|
266
271
|
'Authorize this machine using a browser device code.',
|
|
267
272
|
'',
|
|
273
|
+
'Signs in to production unless you say otherwise — a previous login to a dev',
|
|
274
|
+
'server does not change where the next one goes.',
|
|
275
|
+
'',
|
|
268
276
|
' envs login',
|
|
269
277
|
' envs login --no-browser Print the URL instead of opening it',
|
|
270
278
|
' envs login --host <url> Sign in to another server',
|
|
279
|
+
' envs login --port 3001 Sign in to a dev server on localhost:3001',
|
|
280
|
+
' envs login --dev Find a local dev server and sign in to it',
|
|
271
281
|
],
|
|
272
282
|
logout: [
|
|
273
283
|
'Revoke this machine’s token and remove it locally.',
|
package/src/manifest.mjs
CHANGED
|
@@ -51,13 +51,17 @@ export const CLI_ALIASES = Object.fromEntries(
|
|
|
51
51
|
export const CLI_ENV_VARS = [
|
|
52
52
|
{ name: 'ENVSTASH_TOKEN', purpose: () => 'Use this token instead of the stored one — for CI' },
|
|
53
53
|
{ name: 'ENVSTASH_HOST', purpose: host => `Target another server (default ${host})` },
|
|
54
|
+
{
|
|
55
|
+
name: 'ENVSTASH_PORT',
|
|
56
|
+
purpose: () => 'Target a dev server on localhost at this port (same as --port)',
|
|
57
|
+
},
|
|
54
58
|
{
|
|
55
59
|
name: 'ENVSTASH_CONFIG_DIR',
|
|
56
60
|
purpose: () => 'Where credentials live (default ~/.envstash)',
|
|
57
61
|
},
|
|
58
62
|
{
|
|
59
63
|
name: 'ENVSTASH_DEV',
|
|
60
|
-
purpose: () => '
|
|
64
|
+
purpose: () => 'Look for a local dev server to log in to (same as --dev)',
|
|
61
65
|
},
|
|
62
66
|
{ name: 'ENVSTASH_DEBUG', purpose: () => 'Print stack traces on failure' },
|
|
63
67
|
{ name: 'NO_COLOR', purpose: () => 'Disable colour (also off automatically when piped)' },
|
package/src/session.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { requireToken, resolveHost } from './config.mjs'
|
|
2
|
-
import { detectDevHost, isDevMode } from './dev-host.mjs'
|
|
1
|
+
import { DEFAULT_HOST, requireToken, resolveHost } from './config.mjs'
|
|
2
|
+
import { detectDevHost, isDevMode, resolvePortHost } from './dev-host.mjs'
|
|
3
3
|
import { findRepoRoot, readLink } from './project.mjs'
|
|
4
|
+
import { CliError } from './ui.mjs'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Resolve host and token together.
|
|
@@ -18,6 +19,39 @@ export function requireHostToken(flags) {
|
|
|
18
19
|
return { host, token: requireToken(host, { allowEnvToken: source !== 'link' }) }
|
|
19
20
|
}
|
|
20
21
|
|
|
22
|
+
/**
|
|
23
|
+
* The host the developer named for this command, or null when they named none.
|
|
24
|
+
*
|
|
25
|
+
* The ladder is the usual one — what you typed beats what your shell exported —
|
|
26
|
+
* with `--port` sitting beside `--host` at each rung, because they are two ways
|
|
27
|
+
* of saying the same thing. Passing both is therefore a contradiction rather
|
|
28
|
+
* than a precedence question, and it is refused: silently picking one is how a
|
|
29
|
+
* command ends up talking to a server the developer explicitly excluded.
|
|
30
|
+
*/
|
|
31
|
+
export function resolveExplicitHost(flags) {
|
|
32
|
+
const port = resolvePortHost(flags)
|
|
33
|
+
|
|
34
|
+
if (flags?.host && port?.source === 'flag') {
|
|
35
|
+
throw new CliError(`--host and --port both name a server: ${flags.host} and ${port.host}.`, {
|
|
36
|
+
hint: 'Pass one of them.',
|
|
37
|
+
})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (flags?.host) {
|
|
41
|
+
return { host: resolveHost(flags.host), source: 'flag' }
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (port?.source === 'flag') {
|
|
45
|
+
return port
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (process.env.ENVSTASH_HOST) {
|
|
49
|
+
return { host: resolveHost(undefined), source: 'env' }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return port
|
|
53
|
+
}
|
|
54
|
+
|
|
21
55
|
/**
|
|
22
56
|
* Resolve the host and say where the decision came from.
|
|
23
57
|
*
|
|
@@ -27,8 +61,10 @@ export function requireHostToken(flags) {
|
|
|
27
61
|
* less trust.
|
|
28
62
|
*/
|
|
29
63
|
export function resolveHostSource(flags) {
|
|
30
|
-
|
|
31
|
-
|
|
64
|
+
const explicit = resolveExplicitHost(flags)
|
|
65
|
+
|
|
66
|
+
if (explicit) {
|
|
67
|
+
return explicit
|
|
32
68
|
}
|
|
33
69
|
|
|
34
70
|
try {
|
|
@@ -64,18 +100,28 @@ export function isLinkChosenHost(flags, host) {
|
|
|
64
100
|
/**
|
|
65
101
|
* Host for an interactive login.
|
|
66
102
|
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* production
|
|
103
|
+
* Login does not inherit the stored default the way every other command does,
|
|
104
|
+
* and that is the point: the stored default is wherever the *last* login went,
|
|
105
|
+
* so one `pnpm cli:dev login` used to make every later `pnpm envs login` sign in
|
|
106
|
+
* to localhost — which looks like it worked, right up to the point where a
|
|
107
|
+
* production command finds no credential. Production is the default here, and
|
|
108
|
+
* reaching a dev server is something you ask for.
|
|
109
|
+
*
|
|
110
|
+
* Above that: in dev mode a running local server beats production, so
|
|
111
|
+
* `pnpm cli:dev login` opens the server the developer is running. An explicit
|
|
112
|
+
* `--host`, `--port`, or `ENVSTASH_HOST` wins over detection — asking for a
|
|
113
|
+
* specific server should never be second-guessed. A host this repo's link file
|
|
114
|
+
* pinned still applies, since that is a deliberate per-repo choice, and
|
|
115
|
+
* `login` already refuses one it has never signed in to.
|
|
72
116
|
*
|
|
73
117
|
* `onDevHost` reports what detection found so the command can say so. Deciding
|
|
74
118
|
* and printing are split because the decision is what the tests care about.
|
|
75
119
|
*/
|
|
76
120
|
export async function resolveLoginHost(flags, { onDevHost } = {}) {
|
|
77
|
-
|
|
78
|
-
|
|
121
|
+
const explicit = resolveExplicitHost(flags)
|
|
122
|
+
|
|
123
|
+
if (explicit) {
|
|
124
|
+
return explicit.host
|
|
79
125
|
}
|
|
80
126
|
|
|
81
127
|
if (isDevMode(flags)) {
|
|
@@ -88,5 +134,7 @@ export async function resolveLoginHost(flags, { onDevHost } = {}) {
|
|
|
88
134
|
}
|
|
89
135
|
}
|
|
90
136
|
|
|
91
|
-
|
|
137
|
+
const resolved = resolveHostSource(flags)
|
|
138
|
+
|
|
139
|
+
return resolved.source === 'link' ? resolved.host : DEFAULT_HOST
|
|
92
140
|
}
|