@codenotch/codenotch.cli 1.0.33 → 1.0.35

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.
Files changed (99) hide show
  1. package/dist/commands/index.js +2 -0
  2. package/dist/commands/index.js.map +1 -1
  3. package/dist/commands/project/new.js +227 -1
  4. package/dist/commands/project/new.js.map +1 -1
  5. package/dist/commands/validation/ValidationUtils.d.ts +28 -0
  6. package/dist/commands/validation/ValidationUtils.js +182 -0
  7. package/dist/commands/validation/ValidationUtils.js.map +1 -0
  8. package/dist/commands/validation/files/AppManifestValidator.d.ts +6 -0
  9. package/dist/commands/validation/files/AppManifestValidator.js +93 -0
  10. package/dist/commands/validation/files/AppManifestValidator.js.map +1 -0
  11. package/dist/commands/validation/files/BpmnValidator.d.ts +5 -0
  12. package/dist/commands/validation/files/BpmnValidator.js +32 -0
  13. package/dist/commands/validation/files/BpmnValidator.js.map +1 -0
  14. package/dist/commands/validation/files/DbSchemaValidator.d.ts +5 -0
  15. package/dist/commands/validation/files/DbSchemaValidator.js +16 -0
  16. package/dist/commands/validation/files/DbSchemaValidator.js.map +1 -0
  17. package/dist/commands/validation/files/DocumentValidator.d.ts +5 -0
  18. package/dist/commands/validation/files/DocumentValidator.js +12 -0
  19. package/dist/commands/validation/files/DocumentValidator.js.map +1 -0
  20. package/dist/commands/validation/files/I18nValidator.d.ts +5 -0
  21. package/dist/commands/validation/files/I18nValidator.js +13 -0
  22. package/dist/commands/validation/files/I18nValidator.js.map +1 -0
  23. package/dist/commands/validation/files/JsonValidator.d.ts +5 -0
  24. package/dist/commands/validation/files/JsonValidator.js +13 -0
  25. package/dist/commands/validation/files/JsonValidator.js.map +1 -0
  26. package/dist/commands/validation/files/ManifestCacheValidator.d.ts +5 -0
  27. package/dist/commands/validation/files/ManifestCacheValidator.js +121 -0
  28. package/dist/commands/validation/files/ManifestCacheValidator.js.map +1 -0
  29. package/dist/commands/validation/files/ManifestValidator.d.ts +5 -0
  30. package/dist/commands/validation/files/ManifestValidator.js +14 -0
  31. package/dist/commands/validation/files/ManifestValidator.js.map +1 -0
  32. package/dist/commands/validation/files/ScriptValidator.d.ts +12 -0
  33. package/dist/commands/validation/files/ScriptValidator.js +128 -0
  34. package/dist/commands/validation/files/ScriptValidator.js.map +1 -0
  35. package/dist/commands/validation/files/TableValidator.d.ts +5 -0
  36. package/dist/commands/validation/files/TableValidator.js +19 -0
  37. package/dist/commands/validation/files/TableValidator.js.map +1 -0
  38. package/dist/commands/validation/validate.d.ts +2 -0
  39. package/dist/commands/validation/validate.js +245 -0
  40. package/dist/commands/validation/validate.js.map +1 -0
  41. package/dist/utils/BuildUtils.d.ts +23 -4
  42. package/dist/utils/BuildUtils.js +46 -16
  43. package/dist/utils/BuildUtils.js.map +1 -1
  44. package/dist/utils/FileUtils.d.ts +13 -0
  45. package/dist/utils/FileUtils.js +30 -0
  46. package/dist/utils/FileUtils.js.map +1 -1
  47. package/dist/utils/I18nUtils.js +0 -2
  48. package/dist/utils/I18nUtils.js.map +1 -1
  49. package/package.json +14 -6
  50. package/.github/copilot-instructions.md +0 -3
  51. package/bin/cli.js +0 -3
  52. package/docs/ARCHITECTURE.md +0 -179
  53. package/docs/TODO.md +0 -18
  54. package/src/bin/cli.ts +0 -4
  55. package/src/commands/index.ts +0 -13
  56. package/src/commands/project/compile.ts +0 -20
  57. package/src/commands/project/deploy.ts +0 -11
  58. package/src/commands/project/index.ts +0 -28
  59. package/src/commands/project/inspect.ts +0 -12
  60. package/src/commands/project/new.ts +0 -11
  61. package/src/commands/project/open.ts +0 -21
  62. package/src/commands/project/package.ts +0 -20
  63. package/src/commands/project/refresh.ts +0 -13
  64. package/src/commands/project/remove.ts +0 -12
  65. package/src/commands/project/shared.ts +0 -9
  66. package/src/commands/project/start.ts +0 -24
  67. package/src/commands/project/test.ts +0 -15
  68. package/src/commands/react/build.ts +0 -12
  69. package/src/commands/react/index.ts +0 -12
  70. package/src/commands/react/serve.ts +0 -13
  71. package/src/commands/runtime/clear.ts +0 -11
  72. package/src/commands/runtime/index.ts +0 -24
  73. package/src/commands/runtime/info.ts +0 -47
  74. package/src/commands/runtime/latest.ts +0 -18
  75. package/src/commands/runtime/portal.ts +0 -20
  76. package/src/commands/runtime/start.ts +0 -16
  77. package/src/commands/runtime/stop.ts +0 -13
  78. package/src/commands/runtime/update.ts +0 -12
  79. package/src/commands/system/index.ts +0 -9
  80. package/src/commands/system/info.ts +0 -23
  81. package/src/commands/system/read.ts +0 -25
  82. package/src/index.ts +0 -22
  83. package/src/models/Codenotch.ts +0 -361
  84. package/src/models/DbModels.ts +0 -428
  85. package/src/models/OpenApiModels.ts +0 -145
  86. package/src/utils/BuildUtils.ts +0 -739
  87. package/src/utils/ConsoleUtils.ts +0 -22
  88. package/src/utils/FileUtils.ts +0 -266
  89. package/src/utils/GitHubAuthUtils.ts +0 -83
  90. package/src/utils/I18nUtils.ts +0 -58
  91. package/src/utils/OpenAPIUtils.ts +0 -285
  92. package/src/utils/OsUtils.ts +0 -32
  93. package/src/utils/ProjectCompilationUtils.ts +0 -1002
  94. package/src/utils/ProjectUtils.ts +0 -151
  95. package/src/utils/RuntimeUtils.ts +0 -743
  96. package/src/utils/ServeUtils.ts +0 -175
  97. package/src/utils/ShellUtils.ts +0 -26
  98. package/src/utils/StringUtils.ts +0 -80
  99. package/tsconfig.json +0 -35
@@ -1,743 +0,0 @@
1
-
2
- import os, { version } from "node:os";
3
- import path from "node:path";
4
- import fs from "node:fs/promises";
5
- import { createWriteStream } from "node:fs";
6
- import { pipeline } from "node:stream/promises";
7
- import { Readable } from "node:stream";
8
- import { spawn } from "node:child_process";
9
- import chalk from "chalk";
10
- import decompress from "decompress";
11
- import { IProcessTestReport, IScenarioProjectManifest, ProjectState } from "../models/Codenotch";
12
- import FileUtils from "./FileUtils";
13
- import ProjectUtils from "./ProjectUtils";
14
- import GitHubAuthUtils from "./GitHubAuthUtils";
15
- import ProjectCompilationUtils from "./ProjectCompilationUtils";
16
-
17
- type RuntimeMeta = {
18
- host: string;
19
- port: number;
20
- version?: string;
21
- dataPath?: string;
22
- };
23
-
24
- export default abstract class RuntimeUtils {
25
-
26
- /// The directory where the runtime is installed, we will store the executable and other files there
27
- /// TODO not sure about that, should we store globally, or for each "project" ? Both ? like npm
28
- static readonly CODENOTCH_INSTALL_DIR = process.env.CODENOTCH_PATH?.trim()
29
- ? path.resolve(process.env.CODENOTCH_PATH.trim())
30
- : path.join(os.homedir(), ".codenotch");
31
- static readonly CODENOTCH_DATA_FOLDER = "data";
32
- private static readonly RUNTIME_PID_FILE = path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, "runtime.pid");
33
- private static readonly RUNTIME_META_FILE = path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, "runtime.json");
34
- private static readonly DEFAULT_RUNTIME_HOST = "127.0.0.1";
35
- private static readonly DEFAULT_RUNTIME_PORT = 5005;
36
- private static readonly ADMIN_USER_EMAIL = "admin@example.com";
37
-
38
- //#region Runtime
39
-
40
- private static async readRuntimeMeta(): Promise<RuntimeMeta | null> {
41
- try {
42
- const raw = await fs.readFile(RuntimeUtils.RUNTIME_META_FILE, "utf8");
43
- return JSON.parse(raw) as RuntimeMeta;
44
- } catch {
45
- return null;
46
- }
47
- }
48
-
49
- private static async writeRuntimeMeta(meta: RuntimeMeta): Promise<void> {
50
- await fs.mkdir(RuntimeUtils.CODENOTCH_INSTALL_DIR, { recursive: true });
51
- await fs.writeFile(RuntimeUtils.RUNTIME_META_FILE, JSON.stringify(meta, null, 2), "utf8");
52
- }
53
-
54
- static async getRuntimeBaseUrl(): Promise<string> {
55
-
56
- //return `http://localhost:5002`; // for testing on local visual studio
57
-
58
- const meta = await RuntimeUtils.readRuntimeMeta();
59
- const host = meta?.host ?? RuntimeUtils.DEFAULT_RUNTIME_HOST;
60
- const port = meta?.port ?? RuntimeUtils.DEFAULT_RUNTIME_PORT;
61
- return `http://${host}:${port}`;
62
- }
63
-
64
- static async runtimeFetch<T>(path: string, includeToken: boolean = false, options?: RequestInit): Promise<T> {
65
-
66
- const baseUrl = await RuntimeUtils.getRuntimeBaseUrl();
67
- const isFormData = options?.body instanceof FormData;
68
-
69
- let finalOptions: RequestInit = {
70
- ...options,
71
- credentials: 'include',
72
- headers: {
73
- ...(isFormData ? {} : { 'Content-Type': 'application/json' }),
74
- ...options?.headers,
75
- }
76
- };
77
-
78
- if (includeToken) {
79
- const token = await RuntimeUtils.getAdminToken();
80
- (finalOptions.headers as any)["devAccessToken"] = token;
81
- }
82
-
83
- const url = `${baseUrl}${path}`;
84
- const res = await fetch(url, finalOptions);
85
- if (!res.ok) {
86
- const body = await res.text().catch(() => null);
87
- throw new Error(`Runtime api error: ${url} -> ${res.status} - ${body}`);
88
- }
89
-
90
- if (res.status === 204 || res.headers.get('content-length') === '0') {
91
- return undefined as T;
92
- }
93
-
94
- let content = await res.text();
95
-
96
- try{
97
- return JSON.parse(content) as T;
98
- }
99
- catch(e) {
100
- return content as unknown as T;
101
- }
102
- }
103
-
104
- private static async getAdminToken(): Promise<string> {
105
-
106
- // Use the client_credentials flow to get an admin token
107
- // In dev/local the flow we only need the user email to issue a token
108
- var response = await RuntimeUtils.runtimeFetch<{access_token: string}>(`/_auth/token`, false, {
109
-
110
- method: "POST",
111
- body: new URLSearchParams({
112
- client_id: RuntimeUtils.ADMIN_USER_EMAIL,
113
- grant_type: "client_credentials",
114
- }),
115
- headers: {
116
- "Content-Type": "application/x-www-form-urlencoded"
117
- }
118
- });
119
-
120
- //TODO some way to cache and reuse this
121
-
122
- return response.access_token;
123
- }
124
-
125
- static async ensureRuntimeInstalled(): Promise<void> {
126
-
127
- const isInstalled = await RuntimeUtils.isRuntimeInstalled();
128
- if (!isInstalled) {
129
-
130
- //TODO Here we assume NodeJs is installed and available in PATH, we should check that and propose to install it if not
131
- await RuntimeUtils.installLatestRuntime();
132
- }
133
- }
134
-
135
- static async ensureRuntimeIsRunning(): Promise<void> {
136
-
137
- const isRunning = await RuntimeUtils.isRuntimeRunning();
138
- if (isRunning) {
139
- return;
140
- }
141
-
142
- await RuntimeUtils.startRuntime();
143
- }
144
-
145
- static async isRuntimeInstalled(): Promise<boolean> {
146
- const targetExe = RuntimeUtils.getRuntimeExecutablePath();
147
- try {
148
- await fs.access(targetExe);
149
- return true;
150
- } catch {
151
- return false;
152
- }
153
- }
154
-
155
- static async isRuntimeRunning(): Promise<boolean> {
156
- try {
157
- const pidStr = await fs.readFile(RuntimeUtils.RUNTIME_PID_FILE, "utf8");
158
- const pid = parseInt(pidStr.trim());
159
-
160
- if (isNaN(pid)) {
161
- return false;
162
- }
163
-
164
- // Check if process exists by sending signal 0 (doesn't kill, just checks)
165
- try {
166
- process.kill(pid, 0);
167
- return true;
168
- } catch {
169
- // Process doesn't exist
170
- await fs.unlink(RuntimeUtils.RUNTIME_PID_FILE).catch(() => {});
171
- return false;
172
- }
173
- } catch {
174
- // PID file doesn't exist
175
- return false;
176
- }
177
- }
178
-
179
- static async runRuntimeExecutable(args: string[] = [], detached: boolean, envVars: { [key: string]: string } = {}): Promise<string> {
180
-
181
- const exePath = RuntimeUtils.getRuntimeExecutablePath();
182
-
183
- try {
184
- const child = spawn(exePath, args, {
185
- detached: detached,
186
- stdio: detached ? "ignore" : "pipe",
187
- windowsHide: true,
188
- env: { ...process.env, ...envVars },
189
- });
190
-
191
- if (detached) {
192
- // Store the PID for later reference
193
- await fs.mkdir(RuntimeUtils.CODENOTCH_INSTALL_DIR, { recursive: true });
194
- await fs.writeFile(RuntimeUtils.RUNTIME_PID_FILE, child.pid!.toString());
195
- child.unref(); // Allow parent process to exit independently
196
- return "";
197
- } else {
198
- // Wait for the child process to complete
199
- let stdout = "";
200
- let stderr = "";
201
-
202
- child.stdout?.on("data", (chunk) => {
203
- const text = chunk.toString();
204
- stdout += text;
205
- process.stdout.write(text);
206
- });
207
-
208
- child.stderr?.on("data", (chunk) => {
209
- const text = chunk.toString();
210
- stderr += text;
211
- process.stderr.write(text);
212
- });
213
-
214
- await new Promise<void>((resolve, reject) => {
215
- child.on("exit", (code) => {
216
- if (code === 0) {
217
- resolve();
218
- } else {
219
- const errorOutput = stderr.trim() || stdout.trim();
220
- reject(new Error(`Runtime process exited with code ${code}${errorOutput ? `: ${errorOutput}` : ""}`));
221
- }
222
- });
223
- child.on("error", reject);
224
- });
225
-
226
- return stdout.trim();
227
- }
228
- } catch (error) {
229
- throw new Error(`Failed to run runtime executable: ${error instanceof Error ? error.message : String(error)}`);
230
- }
231
- }
232
-
233
- static getRuntimeExecutablePath(): string {
234
- const platform = process.platform; // win32 | linux | darwin
235
- const exeName = platform === "win32" ? "codenotch.exe" : "codenotch";
236
- return path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, exeName);
237
- }
238
-
239
- static async installLatestRuntime(): Promise<void> {
240
-
241
- let wasRunning = await RuntimeUtils.isRuntimeRunning();
242
- if(wasRunning) {
243
- await RuntimeUtils.stopRuntime();
244
- }
245
-
246
- const platform = process.platform; // win32 | linux | darwin
247
- const exeName = platform === "win32" ? "codenotch.exe" : "codenotch";
248
-
249
- let assetName;
250
- if (platform === "win32") assetName = "codenotch-windows.zip";
251
- else if (platform === "linux") assetName = "codenotch-linux.tar.gz";
252
- else if (platform === "darwin") assetName = "codenotch-macos.tar.gz";
253
- else throw new Error(`Unsupported platform: ${platform}`);
254
-
255
- // We need to authenticate because the repo is private
256
- const token = await GitHubAuthUtils.getToken();
257
- const headers = {
258
- Authorization: `Bearer ${token}`,
259
- };
260
-
261
- console.log(chalk.blue(`Getting latest Codenotch runtime release metadata...`));
262
- const latestReleaseRes = await fetch(
263
- "https://api.github.com/repos/EchinoHub/service-core/releases/latest",
264
- {
265
- headers: {
266
- ...headers,
267
- "Accept": "application/vnd.github+json"
268
- }
269
- }
270
- );
271
-
272
- if (!latestReleaseRes.ok) {
273
- const body = await latestReleaseRes.text().catch(() => "");
274
- throw new Error(`Failed to get latest release: ${latestReleaseRes.status} ${body}`);
275
- }
276
-
277
- //const url = `https://github.com/EchinoHub/service-core/releases/latest/download/${assetName}`;
278
-
279
- const release = await latestReleaseRes.json() as any;
280
- const asset = release.assets.find((a: any) => a.name === assetName);
281
-
282
- if (!asset) {
283
- throw new Error(
284
- `Asset not found: ${assetName}.`
285
- );
286
- }
287
-
288
- const version = release.tag_name;
289
-
290
- console.log(chalk.blue(`Downloading Codenotch runtime from ${asset.url}...`));
291
- const res = await fetch(asset.url, {
292
- redirect: "follow",
293
- headers: {
294
- ...headers,
295
- "Accept": "application/octet-stream"
296
- }
297
- });
298
- if (!res.ok || !res.body) {
299
- console.log(chalk.red(`Failed to download runtime from ${asset.url}`));
300
- throw new Error(`Download failed: ${res.status} ${res.statusText}`);
301
- }
302
-
303
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codenotch-"));
304
- try
305
- {
306
- const archivePath = path.join(tempRoot, assetName);
307
- const extractDir = path.join(tempRoot, "extract");
308
-
309
- await fs.mkdir(RuntimeUtils.CODENOTCH_INSTALL_DIR, { recursive: true });
310
- await fs.mkdir(extractDir, { recursive: true });
311
-
312
- await pipeline(Readable.fromWeb(res.body), createWriteStream(archivePath));
313
- await decompress(archivePath, extractDir);
314
-
315
- const downloadedExe = path.join(extractDir, assetName.split(".")[0], exeName);
316
- const targetExe = path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, exeName);
317
- await fs.copyFile(downloadedExe, targetExe);
318
- if (platform !== "win32") await fs.chmod(targetExe, 0o755);
319
-
320
- // Update the metadata with the version
321
- const existingMeta = await RuntimeUtils.readRuntimeMeta();
322
- await RuntimeUtils.writeRuntimeMeta({
323
- host: existingMeta?.host ?? RuntimeUtils.DEFAULT_RUNTIME_HOST,
324
- port: existingMeta?.port ?? RuntimeUtils.DEFAULT_RUNTIME_PORT,
325
- version: version,
326
- dataPath: existingMeta?.dataPath
327
- });
328
-
329
- console.log(chalk.blue(`The runtime has been installed successfully.`));
330
- }
331
- finally {
332
- FileUtils.delete(tempRoot);
333
- }
334
-
335
- if(wasRunning) {
336
- await RuntimeUtils.startRuntime();
337
- }
338
- }
339
-
340
- static async startRuntime(options?: { port?: number, attach?: boolean, dataPath?: string }): Promise<void> {
341
-
342
- const isRunning = await RuntimeUtils.isRuntimeRunning();
343
- if (isRunning) {
344
- return;
345
- }
346
-
347
- await this.ensureRuntimeInstalled();
348
-
349
- // Start the runtime process in detached mode by default
350
- let detached = true;
351
- if(options?.attach) {
352
- detached = false;
353
- }
354
-
355
- console.log(chalk.blue(`Starting Codenotch runtime ${detached ? "in detached mode" : "in attached mode"}...`));
356
- const port = options?.port ?? RuntimeUtils.DEFAULT_RUNTIME_PORT;
357
- const dataPath = options?.dataPath;
358
-
359
- const version = await RuntimeUtils.getRuntimeVersion();
360
-
361
- // Store the runtime meta (like the port) so we can use it later to connect to the runtime
362
- await RuntimeUtils.writeRuntimeMeta({
363
- host: RuntimeUtils.DEFAULT_RUNTIME_HOST,
364
- port: port,
365
- version: version,
366
- dataPath: dataPath
367
- });
368
-
369
- const runtimeArgs = dataPath ? ["-d", dataPath] : [];
370
- await RuntimeUtils.runRuntimeExecutable(runtimeArgs, detached, { CODENOTCH_PORT: String(port), START_URL: '' }); // pass the port and prevent the runtime from opening the browser automatically
371
-
372
- // Wait until the runtime is responsive
373
- const maxAttempts = 50; // Wait up to 10 seconds
374
- let attempts = 0;
375
- while (attempts < maxAttempts) {
376
- try {
377
- await RuntimeUtils.runtimeFetch("/health");
378
- console.log(chalk.green(`Runtime is ready!`));
379
- return; // Runtime is responsive
380
- } catch(e) {
381
- await new Promise(resolve => setTimeout(resolve, 200));
382
- attempts++;
383
- }
384
- }
385
-
386
- console.log(chalk.red(`Failed to start runtime: runtime did not become responsive within expected time.`));
387
- throw new Error(`Runtime did not become responsive within expected time.`);
388
- }
389
-
390
- static async stopRuntime(): Promise<void> {
391
-
392
- if(await RuntimeUtils.isRuntimeInstalled() === false) {
393
- return;
394
- }
395
-
396
- if(await RuntimeUtils.isRuntimeRunning() === false) {
397
- return;
398
- }
399
-
400
- try {
401
- const pidStr = await fs.readFile(RuntimeUtils.RUNTIME_PID_FILE, "utf8");
402
- const pid = parseInt(pidStr.trim());
403
-
404
- if (!isNaN(pid)) {
405
- // Send SIGTERM to gracefully stop the process
406
- process.kill(pid, "SIGTERM");
407
-
408
- // Wait up to 5 seconds for graceful shutdown
409
- let attempts = 0;
410
- while (attempts < 50 && await RuntimeUtils.isRuntimeRunning()) {
411
- await new Promise(resolve => setTimeout(resolve, 100));
412
- attempts++;
413
- }
414
-
415
- // If still running, force kill
416
- if (await RuntimeUtils.isRuntimeRunning()) {
417
- process.kill(pid, "SIGKILL");
418
- await new Promise(resolve => setTimeout(resolve, 100));
419
- }
420
- }
421
-
422
- // Clean up the PID file
423
- await fs.unlink(RuntimeUtils.RUNTIME_PID_FILE).catch(() => {});
424
- } catch (error) {
425
- throw new Error(`Failed to stop runtime: ${error instanceof Error ? error.message : String(error)}`);
426
- }
427
- }
428
-
429
- static async clearRuntime(): Promise<void> {
430
-
431
- // Remove the runtime data folder, but keep the executable and other files
432
- const dataDir = path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, RuntimeUtils.CODENOTCH_DATA_FOLDER);
433
- try {
434
- await fs.rm(dataDir, { recursive: true, force: true });
435
- console.log(chalk.green(`Runtime data cleared successfully.`));
436
- } catch (error) {
437
- console.log(chalk.red(`Failed to clear runtime data: ${error}`));
438
- throw error;
439
- }
440
- }
441
-
442
- static async getRuntimeVersion(): Promise<string> {
443
-
444
- if(!await RuntimeUtils.isRuntimeInstalled()) {
445
- return "Not installed";
446
- }
447
-
448
- // Use the metadata if available to avoid calling the executable, which can be slow to start
449
- const meta = await RuntimeUtils.readRuntimeMeta();
450
-
451
- if(!meta?.version) {
452
- return await RuntimeUtils.runRuntimeExecutable(["--version"], false);
453
- }
454
-
455
- return meta?.version;
456
- }
457
-
458
- static async getRuntimeDataPath(): Promise<string | undefined> {
459
- const meta = await RuntimeUtils.readRuntimeMeta();
460
- return meta?.dataPath;
461
- }
462
-
463
- static async getLatestRuntimeRelease(): Promise<{version: string, createdAt: string, url: string}> {
464
- const token = await GitHubAuthUtils.getToken();
465
- const headers: Record<string, string> = {
466
- "Accept": "application/vnd.github+json",
467
- "Authorization": `Bearer ${token}`,
468
- };
469
-
470
- const res = await fetch(
471
- "https://api.github.com/repos/EchinoHub/service-core/releases/latest",
472
- { headers }
473
- );
474
-
475
- if (!res.ok) {
476
- const body = await res.text().catch(() => "");
477
- throw new Error(`Failed to get latest release: ${res.status} ${body}`);
478
- }
479
-
480
- const release = await res.json() as any;
481
- return {
482
- version: release.tag_name,
483
- createdAt: release.created_at,
484
- url: release.html_url
485
- }
486
- }
487
-
488
- //#endregion
489
-
490
- //#region Project management
491
-
492
- static async startProject(projectPath: string): Promise<void> {
493
-
494
- //TODO multiproject support
495
- //TODO --watch mode
496
-
497
- const serviceName = await ProjectUtils.getProjectServiceName(projectPath);
498
-
499
- // Ensure the runtime is running
500
- await RuntimeUtils.startRuntime();
501
-
502
- const outputPath = path.join(os.tmpdir(), `cn-build-${Date.now()}.zip`);
503
-
504
- try
505
- {
506
- console.log(chalk.blue(`Building project "${serviceName}"...`));
507
- await ProjectUtils.packageProject(projectPath, outputPath, true);
508
- console.log(chalk.blue(`Deploying project "${serviceName}" to runtime...`));
509
- await new Promise(resolve => setTimeout(resolve, 2000)); // Wait a bit before deploying to ensure the runtime is fully ready, only temporary until runtime fix the ready endpoint to be more reliable
510
- var deploySuccess = await RuntimeUtils.deployProject(serviceName, outputPath);
511
- if (deploySuccess) {
512
- console.log(chalk.green(`Project "${serviceName}" is now running.`));
513
- } else {
514
- console.log(chalk.red(`Project start failed.`));
515
- }
516
- }
517
- finally {
518
- // Remove the build
519
- FileUtils.delete(outputPath);
520
- }
521
- }
522
-
523
- static async testProject(projectPath: string, fileFilter?: string, nameFilter?: string, inPlace: boolean = false): Promise<void> {
524
-
525
- await RuntimeUtils.ensureRuntimeInstalled();
526
-
527
- // For testing we use a separate instance of the runtime
528
- // We run it in attach mode so we get the results in the console
529
- const isPartial = !!(fileFilter || nameFilter);
530
- const finalReportPath = path.join(projectPath, "test-report.json");
531
-
532
- // Runs the runtime test command, writing the report to a given path via -r
533
- const testFunc = async (workPath: string, reportPath: string) =>
534
- await RuntimeUtils.runRuntimeExecutable([
535
- "test",
536
- "-p", workPath,
537
- ...(fileFilter ? ["-f", fileFilter] : []),
538
- ...(nameFilter ? ["-t", nameFilter] : []),
539
- "-r", reportPath,
540
- ], false);
541
-
542
- // To test a project, we need to install it and to install it, we need it to be compiled
543
- let projectCompiled = await ProjectUtils.isManifestCompiled(projectPath);
544
-
545
- // If the project is already compiled we can go ahead and test it
546
- if (projectCompiled) {
547
- const tempReportPath = path.join(os.tmpdir(), `cn-report-${Date.now()}.json`);
548
- try {
549
- await testFunc(projectPath, tempReportPath);
550
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
551
- } finally {
552
- await fs.unlink(tempReportPath).catch(() => {});
553
- }
554
- return;
555
- }
556
-
557
- // We need a "lightly-compiled" project, meaning we need a full manifest (we don't need i18n or to build the React apps)
558
- let compileFunc = async (path: string) => {
559
- const compilation = await ProjectCompilationUtils.compileTestProject(path);
560
-
561
- if(compilation.errors.length > 0) {
562
- console.error(chalk.red("Compilation failed with errors:"));
563
- compilation.errors.forEach(err => console.error(chalk.red(`- ${err}`)));
564
- console.warn(chalk.yellow("Ignoring compilation errors and proceeding with testing..."));
565
- }
566
- };
567
-
568
- // In-place means we can compile and modify files directly in the project folder, we don't need to copy them to a temp folder, this is useful during CI/CD
569
- // Inside the IDE we'll prefer to copy the folder to avoid modifying the source files
570
-
571
- if (inPlace) {
572
- await compileFunc(projectPath);
573
- const tempReportPath = path.join(os.tmpdir(), `cn-report-${Date.now()}.json`);
574
- try {
575
- await testFunc(projectPath, tempReportPath);
576
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
577
- } finally {
578
- await fs.unlink(tempReportPath).catch(() => {});
579
- }
580
- return;
581
- }
582
-
583
- const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "codenotch-"));
584
- try
585
- {
586
- // Copy project
587
- await FileUtils.copyFolder(projectPath, tempRoot, ['.git', 'node_modules', 'dist', 'build', 'out']);
588
- await compileFunc(tempRoot);
589
- const tempReportPath = path.join(tempRoot, "test-report.json");
590
- await testFunc(tempRoot, tempReportPath);
591
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
592
- }
593
- finally {
594
- // Clean up temp folder
595
- //TODO consider keeping it for faster re-testing if project has not changed
596
- await FileUtils.delete(tempRoot);
597
- }
598
- }
599
-
600
- static async deployProject(serviceName: string, packagePath: string): Promise<boolean> {
601
-
602
- // We assume the project is already built and packaged, we just need to deploy it to the runtime
603
-
604
- // Get the information about the project package, we assume it's in the current directory and named "project.zip"
605
- const zipFilePath = packagePath;
606
- try {
607
- await fs.access(zipFilePath);
608
- } catch (error) {
609
- console.log(chalk.red(`Project package not found: ${zipFilePath}`));
610
- throw new Error(`Project package not found: ${zipFilePath}`);
611
- }
612
-
613
- // Ensure the runtime is running
614
- await RuntimeUtils.startRuntime();
615
-
616
- const zipBuffer = await fs.readFile(zipFilePath);
617
- const zipBlob = new Blob([zipBuffer], { type: "application/zip" });
618
-
619
- const form = new FormData();
620
- form.append('serviceName', serviceName);
621
- form.append('file', zipBlob, "package.zip");
622
- await RuntimeUtils.runtimeFetch("/project/upload", true, {
623
- method: "POST",
624
- body: form
625
- });
626
-
627
- // Wait until the project deployment has completed
628
- while (true) {
629
- await new Promise(resolve => setTimeout(resolve, 1000));
630
- try {
631
- const projectState = await RuntimeUtils.runtimeFetch<ProjectState>(`/${serviceName}/project`, true);
632
- if(projectState.status === "READY")
633
- {
634
- console.log(chalk.green(`${serviceName} is ready!`));
635
- return true;
636
- }
637
- else if(projectState.status === "FAILURE" || projectState.status === "REMOVING")
638
- {
639
- console.log(chalk.red(`Failed to deploy ${serviceName}. Errors: ${projectState.errors?.join(", ")}`));
640
- return false;
641
- }
642
- } catch {
643
- // Keep waiting
644
- }
645
- }
646
- }
647
-
648
- static async patchProject(projectPath: string) {
649
- throw new Error('Method not implemented.');
650
- }
651
-
652
- static async removeProject(projectPath: string): Promise<void> {
653
-
654
- const serviceName = await ProjectUtils.getProjectServiceName(projectPath);
655
- await RuntimeUtils.uninstallProject(serviceName);
656
- }
657
-
658
- static async uninstallProject(serviceName: string): Promise<void> {
659
-
660
- // Cannot properly uninstall if the runtime is not installed and running
661
- // Ensure the runtime is running
662
- await RuntimeUtils.startRuntime();
663
-
664
- await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true, {
665
- method: 'DELETE'
666
- });
667
-
668
- /* Normally the runtime handle that
669
- // Erase all files prefixed with the project name in the runtime data folder
670
- const runtimeDataDir = path.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, "data");
671
- const files = await fs.readdir(runtimeDataDir);
672
- for (const file of files) {
673
- if (file.startsWith(project)) {
674
- await fs.unlink(path.join(runtimeDataDir, file));
675
- }
676
- }
677
- */
678
- }
679
-
680
- //#endregion
681
-
682
- //#region Test report helpers
683
-
684
- private static async persistTestReport(sourcePath: string, destPath: string, isPartial: boolean): Promise<void> {
685
-
686
- let newReport: IProcessTestReport;
687
- try {
688
- const content = await fs.readFile(sourcePath, "utf8");
689
- newReport = JSON.parse(content) as IProcessTestReport;
690
- } catch {
691
- console.warn(chalk.yellow("Could not read test report from runtime output."));
692
- return;
693
- }
694
-
695
- if (!isPartial) {
696
- await fs.writeFile(destPath, JSON.stringify(newReport, null, 2), "utf8");
697
- return;
698
- }
699
-
700
- // Merge partial results into any existing report
701
- let existingReport: IProcessTestReport | null = null;
702
- try {
703
- const content = await fs.readFile(destPath, "utf8");
704
- existingReport = JSON.parse(content) as IProcessTestReport;
705
- } catch {
706
- // No existing report — use the new one as-is
707
- }
708
-
709
- const merged = existingReport
710
- ? RuntimeUtils.mergeTestReports(existingReport, newReport)
711
- : newReport;
712
-
713
- await fs.writeFile(destPath, JSON.stringify(merged, null, 2), "utf8");
714
- }
715
-
716
- private static mergeTestReports(existing: IProcessTestReport, partial: IProcessTestReport): IProcessTestReport {
717
-
718
- const updatedTests = [...existing.Tests];
719
-
720
- for (const newTest of partial.Tests) {
721
- const idx = updatedTests.findIndex(t => t.Name === newTest.Name);
722
- if (idx >= 0) {
723
- updatedTests[idx] = newTest;
724
- } else {
725
- updatedTests.push(newTest);
726
- }
727
- }
728
-
729
- const successCount = updatedTests.filter(t => t.Success).length;
730
- const failureCount = updatedTests.filter(t => !t.Success).length;
731
-
732
- return {
733
- ...existing,
734
- ExecutionId: partial.ExecutionId,
735
- Tests: updatedTests,
736
- SuccessCount: successCount,
737
- FailureCount: failureCount,
738
- Success: failureCount === 0,
739
- };
740
- }
741
-
742
- //#endregion
743
- }