@codenotch/codenotch.cli 1.0.53 → 1.0.55

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 (90) hide show
  1. package/README.md +14 -5
  2. package/dist/commands/generate/app.js +22 -14
  3. package/dist/commands/generate/app.js.map +1 -1
  4. package/dist/commands/generate/appmanifest.js +14 -2
  5. package/dist/commands/generate/appmanifest.js.map +1 -1
  6. package/dist/commands/generate/dbcontext.js +9 -2
  7. package/dist/commands/generate/dbcontext.js.map +1 -1
  8. package/dist/commands/generate/i18n.js +15 -0
  9. package/dist/commands/generate/i18n.js.map +1 -1
  10. package/dist/commands/generate/manifest.js +7 -2
  11. package/dist/commands/generate/manifest.js.map +1 -1
  12. package/dist/commands/generate/openapi.js +9 -2
  13. package/dist/commands/generate/openapi.js.map +1 -1
  14. package/dist/commands/generate/pkg.js +7 -2
  15. package/dist/commands/generate/pkg.js.map +1 -1
  16. package/dist/commands/generate/process.js +26 -18
  17. package/dist/commands/generate/process.js.map +1 -1
  18. package/dist/commands/generate/shared.d.ts +5 -0
  19. package/dist/commands/generate/shared.js +12 -0
  20. package/dist/commands/generate/shared.js.map +1 -1
  21. package/dist/commands/project/bump.d.ts +2 -0
  22. package/dist/commands/project/bump.js +130 -0
  23. package/dist/commands/project/bump.js.map +1 -0
  24. package/dist/commands/project/compile.js +11 -2
  25. package/dist/commands/project/compile.js.map +1 -1
  26. package/dist/commands/project/deploy.js +10 -2
  27. package/dist/commands/project/deploy.js.map +1 -1
  28. package/dist/commands/project/index.js +3 -0
  29. package/dist/commands/project/index.js.map +1 -1
  30. package/dist/commands/project/open.js +4 -4
  31. package/dist/commands/project/open.js.map +1 -1
  32. package/dist/commands/project/package.js +60 -4
  33. package/dist/commands/project/package.js.map +1 -1
  34. package/dist/commands/project/refresh.js +12 -3
  35. package/dist/commands/project/refresh.js.map +1 -1
  36. package/dist/commands/project/remove.js +4 -1
  37. package/dist/commands/project/remove.js.map +1 -1
  38. package/dist/commands/project/start.js +8 -4
  39. package/dist/commands/project/start.js.map +1 -1
  40. package/dist/commands/project/test.js +41 -3
  41. package/dist/commands/project/test.js.map +1 -1
  42. package/dist/commands/runtime/clear.js +5 -0
  43. package/dist/commands/runtime/clear.js.map +1 -1
  44. package/dist/commands/runtime/info.js +1 -1
  45. package/dist/commands/runtime/info.js.map +1 -1
  46. package/dist/commands/runtime/portal.js +4 -4
  47. package/dist/commands/runtime/portal.js.map +1 -1
  48. package/dist/commands/runtime/start.js +10 -1
  49. package/dist/commands/runtime/start.js.map +1 -1
  50. package/dist/commands/runtime/stop.js +5 -0
  51. package/dist/commands/runtime/stop.js.map +1 -1
  52. package/dist/commands/runtime/update.js +4 -3
  53. package/dist/commands/runtime/update.js.map +1 -1
  54. package/dist/commands/system/completion.d.ts +7 -0
  55. package/dist/commands/system/completion.js +145 -0
  56. package/dist/commands/system/completion.js.map +1 -0
  57. package/dist/commands/system/index.js +2 -0
  58. package/dist/commands/system/index.js.map +1 -1
  59. package/dist/commands/system/info.js +42 -8
  60. package/dist/commands/system/info.js.map +1 -1
  61. package/dist/commands/validation/validate.js +3 -3
  62. package/dist/commands/validation/validate.js.map +1 -1
  63. package/dist/index.js +32 -4
  64. package/dist/index.js.map +1 -1
  65. package/dist/utils/BuildUtils.js +49 -36
  66. package/dist/utils/BuildUtils.js.map +1 -1
  67. package/dist/utils/ConsoleUtils.d.ts +27 -0
  68. package/dist/utils/ConsoleUtils.js +53 -4
  69. package/dist/utils/ConsoleUtils.js.map +1 -1
  70. package/dist/utils/FileUtils.js +1 -1
  71. package/dist/utils/FileUtils.js.map +1 -1
  72. package/dist/utils/GitHubAuthUtils.js +5 -5
  73. package/dist/utils/GitHubAuthUtils.js.map +1 -1
  74. package/dist/utils/PackageUtils.js +15 -15
  75. package/dist/utils/PackageUtils.js.map +1 -1
  76. package/dist/utils/ProjectCompilationUtils.d.ts +0 -1
  77. package/dist/utils/ProjectCompilationUtils.js +22 -23
  78. package/dist/utils/ProjectCompilationUtils.js.map +1 -1
  79. package/dist/utils/ProjectUtils.d.ts +6 -0
  80. package/dist/utils/ProjectUtils.js +34 -1
  81. package/dist/utils/ProjectUtils.js.map +1 -1
  82. package/dist/utils/RuntimeUtils.d.ts +71 -10
  83. package/dist/utils/RuntimeUtils.js +218 -69
  84. package/dist/utils/RuntimeUtils.js.map +1 -1
  85. package/dist/utils/ShellUtils.js +6 -2
  86. package/dist/utils/ShellUtils.js.map +1 -1
  87. package/dist/utils/TemplateUtils.js +10 -9
  88. package/dist/utils/TemplateUtils.js.map +1 -1
  89. package/package.json +4 -1
  90. package/resources/docs/project.md +21 -8
@@ -1,4 +1,26 @@
1
- import { ProjectState } from "../models/Codenotch";
1
+ import { IProcessTestReport, ProjectState } from "../models/Codenotch";
2
+ /** Outcome of a deployment on the runtime, the errors and warnings being the ones the runtime reported */
3
+ export interface IDeployResult {
4
+ success: boolean;
5
+ serviceName: string;
6
+ /** Status the runtime ended in, undefined when nothing reached it */
7
+ status?: string;
8
+ errors: string[];
9
+ warnings: string[];
10
+ }
11
+ /** Outcome of a refresh: the compilation errors when it stopped there, the deployment ones otherwise */
12
+ export interface IRefreshResult extends IDeployResult {
13
+ /** 'deploy' when the runtime did not hold the project yet, 'update' otherwise */
14
+ mode?: 'deploy' | 'update';
15
+ /** Version the runtime held before an update */
16
+ previousVersion?: string;
17
+ }
18
+ /** Outcome of a test run: the report the runtime wrote, when it wrote one */
19
+ export interface ITestResult {
20
+ report?: IProcessTestReport;
21
+ /** Where the report of the project is persisted */
22
+ reportPath: string;
23
+ }
2
24
  export default abstract class RuntimeUtils {
3
25
  static readonly CODENOTCH_INSTALL_DIR: string;
4
26
  static readonly CODENOTCH_DATA_FOLDER = "data";
@@ -7,20 +29,43 @@ export default abstract class RuntimeUtils {
7
29
  private static readonly DEFAULT_RUNTIME_HOST;
8
30
  private static readonly DEFAULT_RUNTIME_PORT;
9
31
  private static readonly ADMIN_USER_EMAIL;
32
+ /** A deployment the runtime does not conclude within this delay is reported as failed rather than waited for ever */
33
+ private static readonly DEPLOY_TIMEOUT_MS;
10
34
  private static readRuntimeMeta;
11
35
  private static writeRuntimeMeta;
12
36
  static getRuntimeBaseUrl(): Promise<string>;
13
37
  static runtimeFetch<T>(path: string, includeToken?: boolean, options?: RequestInit): Promise<T>;
14
38
  private static getAdminToken;
15
- static ensureRuntimeInstalled(): Promise<void>;
16
- static ensureRuntimeIsRunning(): Promise<void>;
39
+ /**
40
+ * Makes sure the runtime is installed, proposing to install it when it is not.
41
+ * Outside a terminal the proposal is accepted, so scripts and CI keep working unattended.
42
+ * Returns false when the user declined, after a message telling how to install it.
43
+ */
44
+ static ensureRuntimeInstalled(): Promise<boolean>;
45
+ /**
46
+ * Makes sure the runtime is running, proposing to install it and/or start it when it is not.
47
+ * Outside a terminal the proposal is accepted, so scripts and CI keep working unattended.
48
+ * Returns false when the user declined, after a message telling how to do it by hand.
49
+ */
50
+ static ensureRuntimeIsRunning(options?: {
51
+ port?: number;
52
+ dataPath?: string;
53
+ }): Promise<boolean>;
17
54
  static isRuntimeInstalled(): Promise<boolean>;
18
55
  static isRuntimeRunning(): Promise<boolean>;
19
56
  static runRuntimeExecutable(args: string[] | undefined, detached: boolean, envVars?: {
20
57
  [key: string]: string;
21
58
  }): Promise<string>;
22
59
  static getRuntimeExecutablePath(): string;
23
- static installLatestRuntime(): Promise<void>;
60
+ /** "v0.1.5", "0.1.5 " and "V0.1.5" name the same release */
61
+ private static normalizeVersion;
62
+ /**
63
+ * Installs the latest runtime release, or updates the installed runtime to it when it is newer.
64
+ * Returns false when the installed runtime already is that release (`force` reinstalls it anyway).
65
+ */
66
+ static installLatestRuntime(options?: {
67
+ force?: boolean;
68
+ }): Promise<boolean>;
24
69
  static startRuntime(options?: {
25
70
  port?: number;
26
71
  attach?: boolean;
@@ -35,7 +80,11 @@ export default abstract class RuntimeUtils {
35
80
  createdAt: string;
36
81
  url: string;
37
82
  }>;
38
- static startProject(projectPath: string): Promise<void>;
83
+ /**
84
+ * Build, deploy and start the project on the runtime, proposing to install/start the runtime when needed.
85
+ * Returns false when the runtime was declined or the deployment failed.
86
+ */
87
+ static startProject(projectPath: string): Promise<boolean>;
39
88
  /**
40
89
  * Redeploy the project on the runtime: the sources are packaged and uploaded, the runtime
41
90
  * restarts the service with them. The runtime is started when it is not running yet, and a
@@ -44,16 +93,28 @@ export default abstract class RuntimeUtils {
44
93
  */
45
94
  static refreshProject(projectPath: string, options?: {
46
95
  ignoreErrors?: boolean;
47
- }): Promise<boolean>;
96
+ }): Promise<IRefreshResult>;
48
97
  /**
49
98
  * The deployment state the runtime holds for a service, undefined when it does not know it
50
99
  * (never deployed, removed since, or the runtime cannot be reached).
51
100
  */
52
101
  static getProjectState(serviceName: string): Promise<ProjectState | undefined>;
53
- static testProject(projectPath: string, fileFilter?: string, nameFilter?: string, inPlace?: boolean): Promise<void>;
54
- static deployProject(serviceName: string, packagePath: string): Promise<boolean>;
55
- static removeProject(projectPath: string): Promise<void>;
56
- static uninstallProject(serviceName: string): Promise<void>;
102
+ /**
103
+ * Runs the tests of the project with a separate instance of the runtime, proposing to install it when needed.
104
+ * Returns false when the runtime was declined.
105
+ */
106
+ static testProject(projectPath: string, fileFilter?: string, nameFilter?: string, inPlace?: boolean): Promise<ITestResult | undefined>;
107
+ /**
108
+ * Runs the tests and persists their report. A run the runtime ends in error still yields its
109
+ * report when it wrote one: failing tests are told by the report, not by the exit code.
110
+ */
111
+ private static runTestsAndPersist;
112
+ static deployProject(serviceName: string, packagePath: string): Promise<IDeployResult>;
113
+ /** Uninstalls the project of the given folder from the runtime, false when nothing was removed. */
114
+ static removeProject(projectPath: string): Promise<boolean>;
115
+ /** Uninstalls the project from the runtime, false when nothing was removed. */
116
+ static uninstallProject(serviceName: string): Promise<boolean>;
117
+ /** Writes the report of a run to `destPath` (merged into the previous one for a partial run) and returns it, undefined when the runtime wrote none */
57
118
  private static persistTestReport;
58
119
  private static mergeTestReports;
59
120
  }
@@ -1,4 +1,37 @@
1
1
  "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
2
35
  var __importDefault = (this && this.__importDefault) || function (mod) {
3
36
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
37
  };
@@ -13,9 +46,10 @@ const node_child_process_1 = require("node:child_process");
13
46
  const chalk_1 = __importDefault(require("chalk"));
14
47
  const decompress_1 = __importDefault(require("decompress"));
15
48
  const FileUtils_1 = __importDefault(require("./FileUtils"));
16
- const ProjectUtils_1 = __importDefault(require("./ProjectUtils"));
49
+ const ProjectUtils_1 = __importStar(require("./ProjectUtils"));
17
50
  const GitHubAuthUtils_1 = __importDefault(require("./GitHubAuthUtils"));
18
51
  const ProjectCompilationUtils_1 = __importDefault(require("./ProjectCompilationUtils"));
52
+ const ConsoleUtils_1 = __importDefault(require("./ConsoleUtils"));
19
53
  class RuntimeUtils {
20
54
  /// The directory where the runtime is installed, we will store the executable and other files there
21
55
  /// TODO not sure about that, should we store globally, or for each "project" ? Both ? like npm
@@ -28,6 +62,8 @@ class RuntimeUtils {
28
62
  static DEFAULT_RUNTIME_HOST = "127.0.0.1";
29
63
  static DEFAULT_RUNTIME_PORT = 5005;
30
64
  static ADMIN_USER_EMAIL = "admin@example.com";
65
+ /** A deployment the runtime does not conclude within this delay is reported as failed rather than waited for ever */
66
+ static DEPLOY_TIMEOUT_MS = 5 * 60 * 1000;
31
67
  //#region Runtime
32
68
  static async readRuntimeMeta() {
33
69
  try {
@@ -97,19 +133,49 @@ class RuntimeUtils {
97
133
  //TODO some way to cache and reuse this
98
134
  return response.access_token;
99
135
  }
136
+ /**
137
+ * Makes sure the runtime is installed, proposing to install it when it is not.
138
+ * Outside a terminal the proposal is accepted, so scripts and CI keep working unattended.
139
+ * Returns false when the user declined, after a message telling how to install it.
140
+ */
100
141
  static async ensureRuntimeInstalled() {
142
+ if (await RuntimeUtils.isRuntimeInstalled()) {
143
+ return true;
144
+ }
145
+ if (!await ConsoleUtils_1.default.confirm('Codenotch runtime is not installed. Install it now?', true)) {
146
+ console.error(chalk_1.default.yellow(`Codenotch runtime is not installed, run 'cn runtime install' to install it.`));
147
+ return false;
148
+ }
149
+ //TODO Here we assume NodeJs is installed and available in PATH, we should check that and propose to install it if not
150
+ await RuntimeUtils.installLatestRuntime();
151
+ return true;
152
+ }
153
+ /**
154
+ * Makes sure the runtime is running, proposing to install it and/or start it when it is not.
155
+ * Outside a terminal the proposal is accepted, so scripts and CI keep working unattended.
156
+ * Returns false when the user declined, after a message telling how to do it by hand.
157
+ */
158
+ static async ensureRuntimeIsRunning(options) {
159
+ if (await RuntimeUtils.isRuntimeRunning()) {
160
+ return true;
161
+ }
162
+ // A missing runtime is one question, not two: install and start go together
101
163
  const isInstalled = await RuntimeUtils.isRuntimeInstalled();
164
+ const question = isInstalled
165
+ ? 'Codenotch runtime is not running. Start it now?'
166
+ : 'Codenotch runtime is not installed. Install and start it now?';
167
+ if (!await ConsoleUtils_1.default.confirm(question, true)) {
168
+ console.error(chalk_1.default.yellow(isInstalled
169
+ ? `Codenotch runtime is not running, run 'cn runtime start' to start it.`
170
+ : `Codenotch runtime is not installed, run 'cn runtime install' to install it.`));
171
+ return false;
172
+ }
102
173
  if (!isInstalled) {
103
174
  //TODO Here we assume NodeJs is installed and available in PATH, we should check that and propose to install it if not
104
175
  await RuntimeUtils.installLatestRuntime();
105
176
  }
106
- }
107
- static async ensureRuntimeIsRunning() {
108
- const isRunning = await RuntimeUtils.isRuntimeRunning();
109
- if (isRunning) {
110
- return;
111
- }
112
- await RuntimeUtils.startRuntime();
177
+ await RuntimeUtils.startRuntime(options);
178
+ return true;
113
179
  }
114
180
  static async isRuntimeInstalled() {
115
181
  const targetExe = RuntimeUtils.getRuntimeExecutablePath();
@@ -167,7 +233,7 @@ class RuntimeUtils {
167
233
  child.stdout?.on("data", (chunk) => {
168
234
  const text = chunk.toString();
169
235
  stdout += text;
170
- process.stdout.write(text);
236
+ process.stderr.write(text); // the output of the runtime is a log, stdout stays for the results of the CLI
171
237
  });
172
238
  child.stderr?.on("data", (chunk) => {
173
239
  const text = chunk.toString();
@@ -198,11 +264,15 @@ class RuntimeUtils {
198
264
  const exeName = platform === "win32" ? "codenotch.exe" : "codenotch";
199
265
  return node_path_1.default.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, exeName);
200
266
  }
201
- static async installLatestRuntime() {
202
- let wasRunning = await RuntimeUtils.isRuntimeRunning();
203
- if (wasRunning) {
204
- await RuntimeUtils.stopRuntime();
205
- }
267
+ /** "v0.1.5", "0.1.5 " and "V0.1.5" name the same release */
268
+ static normalizeVersion(version) {
269
+ return version.trim().replace(/^v/i, '');
270
+ }
271
+ /**
272
+ * Installs the latest runtime release, or updates the installed runtime to it when it is newer.
273
+ * Returns false when the installed runtime already is that release (`force` reinstalls it anyway).
274
+ */
275
+ static async installLatestRuntime(options) {
206
276
  const platform = process.platform; // win32 | linux | darwin
207
277
  const exeName = platform === "win32" ? "codenotch.exe" : "codenotch";
208
278
  let assetName;
@@ -219,7 +289,7 @@ class RuntimeUtils {
219
289
  const headers = {
220
290
  Authorization: `Bearer ${token}`,
221
291
  };
222
- console.log(chalk_1.default.blue(`Getting latest Codenotch runtime release metadata...`));
292
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Getting latest Codenotch runtime release metadata...`));
223
293
  const latestReleaseRes = await fetch("https://api.github.com/repos/EchinoHub/service-core/releases/latest", {
224
294
  headers: {
225
295
  ...headers,
@@ -237,7 +307,21 @@ class RuntimeUtils {
237
307
  throw new Error(`Asset not found: ${assetName}.`);
238
308
  }
239
309
  const version = release.tag_name;
240
- console.log(chalk_1.default.blue(`Downloading Codenotch runtime from ${asset.url}...`));
310
+ // Downloading the release already installed would only cost time and a restart
311
+ if (!options?.force && await RuntimeUtils.isRuntimeInstalled()) {
312
+ const installedVersion = await RuntimeUtils.getRuntimeVersion();
313
+ if (RuntimeUtils.normalizeVersion(installedVersion) === RuntimeUtils.normalizeVersion(version)) {
314
+ ConsoleUtils_1.default.log(chalk_1.default.green(`Codenotch runtime is already up to date (${version}).`));
315
+ return false;
316
+ }
317
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Updating Codenotch runtime ${installedVersion} to ${version}...`));
318
+ }
319
+ // The executable cannot be replaced under a running runtime
320
+ const wasRunning = await RuntimeUtils.isRuntimeRunning();
321
+ if (wasRunning) {
322
+ await RuntimeUtils.stopRuntime();
323
+ }
324
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Downloading Codenotch runtime from ${asset.url}...`));
241
325
  const res = await fetch(asset.url, {
242
326
  redirect: "follow",
243
327
  headers: {
@@ -246,7 +330,7 @@ class RuntimeUtils {
246
330
  }
247
331
  });
248
332
  if (!res.ok || !res.body) {
249
- console.log(chalk_1.default.red(`Failed to download runtime from ${asset.url}`));
333
+ console.error(chalk_1.default.red(`Failed to download runtime from ${asset.url}`));
250
334
  throw new Error(`Download failed: ${res.status} ${res.statusText}`);
251
335
  }
252
336
  const tempRoot = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codenotch-"));
@@ -270,7 +354,7 @@ class RuntimeUtils {
270
354
  version: version,
271
355
  dataPath: existingMeta?.dataPath
272
356
  });
273
- console.log(chalk_1.default.blue(`The runtime has been installed successfully.`));
357
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`The runtime has been installed successfully.`));
274
358
  }
275
359
  finally {
276
360
  FileUtils_1.default.deleteRecursively(tempRoot);
@@ -278,19 +362,23 @@ class RuntimeUtils {
278
362
  if (wasRunning) {
279
363
  await RuntimeUtils.startRuntime();
280
364
  }
365
+ return true;
281
366
  }
282
367
  static async startRuntime(options) {
283
368
  const isRunning = await RuntimeUtils.isRuntimeRunning();
284
369
  if (isRunning) {
285
370
  return;
286
371
  }
287
- await this.ensureRuntimeInstalled();
372
+ // Installing is a decision of the caller (see ensureRuntimeInstalled / ensureRuntimeIsRunning), not a side effect of starting
373
+ if (!await RuntimeUtils.isRuntimeInstalled()) {
374
+ throw new Error(`Codenotch runtime is not installed, run 'cn runtime install' to install it.`);
375
+ }
288
376
  // Start the runtime process in detached mode by default
289
377
  let detached = true;
290
378
  if (options?.attach) {
291
379
  detached = false;
292
380
  }
293
- console.log(chalk_1.default.blue(`Starting Codenotch runtime ${detached ? "in detached mode" : "in attached mode"}...`));
381
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Starting Codenotch runtime ${detached ? "in detached mode" : "in attached mode"}...`));
294
382
  const port = options?.port ?? RuntimeUtils.DEFAULT_RUNTIME_PORT;
295
383
  const dataPath = options?.dataPath;
296
384
  const version = await RuntimeUtils.getRuntimeVersion();
@@ -309,7 +397,7 @@ class RuntimeUtils {
309
397
  while (attempts < maxAttempts) {
310
398
  try {
311
399
  await RuntimeUtils.runtimeFetch("/health");
312
- console.log(chalk_1.default.green(`Runtime is ready!`));
400
+ ConsoleUtils_1.default.log(chalk_1.default.green(`Runtime is ready!`));
313
401
  return; // Runtime is responsive
314
402
  }
315
403
  catch (e) {
@@ -317,7 +405,7 @@ class RuntimeUtils {
317
405
  attempts++;
318
406
  }
319
407
  }
320
- console.log(chalk_1.default.red(`Failed to start runtime: runtime did not become responsive within expected time.`));
408
+ console.error(chalk_1.default.red(`Failed to start runtime: runtime did not become responsive within expected time.`));
321
409
  throw new Error(`Runtime did not become responsive within expected time.`);
322
410
  }
323
411
  static async stopRuntime() {
@@ -357,10 +445,10 @@ class RuntimeUtils {
357
445
  const dataDir = node_path_1.default.join(RuntimeUtils.CODENOTCH_INSTALL_DIR, RuntimeUtils.CODENOTCH_DATA_FOLDER);
358
446
  try {
359
447
  await promises_1.default.rm(dataDir, { recursive: true, force: true });
360
- console.log(chalk_1.default.green(`Runtime data cleared successfully.`));
448
+ ConsoleUtils_1.default.log(chalk_1.default.green(`Runtime data cleared successfully.`));
361
449
  }
362
450
  catch (error) {
363
- console.log(chalk_1.default.red(`Failed to clear runtime data: ${error}`));
451
+ console.error(chalk_1.default.red(`Failed to clear runtime data: ${error}`));
364
452
  throw error;
365
453
  }
366
454
  }
@@ -399,25 +487,31 @@ class RuntimeUtils {
399
487
  }
400
488
  //#endregion
401
489
  //#region Project management
490
+ /**
491
+ * Build, deploy and start the project on the runtime, proposing to install/start the runtime when needed.
492
+ * Returns false when the runtime was declined or the deployment failed.
493
+ */
402
494
  static async startProject(projectPath) {
403
495
  //TODO multiproject support
404
496
  //TODO --watch mode
405
497
  const serviceName = await ProjectUtils_1.default.getProjectServiceName(projectPath);
406
- // Ensure the runtime is running
407
- await RuntimeUtils.startRuntime();
498
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
499
+ return false;
500
+ }
408
501
  const outputPath = node_path_1.default.join(node_os_1.default.tmpdir(), `cn-build-${Date.now()}.zip`);
409
502
  try {
410
- console.log(chalk_1.default.blue(`Building project "${serviceName}"...`));
503
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Building project "${serviceName}"...`));
411
504
  await ProjectUtils_1.default.packageProject(projectPath, outputPath, true);
412
- console.log(chalk_1.default.blue(`Deploying project "${serviceName}" to runtime...`));
505
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Deploying project "${serviceName}" to runtime...`));
413
506
  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
414
- var deploySuccess = await RuntimeUtils.deployProject(serviceName, outputPath);
415
- if (deploySuccess) {
416
- console.log(chalk_1.default.green(`Project "${serviceName}" is now running.`));
507
+ const deployed = await RuntimeUtils.deployProject(serviceName, outputPath);
508
+ if (deployed.success) {
509
+ ConsoleUtils_1.default.log(chalk_1.default.green(`Project "${serviceName}" is now running.`));
417
510
  }
418
511
  else {
419
- console.log(chalk_1.default.red(`Project start failed.`));
512
+ ConsoleUtils_1.default.error(`Project start failed.`);
420
513
  }
514
+ return deployed.success;
421
515
  }
422
516
  finally {
423
517
  // Remove the build
@@ -432,39 +526,51 @@ class RuntimeUtils {
432
526
  */
433
527
  static async refreshProject(projectPath, options) {
434
528
  const serviceName = await ProjectUtils_1.default.getProjectServiceName(projectPath);
435
- // 'refresh' syncs a running runtime, a stopped one is no reason to give up: start it
529
+ const result = { success: false, serviceName, errors: [], warnings: [] };
530
+ // 'refresh' syncs a running runtime, a stopped one is no reason to give up: propose to start it
436
531
  if (!await RuntimeUtils.isRuntimeRunning()) {
437
- console.log(chalk_1.default.yellow(`Runtime is not running, starting it...`));
438
- await RuntimeUtils.startRuntime();
532
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
533
+ result.errors.push('Codenotch runtime is not running');
534
+ return result;
535
+ }
439
536
  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
440
537
  }
441
538
  // What the runtime holds right now, only to tell a first deployment from an update
442
539
  const currentState = await RuntimeUtils.getProjectState(serviceName);
540
+ result.mode = currentState === undefined ? 'deploy' : 'update';
541
+ result.previousVersion = currentState?.projectVersion;
443
542
  const outputPath = node_path_1.default.join(node_os_1.default.tmpdir(), `cn-refresh-${Date.now()}.zip`);
444
543
  try {
445
- console.log(chalk_1.default.blue(`Building project "${serviceName}"...`));
544
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Building project "${serviceName}"...`));
446
545
  try {
447
- await ProjectUtils_1.default.packageProject(projectPath, outputPath, options?.ignoreErrors ?? false);
546
+ const compilation = await ProjectUtils_1.default.packageProject(projectPath, outputPath, options?.ignoreErrors ?? false);
547
+ result.warnings.push(...compilation.warnings);
448
548
  }
449
549
  catch (error) {
450
550
  // The compilation errors are listed by the packaging, only the way out is left to say
451
- console.error(chalk_1.default.red(error instanceof Error ? error.message : String(error)));
452
- console.error(chalk_1.default.yellow(options?.ignoreErrors
551
+ const message = error instanceof Error ? error.message : String(error);
552
+ result.errors.push(...(error instanceof ProjectUtils_1.CompilationError ? error.errors : [message]));
553
+ ConsoleUtils_1.default.error(message);
554
+ ConsoleUtils_1.default.warn(options?.ignoreErrors
453
555
  ? `Nothing was deployed, the runtime still serves the version it holds.`
454
- : `Nothing was deployed. Fix the errors above, or run with '--ignore-errors' to deploy anyway.`));
455
- return false;
556
+ : `Nothing was deployed. Fix the errors above, or run with '--ignore-errors' to deploy anyway.`);
557
+ return result;
456
558
  }
457
- console.log(chalk_1.default.blue(currentState === undefined
559
+ ConsoleUtils_1.default.log(chalk_1.default.blue(currentState === undefined
458
560
  ? `Project "${serviceName}" is not on the runtime yet, deploying it...`
459
561
  : `Deploying the changes of "${serviceName}" (deployed version ${currentState.projectVersion})...`));
460
562
  const deployed = await RuntimeUtils.deployProject(serviceName, outputPath);
461
- if (deployed) {
462
- console.log(chalk_1.default.green(`Project "${serviceName}" is in sync with its sources.`));
563
+ result.success = deployed.success;
564
+ result.status = deployed.status;
565
+ result.errors.push(...deployed.errors);
566
+ result.warnings.push(...deployed.warnings);
567
+ if (deployed.success) {
568
+ ConsoleUtils_1.default.log(chalk_1.default.green(`Project "${serviceName}" is in sync with its sources.`));
463
569
  }
464
570
  else {
465
- console.log(chalk_1.default.red(`Project refresh failed.`));
571
+ ConsoleUtils_1.default.error(`Project refresh failed.`);
466
572
  }
467
- return deployed;
573
+ return result;
468
574
  }
469
575
  finally {
470
576
  // Remove the build
@@ -483,8 +589,14 @@ class RuntimeUtils {
483
589
  return undefined;
484
590
  }
485
591
  }
592
+ /**
593
+ * Runs the tests of the project with a separate instance of the runtime, proposing to install it when needed.
594
+ * Returns false when the runtime was declined.
595
+ */
486
596
  static async testProject(projectPath, fileFilter, nameFilter, inPlace = false) {
487
- await RuntimeUtils.ensureRuntimeInstalled();
597
+ if (!await RuntimeUtils.ensureRuntimeInstalled()) {
598
+ return undefined;
599
+ }
488
600
  // For testing we use a separate instance of the runtime
489
601
  // We run it in attach mode so we get the results in the console
490
602
  const isPartial = !!(fileFilter || nameFilter);
@@ -532,13 +644,12 @@ class RuntimeUtils {
532
644
  await compileFunc(projectPath);
533
645
  const tempReportPath = node_path_1.default.join(node_os_1.default.tmpdir(), `cn-report-${Date.now()}.json`);
534
646
  try {
535
- await testFunc(projectPath, tempReportPath);
536
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
647
+ const report = await RuntimeUtils.runTestsAndPersist(testFunc, projectPath, tempReportPath, finalReportPath, isPartial);
648
+ return { report, reportPath: finalReportPath };
537
649
  }
538
650
  finally {
539
651
  await promises_1.default.unlink(tempReportPath).catch(() => { });
540
652
  }
541
- return;
542
653
  }
543
654
  const tempRoot = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codenotch-"));
544
655
  try {
@@ -546,8 +657,8 @@ class RuntimeUtils {
546
657
  await FileUtils_1.default.copyFolder(projectPath, tempRoot, ['.git', 'node_modules', 'dist', 'build', 'out']);
547
658
  await compileFunc(tempRoot);
548
659
  const tempReportPath = node_path_1.default.join(tempRoot, "test-report.json");
549
- await testFunc(tempRoot, tempReportPath);
550
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
660
+ const report = await RuntimeUtils.runTestsAndPersist(testFunc, tempRoot, tempReportPath, finalReportPath, isPartial);
661
+ return { report, reportPath: finalReportPath };
551
662
  }
552
663
  finally {
553
664
  // Clean up temp folder
@@ -555,19 +666,38 @@ class RuntimeUtils {
555
666
  await FileUtils_1.default.deleteRecursively(tempRoot);
556
667
  }
557
668
  }
669
+ /**
670
+ * Runs the tests and persists their report. A run the runtime ends in error still yields its
671
+ * report when it wrote one: failing tests are told by the report, not by the exit code.
672
+ */
673
+ static async runTestsAndPersist(testFunc, workPath, tempReportPath, finalReportPath, isPartial) {
674
+ let runError;
675
+ try {
676
+ await testFunc(workPath, tempReportPath);
677
+ }
678
+ catch (error) {
679
+ runError = error;
680
+ }
681
+ const report = await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
682
+ if (report === undefined && runError !== undefined) {
683
+ throw runError;
684
+ }
685
+ return report;
686
+ }
558
687
  static async deployProject(serviceName, packagePath) {
688
+ const result = { success: false, serviceName, errors: [], warnings: [] };
559
689
  // We assume the project is already built and packaged, we just need to deploy it to the runtime
560
- // Get the information about the project package, we assume it's in the current directory and named "project.zip"
561
690
  const zipFilePath = packagePath;
562
691
  try {
563
692
  await promises_1.default.access(zipFilePath);
564
693
  }
565
694
  catch (error) {
566
- console.log(chalk_1.default.red(`Project package not found: ${zipFilePath}`));
567
695
  throw new Error(`Project package not found: ${zipFilePath}`);
568
696
  }
569
- // Ensure the runtime is running
570
- await RuntimeUtils.startRuntime();
697
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
698
+ result.errors.push('Codenotch runtime is not running');
699
+ return result;
700
+ }
571
701
  const zipBuffer = await promises_1.default.readFile(zipFilePath);
572
702
  const zipBlob = new Blob([zipBuffer], { type: "application/zip" });
573
703
  const form = new FormData();
@@ -577,33 +707,49 @@ class RuntimeUtils {
577
707
  method: "POST",
578
708
  body: form
579
709
  });
580
- // Wait until the project deployment has completed
581
- while (true) {
710
+ // Wait until the project deployment has completed, a runtime stuck in between is reported rather than waited for ever
711
+ const deadline = Date.now() + RuntimeUtils.DEPLOY_TIMEOUT_MS;
712
+ while (Date.now() < deadline) {
582
713
  await new Promise(resolve => setTimeout(resolve, 1000));
583
714
  try {
584
715
  const projectState = await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true);
716
+ result.status = projectState.status;
717
+ result.warnings = projectState.warnings ?? [];
585
718
  if (projectState.status === "READY") {
586
- console.log(chalk_1.default.green(`${serviceName} is ready!`));
587
- return true;
719
+ ConsoleUtils_1.default.log(chalk_1.default.green(`${serviceName} is ready!`));
720
+ result.success = true;
721
+ return result;
588
722
  }
589
- else if (projectState.status === "FAILURE" || projectState.status === "REMOVING") {
590
- console.log(chalk_1.default.red(`Failed to deploy ${serviceName}. Errors: ${projectState.errors?.join(", ")}`));
591
- return false;
723
+ if (projectState.status === "FAILURE" || projectState.status === "REMOVING") {
724
+ result.errors = projectState.errors ?? [];
725
+ ConsoleUtils_1.default.error(`Failed to deploy ${serviceName}. Errors: ${result.errors.join(", ")}`);
726
+ return result;
592
727
  }
593
728
  }
594
729
  catch {
595
730
  // Keep waiting
596
731
  }
597
732
  }
733
+ result.errors.push(`The runtime did not report '${serviceName}' as ready within ${RuntimeUtils.DEPLOY_TIMEOUT_MS / 1000} seconds (last status: ${result.status ?? 'unknown'}).`);
734
+ ConsoleUtils_1.default.error(result.errors[result.errors.length - 1]);
735
+ return result;
598
736
  }
737
+ /** Uninstalls the project of the given folder from the runtime, false when nothing was removed. */
599
738
  static async removeProject(projectPath) {
600
739
  const serviceName = await ProjectUtils_1.default.getProjectServiceName(projectPath);
601
- await RuntimeUtils.uninstallProject(serviceName);
740
+ return await RuntimeUtils.uninstallProject(serviceName);
602
741
  }
742
+ /** Uninstalls the project from the runtime, false when nothing was removed. */
603
743
  static async uninstallProject(serviceName) {
604
- // Cannot properly uninstall if the runtime is not installed and running
605
- // Ensure the runtime is running
606
- await RuntimeUtils.startRuntime();
744
+ // Installing a runtime only to delete a project on it makes no sense: without one there is nothing to remove
745
+ if (!await RuntimeUtils.isRuntimeInstalled()) {
746
+ console.error(chalk_1.default.yellow(`Codenotch runtime is not installed, there is no project to remove.`));
747
+ return false;
748
+ }
749
+ // The removal is done by the runtime itself, so it has to run
750
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
751
+ return false;
752
+ }
607
753
  await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true, {
608
754
  method: 'DELETE'
609
755
  });
@@ -617,9 +763,11 @@ class RuntimeUtils {
617
763
  }
618
764
  }
619
765
  */
766
+ return true;
620
767
  }
621
768
  //#endregion
622
769
  //#region Test report helpers
770
+ /** Writes the report of a run to `destPath` (merged into the previous one for a partial run) and returns it, undefined when the runtime wrote none */
623
771
  static async persistTestReport(sourcePath, destPath, isPartial) {
624
772
  let newReport;
625
773
  try {
@@ -628,11 +776,11 @@ class RuntimeUtils {
628
776
  }
629
777
  catch {
630
778
  console.warn(chalk_1.default.yellow("Could not read test report from runtime output."));
631
- return;
779
+ return undefined;
632
780
  }
633
781
  if (!isPartial) {
634
782
  await promises_1.default.writeFile(destPath, JSON.stringify(newReport, null, 2), "utf8");
635
- return;
783
+ return newReport;
636
784
  }
637
785
  // Merge partial results into any existing report
638
786
  let existingReport = null;
@@ -647,6 +795,7 @@ class RuntimeUtils {
647
795
  ? RuntimeUtils.mergeTestReports(existingReport, newReport)
648
796
  : newReport;
649
797
  await promises_1.default.writeFile(destPath, JSON.stringify(merged, null, 2), "utf8");
798
+ return merged;
650
799
  }
651
800
  static mergeTestReports(existing, partial) {
652
801
  const updatedTests = [...existing.Tests];