@agilecustoms/envctl 1.27.0 → 1.28.1

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
@@ -6,7 +6,7 @@
6
6
  ## usage
7
7
 
8
8
  ```shell
9
- terraform init -backend-config=key=laxa1986
9
+ envctl init -backend-config=key=laxa1986
10
10
  envctl apply -var-file=versions.tfvars -var="log_level=debug"
11
11
  envctl delete
12
12
  ```
@@ -35,12 +35,6 @@ npm view @agilecustoms/envctl version # show latest version available (without i
35
35
  5. "Set up connection"
36
36
  5. In GH workflow job use `permissions: id-token: write` and release action with input `npm-publish: true`
37
37
 
38
- ## History/motivation
39
-
40
- `env-api` is a microservice hosted in 'maintenance' account and working as garbage collector: every environment first
41
- created in `env-api` and then 'managed' by `env-api`: it deletes env when it is not in use anymore OR can extend lifetime.
42
- Creation API yields unique ID, so you can safely extend lifetime via this ID
43
-
44
38
  ### Authorization
45
39
 
46
40
  Main use cases:
@@ -58,7 +52,7 @@ Then I thought about Node.js - it is available on dev machines and in GitHub act
58
52
  How to distribute it? First I thought about using `ncc` to bundle in one big .js file
59
53
  (as I do for `publish-s3` and `gha-healthcheck`) but it will be hard to use on dev machine...
60
54
 
61
- So I ended up publishing this client as an npm package in npmjs
55
+ So I ended up publishing this client as a npm package in npmjs
62
56
  - CI environments can install it via GH action `agilecustoms/envctl`
63
57
  - developer will install it globally via `npm install -g @agilecustoms/envctl`
64
58
 
@@ -15,6 +15,7 @@ export class Cli {
15
15
  constructor() {
16
16
  }
17
17
  async run(command, args, options = {}) {
18
+ const successCodes = options.successCodes ?? [0];
18
19
  const timeoutMs = options.timeoutMs ?? NO_TIMEOUT;
19
20
  const interactive = options.interactive ?? !!options.inScanner;
20
21
  const stdio = [interactive ? 'pipe' : 'ignore', 'pipe', 'pipe'];
@@ -93,7 +94,7 @@ export class Cli {
93
94
  reject(new TimeoutException(`Process killed after timeout of ${timeoutMs}ms`));
94
95
  return;
95
96
  }
96
- if (code === 0) {
97
+ if (code !== null && successCodes.includes(code)) {
97
98
  if (errorBuffer) {
98
99
  logger.warn('Process completed successfully, but there were errors:\n' + errorBuffer);
99
100
  }
@@ -15,7 +15,7 @@ export function wrap(callable) {
15
15
  else {
16
16
  logger.error('Unknown error: \n' + error.stack);
17
17
  }
18
- process.exit(1);
18
+ process.exitCode = 1;
19
19
  }
20
20
  };
21
21
  }
@@ -20,7 +20,7 @@ export class LocalStateService {
20
20
  logger.info('Load local state config');
21
21
  const filePath = this.filePath();
22
22
  if (!fs.existsSync(filePath)) {
23
- logger.warn('Local state file does not exist, must have been accidentally deleted, re-initializing');
23
+ logger.warn('Local state file does not exist, create new one');
24
24
  return this.createEmptyConfig(filePath);
25
25
  }
26
26
  const data = fs.readFileSync(filePath, 'utf-8');
@@ -1,4 +1,4 @@
1
- import { basename } from 'node:path';
1
+ import { basename, dirname } from 'node:path';
2
2
  import {} from '../client/index.js';
3
3
  import { KnownException } from '../exceptions.js';
4
4
  import { logger } from '../logger.js';
@@ -17,6 +17,11 @@ export class LogService extends BaseService {
17
17
  const urlPath = new URL(url).pathname;
18
18
  const gzName = basename(urlPath);
19
19
  const timestamp = Number(gzName.slice(0, -7)) * 1000;
20
+ const res = await this.envApi.fetch(url);
21
+ if (!res.ok) {
22
+ const msg = await res.text();
23
+ throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
24
+ }
20
25
  const parts = new Intl.DateTimeFormat(undefined, {
21
26
  year: 'numeric',
22
27
  month: '2-digit',
@@ -28,13 +33,16 @@ export class LogService extends BaseService {
28
33
  }).formatToParts(timestamp);
29
34
  const get = (type) => parts.find(p => p.type === type)?.value ?? '';
30
35
  const outName = `${get('year')}-${get('month')}-${get('day')}_${get('hour')}.${get('minute')}.${get('second')}.log`;
31
- const res = await this.envApi.fetch(url);
32
- if (!res.ok) {
33
- const msg = await res.text();
34
- throw new KnownException(`Failed to download logs: ${res.status} ${res.statusText}${msg ? ` - ${msg}` : ''}`);
35
- }
36
36
  const outPath = await this.cli.writeInTmpFile(res.body, outName);
37
37
  logger.info(`Logs saved to: ${outPath}`);
38
- await this.cli.run('open', ['-R', outPath]);
38
+ if (process.platform === 'darwin') {
39
+ await this.cli.run('open', ['-R', outPath]);
40
+ }
41
+ else if (process.platform === 'win32') {
42
+ await this.cli.run('explorer.exe', [`/select,${outPath}`], { successCodes: [0, 1] });
43
+ }
44
+ else {
45
+ await this.cli.run('xdg-open', [dirname(outPath)]);
46
+ }
39
47
  }
40
48
  }
@@ -65,6 +65,18 @@ export class TerraformAdapter {
65
65
  return acc;
66
66
  }, new Map());
67
67
  }
68
+ async terraform(args, options = {}) {
69
+ try {
70
+ await this.cli.run('terraform', args, options);
71
+ }
72
+ catch (error) {
73
+ if (error instanceof ProcessException && error.code !== null && [-2, -4058].includes(error.code)) {
74
+ const msg = `terraform command failed with code ${error.code}. Perhaps terraform is not installed`;
75
+ throw new KnownException(`${msg}. Details:\n${error.message}`, { cause: error });
76
+ }
77
+ throw error;
78
+ }
79
+ }
68
80
  getBackend() {
69
81
  if (this.backend) {
70
82
  return this.backend;
@@ -126,7 +138,7 @@ export class TerraformAdapter {
126
138
  const config = this.localStateService.load();
127
139
  const fileHash = hash(fileContent);
128
140
  if (fileHash !== config.lockFileHash) {
129
- throw new KnownException(`Make sure you're using envctl init instead of terraform init`);
141
+ throw new KnownException(`Make sure to call 'envctl init' prior to 'envctl plan/apply'`);
130
142
  }
131
143
  const lockUpdated = config.lockHash !== fileHash;
132
144
  config.lockHash = fileHash;
@@ -159,7 +171,7 @@ export class TerraformAdapter {
159
171
  if (!linuxArm64) {
160
172
  logger.info('Lock providers');
161
173
  const vars = this.getVars(args);
162
- await this.cli.run('terraform', ['providers', 'lock', '-platform=linux_arm64', ...vars]);
174
+ await this.terraform(['providers', 'lock', '-platform=linux_arm64', ...vars]);
163
175
  }
164
176
  const fileContent = this.getLockFile();
165
177
  const config = this.localStateService.load();
@@ -232,7 +244,7 @@ export class TerraformAdapter {
232
244
  async init(args) {
233
245
  logger.info('Running: terraform init ' + args.join(' ') + '\n');
234
246
  try {
235
- await this.cli.run('terraform', ['init', ...args], { interactive: true });
247
+ await this.terraform(['init', ...args], { interactive: true });
236
248
  }
237
249
  catch (error) {
238
250
  if (!(error instanceof ProcessException)) {
@@ -248,7 +260,7 @@ export class TerraformAdapter {
248
260
  async _plan(args, attemptNo) {
249
261
  logger.info('Running: terraform plan ' + args.join(' ') + '\n');
250
262
  try {
251
- await this.cli.run('terraform', ['plan', ...args], { interactive: true });
263
+ await this.terraform(['plan', ...args], { interactive: true });
252
264
  }
253
265
  catch (error) {
254
266
  if (!(error instanceof ProcessException)) {
@@ -296,7 +308,7 @@ export class TerraformAdapter {
296
308
  logger.debug('timeout(ms): ' + timeoutMs);
297
309
  logger.info('Running: terraform apply ' + args.join(' ') + '\n');
298
310
  try {
299
- await this.cli.run('terraform', ['apply', ...args], { timeoutMs, interactive: true });
311
+ await this.terraform(['apply', ...args], { timeoutMs, interactive: true });
300
312
  this.printTime();
301
313
  }
302
314
  catch (error) {
@@ -329,7 +341,7 @@ export class TerraformAdapter {
329
341
  };
330
342
  logger.info('Running: terraform destroy ' + args.join(' ') + '\n');
331
343
  try {
332
- await this.cli.run('terraform', ['destroy', ...args], { outScanner, interactive: true });
344
+ await this.terraform(['destroy', ...args], { outScanner, interactive: true });
333
345
  }
334
346
  catch (error) {
335
347
  if (!(error instanceof ProcessException)) {
@@ -373,6 +385,6 @@ export class TerraformAdapter {
373
385
  }
374
386
  async forceUnlock(id) {
375
387
  logger.info('Force unlocking state');
376
- await this.cli.run('terraform', ['force-unlock', '-force', id]);
388
+ await this.terraform(['force-unlock', '-force', id]);
377
389
  }
378
390
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agilecustoms/envctl",
3
- "version": "1.27.0",
3
+ "version": "1.28.1",
4
4
  "description": "node.js CLI client for manage environments",
5
5
  "keywords": [
6
6
  "terraform wrapper",
@@ -31,19 +31,18 @@
31
31
  "_comment0": "to run via `npx` you must use `name` (with scope) and have at least one entry in `bin`. Key does not matter when run via `npx`",
32
32
  "_comment1": "keys become CLI apps (symlinks) when install globally. ex: npm install -g @agilecustoms/envctl; ls $(which alexc-blah) >> lrwxr-xr-x 1 alexc admin 62 Dec 30 14:29 /opt/homebrew/bin/alexc-blah -> ../lib/node_modules/@agilecustoms/envctl-client/dist/index.js",
33
33
  "scripts": {
34
- "prepare": "if [ \"$CI\" != \"true\" ]; then husky; fi",
35
34
  "lint": "eslint *.{ts,mjs} \"src/**/*.ts\" \"test/**/*.ts\"",
36
35
  "lint:fix": "npm run lint -- --fix",
37
36
  "test": "vitest run --coverage",
38
37
  "build": "tsc -p tsconfig.build.json",
39
- "it": "tsc -p tsconfig.build.json --sourceMap true && TEST_CWD=\"$CWD\" node dist/index.js",
38
+ "it": "tsc -p tsconfig.build.json --sourceMap true && cross-env TEST_CWD=$CWD node dist/index.js",
40
39
  "run-help": " npm run it -- --help",
41
40
  "run-version": " npm run it -- --version",
42
41
  "run-configure": "npm run it -- configure",
43
42
  "run-logs": " npm run it -- logs",
44
43
  "-d1-": "",
45
44
  "d1-desc": "dev env (fully fledged ~/.envctl/default.json)",
46
- "d1": "CWD=../tt-core npm run it --",
45
+ "d1": "cross-env CWD=../tt-core npm run it --",
47
46
  "d1-init": " npm run d1 -- init -reconfigure -upgrade -backend-config=key=d1",
48
47
  "d1-status": "npm run d1 -- status --verbose",
49
48
  "d1-apply": " npm run d1 -- apply --auto-approve -var=env_size=min",
@@ -51,29 +50,29 @@
51
50
  "d1-logs": " npm run d1 -- logs",
52
51
  "-d2-": "",
53
52
  "d2-desc": "dev env destroy",
54
- "d2": "CWD=../tt-core npm run it --",
53
+ "d2": "cross-env CWD=../tt-core npm run it --",
55
54
  "d2-init": " npm run d2 -- init -reconfigure -upgrade -backend-config=key=d2",
56
55
  "d2-status": " npm run d2 -- status",
57
56
  "d2-apply": " npm run d2 -- apply --auto-approve",
58
57
  "d2-destroy": "npm run d2 -- destroy -var=env_size=min --auto-approve",
59
58
  "-d3-": "",
60
59
  "d3-desc": "dev env plan apply",
61
- "d3": "CWD=../tt-core npm run it --",
60
+ "d3": "cross-env CWD=../tt-core npm run it --",
62
61
  "d3-init": " npm run d3 -- init -reconfigure -upgrade -backend-config=key=d3",
63
62
  "d3-plan": " npm run d3 -- plan -var=env_size=min -var=env=d3 -out=plan",
64
63
  "d3-apply": " npm run d3 -- apply plan",
65
64
  "d3-delete": "npm run d3 -- delete",
66
65
  "-d4-": "",
67
66
  "d4-desc": "terraform init and then envctl apply",
68
- "d4": "CWD=../tt-core npm run it --",
69
- "d4-init": "cd ../tt-core && rm .terraform.lock.hcl && rm .terraform/envctl.json && terraform init -reconfigure -upgrade -backend-config=key=d4",
67
+ "d4": "cross-env CWD=../tt-core npm run it --",
68
+ "d4-init": "cd ../tt-core && rimraf .terraform.lock.hcl .terraform/envctl.json && terraform init -reconfigure -upgrade -backend-config=key=d4",
70
69
  "d4-init1": " npm run d4 -- init -reconfigure -upgrade -backend-config=key=d4",
71
70
  "d4-apply": " npm run d4 -- apply -auto-approve -var=env_size=min",
72
71
  "d4-delete": "npm run d4 -- delete",
73
72
  "d4-status": "npm run d4 -- status",
74
73
  "-e1-": "",
75
74
  "e1-desc": "ephemeral env in CI (must have ENVCTL_API_KEY_ in env vars)",
76
- "e1": "CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
75
+ "e1": "cross-env CWD=../tt-core CI=true ENVCTL_HOME=non-existing ENVCTL_API_KEY=\"$ENVCTL_API_KEY_\" npm run it --",
77
76
  "e1-create": "npm run e1 -- create-ephemeral e1 --verbose",
78
77
  "e1-init": " npm run e1 -- init -reconfigure -upgrade -backend-config=key=e1",
79
78
  "e1-plan": " npm run e1 -- plan -var=env_size=min -var=env=e1 -out=plan",
@@ -81,15 +80,15 @@
81
80
  "e1-delete": "npm run e1 -- delete",
82
81
  "-tt-": "",
83
82
  "tt-desc": "deploy TT project from local machine",
84
- "tt": "CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
83
+ "tt": "cross-env CWD=../tt-gitops AWS_PROFILE=ac-tt-dev-deployer npm run it --",
85
84
  "run-init": " npm run tt -- init -reconfigure -upgrade -backend-config=key=laxa1986 -var-file=versions.tfvars",
86
85
  "run-status": " npm run tt -- status",
87
86
  "run-apply": " npm run tt -- apply --auto-approve -var=env_size=min -var-file=versions.tfvars",
88
87
  "run-delete": " npm run tt -- delete"
89
88
  },
90
89
  "dependencies": {
91
- "commander": "^14.0.0",
92
90
  "@inquirer/prompts": "^8.0.0",
91
+ "commander": "^14.0.0",
93
92
  "update-notifier": "^7.3.1"
94
93
  },
95
94
  "devDependencies": {
@@ -97,9 +96,11 @@
97
96
  "@types/node": "^22.10.2",
98
97
  "@types/update-notifier": "^6.0.8",
99
98
  "@vitest/coverage-v8": "^4.0.0",
99
+ "cross-env": "^10.1.0",
100
100
  "eslint": "^9.22.0",
101
101
  "eslint-plugin-import": "^2.31.0",
102
102
  "husky": "^9.1.7",
103
+ "rimraf": "^6.1.3",
103
104
  "typescript": "^6.0.0",
104
105
  "typescript-eslint": "^8.8.1",
105
106
  "vitest": "^4.0.0"