hereya-cli 0.34.0 → 0.36.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,379 @@
1
+ export class CloudBackend {
2
+ config;
3
+ constructor(config) {
4
+ this.config = config;
5
+ }
6
+ async addPackageToWorkspace(input) {
7
+ const formData = new FormData();
8
+ formData.append('package', input.package);
9
+ formData.append('infra', input.infra);
10
+ formData.append('env', JSON.stringify(input.env));
11
+ if (input.parameters) {
12
+ formData.append('parameters', JSON.stringify(input.parameters));
13
+ }
14
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/packages`, {
15
+ body: formData,
16
+ headers: {
17
+ 'Authorization': `Bearer ${this.config.accessToken}`,
18
+ },
19
+ method: 'POST',
20
+ });
21
+ if (!response.ok) {
22
+ return {
23
+ reason: JSON.stringify(await response.json()),
24
+ success: false,
25
+ };
26
+ }
27
+ const result = await response.json();
28
+ return {
29
+ success: true,
30
+ workspace: this.convertWorkspace(result.workspace),
31
+ };
32
+ }
33
+ async createWorkspace(input) {
34
+ const formData = new FormData();
35
+ formData.append('name', input.name);
36
+ if (input.mirrorOf) {
37
+ formData.append('mirrorOf', input.mirrorOf);
38
+ }
39
+ const response = await fetch(`${this.config.url}/api/workspaces`, {
40
+ body: formData,
41
+ headers: {
42
+ 'Authorization': `Bearer ${this.config.accessToken}`,
43
+ },
44
+ method: 'POST',
45
+ });
46
+ if (!response.ok) {
47
+ return {
48
+ reason: JSON.stringify(await response.json()),
49
+ success: false,
50
+ };
51
+ }
52
+ const result = await response.json();
53
+ return {
54
+ isNew: true,
55
+ success: true,
56
+ workspace: {
57
+ id: result.workspace.id,
58
+ name: result.workspace.name,
59
+ },
60
+ };
61
+ }
62
+ async deleteWorkspace(input) {
63
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.name)}`, {
64
+ headers: {
65
+ 'Authorization': `Bearer ${this.config.accessToken}`,
66
+ },
67
+ method: 'DELETE',
68
+ });
69
+ if (!response.ok) {
70
+ return {
71
+ reason: JSON.stringify(await response.json()),
72
+ success: false,
73
+ };
74
+ }
75
+ return {
76
+ success: true,
77
+ };
78
+ }
79
+ async exportBackend() {
80
+ const response = await fetch(`${this.config.url}/api/export`, {
81
+ headers: {
82
+ 'Authorization': `Bearer ${this.config.accessToken}`,
83
+ },
84
+ method: 'GET',
85
+ });
86
+ if (!response.ok) {
87
+ return {
88
+ reason: JSON.stringify(await response.json()),
89
+ success: false,
90
+ };
91
+ }
92
+ const result = await response.json();
93
+ if (!result.success) {
94
+ return {
95
+ reason: JSON.stringify(result),
96
+ success: false,
97
+ };
98
+ }
99
+ return {
100
+ data: JSON.stringify(result.data),
101
+ success: true,
102
+ };
103
+ }
104
+ async getProvisioningId(input) {
105
+ const formData = new FormData();
106
+ formData.append('packageCanonicalName', input.packageCanonicalName);
107
+ formData.append('logicalId', input.logicalId);
108
+ if (input.project) {
109
+ formData.append('project', input.project);
110
+ }
111
+ if (input.workspace) {
112
+ formData.append('workspace', input.workspace);
113
+ }
114
+ const response = await fetch(`${this.config.url}/api/provisioning-id`, {
115
+ body: formData,
116
+ headers: {
117
+ 'Authorization': `Bearer ${this.config.accessToken}`,
118
+ },
119
+ method: 'POST',
120
+ });
121
+ if (!response.ok) {
122
+ return {
123
+ reason: JSON.stringify(await response.json()),
124
+ success: false,
125
+ };
126
+ }
127
+ const result = await response.json();
128
+ return {
129
+ id: result.provisioningId.id,
130
+ success: true,
131
+ };
132
+ }
133
+ async getState(input) {
134
+ const url = new URL(`${this.config.url}/api/projects/${encodeURIComponent(input.project)}/state`);
135
+ url.searchParams.append('workspace', input.workspace);
136
+ const response = await fetch(url, {
137
+ headers: {
138
+ 'Authorization': `Bearer ${this.config.accessToken}`,
139
+ },
140
+ method: 'GET',
141
+ });
142
+ if (!response.ok) {
143
+ return {
144
+ found: false,
145
+ reason: JSON.stringify(await response.json()),
146
+ };
147
+ }
148
+ const result = await response.json();
149
+ if (!result.success) {
150
+ return {
151
+ found: false,
152
+ };
153
+ }
154
+ const deploy = {};
155
+ for (const item of result.config.deploy) {
156
+ deploy[item.name] = { version: item.version };
157
+ }
158
+ const packages = {};
159
+ for (const item of result.config.packages) {
160
+ packages[item.name] = { version: item.version };
161
+ }
162
+ return {
163
+ config: {
164
+ deploy,
165
+ packages,
166
+ project: input.project,
167
+ workspace: input.workspace,
168
+ },
169
+ found: true,
170
+ };
171
+ }
172
+ async getWorkspace(name) {
173
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(name)}`, {
174
+ headers: {
175
+ 'Authorization': `Bearer ${this.config.accessToken}`,
176
+ },
177
+ method: 'GET',
178
+ });
179
+ if (!response.ok) {
180
+ const error = await response.json();
181
+ return {
182
+ error: JSON.stringify(error),
183
+ found: true,
184
+ hasError: true,
185
+ };
186
+ }
187
+ const result = await response.json();
188
+ return {
189
+ found: true,
190
+ hasError: false,
191
+ workspace: this.convertWorkspace(result.workspace),
192
+ };
193
+ }
194
+ async getWorkspaceEnv(input) {
195
+ const workspace$ = await this.getWorkspace(input.workspace);
196
+ if (!workspace$.found) {
197
+ return {
198
+ reason: `Workspace ${input.workspace} not found`,
199
+ success: false,
200
+ };
201
+ }
202
+ if (workspace$.hasError) {
203
+ return {
204
+ reason: workspace$.error,
205
+ success: false,
206
+ };
207
+ }
208
+ return {
209
+ env: workspace$.workspace.env ?? {},
210
+ success: true,
211
+ };
212
+ }
213
+ async importBackend(input) {
214
+ const formData = new FormData();
215
+ formData.append('data', input.data);
216
+ const response = await fetch(`${this.config.url}/api/import`, {
217
+ body: formData,
218
+ headers: {
219
+ 'Authorization': `Bearer ${this.config.accessToken}`,
220
+ },
221
+ method: 'POST',
222
+ });
223
+ if (!response.ok) {
224
+ return {
225
+ reason: JSON.stringify(await response.json()),
226
+ success: false,
227
+ };
228
+ }
229
+ return {
230
+ success: true,
231
+ };
232
+ }
233
+ async init(input) {
234
+ const formData = new FormData();
235
+ formData.append('name', input.project);
236
+ formData.append('workspace', input.workspace);
237
+ const response = await fetch(`${this.config.url}/api/projects`, {
238
+ body: formData,
239
+ headers: {
240
+ 'Authorization': `Bearer ${this.config.accessToken}`,
241
+ },
242
+ method: 'POST',
243
+ });
244
+ if (!response.ok) {
245
+ throw new Error(JSON.stringify(await response.json()));
246
+ }
247
+ const result = await response.json();
248
+ return {
249
+ project: {
250
+ id: result.project.id,
251
+ name: result.project.name,
252
+ },
253
+ workspace: {
254
+ id: result.project.defaultWorkspace.id,
255
+ name: result.project.defaultWorkspace.name,
256
+ },
257
+ };
258
+ }
259
+ async listWorkspaces() {
260
+ const response = await fetch(`${this.config.url}/api/workspaces`, {
261
+ headers: {
262
+ 'Authorization': `Bearer ${this.config.accessToken}`,
263
+ },
264
+ method: 'GET',
265
+ });
266
+ if (!response.ok) {
267
+ throw new Error(JSON.stringify(await response.json()));
268
+ }
269
+ const result = await response.json();
270
+ return result.workspaces.map((workspace) => workspace.name);
271
+ }
272
+ async removePackageFromWorkspace(input) {
273
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/packages/${encodeURIComponent(input.package)}`, {
274
+ headers: {
275
+ 'Authorization': `Bearer ${this.config.accessToken}`,
276
+ },
277
+ method: 'DELETE',
278
+ });
279
+ if (!response.ok) {
280
+ return {
281
+ reason: JSON.stringify(await response.json()),
282
+ success: false,
283
+ };
284
+ }
285
+ const result = await response.json();
286
+ return {
287
+ success: true,
288
+ workspace: this.convertWorkspace(result.workspace),
289
+ };
290
+ }
291
+ async saveState(config, workspace) {
292
+ const formData = new FormData();
293
+ if (workspace) {
294
+ formData.append('workspace', workspace);
295
+ }
296
+ if (config.deploy) {
297
+ const deployArray = Object.entries(config.deploy).map(([name, { version }]) => ({ name, version })).sort((a, b) => a.name.localeCompare(b.name));
298
+ formData.append('deploy', JSON.stringify(deployArray));
299
+ }
300
+ if (config.packages) {
301
+ const packagesArray = Object.entries(config.packages).map(([name, { version }]) => ({ name, version })).sort((a, b) => a.name.localeCompare(b.name));
302
+ formData.append('packages', JSON.stringify(packagesArray));
303
+ }
304
+ const response = await fetch(`${this.config.url}/api/projects/${encodeURIComponent(config.project)}/state`, {
305
+ body: formData,
306
+ headers: {
307
+ 'Authorization': `Bearer ${this.config.accessToken}`,
308
+ },
309
+ method: 'POST',
310
+ });
311
+ if (!response.ok) {
312
+ throw new Error(JSON.stringify(await response.json()));
313
+ }
314
+ }
315
+ async setEnvVar(input) {
316
+ const formData = new FormData();
317
+ formData.append('key', input.name);
318
+ formData.append('value', input.value);
319
+ formData.append('infrastructure', input.infrastructure);
320
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/env`, {
321
+ body: formData,
322
+ headers: {
323
+ 'Authorization': `Bearer ${this.config.accessToken}`,
324
+ },
325
+ method: 'POST',
326
+ });
327
+ if (!response.ok) {
328
+ return {
329
+ reason: JSON.stringify(await response.json()),
330
+ success: false,
331
+ };
332
+ }
333
+ return {
334
+ success: true,
335
+ };
336
+ }
337
+ async unsetEnvVar(input) {
338
+ const response = await fetch(`${this.config.url}/api/workspaces/${encodeURIComponent(input.workspace)}/env/${encodeURIComponent(input.name)}`, {
339
+ headers: {
340
+ 'Authorization': `Bearer ${this.config.accessToken}`,
341
+ },
342
+ method: 'DELETE',
343
+ });
344
+ if (!response.ok) {
345
+ return {
346
+ reason: JSON.stringify(await response.json()),
347
+ success: false,
348
+ };
349
+ }
350
+ return {
351
+ success: true,
352
+ };
353
+ }
354
+ convertWorkspace(workspace) {
355
+ const env = {};
356
+ for (const pkg of workspace.packages) {
357
+ for (const e of pkg.env) {
358
+ env[e.key] = `${e.infrastructure}:${e.value}`;
359
+ }
360
+ }
361
+ for (const e of workspace.env) {
362
+ env[e.key] = `${e.infrastructure}:${e.value}`;
363
+ }
364
+ const packages = {};
365
+ for (const pkg of workspace.packages) {
366
+ packages[pkg.name] = {
367
+ parameters: pkg.parameters,
368
+ version: pkg.version,
369
+ };
370
+ }
371
+ return {
372
+ env,
373
+ id: workspace.id,
374
+ mirrorOf: workspace.mirrorOf?.name,
375
+ name: workspace.name,
376
+ packages,
377
+ };
378
+ }
379
+ }
@@ -0,0 +1,16 @@
1
+ export declare function loginToCloudBackend(url: string): Promise<{
2
+ accessToken: string;
3
+ clientId: string;
4
+ refreshToken: string;
5
+ success: true;
6
+ } | {
7
+ error: string;
8
+ success: false;
9
+ }>;
10
+ export declare function registerDevice(url: string): Promise<{
11
+ clientId: string;
12
+ success: true;
13
+ } | {
14
+ error: string;
15
+ success: false;
16
+ }>;
@@ -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
+ };
@@ -5,10 +5,12 @@ export interface Backend {
5
5
  addPackageToWorkspace(input: AddPackageToWorkspaceInput): Promise<AddPackageToWorkspaceOutput>;
6
6
  createWorkspace(input: CreateWorkspaceInput): Promise<CreateWorkspaceOutput>;
7
7
  deleteWorkspace(input: DeleteWorkspaceInput): Promise<DeleteWorkspaceOutput>;
8
+ exportBackend(): Promise<ExportBackendOutput>;
8
9
  getProvisioningId(input: GetProvisioningIdInput): Promise<GetProvisioningIdOutput>;
9
10
  getState(input: GetStateInput): Promise<GetStateOutput>;
10
11
  getWorkspace(workspace: string): Promise<GetWorkspaceOutput>;
11
12
  getWorkspaceEnv(input: GetWorkspaceEnvInput): Promise<GetWorkspaceEnvOutput>;
13
+ importBackend(input: ImportBackendInput): Promise<ImportBackendOutput>;
12
14
  init(options: InitProjectInput): Promise<InitProjectOutput>;
13
15
  listWorkspaces(): Promise<string[]>;
14
16
  removePackageFromWorkspace(input: RemovePackageFromWorkspaceInput): Promise<RemovePackageFromWorkspaceOutput>;
@@ -100,6 +102,13 @@ export type CreateWorkspaceOutput = {
100
102
  reason: string;
101
103
  success: false;
102
104
  };
105
+ export type ExportBackendOutput = {
106
+ data: string;
107
+ success: true;
108
+ } | {
109
+ reason: string;
110
+ success: false;
111
+ };
103
112
  export type GetWorkspaceEnvInput = {
104
113
  project?: string;
105
114
  workspace: string;
@@ -133,6 +142,7 @@ export type GetStateOutput = {
133
142
  found: true;
134
143
  } | {
135
144
  found: false;
145
+ reason?: string;
136
146
  };
137
147
  export type DeleteWorkspaceInput = {
138
148
  name: string;
@@ -157,7 +167,17 @@ export type GetProvisioningIdOutput = {
157
167
  reason: string;
158
168
  success: false;
159
169
  };
170
+ export type ImportBackendInput = {
171
+ data: string;
172
+ };
173
+ export type ImportBackendOutput = {
174
+ reason: string;
175
+ success: false;
176
+ } | {
177
+ success: true;
178
+ };
160
179
  export type SetEnvVarInput = {
180
+ infrastructure: InfrastructureType;
161
181
  name: string;
162
182
  value: string;
163
183
  workspace: string;
@@ -169,6 +189,7 @@ export type SetEnvVarOutput = {
169
189
  success: true;
170
190
  };
171
191
  export type UnsetEnvVarInput = {
192
+ infrastructure: InfrastructureType;
172
193
  name: string;
173
194
  workspace: string;
174
195
  };
@@ -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
  };