hereya-cli 0.31.0 → 0.32.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,413 @@
1
+ import { randomUUID } from 'node:crypto';
2
+ import { stringify } from 'yaml';
3
+ import { parseYaml } from '../lib/yaml-utils.js';
4
+ import { WorkspaceSchema, } from './common.js';
5
+ export class FileBackend {
6
+ fileStorage;
7
+ constructor(fileStorage) {
8
+ this.fileStorage = fileStorage;
9
+ }
10
+ async addPackageToWorkspace(input) {
11
+ const workspace$ = await this.getWorkspace(input.workspace);
12
+ if (!workspace$.found) {
13
+ return {
14
+ reason: `Workspace ${input.workspace} not found`,
15
+ success: false,
16
+ };
17
+ }
18
+ if (workspace$.hasError) {
19
+ return {
20
+ reason: workspace$.error,
21
+ success: false,
22
+ };
23
+ }
24
+ const { workspace } = workspace$;
25
+ if (workspace.mirrorOf) {
26
+ return {
27
+ reason: `Cannot add package to mirrored workspace ${input.workspace}`,
28
+ success: false,
29
+ };
30
+ }
31
+ workspace.packages = {
32
+ ...workspace.packages,
33
+ [input.package]: {
34
+ parameters: input.parameters,
35
+ version: '',
36
+ },
37
+ };
38
+ const newEnv = Object.fromEntries(Object.entries(input.env).map(([key, value]) => [key, `${input.infra}:${value}`]));
39
+ workspace.env = {
40
+ ...workspace.env,
41
+ ...newEnv,
42
+ };
43
+ try {
44
+ await this.saveWorkspace(workspace, input.workspace);
45
+ return {
46
+ success: true,
47
+ workspace,
48
+ };
49
+ }
50
+ catch (error) {
51
+ return {
52
+ reason: error.message,
53
+ success: false,
54
+ };
55
+ }
56
+ }
57
+ async createWorkspace(input) {
58
+ const workspace$ = await this.getWorkspace(input.name);
59
+ if (workspace$.found) {
60
+ return workspace$.hasError
61
+ ? {
62
+ reason: workspace$.error,
63
+ success: false,
64
+ }
65
+ : {
66
+ isNew: false,
67
+ success: true,
68
+ workspace: workspace$.workspace,
69
+ };
70
+ }
71
+ if (input.mirrorOf) {
72
+ const mirroredWorkspace$ = await this.getWorkspace(input.mirrorOf);
73
+ if (!mirroredWorkspace$.found) {
74
+ return {
75
+ reason: `Mirrored workspace ${input.mirrorOf} not found`,
76
+ success: false,
77
+ };
78
+ }
79
+ if (mirroredWorkspace$.hasError) {
80
+ return {
81
+ reason: mirroredWorkspace$.error,
82
+ success: false,
83
+ };
84
+ }
85
+ }
86
+ const workspace = {
87
+ id: input.name,
88
+ mirrorOf: input.mirrorOf,
89
+ name: input.name,
90
+ };
91
+ try {
92
+ await this.saveWorkspace(workspace, input.name);
93
+ return {
94
+ isNew: true,
95
+ success: true,
96
+ workspace,
97
+ };
98
+ }
99
+ catch (error) {
100
+ return {
101
+ reason: error.message,
102
+ success: false,
103
+ };
104
+ }
105
+ }
106
+ async deleteWorkspace(input) {
107
+ const workspace$ = await this.getWorkspace(input.name);
108
+ if (!workspace$.found) {
109
+ return {
110
+ message: `Workspace ${input.name} does not exist`,
111
+ success: true,
112
+ };
113
+ }
114
+ if (workspace$.hasError) {
115
+ return {
116
+ reason: workspace$.error,
117
+ success: false,
118
+ };
119
+ }
120
+ const { workspace } = workspace$;
121
+ const workspaceNames = await this.listWorkspaces();
122
+ const allWorkspaces = await Promise.all(workspaceNames.map(async (workspaceName) => {
123
+ const w$ = await this.getWorkspace(workspaceName);
124
+ if (!w$.found || w$.hasError) {
125
+ throw new Error(`Workspace ${workspaceName} not found or has an error`);
126
+ }
127
+ return w$.workspace;
128
+ }));
129
+ if (allWorkspaces.some((w) => w.mirrorOf === input.name)) {
130
+ return {
131
+ reason: `Cannot delete workspace ${input.name} because it is mirrored by ${allWorkspaces
132
+ .filter((w) => w.mirrorOf === input.name)
133
+ .map((w) => w.name)
134
+ .join(', ')}`,
135
+ success: false,
136
+ };
137
+ }
138
+ if (!workspace.mirrorOf && Object.keys(workspace.packages ?? {}).length > 0) {
139
+ return {
140
+ reason: `Cannot delete workspace ${input.name} because it has packages`,
141
+ success: false,
142
+ };
143
+ }
144
+ const result = await this.fileStorage.deleteFile({
145
+ paths: [`state/workspaces/${input.name}.yaml`, `state/workspaces/${input.name}.yml`],
146
+ });
147
+ if (result.success) {
148
+ return {
149
+ success: true,
150
+ };
151
+ }
152
+ return {
153
+ reason: result.error ?? '',
154
+ success: false,
155
+ };
156
+ }
157
+ async getProvisioningId(input) {
158
+ const idPaths = [`provisioning/${input.logicalId}.yaml`, `provisioning/${input.logicalId}.yml`];
159
+ const newId = `p-${randomUUID()}`;
160
+ const result = await this.fileStorage.getFileContent({ paths: idPaths });
161
+ if (!result.found) {
162
+ await this.fileStorage.saveFileContent({ content: stringify({ id: newId }), paths: idPaths });
163
+ return {
164
+ id: newId,
165
+ success: true,
166
+ };
167
+ }
168
+ const { data, error } = await parseYaml(result.content);
169
+ if (error || !data.id) {
170
+ await this.fileStorage.saveFileContent({ content: stringify({ id: newId }), paths: idPaths });
171
+ return {
172
+ id: newId,
173
+ success: true,
174
+ };
175
+ }
176
+ return {
177
+ id: data.id,
178
+ success: true,
179
+ };
180
+ }
181
+ async getState(input) {
182
+ const paths = [
183
+ ['state', 'projects', input.workspace, `${input.project}.yaml`].join('/'),
184
+ ['state', 'projects', input.workspace, `${input.project}.yml`].join('/'),
185
+ ];
186
+ const result = await this.fileStorage.getFileContent({ paths });
187
+ if (!result.found) {
188
+ return {
189
+ found: false,
190
+ };
191
+ }
192
+ const { data, error } = await parseYaml(result.content);
193
+ if (error) {
194
+ throw new Error(`Could not parse project state: ${error}`);
195
+ }
196
+ return {
197
+ config: data,
198
+ found: true,
199
+ };
200
+ }
201
+ async getWorkspace(workspace) {
202
+ const paths = [
203
+ ['state', 'workspaces', `${workspace}.yaml`].join('/'),
204
+ ['state', 'workspaces', `${workspace}.yml`].join('/'),
205
+ ];
206
+ const result = await this.fileStorage.getFileContent({ paths });
207
+ if (!result.found) {
208
+ return {
209
+ found: false,
210
+ };
211
+ }
212
+ const { data, error } = await parseYaml(result.content);
213
+ if (error) {
214
+ return {
215
+ error,
216
+ found: true,
217
+ hasError: true,
218
+ };
219
+ }
220
+ const workspace$ = WorkspaceSchema.safeParse(data);
221
+ if (!workspace$.success) {
222
+ return {
223
+ error: workspace$.error.message,
224
+ found: true,
225
+ hasError: true,
226
+ };
227
+ }
228
+ let mirroredWorkspace;
229
+ if (workspace$.data.mirrorOf) {
230
+ const mirroredWorkspace$ = await this.getWorkspace(workspace$.data.mirrorOf);
231
+ if (!mirroredWorkspace$.found) {
232
+ return {
233
+ error: `Mirrored workspace ${workspace$.data.mirrorOf} not found`,
234
+ found: true,
235
+ hasError: true,
236
+ };
237
+ }
238
+ if (mirroredWorkspace$.hasError) {
239
+ return {
240
+ error: mirroredWorkspace$.error,
241
+ found: true,
242
+ hasError: true,
243
+ };
244
+ }
245
+ mirroredWorkspace = mirroredWorkspace$.workspace;
246
+ }
247
+ return {
248
+ found: true,
249
+ hasError: false,
250
+ workspace: mirroredWorkspace
251
+ ? {
252
+ ...workspace$.data,
253
+ env: {
254
+ ...mirroredWorkspace.env,
255
+ ...workspace$.data.env,
256
+ },
257
+ packages: {
258
+ ...mirroredWorkspace.packages,
259
+ },
260
+ }
261
+ : {
262
+ ...workspace$.data,
263
+ packages: {
264
+ ...workspace$.data.packages,
265
+ },
266
+ },
267
+ };
268
+ }
269
+ async getWorkspaceEnv(input) {
270
+ const workspace$ = await this.getWorkspace(input.workspace);
271
+ if (!workspace$.found) {
272
+ return {
273
+ reason: `Workspace ${input.workspace} not found`,
274
+ success: false,
275
+ };
276
+ }
277
+ if (workspace$.hasError) {
278
+ return {
279
+ reason: workspace$.error,
280
+ success: false,
281
+ };
282
+ }
283
+ return {
284
+ env: workspace$.workspace.env ?? {},
285
+ success: true,
286
+ };
287
+ }
288
+ async init(options) {
289
+ return {
290
+ project: {
291
+ id: options.project,
292
+ name: options.project,
293
+ },
294
+ workspace: {
295
+ id: options.workspace,
296
+ name: options.workspace,
297
+ },
298
+ };
299
+ }
300
+ async listWorkspaces() {
301
+ const workspaces$ = await this.fileStorage.listFileNames({ directory: 'state/workspaces' });
302
+ if (workspaces$.success) {
303
+ return workspaces$.files
304
+ .filter((workspace) => workspace.endsWith('.yaml') || workspace.endsWith('.yml'))
305
+ .map((workspace) => workspace.replace(/\.yaml|\.yml$/, ''))
306
+ .sort();
307
+ }
308
+ throw new Error(`Could not list workspaces: ${workspaces$.error}`);
309
+ }
310
+ async removePackageFromWorkspace(input) {
311
+ const workspace$ = await this.getWorkspace(input.workspace);
312
+ if (!workspace$.found) {
313
+ return {
314
+ reason: `Workspace ${input.workspace} not found`,
315
+ success: false,
316
+ };
317
+ }
318
+ if (workspace$.hasError) {
319
+ return {
320
+ reason: workspace$.error,
321
+ success: false,
322
+ };
323
+ }
324
+ const { workspace } = workspace$;
325
+ if (workspace.mirrorOf) {
326
+ return {
327
+ reason: `Cannot remove package from mirrored workspace ${input.workspace}`,
328
+ success: false,
329
+ };
330
+ }
331
+ workspace.packages = Object.fromEntries(Object.entries(workspace.packages ?? {}).filter(([key]) => key !== input.package));
332
+ workspace.env = Object.fromEntries(Object.entries(workspace.env ?? {}).filter(([key]) => !(key in input.env)));
333
+ try {
334
+ await this.saveWorkspace(workspace, input.workspace);
335
+ return {
336
+ success: true,
337
+ workspace,
338
+ };
339
+ }
340
+ catch (error) {
341
+ return {
342
+ reason: error.message,
343
+ success: false,
344
+ };
345
+ }
346
+ }
347
+ async saveState(config, workspace) {
348
+ const paths = [
349
+ ['state', 'projects', workspace ?? config.workspace, `${config.project}.yaml`].join('/'),
350
+ ['state', 'projects', workspace ?? config.workspace, `${config.project}.yml`].join('/'),
351
+ ];
352
+ const object = {
353
+ ...config,
354
+ workspace: workspace ?? config.workspace,
355
+ };
356
+ await this.fileStorage.saveFileContent({ content: stringify(object), paths });
357
+ }
358
+ async setEnvVar(input) {
359
+ const workspace$ = await this.getWorkspace(input.workspace);
360
+ if (!workspace$.found) {
361
+ return {
362
+ reason: `Workspace ${input.workspace} not found`,
363
+ success: false,
364
+ };
365
+ }
366
+ if (workspace$.hasError) {
367
+ return {
368
+ reason: workspace$.error,
369
+ success: false,
370
+ };
371
+ }
372
+ const { workspace } = workspace$;
373
+ workspace.env = {
374
+ ...workspace.env,
375
+ [input.name]: input.value,
376
+ };
377
+ await this.saveWorkspace(workspace, input.workspace);
378
+ return {
379
+ success: true,
380
+ };
381
+ }
382
+ async unsetEnvVar(input) {
383
+ const workspace$ = await this.getWorkspace(input.workspace);
384
+ if (!workspace$.found) {
385
+ return {
386
+ reason: `Workspace ${input.workspace} not found`,
387
+ success: false,
388
+ };
389
+ }
390
+ if (workspace$.hasError) {
391
+ return {
392
+ reason: workspace$.error,
393
+ success: false,
394
+ };
395
+ }
396
+ const { workspace } = workspace$;
397
+ const value = workspace.env?.[input.name];
398
+ if (!value) {
399
+ return {
400
+ success: true,
401
+ };
402
+ }
403
+ workspace.env = Object.fromEntries(Object.entries(workspace.env ?? {}).filter(([key]) => key !== input.name));
404
+ await this.saveWorkspace(workspace, input.workspace);
405
+ return {
406
+ success: true,
407
+ };
408
+ }
409
+ async saveWorkspace(data, name) {
410
+ const paths = [['state', 'workspaces', `${name}.yaml`].join('/'), ['state', 'workspaces', `${name}.yml`].join('/')];
411
+ await this.fileStorage.saveFileContent({ content: stringify(data), paths });
412
+ }
413
+ }
@@ -1,4 +1,6 @@
1
1
  import { Backend } from './common.js';
2
- import { LocalBackend } from './local.js';
3
- export declare const localBackend: LocalBackend;
4
2
  export declare function getBackend(): Promise<Backend>;
3
+ export declare enum BackendType {
4
+ Local = "local",
5
+ S3 = "s3"
6
+ }
@@ -1,5 +1,34 @@
1
- import { LocalBackend } from './local.js';
2
- export const localBackend = new LocalBackend();
1
+ import { getAwsConfig } from '../infrastructure/aws-config.js';
2
+ import { getCurrentBackendType } from './config.js';
3
+ import { LocalFileBackend } from './local.js';
4
+ import { S3FileBackend } from './s3.js';
5
+ let backend;
3
6
  export async function getBackend() {
4
- return localBackend;
7
+ if (backend) {
8
+ return backend;
9
+ }
10
+ const backendType = await getCurrentBackendType();
11
+ switch (backendType) {
12
+ case BackendType.Local: {
13
+ backend = new LocalFileBackend();
14
+ break;
15
+ }
16
+ case BackendType.S3: {
17
+ const config = await getAwsConfig();
18
+ if (!config.backendBucket) {
19
+ throw new Error('Backend bucket not found. Please run `hereya bootstrap aws` first.');
20
+ }
21
+ backend = new S3FileBackend(config.backendBucket);
22
+ break;
23
+ }
24
+ default: {
25
+ throw new Error(`Unsupported backend type: ${backendType}`);
26
+ }
27
+ }
28
+ return backend;
5
29
  }
30
+ export var BackendType;
31
+ (function (BackendType) {
32
+ BackendType["Local"] = "local";
33
+ BackendType["S3"] = "s3";
34
+ })(BackendType || (BackendType = {}));
@@ -1,20 +1,4 @@
1
- import { Config } from '../lib/config/common.js';
2
- import { AddPackageToWorkspaceInput, AddPackageToWorkspaceOutput, Backend, CreateWorkspaceInput, CreateWorkspaceOutput, DeleteWorkspaceInput, DeleteWorkspaceOutput, GetProvisioningIdInput, GetProvisioningIdOutput, GetStateInput, GetStateOutput, GetWorkspaceEnvInput, GetWorkspaceEnvOutput, GetWorkspaceOutput, InitProjectInput, InitProjectOutput, RemovePackageFromWorkspaceInput, RemovePackageFromWorkspaceOutput, SetEnvVarInput, SetEnvVarOutput, UnsetEnvVarInput, UnsetEnvVarOutput } from './common.js';
3
- export declare class LocalBackend implements Backend {
4
- addPackageToWorkspace(input: AddPackageToWorkspaceInput): Promise<AddPackageToWorkspaceOutput>;
5
- createWorkspace(input: CreateWorkspaceInput): Promise<CreateWorkspaceOutput>;
6
- deleteWorkspace(input: DeleteWorkspaceInput): Promise<DeleteWorkspaceOutput>;
7
- getProvisioningId(input: GetProvisioningIdInput): Promise<GetProvisioningIdOutput>;
8
- getState(input: GetStateInput): Promise<GetStateOutput>;
9
- getWorkspace(workspace: string): Promise<GetWorkspaceOutput>;
10
- getWorkspaceEnv(input: GetWorkspaceEnvInput): Promise<GetWorkspaceEnvOutput>;
11
- init(options: InitProjectInput): Promise<InitProjectOutput>;
12
- listWorkspaces(): Promise<string[]>;
13
- removePackageFromWorkspace(input: RemovePackageFromWorkspaceInput): Promise<RemovePackageFromWorkspaceOutput>;
14
- saveState(config: Config, workspace?: string): Promise<void>;
15
- setEnvVar(input: SetEnvVarInput): Promise<SetEnvVarOutput>;
16
- unsetEnvVar(input: UnsetEnvVarInput): Promise<UnsetEnvVarOutput>;
17
- private getProjectStatePath;
18
- private getWorkspacePath;
19
- private saveWorkspace;
1
+ import { FileBackend } from './file.js';
2
+ export declare class LocalFileBackend extends FileBackend {
3
+ constructor(basePath?: string);
20
4
  }