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 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` | Log in to a local dev server when one is running |
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
- ### Logging in to a local server
161
+ ### Working against a local server
161
162
 
162
- `envs login --dev` (or `ENVSTASH_DEV=1`) looks for an EnvStash dev server on
163
- ports 3000, 3001, 3002 and 3003 and logs in to the first one that answers, so the
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
- A port is only accepted if it responds as an EnvStash server, so an unrelated
169
- process holding 3000 is skipped rather than logged in to.
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "envstash-cli",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Git-style CLI for EnvStash — keep your .env files in sync with your cloud account.",
5
5
  "keywords": [
6
6
  "env",
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: () => 'Log in to a local dev server when one is running (same as --dev)',
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
- if (flags?.host || process.env.ENVSTASH_HOST) {
31
- return { host: resolveHost(flags?.host), source: flags?.host ? 'flag' : 'env' }
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
- * The usual precedence with one step inserted: in dev mode a running local
68
- * server beats the stored default, so `pnpm cli:dev login` opens the server the
69
- * developer is running. An explicit `--host` or `ENVSTASH_HOST` still wins over
70
- * detection — asking for a specific server should never be second-guessed — and
71
- * production remains the last resort when no dev server answers.
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
- if (flags?.host || process.env.ENVSTASH_HOST) {
78
- return resolveHost(flags?.host)
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
- return resolveHostFor(flags)
137
+ const resolved = resolveHostSource(flags)
138
+
139
+ return resolved.source === 'link' ? resolved.host : DEFAULT_HOST
92
140
  }