@dotenvx/dotenvx-ops 0.28.1 → 0.29.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/CHANGELOG.md CHANGED
@@ -2,7 +2,14 @@
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.28.1...main)
5
+ [Unreleased](https://github.com/dotenvx/dotenvx-ops/compare/v0.29.0...main)
6
+
7
+ ## [0.29.0](https://github.com/dotenvx/dotenvx-ops/compare/v0.28.1...v0.29.0) (2026-01-07)
8
+
9
+ ### Added
10
+
11
+ * Add automatic login to `backup` command ([#19](https://github.com/dotenvx/dotenvx-ops/pull/19))
12
+ * Add `settings path` command ([#19](https://github.com/dotenvx/dotenvx-ops/pull/19))
6
13
 
7
14
  ## [0.28.1](https://github.com/dotenvx/dotenvx-ops/compare/v0.28.0...v0.28.1) (2026-01-02)
8
15
 
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "0.28.1",
2
+ "version": "0.29.0",
3
3
  "name": "@dotenvx/dotenvx-ops",
4
4
  "description": "production grade dotenvx–with operational primitives",
5
5
  "author": "@motdotla",
@@ -1,27 +1,62 @@
1
- // const fs = require('fs')
2
- // const crypto = require('crypto')
3
-
1
+ const open = require('open')
4
2
  const { logger } = require('@dotenvx/dotenvx')
5
3
 
6
4
  const { createSpinner } = require('./../../lib/helpers/createSpinner')
5
+ const clipboardy = require('./../../lib/helpers/clipboardy')
6
+ const confirm = require('./../../lib/helpers/confirm')
7
+ const formatCode = require('./../../lib/helpers/formatCode')
8
+ const truncate = require('./../../lib/helpers/truncate')
7
9
 
10
+ const LoggedIn = require('./../../lib/services/loggedIn')
11
+ const Login = require('./../../lib/services/login')
12
+ const LoginPoll = require('./../../lib/services/loginPoll')
8
13
  const Backup = require('./../../lib/services/backup')
9
14
 
10
- const spinner = createSpinner('backing up')
15
+ const spinner = createSpinner('waiting on browser authorization')
11
16
 
12
17
  async function backup () {
13
18
  // debug opts
14
19
  const options = this.opts()
15
20
 
21
+ const hostname = options.hostname
22
+
16
23
  try {
17
24
  logger.debug(`options: ${JSON.stringify(options)}`)
18
25
 
19
- spinner.start()
26
+ const loggedIn = await new LoggedIn(hostname).run()
27
+ if (!loggedIn) {
28
+ const {
29
+ deviceCode,
30
+ userCode,
31
+ verificationUri,
32
+ verificationUriComplete,
33
+ interval
34
+ } = await new Login(hostname).run()
35
+
36
+ try { clipboardy.writeSync(userCode) } catch (_e) {}
37
+
38
+ logger.debug(`POST ${hostname} with deviceCode ${deviceCode} at interval ${interval}`)
39
+ logger.info(`press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...`)
40
+
41
+ // begin polling
42
+ const pollPromise = new LoginPoll(hostname, deviceCode, interval).run()
43
+ spinner.start()
44
+
45
+ // optionally allow user to open browser
46
+ confirm({ message: `press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...` })
47
+ .then(answer => answer && open(verificationUriComplete))
48
+ .catch(() => {}) // ignore
49
+
50
+ const data = await pollPromise
51
+ spinner.succeed(`logged in [${data.username}] to this device and activated token [${truncate(data.access_token, 11)}]`)
52
+ }
53
+
54
+ spinner.start('backing up')
20
55
 
21
56
  const {
22
57
  projectUsernameName,
23
58
  files
24
- } = await new Backup(options.hostname, options.force).run()
59
+ } = await new Backup(hostname, options.org).run()
25
60
  logger.debug(`files: ${JSON.stringify(files)}`)
26
61
 
27
62
  // write output files
@@ -1,69 +1,17 @@
1
1
  const open = require('open')
2
2
  const { logger } = require('@dotenvx/dotenvx')
3
3
 
4
- const Session = require('./../../db/session')
4
+ const Login = require('./../../lib/services/login')
5
+ const LoginPoll = require('./../../lib/services/loginPoll')
5
6
 
6
7
  const clipboardy = require('./../../lib/helpers/clipboardy')
7
8
  const { createSpinner } = require('./../../lib/helpers/createSpinner')
8
9
  const confirm = require('./../../lib/helpers/confirm')
9
10
  const truncate = require('./../../lib/helpers/truncate')
10
11
  const formatCode = require('./../../lib/helpers/formatCode')
11
- const dotenvxProjectId = require('./../../lib/helpers/dotenvxProjectId')
12
12
 
13
13
  const spinner = createSpinner('waiting on browser authorization')
14
14
 
15
- // api calls
16
- const PostOauthToken = require('./../../lib/api/postOauthToken')
17
- const PostOauthDeviceCode = require('./../../lib/api/postOauthDeviceCode')
18
-
19
- async function pollTokenUrl (hostname, deviceCode, interval) {
20
- logger.debug(`POST ${hostname} with deviceCode ${deviceCode} at interval ${interval}`)
21
-
22
- const sesh = new Session()
23
-
24
- while (true) {
25
- try {
26
- let data
27
- try {
28
- data = await new PostOauthToken(hostname, deviceCode).run()
29
- if (data.access_token) {
30
- const id = data.id
31
- const username = data.username
32
- const accessToken = data.access_token
33
-
34
- // log in user
35
- sesh.login(hostname, id, username, accessToken)
36
- spinner.succeed(`logged in [${username}] to this device and activated token [${truncate(accessToken, 11)}]`)
37
- // logger.help('⮕ next run [dotenvx-ops sync]')
38
-
39
- process.exit(0)
40
- } else {
41
- await new Promise(resolve => setTimeout(resolve, interval * 1000))
42
- }
43
- } catch (error) {
44
- // continue polling if authorization_pending
45
- if (error.code === 'authorization_pending') {
46
- const newInterval = interval + 1 // grow the interval
47
- await new Promise(resolve => setTimeout(resolve, newInterval * 1000))
48
- } else {
49
- throw error
50
- }
51
- }
52
- } catch (error) {
53
- spinner.stop()
54
- if (error.message) {
55
- logger.error(error.message)
56
- } else {
57
- logger.error(error)
58
- }
59
- if (error.help) {
60
- logger.help(error.help)
61
- }
62
- process.exit(1)
63
- }
64
- }
65
- }
66
-
67
15
  async function login () {
68
16
  const options = this.opts()
69
17
  logger.debug(`options: ${JSON.stringify(options)}`)
@@ -71,35 +19,32 @@ async function login () {
71
19
  const hostname = options.hostname
72
20
 
73
21
  try {
74
- const sesh = new Session()
75
- const devicePublicKey = sesh.devicePublicKey()
76
- const systemInformation = await sesh.systemInformation()
77
- const _dotenvxProjectId = dotenvxProjectId(process.cwd(), false)
78
- const data = await new PostOauthDeviceCode(hostname, devicePublicKey, systemInformation, _dotenvxProjectId).run()
79
-
80
- const deviceCode = data.device_code
81
- const userCode = data.user_code
82
- const verificationUri = data.verification_uri
83
- const verificationUriComplete = data.verification_uri_complete
84
- const interval = data.interval
22
+ const {
23
+ deviceCode,
24
+ userCode,
25
+ verificationUri,
26
+ verificationUriComplete,
27
+ interval
28
+ } = await new Login(options.hostname).run()
85
29
 
86
30
  try { clipboardy.writeSync(userCode) } catch (_e) {}
87
31
 
88
- // begin polling
89
- pollTokenUrl(hostname, deviceCode, interval)
90
-
32
+ logger.debug(`POST ${hostname} with deviceCode ${deviceCode} at interval ${interval}`)
91
33
  logger.info(`press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...`)
34
+
35
+ // begin polling
36
+ const pollPromise = new LoginPoll(hostname, deviceCode, interval).run()
92
37
  spinner.start()
93
38
 
94
- // // optionally allow user to open browser
95
- const answer = await confirm({ message: `press Enter to open [${verificationUri}] and enter code [${formatCode(userCode)}]...` })
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
96
43
 
97
- if (answer) {
98
- await open(verificationUriComplete)
99
- } else {
100
- spinner.stop()
101
- process.exit(1)
102
- }
44
+ 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]')
47
+ process.exit(0)
103
48
  } catch (error) {
104
49
  spinner.stop()
105
50
  if (error.message) {
@@ -0,0 +1,21 @@
1
+ const { logger } = require('@dotenvx/dotenvx')
2
+
3
+ const Session = require('./../../../db/session')
4
+
5
+ function path () {
6
+ try {
7
+ const sesh = new Session()
8
+ const path = sesh.path()
9
+ if (path && path.length > 1) {
10
+ process.stdout.write(path)
11
+ } else {
12
+ logger.error('missing path. Try generating one with [dotenvx-ops login].')
13
+ process.exit(1)
14
+ }
15
+ } catch (error) {
16
+ logger.error(error.message)
17
+ process.exit(1)
18
+ }
19
+ }
20
+
21
+ module.exports = path
@@ -36,4 +36,11 @@ settings
36
36
  .description('print hostname')
37
37
  .action(hostnameAction)
38
38
 
39
+ // dotenvx-ops settings path
40
+ const pathAction = require('./../actions/settings/path')
41
+ settings
42
+ .command('path')
43
+ .description('print path to settings file')
44
+ .action(pathAction)
45
+
39
46
  module.exports = settings
@@ -35,6 +35,7 @@ const backupAction = require('./actions/backup')
35
35
  program
36
36
  .command('backup')
37
37
  .description('backup .env.keys file(s)')
38
+ .option('--org <organizationSlug>')
38
39
  .option('-h, --hostname <url>', 'set hostname', sesh.hostname())
39
40
  .action(backupAction)
40
41
 
package/src/db/session.js CHANGED
@@ -51,6 +51,10 @@ class Session {
51
51
  return new Device().publicKey()
52
52
  }
53
53
 
54
+ path () {
55
+ return this.store.path
56
+ }
57
+
54
58
  async systemInformation () {
55
59
  const system = await si.system()
56
60
  const osInfo = await si.osInfo()
@@ -8,6 +8,7 @@ function buildApiError (statusCode, json) {
8
8
  error.code = code
9
9
  error.help = help
10
10
  error.meta = meta
11
+ error.json = json
11
12
 
12
13
  return error
13
14
  }
@@ -2,6 +2,7 @@ const fs = require('fs')
2
2
  const path = require('path')
3
3
  const si = require('systeminformation')
4
4
  const dotenvx = require('@dotenvx/dotenvx')
5
+ const prompts = require('@inquirer/prompts')
5
6
 
6
7
  const Session = require('./../../db/session')
7
8
 
@@ -10,11 +11,13 @@ const gitBranch = require('./../helpers/gitBranch')
10
11
  const dotenvxProjectId = require('./../helpers/dotenvxProjectId')
11
12
 
12
13
  // api calls
14
+ const GetAccount = require('./../api/getAccount')
13
15
  const PostBackup = require('./../api/postBackup')
14
16
 
15
17
  class Backup {
16
- constructor (hostname) {
18
+ constructor (hostname, org = null) {
17
19
  this.hostname = hostname
20
+ this.org = org
18
21
  this.cwd = process.cwd()
19
22
  }
20
23
 
@@ -22,12 +25,38 @@ class Backup {
22
25
  const sesh = new Session()
23
26
  const token = sesh.token()
24
27
  const devicePublicKey = sesh.devicePublicKey()
28
+ let org = this.org
25
29
 
26
30
  // required
27
31
  const files = this._files()
28
32
  const payload = { files }
29
33
  const encoded = Buffer.from(JSON.stringify(payload)).toString('base64')
30
- const _dotenvxProjectId = dotenvxProjectId(this.cwd)
34
+
35
+ // user must be logged in to use feature
36
+ const accountJson = await new GetAccount(this.hostname, token).run()
37
+
38
+ // optional project id
39
+ const _dotenvxProjectId = dotenvxProjectId(this.cwd, false)
40
+
41
+ // missing .env.x file
42
+ if (!_dotenvxProjectId) {
43
+ // set org
44
+ if (!org) {
45
+ const choices = accountJson.organizations.map(o => ({
46
+ name: o.provider_slug,
47
+ value: o.provider_slug
48
+ }))
49
+
50
+ if (choices.length === 1) {
51
+ org = choices[0].value // just use first choice
52
+ } else {
53
+ org = await prompts.select({
54
+ message: 'Select dotenvx organization',
55
+ choices
56
+ })
57
+ }
58
+ }
59
+ }
31
60
 
32
61
  // optional
33
62
  const _pwd = this.cwd
@@ -0,0 +1,24 @@
1
+ const Session = require('./../../db/session')
2
+ const GetAccount = require('./../../lib/api/getAccount')
3
+
4
+ class LoggedIn {
5
+ constructor (hostname) {
6
+ this.hostname = hostname
7
+ }
8
+
9
+ async run () {
10
+ const hostname = this.hostname
11
+ const sesh = new Session()
12
+ const token = sesh.token()
13
+
14
+ try {
15
+ await new GetAccount(hostname, token).run()
16
+ return true
17
+ } catch (error) {
18
+ if (error.code === 'unauthorized') return false
19
+ throw error
20
+ }
21
+ }
22
+ }
23
+
24
+ module.exports = LoggedIn
@@ -0,0 +1,40 @@
1
+ const Session = require('./../../db/session')
2
+
3
+ const dotenvxProjectId = require('./../../lib/helpers/dotenvxProjectId')
4
+
5
+ // api calls
6
+ const PostOauthDeviceCode = require('./../../lib/api/postOauthDeviceCode')
7
+
8
+ class Login {
9
+ constructor (hostname) {
10
+ this.hostname = hostname
11
+ this.cwd = process.cwd()
12
+ }
13
+
14
+ async run () {
15
+ const hostname = this.hostname
16
+ const cwd = this.cwd
17
+
18
+ const sesh = new Session()
19
+ const devicePublicKey = sesh.devicePublicKey()
20
+ const systemInformation = await sesh.systemInformation()
21
+ const _dotenvxProjectId = dotenvxProjectId(cwd, false)
22
+ const data = await new PostOauthDeviceCode(hostname, devicePublicKey, systemInformation, _dotenvxProjectId).run()
23
+
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
29
+
30
+ return {
31
+ deviceCode,
32
+ userCode,
33
+ verificationUri,
34
+ verificationUriComplete,
35
+ interval
36
+ }
37
+ }
38
+ }
39
+
40
+ module.exports = Login
@@ -0,0 +1,43 @@
1
+ const Session = require('./../../db/session')
2
+
3
+ // api calls
4
+ const PostOauthToken = require('./../../lib/api/postOauthToken')
5
+
6
+ class LoginPoll {
7
+ constructor (hostname, deviceCode, interval) {
8
+ this.hostname = hostname
9
+ this.deviceCode = deviceCode
10
+ this.interval = interval
11
+ }
12
+
13
+ async run () {
14
+ const hostname = this.hostname
15
+ const deviceCode = this.deviceCode
16
+ const interval = this.interval
17
+
18
+ const sesh = new Session()
19
+
20
+ while (true) {
21
+ try {
22
+ const data = await new PostOauthToken(hostname, deviceCode).run()
23
+
24
+ if (data.access_token) {
25
+ sesh.login(hostname, data.id, data.username, data.access_token) // log in user
26
+ return data
27
+ }
28
+
29
+ await new Promise(resolve => setTimeout(resolve, interval * 1000))
30
+ } catch (error) {
31
+ // continue polling if authorization_pending
32
+ if (error.code === 'authorization_pending') {
33
+ const newInterval = interval + 1 // grow the interval
34
+ await new Promise(resolve => setTimeout(resolve, newInterval * 1000))
35
+ } else {
36
+ throw error
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ module.exports = LoginPoll