hereya-cli 0.13.0 → 0.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.
@@ -1,11 +1,11 @@
1
1
  import { Command, Flags } from '@oclif/core';
2
+ import { Listr, ListrLogLevels, ListrLogger } from 'listr2';
2
3
  import { getBackend } from '../../backend/index.js';
3
4
  import { destroyPackage, provisionPackage } from '../../infrastructure/index.js';
4
5
  import { getConfigManager } from '../../lib/config/index.js';
5
6
  import { getEnvManager } from '../../lib/env/index.js';
6
- import { getLogger } from '../../lib/log.js';
7
7
  import { getParameterManager } from '../../lib/parameter/index.js';
8
- import { setDebug } from '../../lib/shell.js';
8
+ import { delay, setDebug } from '../../lib/shell.js';
9
9
  export default class Up extends Command {
10
10
  static description = 'Provision all packages in the project.';
11
11
  static examples = ['<%= config.bin %> <%= command.id %>'];
@@ -31,109 +31,201 @@ export default class Up extends Command {
31
31
  async run() {
32
32
  const { flags } = await this.parse(Up);
33
33
  setDebug(flags.debug);
34
- const logger = getLogger();
35
34
  const projectRootDir = flags.chdir || process.env.HEREYA_PROJECT_ROOT_DIR;
36
- const configManager = getConfigManager();
37
- const loadConfigOutput = await configManager.loadConfig({ projectRootDir });
38
- if (!loadConfigOutput.found) {
39
- this.warn(`Project not initialized. Run 'hereya init' first.`);
40
- return;
35
+ const myLogger = new ListrLogger({ useIcons: false });
36
+ const task = new Listr([
37
+ {
38
+ async task(ctx, task) {
39
+ return task.newListr([
40
+ {
41
+ async task(ctx) {
42
+ const configManager = getConfigManager();
43
+ const loadConfigOutput = await configManager.loadConfig({ projectRootDir });
44
+ if (!loadConfigOutput.found) {
45
+ throw new Error("Project not initialized. Run 'hereya init' first.");
46
+ }
47
+ ctx.configOutput = loadConfigOutput;
48
+ await delay(500);
49
+ },
50
+ title: 'Loading project config',
51
+ },
52
+ {
53
+ async task(ctx) {
54
+ const backend = await getBackend();
55
+ const workspace = flags.workspace || ctx.configOutput.config.workspace;
56
+ const getWorkspaceEnvOutput = await backend.getWorkspaceEnv({
57
+ project: ctx.configOutput.config.project,
58
+ workspace,
59
+ });
60
+ if (!getWorkspaceEnvOutput.success) {
61
+ throw new Error(getWorkspaceEnvOutput.reason);
62
+ }
63
+ ctx.workspaceEnvOutput = getWorkspaceEnvOutput;
64
+ ctx.workspace = workspace;
65
+ await delay(500);
66
+ },
67
+ title: 'Loading workspace environment variables',
68
+ },
69
+ {
70
+ async task(ctx) {
71
+ const backend = await getBackend();
72
+ const savedStateOutput = await backend.getState({
73
+ project: ctx.configOutput.config.project,
74
+ });
75
+ if (savedStateOutput.found) {
76
+ ctx.savedStateOutput = savedStateOutput;
77
+ }
78
+ await delay(500);
79
+ },
80
+ title: 'Loading project current state',
81
+ },
82
+ {
83
+ async task(ctx) {
84
+ const packages = Object.keys(ctx.configOutput.config.packages ?? {});
85
+ const savedPackages = Object.keys(ctx.savedStateOutput?.config?.packages ?? {});
86
+ const removedPackages = savedPackages.filter((packageName) => !packages.includes(packageName));
87
+ ctx.removedPackages = removedPackages;
88
+ ctx.packages = packages;
89
+ await delay(500);
90
+ },
91
+ title: 'Searching for removed packages',
92
+ },
93
+ {
94
+ skip: (ctx) => !ctx.removedPackages || ctx.removedPackages.length === 0,
95
+ async task(ctx) {
96
+ const { configOutput, removed, removedPackages, workspace, workspaceEnvOutput } = ctx;
97
+ if (!removedPackages || removedPackages.length === 0) {
98
+ return;
99
+ }
100
+ return task.newListr(removedPackages.map((packageName) => ({
101
+ async task() {
102
+ const parameterManager = getParameterManager();
103
+ const { parameters } = await parameterManager.getPackageParameters({
104
+ package: packageName,
105
+ projectRootDir,
106
+ workspace,
107
+ });
108
+ const destroyOutput = await destroyPackage({
109
+ env: workspaceEnvOutput.env,
110
+ isDeploying: flags.deploy,
111
+ package: packageName,
112
+ parameters,
113
+ project: configOutput.config.project,
114
+ workspace,
115
+ });
116
+ if (!destroyOutput.success) {
117
+ throw new Error(destroyOutput.reason);
118
+ }
119
+ const { env, metadata } = destroyOutput;
120
+ const output = removed || [];
121
+ output.push({ env, metadata, packageName });
122
+ ctx.removed = output;
123
+ },
124
+ title: `Destroying ${packageName}`,
125
+ })), { concurrent: true });
126
+ },
127
+ title: `Destroying removed packages`,
128
+ },
129
+ {
130
+ skip: (ctx) => !ctx.packages || ctx.packages.length === 0,
131
+ async task(ctx) {
132
+ if (!ctx.packages || ctx.packages.length === 0) {
133
+ return;
134
+ }
135
+ return task.newListr(ctx.packages.map((packageName) => ({
136
+ async task() {
137
+ const parameterManager = getParameterManager();
138
+ const { parameters } = await parameterManager.getPackageParameters({
139
+ package: packageName,
140
+ projectRootDir,
141
+ workspace: ctx.workspace,
142
+ });
143
+ const provisionOutput = await provisionPackage({
144
+ env: ctx.workspaceEnvOutput.env,
145
+ isDeploying: flags.deploy,
146
+ package: packageName,
147
+ parameters,
148
+ project: ctx.configOutput.config.project,
149
+ workspace: ctx.workspace,
150
+ });
151
+ if (!provisionOutput.success) {
152
+ throw new Error(provisionOutput.reason);
153
+ }
154
+ const { env, metadata } = provisionOutput;
155
+ const output = ctx.added || [];
156
+ output.push({ env, metadata, packageName });
157
+ ctx.added = output;
158
+ },
159
+ title: `Provisioning ${packageName}`,
160
+ })), { concurrent: true });
161
+ },
162
+ title: `Provisioning packages`,
163
+ },
164
+ {
165
+ skip: (ctx) => !ctx.removed || ctx.removed.length === 0,
166
+ async task(ctx) {
167
+ const { removed, workspace } = ctx;
168
+ if (!removed || removed.length === 0) {
169
+ return;
170
+ }
171
+ const envManager = getEnvManager();
172
+ for (const { env, metadata } of removed) {
173
+ // eslint-disable-next-line no-await-in-loop
174
+ await Promise.all([
175
+ envManager.removeProjectEnv({
176
+ env,
177
+ infra: metadata.originalInfra ?? metadata.infra,
178
+ projectRootDir,
179
+ workspace,
180
+ }),
181
+ ]);
182
+ }
183
+ await delay(500);
184
+ },
185
+ title: 'Removing env vars from removed packages',
186
+ },
187
+ {
188
+ skip: (ctx) => !ctx.added || ctx.added.length === 0,
189
+ async task(ctx) {
190
+ if (!ctx.added || ctx.added.length === 0) {
191
+ return;
192
+ }
193
+ const envManager = getEnvManager();
194
+ for (const { env, metadata } of ctx.added) {
195
+ // eslint-disable-next-line no-await-in-loop
196
+ await envManager.addProjectEnv({
197
+ env,
198
+ infra: metadata.originalInfra ?? metadata.infra,
199
+ projectRootDir,
200
+ workspace: ctx.workspace,
201
+ });
202
+ }
203
+ await delay(500);
204
+ },
205
+ title: 'Adding env vars from added packages',
206
+ },
207
+ {
208
+ async task() {
209
+ const backend = await getBackend();
210
+ const configManager = getConfigManager();
211
+ const { config: newConfig } = await configManager.loadConfig({ projectRootDir });
212
+ await backend.saveState(newConfig);
213
+ await delay(500);
214
+ },
215
+ title: 'Saving state',
216
+ },
217
+ ], { concurrent: false });
218
+ },
219
+ title: 'Waking up the project',
220
+ },
221
+ ]);
222
+ try {
223
+ await task.run();
224
+ myLogger.log(ListrLogLevels.COMPLETED, 'Project waked up successfully');
41
225
  }
42
- const { config } = loadConfigOutput;
43
- const packages = Object.keys(config.packages ?? {});
44
- const backend = await getBackend();
45
- const savedStateOutput = await backend.getState({
46
- project: config.project,
47
- });
48
- let savedPackages = [];
49
- if (savedStateOutput.found) {
50
- savedPackages = Object.keys(savedStateOutput.config.packages ?? {});
226
+ catch (error) {
227
+ myLogger.log(ListrLogLevels.FAILED, error);
228
+ this.error(error.message);
51
229
  }
52
- const removedPackages = savedPackages.filter((packageName) => !packages.includes(packageName));
53
- const workspace = flags.workspace || config.workspace;
54
- const getWorkspaceEnvOutput = await backend.getWorkspaceEnv({
55
- project: config.project,
56
- workspace,
57
- });
58
- if (!getWorkspaceEnvOutput.success) {
59
- this.error(getWorkspaceEnvOutput.reason);
60
- }
61
- const { env: workspaceEnv } = getWorkspaceEnvOutput;
62
- const parameterManager = getParameterManager();
63
- if (removedPackages.length > 0) {
64
- logger.log(`Destroying ${removedPackages.length} removed packages`);
65
- }
66
- const removed = await Promise.all(removedPackages.map(async (packageName) => {
67
- const { parameters } = await parameterManager.getPackageParameters({
68
- package: packageName,
69
- projectRootDir,
70
- workspace,
71
- });
72
- const destroyOutput = await destroyPackage({
73
- env: workspaceEnv,
74
- isDeploying: flags.deploy,
75
- package: packageName,
76
- parameters,
77
- project: config.project,
78
- workspace,
79
- });
80
- if (!destroyOutput.success) {
81
- this.error(destroyOutput.reason);
82
- }
83
- const { env, metadata } = destroyOutput;
84
- return { env, metadata, packageName };
85
- }));
86
- if (removedPackages.length > 0) {
87
- logger.done(`Destroyed ${removedPackages.length} removed packages`);
88
- }
89
- logger.log(`Provisioning ${packages.length} packages`);
90
- const added = await Promise.all(packages.map(async (packageName) => {
91
- const { parameters } = await parameterManager.getPackageParameters({
92
- package: packageName,
93
- projectRootDir,
94
- workspace,
95
- });
96
- const provisionOutput = await provisionPackage({
97
- env: workspaceEnv,
98
- isDeploying: flags.deploy,
99
- package: packageName,
100
- parameters,
101
- project: config.project,
102
- workspace,
103
- });
104
- if (!provisionOutput.success) {
105
- this.error(provisionOutput.reason);
106
- }
107
- const { env, metadata } = provisionOutput;
108
- return { env, metadata, packageName };
109
- }));
110
- logger.done(`Provisioned ${packages.length} packages`);
111
- const envManager = getEnvManager();
112
- for (const { env, metadata } of removed) {
113
- // eslint-disable-next-line no-await-in-loop
114
- await Promise.all([
115
- envManager.removeProjectEnv({
116
- env,
117
- infra: metadata.originalInfra ?? metadata.infra,
118
- projectRootDir,
119
- workspace,
120
- }),
121
- ]);
122
- }
123
- if (removedPackages.length > 0) {
124
- logger.done(`Removed env vars from ${removedPackages.length} removed packages`);
125
- }
126
- for (const { env, metadata } of added) {
127
- // eslint-disable-next-line no-await-in-loop
128
- await envManager.addProjectEnv({
129
- env,
130
- infra: metadata.originalInfra ?? metadata.infra,
131
- projectRootDir,
132
- workspace,
133
- });
134
- }
135
- logger.done('Saved exported environment variables');
136
- const { config: newConfig } = await configManager.loadConfig({ projectRootDir });
137
- await backend.saveState(newConfig);
138
230
  }
139
231
  }
@@ -47,3 +47,4 @@ export type GetInfrastructureOutput = {
47
47
  infrastructure: Infrastructure;
48
48
  supported: true;
49
49
  };
50
+ export type PackageMetadata = z.infer<typeof PackageMetadata>;
package/dist/lib/log.js CHANGED
@@ -1,27 +1,11 @@
1
- import cliSpinners from 'cli-spinners';
2
- import ora from 'ora';
3
- let spinner = null;
1
+ import { ListrLogLevels, ListrLogger } from 'listr2';
2
+ const myLogger = new ListrLogger({ useIcons: false });
4
3
  const logger = {
5
4
  done(message) {
6
- if (!spinner) {
7
- spinner = ora({
8
- spinner: cliSpinners.aesthetic,
9
- text: message,
10
- });
11
- }
12
- spinner.succeed(message);
13
- spinner = null;
5
+ myLogger.log(ListrLogLevels.COMPLETED, message);
14
6
  },
15
7
  log(message) {
16
- if (spinner) {
17
- spinner.text = message;
18
- return;
19
- }
20
- spinner = ora({
21
- spinner: cliSpinners.aesthetic,
22
- text: message,
23
- });
24
- spinner.start();
8
+ myLogger.log(ListrLogLevels.OUTPUT, message);
25
9
  },
26
10
  };
27
11
  export function getLogger() {
@@ -1,4 +1,5 @@
1
1
  export interface PackageManager {
2
+ downloadPackage: (pkgUrl: string, destPath: string) => Promise<string>;
2
3
  getRepoContent: (input: GetRepoContentInput) => Promise<GetRepoContentOutput>;
3
4
  }
4
5
  export type GetRepoContentInput = {
@@ -9,6 +10,7 @@ export type GetRepoContentInput = {
9
10
  export type GetRepoContentOutput = {
10
11
  content: string;
11
12
  found: true;
13
+ pkgUrl: string;
12
14
  } | {
13
15
  found: false;
14
16
  reason: string;
@@ -1,4 +1,5 @@
1
1
  import type { GetRepoContentInput, GetRepoContentOutput, PackageManager } from './common.js';
2
2
  export declare class GitHubPackageManager implements PackageManager {
3
- getRepoContent({ owner, path, repo }: GetRepoContentInput): Promise<GetRepoContentOutput>;
3
+ downloadPackage(pkgUrl: string, destPath: string): Promise<string>;
4
+ getRepoContent({ owner, path: filePath, repo }: GetRepoContentInput): Promise<GetRepoContentOutput>;
4
5
  }
@@ -1,32 +1,61 @@
1
- import { Octokit } from '@octokit/rest';
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import fs from 'node:fs/promises';
8
+ import os from 'node:os';
9
+ import path from 'node:path';
10
+ import { simpleGit } from 'simple-git';
11
+ import { BackOffPolicy, Retryable } from 'typescript-retry-decorator';
12
+ import { isNotEmpty } from '../filesystem.js';
2
13
  export class GitHubPackageManager {
3
- async getRepoContent({ owner, path, repo }) {
4
- const octokit = new Octokit();
14
+ // eslint-disable-next-line new-cap
15
+ async downloadPackage(pkgUrl, destPath) {
16
+ if (await isNotEmpty(destPath)) {
17
+ return destPath;
18
+ }
19
+ await fs.mkdir(destPath, { recursive: true });
20
+ // Initialize simple-git
21
+ const git = simpleGit();
22
+ // Clone repository into temp directory
23
+ await git.clone(pkgUrl, destPath, ['--depth=1']);
24
+ return destPath;
25
+ }
26
+ async getRepoContent({ owner, path: filePath, repo }) {
27
+ const pkgUrl = `https://github.com/${owner}/${repo}`;
28
+ const tmpFolder = path.join(os.tmpdir(), 'hereya', 'github', owner, repo);
5
29
  try {
6
- const response = await octokit.rest.repos.getContent({
7
- headers: {
8
- 'Accept': 'application/vnd.github.raw+json'
9
- },
10
- owner,
11
- path,
12
- repo
13
- });
14
- if (response.status !== 200) {
30
+ const destPath = await this.downloadPackage(pkgUrl, tmpFolder);
31
+ if (await fs.stat(path.join(destPath, filePath))) {
32
+ const content = await fs.readFile(path.join(destPath, filePath), 'utf8');
33
+ // remove the tmp folder
34
+ await fs.rm(destPath, { recursive: true });
15
35
  return {
16
- found: false,
17
- reason: `Failed to fetch content: ${response.status}`
36
+ content,
37
+ found: true,
38
+ pkgUrl,
18
39
  };
19
40
  }
20
41
  return {
21
- content: response.data,
22
- found: true
42
+ found: false,
43
+ reason: `File ${filePath} not found in ${pkgUrl}`,
23
44
  };
24
45
  }
25
46
  catch (error) {
26
47
  return {
27
48
  found: false,
28
- reason: error.message
49
+ reason: error.message,
29
50
  };
30
51
  }
31
52
  }
32
53
  }
54
+ __decorate([
55
+ Retryable({
56
+ backOff: 1000,
57
+ backOffPolicy: BackOffPolicy.ExponentialBackOffPolicy,
58
+ exponentialOption: { maxInterval: 4000, multiplier: 3 },
59
+ maxAttempts: 3,
60
+ })
61
+ ], GitHubPackageManager.prototype, "downloadPackage", null);
@@ -1,10 +1,7 @@
1
- import * as fs from 'node:fs/promises';
2
- import { simpleGit } from 'simple-git';
3
1
  import * as yaml from 'yaml';
4
2
  import { z } from 'zod';
5
3
  import { IacType } from '../../iac/common.js';
6
4
  import { InfrastructureType } from '../../infrastructure/common.js';
7
- import { isNotEmpty } from '../filesystem.js';
8
5
  import { GitHubPackageManager } from './github.js';
9
6
  export const packageManager = new GitHubPackageManager();
10
7
  export function getPackageManager() {
@@ -16,14 +13,13 @@ export async function resolvePackage(input) {
16
13
  return { found: false, reason: 'Invalid package format. Use owner/repository' };
17
14
  }
18
15
  const [owner, repo] = pkgParts;
19
- const pkgUrl = `https://github.com/${input.package}`;
20
16
  const packageManager = getPackageManager();
21
17
  const metadataContentCandidates = (await Promise.all([
22
18
  packageManager.getRepoContent({ owner, path: 'hereyarc.yaml', repo }),
23
19
  packageManager.getRepoContent({ owner, path: 'hereyarc.yml', repo }),
24
20
  ])).filter((content$) => content$.found);
25
21
  if (metadataContentCandidates.length === 0) {
26
- return { found: false, reason: `No hereya metadata file found in ${pkgUrl}` };
22
+ return { found: false, reason: `No hereya metadata file found in ${input.package}` };
27
23
  }
28
24
  const metadataContent$ = metadataContentCandidates[0];
29
25
  try {
@@ -34,15 +30,11 @@ export async function resolvePackage(input) {
34
30
  if (input.isDeploying && metadata.onDeploy) {
35
31
  return resolvePackage({ package: metadata.onDeploy.pkg });
36
32
  }
37
- // if (process.env.HEREYA_OVERRIDE_INFRA) {
38
- // metadata.originalInfra = metadata.infra
39
- // metadata.infra = process.env.HEREYA_OVERRIDE_INFRA as InfrastructureType
40
- // }
41
33
  return {
42
34
  canonicalName: getPackageCanonicalName(input.package),
43
35
  found: true,
44
36
  metadata,
45
- packageUri: pkgUrl,
37
+ packageUri: metadataContent$.pkgUrl,
46
38
  pkgName: input.package,
47
39
  };
48
40
  }
@@ -54,15 +46,8 @@ export function getPackageCanonicalName(packageName) {
54
46
  return packageName.replace('/', '-');
55
47
  }
56
48
  export async function downloadPackage(pkgUrl, destPath) {
57
- if (await isNotEmpty(destPath)) {
58
- return destPath;
59
- }
60
- await fs.mkdir(destPath, { recursive: true });
61
- // Initialize simple-git
62
- const git = simpleGit();
63
- // Clone repository into temp directory
64
- await git.clone(pkgUrl, destPath, ['--depth=1']);
65
- return destPath;
49
+ const packageManager = getPackageManager();
50
+ return packageManager.downloadPackage(pkgUrl, destPath);
66
51
  }
67
52
  export const PackageMetadata = z.object({
68
53
  dependencies: z.record(z.string()).optional(),
@@ -9,3 +9,4 @@ export type RunShellOptions = {
9
9
  stdio?: StdioOptions;
10
10
  };
11
11
  export declare function runShell(cmd: string, args: string[], options?: RunShellOptions): ReturnType<typeof spawnSync>;
12
+ export declare function delay(ms: number): Promise<unknown> | undefined;
package/dist/lib/shell.js CHANGED
@@ -21,3 +21,11 @@ export function runShell(cmd, args, options = {}) {
21
21
  }
22
22
  return result;
23
23
  }
24
+ export function delay(ms) {
25
+ if (process.env.NODE_ENV === 'test') {
26
+ return;
27
+ }
28
+ return new Promise((resolve) => {
29
+ setTimeout(resolve, ms);
30
+ });
31
+ }