hereya-cli 0.33.0 → 0.35.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.
@@ -0,0 +1,145 @@
1
+ /* eslint-disable n/no-unsupported-features/node-builtins */
2
+ import machineId from 'node-machine-id';
3
+ import crypto from 'node:crypto';
4
+ import http from 'node:http';
5
+ import net from 'node:net';
6
+ import { browserUtils } from './utils.js';
7
+ export async function loginToCloudBackend(url) {
8
+ const registerResult = await registerDevice(url);
9
+ if (!registerResult.success) {
10
+ return registerResult;
11
+ }
12
+ const { clientId } = registerResult;
13
+ const codeVerifier = crypto.randomBytes(32).toString('hex');
14
+ const codeChallengeRaw = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(codeVerifier));
15
+ const codeChallenge = Buffer.from(codeChallengeRaw).toString('base64');
16
+ const availablePort = await getAvailablePort(3000);
17
+ const myRedirectUri = `http://localhost:${availablePort}/auth/cli/callback`;
18
+ const redirectUrl = `${url}/auth/cli/login?client_id=${clientId}&code_challenge=${codeChallenge}&redirect_uri=${myRedirectUri}`;
19
+ console.log('Opening browser to', redirectUrl);
20
+ browserUtils.open(redirectUrl);
21
+ const codeResult = await waitForAuthorizationCode(availablePort);
22
+ if (!codeResult.success) {
23
+ return {
24
+ error: codeResult.error,
25
+ success: false,
26
+ };
27
+ }
28
+ const { code } = codeResult;
29
+ const formData = new FormData();
30
+ formData.append('clientId', clientId);
31
+ formData.append('code', code);
32
+ formData.append('codeVerifier', codeVerifier);
33
+ const tokenResult = await fetch(`${url}/auth/cli/code`, {
34
+ body: formData,
35
+ method: 'POST',
36
+ });
37
+ if (!tokenResult.ok) {
38
+ return {
39
+ error: `Failed to get token: ${JSON.stringify(await tokenResult.json())}`,
40
+ success: false,
41
+ };
42
+ }
43
+ const token = (await tokenResult.json());
44
+ return {
45
+ accessToken: token.data.accessToken,
46
+ clientId,
47
+ refreshToken: token.data.refreshToken,
48
+ success: true,
49
+ };
50
+ }
51
+ export async function registerDevice(url) {
52
+ const registerUrl = `${url}/auth/cli/register`;
53
+ const deviceId = await machineId.machineId();
54
+ const formData = new FormData();
55
+ formData.append('deviceId', deviceId);
56
+ const response = await fetch(registerUrl, {
57
+ body: formData,
58
+ method: 'POST',
59
+ });
60
+ if (!response.ok) {
61
+ return {
62
+ error: `Failed to register device: ${JSON.stringify(await response.json())}`,
63
+ success: false,
64
+ };
65
+ }
66
+ const result = (await response.json());
67
+ return {
68
+ clientId: result.data.clientId,
69
+ success: true,
70
+ };
71
+ }
72
+ async function getAvailablePort(startPort) {
73
+ return new Promise((resolve) => {
74
+ const server = net.createServer();
75
+ server.unref();
76
+ server.on('error', () => {
77
+ server.listen(++startPort);
78
+ });
79
+ server.listen(startPort, () => {
80
+ server.close(() => resolve(startPort));
81
+ });
82
+ });
83
+ }
84
+ async function waitForAuthorizationCode(port) {
85
+ return new Promise((resolve) => {
86
+ const server = http.createServer((req, res) => {
87
+ const searchParams = new URLSearchParams(req.url.split('?')[1]);
88
+ const code = searchParams.get('code');
89
+ if (code) {
90
+ resolve({ code, success: true });
91
+ }
92
+ else {
93
+ resolve({ error: 'No authorization code received', success: false });
94
+ }
95
+ res.writeHead(200, { 'Content-Type': 'text/html' });
96
+ res.end(`
97
+ <!DOCTYPE html>
98
+ <html>
99
+ <head>
100
+ <style>
101
+ body {
102
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
103
+ display: flex;
104
+ justify-content: center;
105
+ align-items: center;
106
+ height: 100vh;
107
+ margin: 0;
108
+ background: #f5f5f5;
109
+ }
110
+ .card {
111
+ background: white;
112
+ padding: 2rem;
113
+ border-radius: 8px;
114
+ box-shadow: 0 2px 4px rgba(0,0,0,0.1);
115
+ text-align: center;
116
+ }
117
+ .success-icon {
118
+ color: #10B981;
119
+ font-size: 3rem;
120
+ margin-bottom: 1rem;
121
+ }
122
+ .message {
123
+ color: #666;
124
+ font-size: 0.9rem;
125
+ margin-top: 1rem;
126
+ }
127
+ </style>
128
+ </head>
129
+ <body>
130
+ <div class="card">
131
+ <div class="success-icon">✓</div>
132
+ <h2>Authorization Successful!</h2>
133
+ <p class="message">You can close this tab now (Ctrl+W or Cmd+W)</p>
134
+ </div>
135
+ </body>
136
+ </html>
137
+ `);
138
+ server.closeAllConnections();
139
+ server.close();
140
+ });
141
+ server.listen(port, () => {
142
+ console.log(`Waiting for authorization code on port ${port}...`);
143
+ });
144
+ });
145
+ }
@@ -0,0 +1,9 @@
1
+ export declare function logoutFromCloudBackend({ secret, url }: {
2
+ secret: null | {
3
+ refreshToken: string;
4
+ };
5
+ url: string;
6
+ }): Promise<Response | {
7
+ didDelete: boolean;
8
+ success: boolean;
9
+ }>;
@@ -0,0 +1,12 @@
1
+ /* eslint-disable n/no-unsupported-features/node-builtins */
2
+ export async function logoutFromCloudBackend({ secret, url }) {
3
+ if (!secret) {
4
+ return { didDelete: false, success: true };
5
+ }
6
+ return fetch(`${url}/auth/cli/logout`, {
7
+ headers: {
8
+ Authorization: `Bearer ${secret.refreshToken}`,
9
+ },
10
+ method: 'POST',
11
+ });
12
+ }
@@ -0,0 +1,3 @@
1
+ export declare const browserUtils: {
2
+ open(url: string): void;
3
+ };
@@ -0,0 +1,6 @@
1
+ import open from 'open';
2
+ export const browserUtils = {
3
+ open(url) {
4
+ open(url);
5
+ },
6
+ };
@@ -133,6 +133,7 @@ export type GetStateOutput = {
133
133
  found: true;
134
134
  } | {
135
135
  found: false;
136
+ reason?: string;
136
137
  };
137
138
  export type DeleteWorkspaceInput = {
138
139
  name: string;
@@ -158,6 +159,7 @@ export type GetProvisioningIdOutput = {
158
159
  success: false;
159
160
  };
160
161
  export type SetEnvVarInput = {
162
+ infrastructure: InfrastructureType;
161
163
  name: string;
162
164
  value: string;
163
165
  workspace: string;
@@ -169,6 +171,7 @@ export type SetEnvVarOutput = {
169
171
  success: true;
170
172
  };
171
173
  export type UnsetEnvVarInput = {
174
+ infrastructure: InfrastructureType;
172
175
  name: string;
173
176
  workspace: string;
174
177
  };
@@ -1,10 +1,37 @@
1
- import { BackendType } from "./index.js";
1
+ import { BackendType } from './index.js';
2
2
  export declare function getCurrentBackendType(): Promise<BackendType>;
3
3
  export declare function setBackendType(type: BackendType): Promise<void>;
4
- export declare function loadBackendConfig(): Promise<{
5
- current: BackendType;
4
+ export declare function loadBackendConfig(): Promise<BackendConfig>;
5
+ export declare function saveCloudCredentials(credentials: {
6
+ accessToken: string;
7
+ clientId: string;
8
+ refreshToken: string;
9
+ url: string;
10
+ }): Promise<void>;
11
+ export declare function deleteCloudCredentials(): Promise<{
12
+ didDelete: boolean;
13
+ originalConfig: {
14
+ cloud?: {
15
+ clientId: string;
16
+ url: string;
17
+ };
18
+ current: BackendType;
19
+ };
20
+ secret: {
21
+ accessToken: string;
22
+ refreshToken: string;
23
+ } | null;
24
+ success: boolean;
6
25
  }>;
26
+ export declare function getCloudCredentials(clientId: string): Promise<{
27
+ accessToken: string;
28
+ refreshToken: string;
29
+ } | null>;
7
30
  export declare function getBackendConfigPath(): string;
8
31
  export type BackendConfig = {
32
+ cloud?: {
33
+ clientId: string;
34
+ url: string;
35
+ };
9
36
  current: BackendType;
10
37
  };
@@ -1,7 +1,8 @@
1
1
  import os from 'node:os';
2
2
  import path from 'node:path';
3
- import { load, save } from "../lib/yaml-utils.js";
4
- import { BackendType } from "./index.js";
3
+ import { load, save } from '../lib/yaml-utils.js';
4
+ import { BackendType } from './index.js';
5
+ import { secretManager } from './secrets.js';
5
6
  export async function getCurrentBackendType() {
6
7
  const config = await loadBackendConfig();
7
8
  return config.current;
@@ -18,6 +19,39 @@ export async function loadBackendConfig() {
18
19
  }
19
20
  return data;
20
21
  }
22
+ export async function saveCloudCredentials(credentials) {
23
+ await secretManager.saveSecret(credentials.clientId, {
24
+ accessToken: credentials.accessToken,
25
+ refreshToken: credentials.refreshToken,
26
+ });
27
+ const config = await loadBackendConfig();
28
+ config.cloud = {
29
+ clientId: credentials.clientId,
30
+ url: credentials.url,
31
+ };
32
+ config.current = BackendType.Cloud;
33
+ await save(config, getBackendConfigPath());
34
+ }
35
+ export async function deleteCloudCredentials() {
36
+ const config = await loadBackendConfig();
37
+ const originalConfig = { ...config };
38
+ if (!config.cloud) {
39
+ return { didDelete: false, originalConfig, secret: null, success: true };
40
+ }
41
+ const secret = await secretManager.getSecret(config.cloud.clientId);
42
+ if (secret) {
43
+ await secretManager.deleteSecret(config.cloud.clientId);
44
+ }
45
+ config.cloud = undefined;
46
+ if (config.current === BackendType.Cloud) {
47
+ config.current = BackendType.Local;
48
+ }
49
+ await save(config, getBackendConfigPath());
50
+ return { didDelete: true, originalConfig, secret, success: true };
51
+ }
52
+ export async function getCloudCredentials(clientId) {
53
+ return secretManager.getSecret(clientId);
54
+ }
21
55
  export function getBackendConfigPath() {
22
56
  return path.join(os.homedir(), '.hereya', 'backend.yaml');
23
57
  }
@@ -24,7 +24,7 @@ export class FileBackend {
24
24
  const { workspace } = workspace$;
25
25
  if (workspace.mirrorOf) {
26
26
  return {
27
- reason: `Cannot add package to mirrored workspace ${input.workspace}`,
27
+ reason: `Cannot add package to mirroring workspace ${input.workspace}`,
28
28
  success: false,
29
29
  };
30
30
  }
@@ -372,7 +372,7 @@ export class FileBackend {
372
372
  const { workspace } = workspace$;
373
373
  workspace.env = {
374
374
  ...workspace.env,
375
- [input.name]: input.value,
375
+ [input.name]: `${input.infrastructure}:${input.value}`,
376
376
  };
377
377
  await this.saveWorkspace(workspace, input.workspace);
378
378
  return {
@@ -1,6 +1,8 @@
1
1
  import { Backend } from './common.js';
2
- export declare function getBackend(): Promise<Backend>;
2
+ export declare function getBackend(type?: BackendType): Promise<Backend>;
3
+ export declare function setBackendType(type: BackendType): void;
3
4
  export declare enum BackendType {
5
+ Cloud = "cloud",
4
6
  Local = "local",
5
7
  S3 = "s3"
6
8
  }
@@ -1,14 +1,33 @@
1
1
  import { getAwsConfig } from '../infrastructure/aws-config.js';
2
- import { getCurrentBackendType } from './config.js';
2
+ import { CloudBackend } from './cloud/cloud-backend.js';
3
+ import { getCloudCredentials, loadBackendConfig } from './config.js';
3
4
  import { LocalFileBackend } from './local.js';
4
5
  import { S3FileBackend } from './s3.js';
5
6
  let backend;
6
- export async function getBackend() {
7
+ let currentBackendType;
8
+ export async function getBackend(type) {
7
9
  if (backend) {
8
10
  return backend;
9
11
  }
10
- const backendType = await getCurrentBackendType();
12
+ const backendConfig = await loadBackendConfig();
13
+ const backendType = type ?? currentBackendType ?? backendConfig.current;
11
14
  switch (backendType) {
15
+ case BackendType.Cloud: {
16
+ if (!backendConfig.cloud) {
17
+ throw new Error('Cloud credentials not found. Please run `hereya login` first.');
18
+ }
19
+ const credentials = await getCloudCredentials(backendConfig.cloud.clientId);
20
+ if (!credentials) {
21
+ throw new Error('Cloud credentials not found. Please run `hereya login` first.');
22
+ }
23
+ backend = new CloudBackend({
24
+ accessToken: credentials.accessToken,
25
+ clientId: backendConfig.cloud.clientId,
26
+ refreshToken: credentials.refreshToken,
27
+ url: backendConfig.cloud.url,
28
+ });
29
+ break;
30
+ }
12
31
  case BackendType.Local: {
13
32
  backend = new LocalFileBackend();
14
33
  break;
@@ -27,8 +46,12 @@ export async function getBackend() {
27
46
  }
28
47
  return backend;
29
48
  }
49
+ export function setBackendType(type) {
50
+ currentBackendType = type;
51
+ }
30
52
  export var BackendType;
31
53
  (function (BackendType) {
54
+ BackendType["Cloud"] = "cloud";
32
55
  BackendType["Local"] = "local";
33
56
  BackendType["S3"] = "s3";
34
57
  })(BackendType || (BackendType = {}));
@@ -0,0 +1,5 @@
1
+ export declare const secretManager: {
2
+ deleteSecret(name: string): Promise<void>;
3
+ getSecret<T>(name: string): Promise<null | T>;
4
+ saveSecret<T>(name: string, value: T): Promise<void>;
5
+ };
@@ -0,0 +1,66 @@
1
+ import fs from 'node:fs/promises';
2
+ import os from 'node:os';
3
+ import path from 'node:path';
4
+ const SECRETS_DIR = path.join(os.homedir(), '.hereya', 'secrets');
5
+ async function ensureSecretsDir() {
6
+ try {
7
+ await fs.mkdir(SECRETS_DIR, { recursive: true });
8
+ }
9
+ catch {
10
+ // Ignore if directory already exists
11
+ }
12
+ }
13
+ async function getSecretPath(name) {
14
+ await ensureSecretsDir();
15
+ return path.join(SECRETS_DIR, `${name}.json`);
16
+ }
17
+ export const secretManager = {
18
+ async deleteSecret(name) {
19
+ try {
20
+ const keytar = await import('keytar').then((m) => m.default);
21
+ await keytar.deletePassword('hereya', name);
22
+ }
23
+ catch {
24
+ // Fallback to file-based storage
25
+ const secretPath = await getSecretPath(name);
26
+ try {
27
+ await fs.unlink(secretPath);
28
+ }
29
+ catch {
30
+ // Ignore if file doesn't exist
31
+ }
32
+ }
33
+ },
34
+ async getSecret(name) {
35
+ try {
36
+ const keytar = await import('keytar').then((m) => m.default);
37
+ const value = await keytar.getPassword('hereya', name);
38
+ if (!value) {
39
+ return null;
40
+ }
41
+ return JSON.parse(value);
42
+ }
43
+ catch {
44
+ // Fallback to file-based storage
45
+ const secretPath = await getSecretPath(name);
46
+ try {
47
+ const value = await fs.readFile(secretPath, 'utf8');
48
+ return JSON.parse(value);
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ },
55
+ async saveSecret(name, value) {
56
+ try {
57
+ const keytar = await import('keytar').then((m) => m.default);
58
+ await keytar.setPassword('hereya', name, JSON.stringify(value));
59
+ }
60
+ catch {
61
+ // Fallback to file-based storage
62
+ const secretPath = await getSecretPath(name);
63
+ await fs.writeFile(secretPath, JSON.stringify(value), 'utf8');
64
+ }
65
+ },
66
+ };
@@ -1,28 +1,36 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { BackendType, setBackendType } from '../../backend/index.js';
2
3
  import { getInfrastructure } from '../../infrastructure/index.js';
4
+ import { getLogPath, setDebug } from '../../lib/log.js';
3
5
  export default class Bootstrap extends Command {
4
6
  static args = {
5
7
  infrastructureType: Args.string({
6
8
  description: 'infrastructure to bootstrap. Options are local, aws',
7
- required: true
8
- })
9
+ required: true,
10
+ }),
9
11
  };
10
12
  static description = 'Install necessary resources for hereya operations in an infrastructure.';
11
- static examples = [
12
- '<%= config.bin %> <%= command.id %> aws',
13
- '<%= config.bin %> <%= command.id %> local'
14
- ];
13
+ static examples = ['<%= config.bin %> <%= command.id %> aws', '<%= config.bin %> <%= command.id %> local'];
15
14
  static flags = {
16
15
  force: Flags.boolean({ char: 'f', description: 'redeploy hereya resources if already deployed' }),
17
16
  };
18
17
  async run() {
19
18
  const { args, flags } = await this.parse(Bootstrap);
19
+ setBackendType(BackendType.Local);
20
20
  const infrastructure$ = getInfrastructure({ type: args.infrastructureType });
21
21
  if (!infrastructure$.supported) {
22
22
  this.warn(infrastructure$.reason);
23
23
  return;
24
24
  }
25
25
  const { infrastructure } = infrastructure$;
26
- await infrastructure.bootstrap({ force: flags.force });
26
+ setDebug(true);
27
+ try {
28
+ await infrastructure.bootstrap({ force: flags.force });
29
+ }
30
+ catch (error) {
31
+ this.error(`${error.message}
32
+
33
+ See ${getLogPath()} for more details`);
34
+ }
27
35
  }
28
36
  }
@@ -36,7 +36,7 @@ export default class Init extends Command {
36
36
  workspace: flags.workspace,
37
37
  });
38
38
  const content = {
39
- project: initProjectOutput.project.id,
39
+ project: initProjectOutput.project.name,
40
40
  workspace: initProjectOutput.workspace.name,
41
41
  };
42
42
  await configManager.saveConfig({ config: content, projectRootDir });
@@ -0,0 +1,9 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Login extends Command {
3
+ static args: {
4
+ url: import("@oclif/core/interfaces").Arg<string, Record<string, unknown>>;
5
+ };
6
+ static description: string;
7
+ static examples: string[];
8
+ run(): Promise<void>;
9
+ }
@@ -0,0 +1,27 @@
1
+ import { Args, Command } from '@oclif/core';
2
+ import { loginToCloudBackend } from '../../backend/cloud/login.js';
3
+ import { saveCloudCredentials } from '../../backend/config.js';
4
+ export default class Login extends Command {
5
+ static args = {
6
+ url: Args.string({ description: 'URL of the Hereya Cloud backend', required: true }),
7
+ };
8
+ static description = 'Login to the Hereya Cloud backend';
9
+ static examples = [
10
+ '<%= config.bin %> <%= command.id %> https://cloud.hereya.dev',
11
+ '<%= config.bin %> <%= command.id %> http://localhost:5173',
12
+ ];
13
+ async run() {
14
+ const { args } = await this.parse(Login);
15
+ const result = await loginToCloudBackend(args.url);
16
+ if (!result.success) {
17
+ this.error(result.error);
18
+ }
19
+ await saveCloudCredentials({
20
+ accessToken: result.accessToken,
21
+ clientId: result.clientId,
22
+ refreshToken: result.refreshToken,
23
+ url: args.url,
24
+ });
25
+ this.log(`Logged in to ${args.url}`);
26
+ }
27
+ }
@@ -0,0 +1,6 @@
1
+ import { Command } from '@oclif/core';
2
+ export default class Logout extends Command {
3
+ static description: string;
4
+ static examples: string[];
5
+ run(): Promise<void>;
6
+ }
@@ -0,0 +1,23 @@
1
+ import { Command } from '@oclif/core';
2
+ import { logoutFromCloudBackend } from '../../backend/cloud/logout.js';
3
+ import { deleteCloudCredentials } from '../../backend/config.js';
4
+ export default class Logout extends Command {
5
+ static description = 'Logout from Hereya Cloud';
6
+ static examples = ['<%= config.bin %> <%= command.id %>'];
7
+ async run() {
8
+ await this.parse(Logout);
9
+ const result = await deleteCloudCredentials();
10
+ if (result.originalConfig.cloud) {
11
+ await logoutFromCloudBackend({
12
+ secret: result.secret,
13
+ url: result.originalConfig.cloud.url,
14
+ });
15
+ }
16
+ if (result.didDelete) {
17
+ this.log('Logged out from Hereya Cloud');
18
+ }
19
+ else {
20
+ this.log('Not logged in to Hereya Cloud');
21
+ }
22
+ }
23
+ }
@@ -1,5 +1,7 @@
1
1
  import { Args, Command, Flags } from '@oclif/core';
2
+ import { BackendType, setBackendType } from '../../backend/index.js';
2
3
  import { getInfrastructure } from '../../infrastructure/index.js';
4
+ import { getLogPath, setDebug } from '../../lib/log.js';
3
5
  export default class Unbootstrap extends Command {
4
6
  static args = {
5
7
  infrastructureType: Args.string({
@@ -17,12 +19,21 @@ export default class Unbootstrap extends Command {
17
19
  };
18
20
  async run() {
19
21
  const { args, flags } = await this.parse(Unbootstrap);
22
+ setBackendType(BackendType.Local);
20
23
  const infrastructure$ = getInfrastructure({ type: args.infrastructureType });
21
24
  if (!infrastructure$.supported) {
22
25
  this.warn(infrastructure$.reason);
23
26
  return;
24
27
  }
25
28
  const { infrastructure } = infrastructure$;
26
- await infrastructure.unbootstrap({ force: flags.force });
29
+ setDebug(true);
30
+ try {
31
+ await infrastructure.unbootstrap({ force: flags.force });
32
+ }
33
+ catch (error) {
34
+ this.error(`${error.message}
35
+
36
+ See ${getLogPath()} for more details`);
37
+ }
27
38
  }
28
39
  }
@@ -76,8 +76,9 @@ export class LocalExecutor {
76
76
  return { reason: setEnvVarOutput.reason, success: false };
77
77
  }
78
78
  return backend.setEnvVar({
79
+ infrastructure: input.infra,
79
80
  name: input.name,
80
- value: `${input.infra}:${setEnvVarOutput.value}`,
81
+ value: setEnvVarOutput.value,
81
82
  workspace: input.workspace,
82
83
  });
83
84
  }
@@ -116,6 +117,7 @@ export class LocalExecutor {
116
117
  };
117
118
  }
118
119
  return backend.unsetEnvVar({
120
+ infrastructure: infra,
119
121
  name: input.name,
120
122
  workspace: input.workspace,
121
123
  });
@@ -1,12 +1,23 @@
1
1
  import { GetParameterCommand, SSMClient } from '@aws-sdk/client-ssm';
2
+ import { getDefaultLogger } from '../lib/log.js';
2
3
  const configKey = '/hereya-bootstrap/config';
3
4
  export function getAwsConfigKey() {
4
5
  return configKey;
5
6
  }
6
7
  export async function getAwsConfig() {
7
- const ssmClient = new SSMClient({});
8
- const ssmParameter = await ssmClient.send(new GetParameterCommand({
9
- Name: configKey,
10
- }));
11
- return JSON.parse(ssmParameter.Parameter?.Value ?? '{}');
8
+ try {
9
+ const ssmClient = new SSMClient({});
10
+ const ssmParameter = await ssmClient.send(new GetParameterCommand({
11
+ Name: configKey,
12
+ }));
13
+ return JSON.parse(ssmParameter.Parameter?.Value ?? '{}');
14
+ }
15
+ catch (error) {
16
+ getDefaultLogger().error(`Could not get AWS config: ${error.message}`);
17
+ return {
18
+ backendBucket: '',
19
+ terraformStateBucketName: '',
20
+ terraformStateLockTableName: '',
21
+ };
22
+ }
12
23
  }