@agilecustoms/envctl 1.13.3 → 1.14.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.
@@ -8,7 +8,6 @@ import { pipeline } from 'node:stream/promises';
8
8
  import { clearTimeout } from 'node:timers';
9
9
  import path from 'path';
10
10
  import * as readline from 'readline';
11
- import inquirer from 'inquirer';
12
11
  import { ProcessException, TimeoutException } from '../exceptions.js';
13
12
  import { logger } from '../logger.js';
14
13
  export const NO_TIMEOUT = 0;
@@ -111,15 +110,6 @@ export class Cli {
111
110
  });
112
111
  });
113
112
  }
114
- async promptYesNo(message, defaultValue = false) {
115
- const { answer } = await inquirer.prompt([{
116
- type: 'confirm',
117
- name: 'answer',
118
- message,
119
- default: defaultValue,
120
- }]);
121
- return answer;
122
- }
123
113
  getKind() {
124
114
  return path.basename(process.cwd());
125
115
  }
@@ -1,5 +1,6 @@
1
1
  export const _keys = {
2
2
  FORCE: 'Force deletion without confirmation',
3
3
  KEY: 'Environment name/identifier - taken from remote stake key, must be unique for a given customer',
4
- KIND: 'Environment kind: complete project (default) or some slice such as tt-core + tt-web'
4
+ KIND: 'Environment kind',
5
+ PROFILE: 'Profile to use'
5
6
  };
@@ -1,5 +1,5 @@
1
+ import { input, password } from '@inquirer/prompts';
1
2
  import { Command } from 'commander';
2
- import inquirer from 'inquirer';
3
3
  import { ConfigService } from '../service/index.js';
4
4
  import { wrap } from './_utils.js';
5
5
  export function configure(program, configService) {
@@ -7,15 +7,10 @@ export function configure(program, configService) {
7
7
  .command('configure')
8
8
  .description('Configure user settings on your local machine')
9
9
  .action(wrap(async () => {
10
- const answers = await inquirer.prompt([
11
- {
12
- type: 'password',
13
- name: 'apiKey',
14
- message: 'apiKey is required to deploy any env\n'
15
- + '(prefer GitHub username)\n'
16
- + 'apiKey:',
17
- },
18
- ]);
19
- configService.saveConfig({ apiKey: answers.apiKey });
10
+ const profile = await input({ message: 'Profile name', default: 'default', required: true });
11
+ const apiKey = await password({ message: 'API key', mask: true });
12
+ const varKey = await input({ message: 'Terraform variable to auto populate with remote state key (empty to skip)' });
13
+ const varEphemeral = await input({ message: 'Terraform variable to auto populate with flag "ephemeral" (empty to skip)' });
14
+ configService.saveConfig(profile, { apiKey, varKey, varEphemeral });
20
15
  }));
21
16
  }
@@ -7,9 +7,10 @@ export function createEphemeral(program, configService, envService) {
7
7
  .command('create-ephemeral')
8
8
  .description('Create bare env w/o resources. Used in CI as first step just to pre-generate token for extension')
9
9
  .requiredOption('--key <key>', _keys.KEY)
10
+ .option('--profile <profile>', _keys.PROFILE)
10
11
  .action(wrap(async (options) => {
11
- configService.checkApiKey();
12
- const { key } = options;
12
+ const { key, profile } = options;
13
+ configService.init(profile);
13
14
  const token = await envService.createEphemeral(key);
14
15
  console.log(token);
15
16
  }));
@@ -8,9 +8,10 @@ export function deleteIt(program, configService, envService) {
8
8
  .description('Delete a development environment')
9
9
  .option('--key <key>', _keys.KEY)
10
10
  .option('--force', _keys.FORCE)
11
+ .option('--profile <profile>', _keys.PROFILE)
11
12
  .action(wrap(async (options) => {
12
- configService.checkApiKey();
13
- const { key, force } = options;
13
+ const { key, force, profile } = options;
14
+ configService.init(profile);
14
15
  await envService.delete(Boolean(force), key);
15
16
  }));
16
17
  }
@@ -1,14 +1,17 @@
1
1
  import { Command } from 'commander';
2
2
  import { ConfigService, EnvService } from '../service/index.js';
3
+ import { _keys } from './_keys.js';
3
4
  import { wrap } from './_utils.js';
4
5
  export function deploy(program, configService, envService) {
5
6
  program
6
7
  .command('deploy')
7
8
  .description('Create new or update existing environment')
9
+ .option('--profile <profile>', _keys.PROFILE)
8
10
  .allowUnknownOption(true)
9
11
  .argument('[args...]')
10
- .action(wrap(async (tfArgs) => {
11
- configService.checkApiKey();
12
+ .action(wrap(async (tfArgs, options) => {
13
+ const { profile } = options;
14
+ configService.init(profile);
12
15
  await envService.deploy(tfArgs);
13
16
  }));
14
17
  }
@@ -10,11 +10,12 @@ export function destroy(program, configService, envService) {
10
10
  + ' Unlike "delete" command, this command doesn\'t have --key option, it can only delete env after key you provided during terraform init.'
11
11
  + ' Main use case - test deletion process, basically that you have enough permissions to delete resources')
12
12
  .option('--force', _keys.FORCE)
13
+ .option('--profile <profile>', _keys.PROFILE)
13
14
  .allowUnknownOption(true)
14
15
  .argument('[args...]')
15
16
  .action(wrap(async (tfArgs, options) => {
16
- configService.checkApiKey();
17
- const { force } = options;
17
+ const { force, profile } = options;
18
+ configService.init(profile);
18
19
  await envService.destroy(tfArgs, Boolean(force));
19
20
  }));
20
21
  }
@@ -7,9 +7,10 @@ export function logs(program, configService, logService) {
7
7
  .command('logs')
8
8
  .description('Get most recent env destroy logs')
9
9
  .option('--key <key>', _keys.KEY)
10
+ .option('--profile <profile>', _keys.PROFILE)
10
11
  .action(wrap(async (options) => {
11
- configService.checkApiKey();
12
- const { key } = options;
12
+ const { key, profile } = options;
13
+ configService.init(profile);
13
14
  await logService.getLogs(key);
14
15
  }));
15
16
  }
@@ -1,14 +1,18 @@
1
1
  import { Command } from 'commander';
2
- import { EnvService } from '../service/index.js';
2
+ import { ConfigService, EnvService } from '../service/index.js';
3
+ import { _keys } from './_keys.js';
3
4
  import { wrap } from './_utils.js';
4
- export function plan(program, envService) {
5
+ export function plan(program, configService, envService) {
5
6
  program
6
7
  .command('plan')
7
8
  .description('High level wrapper for terraform plan. Compliments deploy command: if you plan to deploy env with envctl deploy,'
8
9
  + ' then it is recommended to do plan with envctl plan to guarantee consistent behavior')
10
+ .option('--profile <profile>', _keys.PROFILE)
9
11
  .allowUnknownOption(true)
10
12
  .argument('[args...]')
11
- .action(wrap(async (tfArgs) => {
13
+ .action(wrap(async (tfArgs, options) => {
14
+ const { profile } = options;
15
+ configService.init(profile);
12
16
  await envService.plan(tfArgs);
13
17
  }));
14
18
  }
@@ -6,9 +6,11 @@ export function status(program, configService, envService) {
6
6
  program
7
7
  .command('status')
8
8
  .description('Get env status')
9
+ .option('--profile <profile>', _keys.PROFILE)
9
10
  .argument('[key]', _keys.KEY)
10
- .action(wrap(async (key) => {
11
- configService.checkApiKey();
11
+ .action(wrap(async (key, options) => {
12
+ const { profile } = options;
13
+ configService.init(profile);
12
14
  await envService.status(key);
13
15
  }));
14
16
  }
package/dist/container.js CHANGED
@@ -14,7 +14,7 @@ export function buildContainer(appVersion) {
14
14
  const envApiClient = new EnvApiClient(httpClient);
15
15
  const localStateService = new LocalStateService();
16
16
  const nonInteractive = !process.stdout.isTTY || process.env.CI === 'true';
17
- const terraformAdapter = new TerraformAdapter(Date.now, cli, localStateService, backends, nonInteractive);
17
+ const terraformAdapter = new TerraformAdapter(Date.now, configService, cli, localStateService, backends, nonInteractive);
18
18
  const envService = new EnvService(cli, envApiClient, terraformAdapter);
19
19
  const logService = new LogService(cli, envApiClient, terraformAdapter);
20
20
  return { configService, envService, logService };
package/dist/index.js CHANGED
@@ -23,6 +23,6 @@ deleteIt(program, configService, envService);
23
23
  deploy(program, configService, envService);
24
24
  destroy(program, configService, envService);
25
25
  logs(program, configService, logService);
26
- plan(program, envService);
26
+ plan(program, configService, envService);
27
27
  status(program, configService, envService);
28
28
  program.parse(process.argv);
@@ -2,11 +2,11 @@ import * as fs from 'node:fs';
2
2
  import * as os from 'node:os';
3
3
  import path from 'path';
4
4
  import { KnownException } from '../exceptions.js';
5
- const CONFIG_PATH = path.join(os.homedir(), '.envctl', 'default.json');
5
+ const CONFIG_DIR = path.join(os.homedir(), '.envctl');
6
6
  var EnvKey;
7
7
  (function (EnvKey) {
8
- EnvKey["ENV_API_KEY"] = "ENVCTL_API_KEY";
9
- EnvKey["ENV_MAP_KEY_TO"] = "ENVCTL_MAP_KEY_TO";
8
+ EnvKey["API_KEY"] = "ENVCTL_API_KEY";
9
+ EnvKey["PROFILE"] = "ENVCTL_PROFILE";
10
10
  })(EnvKey || (EnvKey = {}));
11
11
  function env(key) {
12
12
  return process.env[key];
@@ -18,36 +18,47 @@ export class ConfigService {
18
18
  config;
19
19
  constructor() {
20
20
  }
21
- saveConfig(config) {
22
- const mergedConfig = { ...this.loadConfig(), ...config };
23
- const data = JSON.stringify(mergedConfig, null, 2);
24
- fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
25
- fs.writeFileSync(CONFIG_PATH, data);
26
- }
27
- loadConfig() {
21
+ loadConfig(profile) {
28
22
  if (this.config)
29
- return this.config;
30
- if (fs.existsSync(CONFIG_PATH)) {
31
- const data = fs.readFileSync(CONFIG_PATH, 'utf-8');
23
+ throw new Error('load config second time?');
24
+ if (!profile) {
25
+ profile = env(EnvKey.PROFILE) || 'default';
26
+ }
27
+ const configPath = path.join(CONFIG_DIR, `${profile}.json`);
28
+ if (fs.existsSync(configPath)) {
29
+ const data = fs.readFileSync(configPath, 'utf-8');
32
30
  this.config = JSON.parse(data);
33
- return this.config;
34
31
  }
35
- return null;
32
+ return this.config;
36
33
  }
37
- getApiKey() {
38
- let apiKey = env(EnvKey.ENV_API_KEY);
39
- if (!apiKey) {
40
- apiKey = this.loadConfig()?.apiKey;
41
- }
42
- return apiKey;
34
+ saveConfig(profile, config) {
35
+ const mergedConfig = { ...this.loadConfig(profile), ...config };
36
+ const data = JSON.stringify(mergedConfig, null, 2);
37
+ fs.mkdirSync(CONFIG_DIR, { recursive: true });
38
+ const configPath = path.join(CONFIG_DIR, `${profile}.json`);
39
+ fs.writeFileSync(configPath, data);
43
40
  }
44
- checkApiKey() {
41
+ init(profile) {
42
+ this.loadConfig(profile);
45
43
  const apiKey = this.getApiKey();
46
44
  if (!apiKey) {
47
45
  if (isCI()) {
48
- throw new KnownException('API key is missing, set env variable ' + EnvKey.ENV_API_KEY);
46
+ throw new KnownException('API key is missing, set env variable ' + EnvKey.API_KEY);
49
47
  }
50
- throw new KnownException('API key is missing, call \'envctl configure\' or set env variable ' + EnvKey.ENV_API_KEY);
48
+ throw new KnownException('API key is missing, call \'envctl configure\' or set env variable ' + EnvKey.API_KEY);
51
49
  }
52
50
  }
51
+ getApiKey() {
52
+ let apiKey = env(EnvKey.API_KEY);
53
+ if (!apiKey) {
54
+ apiKey = this.config?.apiKey;
55
+ }
56
+ return apiKey;
57
+ }
58
+ getVarKey() {
59
+ return this.config?.varKey;
60
+ }
61
+ getVarEphemeral() {
62
+ return this.config?.varEphemeral;
63
+ }
53
64
  }
@@ -1,3 +1,4 @@
1
+ import { confirm } from '@inquirer/prompts';
1
2
  import { Cli } from '../client/Cli.js';
2
3
  import { EnvApiClient } from '../client/index.js';
3
4
  import { KnownException } from '../exceptions.js';
@@ -129,8 +130,9 @@ export class EnvService extends BaseService {
129
130
  this.handleDeleteStatuses(env);
130
131
  const status = env.status;
131
132
  if (status === EnvStatus.Deploying || status === EnvStatus.Updating) {
132
- const answerYes = await this.cli.promptYesNo(`Env status is ${status}, likely due to an error from a previous run\n
133
- Do you want to proceed with deployment?`);
133
+ const message = `Env status is ${status}, likely due to an error from a previous run\n
134
+ Do you want to proceed with deployment?`;
135
+ const answerYes = await confirm({ message });
134
136
  if (!answerYes) {
135
137
  logger.info('Aborting deployment');
136
138
  return;
@@ -1,6 +1,7 @@
1
1
  import fs from 'fs';
2
2
  import { createHash } from 'node:crypto';
3
3
  import path from 'path';
4
+ import { confirm } from '@inquirer/prompts';
4
5
  import { NO_TIMEOUT } from '../client/Cli.js';
5
6
  import { AbortedException, KnownException, ProcessException, TimeoutException } from '../exceptions.js';
6
7
  import { logger } from '../logger.js';
@@ -17,13 +18,15 @@ function hash(data) {
17
18
  }
18
19
  export class TerraformAdapter {
19
20
  now;
21
+ configService;
20
22
  cli;
21
23
  localStateService;
22
24
  nonInteractive;
23
25
  backends;
24
26
  backend = undefined;
25
- constructor(now, cli, localStateService, backends, nonInteractive) {
27
+ constructor(now, configService, cli, localStateService, backends, nonInteractive) {
26
28
  this.now = now;
29
+ this.configService = configService;
27
30
  this.cli = cli;
28
31
  this.localStateService = localStateService;
29
32
  this.nonInteractive = nonInteractive;
@@ -120,9 +123,20 @@ export class TerraformAdapter {
120
123
  tfArgs(env, args, vars = []) {
121
124
  const varDefs = this.getVarDefs();
122
125
  const argVars = this.getArgVars(args);
123
- const specialFields = { env: env.key, ephemeral: env.ephemeral };
124
126
  const extraArgs = [];
125
- for (const name of ['env', 'ephemeral']) {
127
+ if (this.nonInteractive && !args.some(arg => arg.startsWith('-input'))) {
128
+ extraArgs.push('-input=false');
129
+ }
130
+ const specialFields = {};
131
+ const varKey = this.configService.getVarKey();
132
+ if (varKey) {
133
+ specialFields[varKey] = env.key;
134
+ }
135
+ const varEphemeral = this.configService.getVarEphemeral();
136
+ if (varEphemeral) {
137
+ specialFields[varEphemeral] = String(env.ephemeral);
138
+ }
139
+ for (const name of Object.keys(specialFields)) {
126
140
  if (varDefs[name] && !argVars[name]) {
127
141
  extraArgs.push('-var=' + name + '=' + specialFields[name]);
128
142
  }
@@ -132,9 +146,6 @@ export class TerraformAdapter {
132
146
  extraArgs.push('-var=' + key + '=' + value);
133
147
  }
134
148
  });
135
- if (this.nonInteractive && !args.some(arg => arg.startsWith('-input'))) {
136
- extraArgs.push('-input=false');
137
- }
138
149
  return [...extraArgs, ...args];
139
150
  }
140
151
  getVarDefs() {
@@ -177,25 +188,6 @@ export class TerraformAdapter {
177
188
  });
178
189
  return result;
179
190
  }
180
- getNewVars(args, envVars, onDemandVars) {
181
- const varDefs = this.getVarDefs();
182
- const argVars = this.getArgVars(args);
183
- const vars = { ...argVars, ...onDemandVars };
184
- const newVars = {};
185
- Object.entries(vars).forEach(([key, value]) => {
186
- const varDef = varDefs[key];
187
- if (!varDef) {
188
- logger.warn(`Terraform variable ${key} passed, but not found in .tf files`);
189
- return;
190
- }
191
- if (varDef.sensitive)
192
- return;
193
- if (!envVars || !envVars[key]) {
194
- newVars[key] = value;
195
- }
196
- });
197
- return newVars;
198
- }
199
191
  async plan(env, args, onDemandVars = {}) {
200
192
  args = this.tfArgs(env, args);
201
193
  await this._plan(args, onDemandVars, 1);
@@ -330,9 +322,10 @@ export class TerraformAdapter {
330
322
  }
331
323
  }
332
324
  async promptUnlock(id) {
333
- const answerYes = await this.cli.promptYesNo('Terraform state is locked. Most often due to previously interrupted process.\n'
325
+ const message = 'Terraform state is locked. Most often due to previously interrupted process.\n'
334
326
  + 'Rarely (main lock intention) is when another process actively work with the state to avoid inconsistent modification.\n'
335
- + 'Do you want to force unlock?: ');
327
+ + 'Do you want to force unlock?: ';
328
+ const answerYes = await confirm({ message });
336
329
  if (!answerYes) {
337
330
  throw new AbortedException();
338
331
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilecustoms/envctl",
3
- "version": "1.13.3",
3
+ "version": "1.14.0",
4
4
  "description": "node.js CLI client for manage environments",
5
5
  "keywords": [
6
6
  "terraform wrapper",
@@ -66,7 +66,7 @@
66
66
  },
67
67
  "dependencies": {
68
68
  "commander": "^14.0.0",
69
- "inquirer": "^13.0.0",
69
+ "@inquirer/prompts": "^8.0.0",
70
70
  "update-notifier": "^7.3.1"
71
71
  },
72
72
  "devDependencies": {