@dotenvx/dotenvx-ops 0.37.4 → 0.37.5

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/CHANGELOG.md CHANGED
@@ -2,7 +2,13 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
4
4
 
5
- [Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.37.4...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.37.5...main)
6
+
7
+ ## [0.37.5](https://github.com/dotenvx/dotenvx-ops/compare/v0.37.4...v0.37.5) (2026-04-08)
8
+
9
+ ### Changed
10
+
11
+ * Better `login` and `logout` experience ([#33](https://github.com/dotenvx/dotenvx-ops/pull/33))
6
12
 
7
13
  ## [0.37.4](https://github.com/dotenvx/dotenvx-ops/compare/v0.37.3...v0.37.4) (2026-03-26)
8
14
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
- "version": "0.37.4",
2
+ "version": "0.37.5",
3
3
  "name": "@dotenvx/dotenvx-ops",
4
- "description": "Secrets for developers–from the creator of `dotenv` and `dotenvx`",
4
+ "description": "Secrets for agents–from the creator of `dotenv` and `dotenvx`",
5
5
  "author": "@motdotla",
6
6
  "keywords": [
7
7
  "dotenv",
@@ -41,7 +41,7 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@clack/core": "^0.4.2",
44
- "@dotenvx/dotenvx": "^1.54.1",
44
+ "@dotenvx/dotenvx": "^1.60.2",
45
45
  "@inquirer/prompts": "^7.10.1",
46
46
  "arch": "^2.1.1",
47
47
  "commander": "^11.1.0",
@@ -53,7 +53,8 @@
53
53
  "open": "^8.4.2",
54
54
  "playwright": "^1.57.0",
55
55
  "systeminformation": "^5.22.11",
56
- "undici": "^7.11.0"
56
+ "undici": "^7.11.0",
57
+ "yocto-spinner": "^1.1.0"
57
58
  },
58
59
  "devDependencies": {
59
60
  "@yao-pkg/pkg": "^5.14.2",
@@ -2,6 +2,7 @@ const fs = require('fs')
2
2
  const open = require('open')
3
3
 
4
4
  const { logger } = require('@dotenvx/dotenvx')
5
+ const Session = require('./../../db/session')
5
6
 
6
7
  const { createSpinner } = require('./../../lib/helpers/createSpinner')
7
8
  const clipboardy = require('./../../lib/helpers/clipboardy')
@@ -20,7 +21,8 @@ async function backup () {
20
21
  // debug opts
21
22
  const options = this.opts()
22
23
 
23
- const hostname = options.hostname
24
+ const sesh = new Session()
25
+ const hostname = options.hostname || sesh.hostname()
24
26
 
25
27
  try {
26
28
  logger.debug(`options: ${JSON.stringify(options)}`)
@@ -1,4 +1,5 @@
1
1
  const { logger } = require('@dotenvx/dotenvx')
2
+ const Session = require('./../../db/session')
2
3
 
3
4
  const main = require('./../../lib/main')
4
5
 
@@ -7,7 +8,8 @@ async function get (uri) {
7
8
  const options = this.opts()
8
9
  logger.debug(`options: ${JSON.stringify(options)}`)
9
10
 
10
- const hostname = options.hostname
11
+ const sesh = new Session()
12
+ const hostname = options.hostname || sesh.hostname()
11
13
  const token = options.token
12
14
 
13
15
  try {
@@ -1,4 +1,5 @@
1
1
  const { logger } = require('@dotenvx/dotenvx')
2
+ const Session = require('./../../db/session')
2
3
 
3
4
  const main = require('./../../lib/main')
4
5
 
@@ -7,7 +8,8 @@ async function keypair (publicKey) {
7
8
  const options = this.opts()
8
9
  logger.debug(`options: ${JSON.stringify(options)}`)
9
10
 
10
- const hostname = options.hostname
11
+ const sesh = new Session()
12
+ const hostname = options.hostname || sesh.hostname()
11
13
  const token = options.token
12
14
  try {
13
15
  const kp = await main.keypair(publicKey, { hostname, token })
@@ -1,22 +1,64 @@
1
1
  const open = require('open')
2
2
  const { logger } = require('@dotenvx/dotenvx')
3
+ const Session = require('./../../db/session')
3
4
 
4
5
  const Login = require('./../../lib/services/login')
5
6
  const LoginPoll = require('./../../lib/services/loginPoll')
6
7
 
7
8
  const clipboardy = require('./../../lib/helpers/clipboardy')
8
- const { createSpinner } = require('./../../lib/helpers/createSpinner')
9
- const confirm = require('./../../lib/helpers/confirm')
10
- const truncate = require('./../../lib/helpers/truncate')
9
+ const createSpinner2 = require('../../lib/helpers/createSpinner2')
11
10
  const formatCode = require('./../../lib/helpers/formatCode')
12
11
 
13
- const spinner = createSpinner('waiting on browser authorization')
12
+ const FRAMES = ['◐', '◓', '◑', '◒']
13
+
14
+ function listenForOpenKey (onOpen) {
15
+ const stdin = process.stdin
16
+ if (!stdin.isTTY) return () => {}
17
+
18
+ const canSetRawMode = typeof stdin.setRawMode === 'function'
19
+ const wasRawMode = Boolean(stdin.isRaw)
20
+
21
+ const cleanup = () => {
22
+ stdin.off('data', onData)
23
+ if (canSetRawMode) stdin.setRawMode(wasRawMode)
24
+ stdin.pause()
25
+ }
26
+
27
+ const onData = (chunk) => {
28
+ const key = String(chunk)
29
+ const lower = key.toLowerCase()
30
+
31
+ if (key === '\u0003') { // Ctrl+C
32
+ cleanup()
33
+ process.kill(process.pid, 'SIGINT')
34
+ return
35
+ }
36
+
37
+ if (key === '\r' || key === '\n' || lower === 'y') {
38
+ cleanup()
39
+ Promise.resolve(onOpen()).catch(() => {})
40
+ return
41
+ }
42
+
43
+ if (lower === 'n') cleanup()
44
+ }
45
+
46
+ if (canSetRawMode) stdin.setRawMode(true)
47
+ stdin.resume()
48
+ stdin.on('data', onData)
49
+
50
+ return cleanup
51
+ }
14
52
 
15
53
  async function login () {
16
54
  const options = this.opts()
55
+ const spinner = await createSpinner2({ ...options, text: 'logging in', frames: FRAMES })
56
+
17
57
  logger.debug(`options: ${JSON.stringify(options)}`)
18
58
 
19
- const hostname = options.hostname
59
+ const sesh = new Session()
60
+ const hostname = options.hostname || sesh.hostname()
61
+ let cleanupOpenKeyListener = () => {}
20
62
 
21
63
  try {
22
64
  const {
@@ -25,36 +67,32 @@ async function login () {
25
67
  verificationUri,
26
68
  verificationUriComplete,
27
69
  interval
28
- } = await new Login(options.hostname).run()
70
+ } = await new Login(hostname).run()
29
71
 
30
- try { clipboardy.writeSync(userCode) } catch (_e) {}
72
+ try { await clipboardy.write(userCode) } catch (_e) {}
73
+
74
+ const promptMessage = `◌ press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...`
31
75
 
32
76
  logger.debug(`POST ${hostname} with deviceCode ${deviceCode} at interval ${interval}`)
33
- logger.info(`press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...`)
77
+ logger.info(promptMessage)
34
78
 
35
79
  // begin polling
36
80
  const pollPromise = new LoginPoll(hostname, deviceCode, interval).run()
37
- spinner.start()
38
-
39
- // optionally allow user to open browser
40
- confirm({ message: `press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...` })
41
- .then(answer => answer && open(verificationUriComplete))
42
- .catch(() => {}) // ignore
43
-
81
+ cleanupOpenKeyListener = listenForOpenKey(() => open(verificationUriComplete))
44
82
  const data = await pollPromise
45
- spinner.succeed(`logged in [${data.username}] to this device and activated token [${truncate(data.access_token, 11)}]`)
46
- // logger.help('⮕ next run [dotenvx-ops backup]')
83
+
84
+ cleanupOpenKeyListener()
85
+ if (spinner) spinner.stop()
86
+ logger.success(`◉ logged in (${data.username})`)
47
87
  process.exit(0)
48
88
  } catch (error) {
49
- spinner.stop()
89
+ cleanupOpenKeyListener()
90
+ if (spinner) spinner.stop()
50
91
  if (error.message) {
51
92
  logger.error(error.message)
52
93
  } else {
53
94
  logger.error(error)
54
95
  }
55
- if (error.help) {
56
- logger.help(error.help)
57
- }
58
96
  if (error.stack) {
59
97
  logger.debug(error.stack)
60
98
  }
@@ -1,27 +1,24 @@
1
1
  const { logger } = require('@dotenvx/dotenvx')
2
+ const Session = require('./../../db/session')
2
3
 
3
- const { createSpinner } = require('./../../lib/helpers/createSpinner')
4
- const truncate = require('./../../lib/helpers/truncate')
4
+ const createSpinner2 = require('../../lib/helpers/createSpinner2')
5
5
  const Logout = require('./../../lib/services/logout')
6
6
 
7
- const spinner = createSpinner('waiting on browser authorization')
7
+ const FRAMES = ['◐', '◓', '◑', '◒']
8
8
 
9
9
  async function logout () {
10
10
  const options = this.opts()
11
+ const spinner = await createSpinner2({ ...options, text: 'logging out', frames: FRAMES })
12
+
11
13
  logger.debug(`options: ${JSON.stringify(options)}`)
12
14
 
13
- const hostname = options.hostname
15
+ const sesh = new Session()
16
+ const hostname = options.hostname || sesh.hostname()
14
17
 
15
18
  try {
16
- spinner.start()
17
-
18
- const {
19
- username,
20
- accessToken
21
- } = await new Logout(hostname).run()
22
-
23
- spinner.succeed(`logged out [${username}] from this device and revoked token [${truncate(accessToken, 11)}]`)
24
- // logger.help(`⮕ next visit [${settingsDevicesUrl}] to view your devices`)
19
+ const data = await new Logout(hostname).run()
20
+ spinner.stop()
21
+ logger.success(`◌ logged out (${data.username})`)
25
22
  } catch (error) {
26
23
  spinner.stop()
27
24
  if (error.message) {
@@ -29,9 +26,6 @@ async function logout () {
29
26
  } else {
30
27
  logger.error(error)
31
28
  }
32
- if (error.help) {
33
- logger.help(error.help)
34
- }
35
29
  if (error.stack) {
36
30
  logger.debug(error.stack)
37
31
  }
@@ -1,4 +1,5 @@
1
1
  const { logger } = require('@dotenvx/dotenvx')
2
+ const Session = require('./../../db/session')
2
3
 
3
4
  const main = require('./../../lib/main')
4
5
 
@@ -9,7 +10,8 @@ async function observe (base64) {
9
10
  const options = this.opts()
10
11
  logger.debug(`options: ${JSON.stringify(options)}`)
11
12
 
12
- const hostname = options.hostname
13
+ const sesh = new Session()
14
+ const hostname = options.hostname || sesh.hostname()
13
15
  const token = options.token
14
16
  const dotenvxProjectId = options.dotenvxProjectId
15
17
 
@@ -1,13 +1,15 @@
1
1
  const _open = require('open')
2
2
 
3
3
  const { logger } = require('@dotenvx/dotenvx')
4
+ const Session = require('./../../db/session')
4
5
 
5
6
  const dotenvxProjectId = require('./../../lib/helpers/dotenvxProjectId')
6
7
 
7
8
  async function open () {
8
9
  // debug opts
9
10
  const options = this.opts()
10
- const hostname = options.hostname
11
+ const sesh = new Session()
12
+ const hostname = options.hostname || sesh.hostname()
11
13
 
12
14
  try {
13
15
  const uid = dotenvxProjectId(this.cwd)
@@ -13,7 +13,7 @@ async function connect () {
13
13
 
14
14
  try {
15
15
  const sesh = new Session()
16
- const hostname = options.hostname
16
+ const hostname = options.hostname || sesh.hostname()
17
17
  const token = options.token || sesh.token()
18
18
  let org = options.org
19
19
  let username = options.username
@@ -13,7 +13,7 @@ async function connect () {
13
13
 
14
14
  try {
15
15
  const sesh = new Session()
16
- const hostname = options.hostname
16
+ const hostname = options.hostname || sesh.hostname()
17
17
  const token = options.token || sesh.token()
18
18
  let org = options.org
19
19
  let username = options.username
@@ -13,7 +13,7 @@ async function connect () {
13
13
 
14
14
  try {
15
15
  const sesh = new Session()
16
- const hostname = options.hostname
16
+ const hostname = options.hostname || sesh.hostname()
17
17
  const token = options.token || sesh.token()
18
18
  let org = options.org
19
19
  let username = options.username
@@ -13,7 +13,7 @@ async function rotate (uri) {
13
13
  spinner.start()
14
14
 
15
15
  const sesh = new Session()
16
- const hostname = options.hostname
16
+ const hostname = options.hostname || sesh.hostname()
17
17
  const token = options.token || sesh.token()
18
18
 
19
19
  const { url, rotUid } = await new Rotate(hostname, token, uri).run()
@@ -1,4 +1,5 @@
1
1
  const { logger } = require('@dotenvx/dotenvx')
2
+ const Session = require('./../../db/session')
2
3
 
3
4
  const main = require('./../../lib/main')
4
5
 
@@ -7,7 +8,8 @@ async function set (uri, value) {
7
8
  const options = this.opts()
8
9
  logger.debug(`options: ${JSON.stringify(options)}`)
9
10
 
10
- const hostname = options.hostname
11
+ const sesh = new Session()
12
+ const hostname = options.hostname || sesh.hostname()
11
13
  const token = options.token
12
14
 
13
15
  try {
@@ -1,6 +1,7 @@
1
1
  const fs = require('fs')
2
2
 
3
3
  const { logger } = require('@dotenvx/dotenvx')
4
+ const Session = require('./../../db/session')
4
5
 
5
6
  const { createSpinner } = require('./../../lib/helpers/createSpinner')
6
7
  const pluralize = require('./../../lib/helpers/pluralize')
@@ -14,6 +15,8 @@ const spinner = createSpinner('syncing')
14
15
  async function sync () {
15
16
  // debug opts
16
17
  const options = this.opts()
18
+ const sesh = new Session()
19
+ const hostname = options.hostname || sesh.hostname()
17
20
 
18
21
  try {
19
22
  logger.debug(`options: ${JSON.stringify(options)}`)
@@ -23,7 +26,7 @@ async function sync () {
23
26
  const {
24
27
  projectUsernameName,
25
28
  files
26
- } = await new Sync(options.hostname, options.force).run()
29
+ } = await new Sync(hostname, options.force).run()
27
30
  logger.debug(`files: ${JSON.stringify(files)}`)
28
31
 
29
32
  // write output files
@@ -66,7 +69,7 @@ async function sync () {
66
69
  spinner.start()
67
70
 
68
71
  const synchronizationId = error.meta.synchronization_id
69
- const { conflictedFiles } = await new SyncConflict(options.hostname, synchronizationId).run()
72
+ const { conflictedFiles } = await new SyncConflict(hostname, synchronizationId).run()
70
73
 
71
74
  spinner.stop()
72
75
 
@@ -3,7 +3,7 @@ const { Command } = require('commander')
3
3
  const gateway = new Command('gateway')
4
4
 
5
5
  gateway
6
- .description('[INTERNAL] 🛡️ gateway')
6
+ .description('[INTERNAL] gateway')
7
7
  .allowUnknownOption()
8
8
 
9
9
  // dotenvx-ops gateway start
@@ -3,7 +3,7 @@ const { Command } = require('commander')
3
3
  const settings = new Command('settings')
4
4
 
5
5
  settings
6
- .description('⚙️ settings')
6
+ .description('settings')
7
7
  .allowUnknownOption()
8
8
 
9
9
  // dotenvx-ops settings username
@@ -7,8 +7,6 @@ const program = new Command()
7
7
  const { setLogLevel } = require('@dotenvx/dotenvx')
8
8
 
9
9
  const packageJson = require('./../lib/helpers/packageJson')
10
- const Session = require('./../db/session')
11
- const sesh = new Session()
12
10
 
13
11
  // global log levels
14
12
  program
@@ -34,9 +32,9 @@ program
34
32
  const backupAction = require('./actions/backup')
35
33
  program
36
34
  .command('backup')
37
- .description('back up .env.keys')
35
+ .description('[INTERNAL] back up .env.keys')
38
36
  .option('--org <organizationSlug>')
39
- .option('-h, --hostname <url>', 'set hostname', sesh.hostname())
37
+ .option('-h, --hostname <url>', 'set hostname')
40
38
  .action(backupAction)
41
39
 
42
40
  // dotenvx-ops open
@@ -44,7 +42,7 @@ const openAction = require('./actions/open')
44
42
  program
45
43
  .command('open')
46
44
  .description('[INTERNAL] open project')
47
- .option('-h, --hostname <url>', 'set hostname', sesh.hostname())
45
+ .option('-h, --hostname <url>', 'set hostname')
48
46
  .action(openAction)
49
47
 
50
48
  // dotenvx-ops observe base64String
@@ -54,7 +52,7 @@ program.command('observe')
54
52
  .description('[INTERNAL] observe')
55
53
  .allowUnknownOption()
56
54
  .argument('BASE64', 'BASE64')
57
- .option('--hostname <url>', 'set hostname', sesh.hostname())
55
+ .option('--hostname <url>', 'set hostname')
58
56
  .option('--token <token>', 'set token')
59
57
  .option('--dotenvxProjectId <identifier>', 'set DOTENVX_PROJECT_ID')
60
58
  .action(function (...args) {
@@ -65,8 +63,8 @@ program.command('observe')
65
63
  const syncAction = require('./actions/sync')
66
64
  program
67
65
  .command('sync')
68
- .description('sync .env*')
69
- .option('-h, --hostname <url>', 'set hostname', sesh.hostname())
66
+ .description('[INTERNAL] sync .env*')
67
+ .option('-h, --hostname <url>', 'set hostname')
70
68
  .option('--force', 'force changes')
71
69
  .action(syncAction)
72
70
 
@@ -76,7 +74,7 @@ program
76
74
  .command('get')
77
75
  .description('[INTERNAL] fetch secret')
78
76
  .argument('URI', 'URI')
79
- .option('--hostname <url>', 'set hostname', sesh.hostname())
77
+ .option('--hostname <url>', 'set hostname')
80
78
  .option('--token <token>', 'set token')
81
79
  .action(getAction)
82
80
 
@@ -87,7 +85,7 @@ program
87
85
  .description('[INTERNAL] set secret')
88
86
  .argument('URI', 'URI')
89
87
  .argument('value', 'value')
90
- .option('--hostname <url>', 'set hostname', sesh.hostname())
88
+ .option('--hostname <url>', 'set hostname')
91
89
  .option('--token <token>', 'set token')
92
90
  .action(setAction)
93
91
 
@@ -99,7 +97,7 @@ const loginAction = require('./actions/login')
99
97
  program
100
98
  .command('login')
101
99
  .description('log in')
102
- .option('--hostname <url>', 'set hostname', sesh.hostname())
100
+ .option('--hostname <url>', 'set hostname')
103
101
  .action(loginAction)
104
102
 
105
103
  // dotenvx-ops logout
@@ -107,7 +105,7 @@ const logoutAction = require('./actions/logout')
107
105
  program
108
106
  .command('logout')
109
107
  .description('log out')
110
- .option('--hostname <url>', 'set hostname', sesh.hostname())
108
+ .option('--hostname <url>', 'set hostname')
111
109
  .action(logoutAction)
112
110
 
113
111
  // dotenvx-ops status
@@ -121,9 +119,9 @@ program
121
119
  const keypairAction = require('./actions/keypair')
122
120
  program
123
121
  .command('keypair')
124
- .description('[INTERNAL] generate keypair')
122
+ .description('generate keypair')
125
123
  .argument('[publicKey]', 'existing public key')
126
- .option('-h, --hostname <url>', 'set hostname', sesh.hostname())
124
+ .option('-h, --hostname <url>', 'set hostname')
127
125
  .option('--token <token>', 'set token')
128
126
  .option('--pp, --pretty-print', 'pretty print output')
129
127
  .action(keypairAction)
@@ -1,5 +1,6 @@
1
1
  const { http } = require('../../lib/helpers/http')
2
2
  const buildApiError = require('../../lib/helpers/buildApiError')
3
+ const normalizeToken = require('../../lib/helpers/normalizeToken')
3
4
 
4
5
  class PostLogout {
5
6
  constructor (hostname, token) {
@@ -8,7 +9,7 @@ class PostLogout {
8
9
  }
9
10
 
10
11
  async run () {
11
- const token = this.token
12
+ const token = normalizeToken(this.token)
12
13
  const url = `${this.hostname}/api/logout`
13
14
 
14
15
  const resp = await http(url, {
@@ -0,0 +1,24 @@
1
+ const FRAMES = ['◇', '⬖', '◆', '⬗']
2
+ const FRAME_INTERVAL_MS = 80
3
+
4
+ async function createSpinner2 (options = {}) {
5
+ const stream = process.stderr
6
+ const hasCursorControls = typeof stream.cursorTo === 'function' && typeof stream.clearLine === 'function'
7
+ const enabled = Boolean(stream.isTTY && hasCursorControls && !options.quiet && !options.verbose && !options.debug)
8
+ if (!enabled) return null
9
+
10
+ const text = options.text || 'thinking'
11
+ const frames = options.frames || FRAMES
12
+
13
+ const { default: yoctoSpinner } = await import('yocto-spinner')
14
+ return yoctoSpinner({
15
+ text,
16
+ spinner: {
17
+ frames,
18
+ interval: FRAME_INTERVAL_MS
19
+ },
20
+ stream
21
+ }).start()
22
+ }
23
+
24
+ module.exports = createSpinner2
@@ -0,0 +1,5 @@
1
+ function normalizeToken (token) {
2
+ return token == null ? '' : token
3
+ }
4
+
5
+ module.exports = normalizeToken
@@ -1,38 +1,26 @@
1
1
  const Session = require('./../../db/session')
2
2
 
3
- const dotenvxProjectId = require('./../../lib/helpers/dotenvxProjectId')
4
-
5
3
  // api calls
6
4
  const PostOauthDeviceCode = require('./../../lib/api/postOauthDeviceCode')
7
5
 
8
6
  class Login {
9
7
  constructor (hostname) {
10
8
  this.hostname = hostname
11
- this.cwd = process.cwd()
12
9
  }
13
10
 
14
11
  async run () {
15
- const hostname = this.hostname
16
- const cwd = this.cwd
17
-
18
12
  const sesh = new Session()
19
13
  const devicePublicKey = sesh.devicePublicKey()
20
14
  const systemInformation = await sesh.systemInformation()
21
- const _dotenvxProjectId = dotenvxProjectId(cwd, false)
22
- const data = await new PostOauthDeviceCode(hostname, devicePublicKey, systemInformation, _dotenvxProjectId).run()
23
15
 
24
- const deviceCode = data.device_code
25
- const userCode = data.user_code
26
- const verificationUri = data.verification_uri
27
- const verificationUriComplete = data.verification_uri_complete
28
- const interval = data.interval
16
+ const data = await new PostOauthDeviceCode(this.hostname, devicePublicKey, systemInformation).run()
29
17
 
30
18
  return {
31
- deviceCode,
32
- userCode,
33
- verificationUri,
34
- verificationUriComplete,
35
- interval
19
+ deviceCode: data.device_code,
20
+ userCode: data.user_code,
21
+ verificationUri: data.verification_uri,
22
+ verificationUriComplete: data.verification_uri_complete,
23
+ interval: data.interval
36
24
  }
37
25
  }
38
26
  }