@codenotch/codenotch.cli 1.0.52 → 1.0.54

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 (102) 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 +49 -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/react/index.js +1 -4
  43. package/dist/commands/react/index.js.map +1 -1
  44. package/dist/commands/runtime/clear.js +5 -0
  45. package/dist/commands/runtime/clear.js.map +1 -1
  46. package/dist/commands/runtime/info.js +1 -1
  47. package/dist/commands/runtime/info.js.map +1 -1
  48. package/dist/commands/runtime/portal.js +4 -4
  49. package/dist/commands/runtime/portal.js.map +1 -1
  50. package/dist/commands/runtime/start.js +10 -1
  51. package/dist/commands/runtime/start.js.map +1 -1
  52. package/dist/commands/runtime/stop.js +5 -0
  53. package/dist/commands/runtime/stop.js.map +1 -1
  54. package/dist/commands/runtime/update.js +4 -3
  55. package/dist/commands/runtime/update.js.map +1 -1
  56. package/dist/commands/system/completion.d.ts +7 -0
  57. package/dist/commands/system/completion.js +145 -0
  58. package/dist/commands/system/completion.js.map +1 -0
  59. package/dist/commands/system/index.js +2 -0
  60. package/dist/commands/system/index.js.map +1 -1
  61. package/dist/commands/system/info.js +42 -8
  62. package/dist/commands/system/info.js.map +1 -1
  63. package/dist/commands/validation/validate.js +3 -3
  64. package/dist/commands/validation/validate.js.map +1 -1
  65. package/dist/index.js +32 -4
  66. package/dist/index.js.map +1 -1
  67. package/dist/utils/BuildUtils.d.ts +6 -5
  68. package/dist/utils/BuildUtils.js +61 -45
  69. package/dist/utils/BuildUtils.js.map +1 -1
  70. package/dist/utils/ConsoleUtils.d.ts +27 -0
  71. package/dist/utils/ConsoleUtils.js +53 -4
  72. package/dist/utils/ConsoleUtils.js.map +1 -1
  73. package/dist/utils/FileUtils.js +1 -1
  74. package/dist/utils/FileUtils.js.map +1 -1
  75. package/dist/utils/GitHubAuthUtils.js +5 -5
  76. package/dist/utils/GitHubAuthUtils.js.map +1 -1
  77. package/dist/utils/PackageUtils.js +15 -15
  78. package/dist/utils/PackageUtils.js.map +1 -1
  79. package/dist/utils/ProjectCompilationUtils.d.ts +0 -1
  80. package/dist/utils/ProjectCompilationUtils.js +22 -23
  81. package/dist/utils/ProjectCompilationUtils.js.map +1 -1
  82. package/dist/utils/ProjectTemplates.js +2 -2
  83. package/dist/utils/ProjectTemplates.js.map +1 -1
  84. package/dist/utils/ProjectUtils.d.ts +6 -0
  85. package/dist/utils/ProjectUtils.js +34 -1
  86. package/dist/utils/ProjectUtils.js.map +1 -1
  87. package/dist/utils/RuntimeUtils.d.ts +84 -9
  88. package/dist/utils/RuntimeUtils.js +263 -58
  89. package/dist/utils/RuntimeUtils.js.map +1 -1
  90. package/dist/utils/ShellUtils.js +6 -2
  91. package/dist/utils/ShellUtils.js.map +1 -1
  92. package/dist/utils/TemplateUtils.js +26 -14
  93. package/dist/utils/TemplateUtils.js.map +1 -1
  94. package/package.json +4 -3
  95. package/resources/docs/apps.md +0 -1
  96. package/resources/docs/project.md +21 -8
  97. package/dist/commands/react/serve.d.ts +0 -2
  98. package/dist/commands/react/serve.js +0 -52
  99. package/dist/commands/react/serve.js.map +0 -1
  100. package/dist/utils/ServeUtils.d.ts +0 -9
  101. package/dist/utils/ServeUtils.js +0 -172
  102. package/dist/utils/ServeUtils.js.map +0 -1
@@ -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,33 +487,116 @@ 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...`));
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
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.`));
510
+ }
511
+ else {
512
+ ConsoleUtils_1.default.error(`Project start failed.`);
513
+ }
514
+ return deployed.success;
515
+ }
516
+ finally {
517
+ // Remove the build
518
+ FileUtils_1.default.deleteRecursively(outputPath);
519
+ }
520
+ }
521
+ /**
522
+ * Redeploy the project on the runtime: the sources are packaged and uploaded, the runtime
523
+ * restarts the service with them. The runtime is started when it is not running yet, and a
524
+ * project the runtime does not know yet is deployed as a whole.
525
+ * Returns false when the deployment failed, the errors being printed by the runtime call.
526
+ */
527
+ static async refreshProject(projectPath, options) {
528
+ const serviceName = await ProjectUtils_1.default.getProjectServiceName(projectPath);
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
531
+ if (!await RuntimeUtils.isRuntimeRunning()) {
532
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
533
+ result.errors.push('Codenotch runtime is not running');
534
+ return result;
535
+ }
413
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
414
- var deploySuccess = await RuntimeUtils.deployProject(serviceName, outputPath);
415
- if (deploySuccess) {
416
- console.log(chalk_1.default.green(`Project "${serviceName}" is now running.`));
537
+ }
538
+ // What the runtime holds right now, only to tell a first deployment from an update
539
+ const currentState = await RuntimeUtils.getProjectState(serviceName);
540
+ result.mode = currentState === undefined ? 'deploy' : 'update';
541
+ result.previousVersion = currentState?.projectVersion;
542
+ const outputPath = node_path_1.default.join(node_os_1.default.tmpdir(), `cn-refresh-${Date.now()}.zip`);
543
+ try {
544
+ ConsoleUtils_1.default.log(chalk_1.default.blue(`Building project "${serviceName}"...`));
545
+ try {
546
+ const compilation = await ProjectUtils_1.default.packageProject(projectPath, outputPath, options?.ignoreErrors ?? false);
547
+ result.warnings.push(...compilation.warnings);
548
+ }
549
+ catch (error) {
550
+ // The compilation errors are listed by the packaging, only the way out is left to say
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
555
+ ? `Nothing was deployed, the runtime still serves the version it holds.`
556
+ : `Nothing was deployed. Fix the errors above, or run with '--ignore-errors' to deploy anyway.`);
557
+ return result;
558
+ }
559
+ ConsoleUtils_1.default.log(chalk_1.default.blue(currentState === undefined
560
+ ? `Project "${serviceName}" is not on the runtime yet, deploying it...`
561
+ : `Deploying the changes of "${serviceName}" (deployed version ${currentState.projectVersion})...`));
562
+ const deployed = await RuntimeUtils.deployProject(serviceName, outputPath);
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.`));
417
569
  }
418
570
  else {
419
- console.log(chalk_1.default.red(`Project start failed.`));
571
+ ConsoleUtils_1.default.error(`Project refresh failed.`);
420
572
  }
573
+ return result;
421
574
  }
422
575
  finally {
423
576
  // Remove the build
424
577
  FileUtils_1.default.deleteRecursively(outputPath);
425
578
  }
426
579
  }
580
+ /**
581
+ * The deployment state the runtime holds for a service, undefined when it does not know it
582
+ * (never deployed, removed since, or the runtime cannot be reached).
583
+ */
584
+ static async getProjectState(serviceName) {
585
+ try {
586
+ return await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true);
587
+ }
588
+ catch {
589
+ return undefined;
590
+ }
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
+ */
427
596
  static async testProject(projectPath, fileFilter, nameFilter, inPlace = false) {
428
- await RuntimeUtils.ensureRuntimeInstalled();
597
+ if (!await RuntimeUtils.ensureRuntimeInstalled()) {
598
+ return undefined;
599
+ }
429
600
  // For testing we use a separate instance of the runtime
430
601
  // We run it in attach mode so we get the results in the console
431
602
  const isPartial = !!(fileFilter || nameFilter);
@@ -473,13 +644,12 @@ class RuntimeUtils {
473
644
  await compileFunc(projectPath);
474
645
  const tempReportPath = node_path_1.default.join(node_os_1.default.tmpdir(), `cn-report-${Date.now()}.json`);
475
646
  try {
476
- await testFunc(projectPath, tempReportPath);
477
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
647
+ const report = await RuntimeUtils.runTestsAndPersist(testFunc, projectPath, tempReportPath, finalReportPath, isPartial);
648
+ return { report, reportPath: finalReportPath };
478
649
  }
479
650
  finally {
480
651
  await promises_1.default.unlink(tempReportPath).catch(() => { });
481
652
  }
482
- return;
483
653
  }
484
654
  const tempRoot = await promises_1.default.mkdtemp(node_path_1.default.join(node_os_1.default.tmpdir(), "codenotch-"));
485
655
  try {
@@ -487,8 +657,8 @@ class RuntimeUtils {
487
657
  await FileUtils_1.default.copyFolder(projectPath, tempRoot, ['.git', 'node_modules', 'dist', 'build', 'out']);
488
658
  await compileFunc(tempRoot);
489
659
  const tempReportPath = node_path_1.default.join(tempRoot, "test-report.json");
490
- await testFunc(tempRoot, tempReportPath);
491
- await RuntimeUtils.persistTestReport(tempReportPath, finalReportPath, isPartial);
660
+ const report = await RuntimeUtils.runTestsAndPersist(testFunc, tempRoot, tempReportPath, finalReportPath, isPartial);
661
+ return { report, reportPath: finalReportPath };
492
662
  }
493
663
  finally {
494
664
  // Clean up temp folder
@@ -496,19 +666,38 @@ class RuntimeUtils {
496
666
  await FileUtils_1.default.deleteRecursively(tempRoot);
497
667
  }
498
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
+ }
499
687
  static async deployProject(serviceName, packagePath) {
688
+ const result = { success: false, serviceName, errors: [], warnings: [] };
500
689
  // We assume the project is already built and packaged, we just need to deploy it to the runtime
501
- // Get the information about the project package, we assume it's in the current directory and named "project.zip"
502
690
  const zipFilePath = packagePath;
503
691
  try {
504
692
  await promises_1.default.access(zipFilePath);
505
693
  }
506
694
  catch (error) {
507
- console.log(chalk_1.default.red(`Project package not found: ${zipFilePath}`));
508
695
  throw new Error(`Project package not found: ${zipFilePath}`);
509
696
  }
510
- // Ensure the runtime is running
511
- await RuntimeUtils.startRuntime();
697
+ if (!await RuntimeUtils.ensureRuntimeIsRunning()) {
698
+ result.errors.push('Codenotch runtime is not running');
699
+ return result;
700
+ }
512
701
  const zipBuffer = await promises_1.default.readFile(zipFilePath);
513
702
  const zipBlob = new Blob([zipBuffer], { type: "application/zip" });
514
703
  const form = new FormData();
@@ -518,36 +707,49 @@ class RuntimeUtils {
518
707
  method: "POST",
519
708
  body: form
520
709
  });
521
- // Wait until the project deployment has completed
522
- 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) {
523
713
  await new Promise(resolve => setTimeout(resolve, 1000));
524
714
  try {
525
715
  const projectState = await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true);
716
+ result.status = projectState.status;
717
+ result.warnings = projectState.warnings ?? [];
526
718
  if (projectState.status === "READY") {
527
- console.log(chalk_1.default.green(`${serviceName} is ready!`));
528
- return true;
719
+ ConsoleUtils_1.default.log(chalk_1.default.green(`${serviceName} is ready!`));
720
+ result.success = true;
721
+ return result;
529
722
  }
530
- else if (projectState.status === "FAILURE" || projectState.status === "REMOVING") {
531
- console.log(chalk_1.default.red(`Failed to deploy ${serviceName}. Errors: ${projectState.errors?.join(", ")}`));
532
- 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;
533
727
  }
534
728
  }
535
729
  catch {
536
730
  // Keep waiting
537
731
  }
538
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;
539
736
  }
540
- static async patchProject(projectPath) {
541
- throw new Error('Method not implemented.');
542
- }
737
+ /** Uninstalls the project of the given folder from the runtime, false when nothing was removed. */
543
738
  static async removeProject(projectPath) {
544
739
  const serviceName = await ProjectUtils_1.default.getProjectServiceName(projectPath);
545
- await RuntimeUtils.uninstallProject(serviceName);
740
+ return await RuntimeUtils.uninstallProject(serviceName);
546
741
  }
742
+ /** Uninstalls the project from the runtime, false when nothing was removed. */
547
743
  static async uninstallProject(serviceName) {
548
- // Cannot properly uninstall if the runtime is not installed and running
549
- // Ensure the runtime is running
550
- 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
+ }
551
753
  await RuntimeUtils.runtimeFetch(`/${serviceName}/project`, true, {
552
754
  method: 'DELETE'
553
755
  });
@@ -561,9 +763,11 @@ class RuntimeUtils {
561
763
  }
562
764
  }
563
765
  */
766
+ return true;
564
767
  }
565
768
  //#endregion
566
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 */
567
771
  static async persistTestReport(sourcePath, destPath, isPartial) {
568
772
  let newReport;
569
773
  try {
@@ -572,11 +776,11 @@ class RuntimeUtils {
572
776
  }
573
777
  catch {
574
778
  console.warn(chalk_1.default.yellow("Could not read test report from runtime output."));
575
- return;
779
+ return undefined;
576
780
  }
577
781
  if (!isPartial) {
578
782
  await promises_1.default.writeFile(destPath, JSON.stringify(newReport, null, 2), "utf8");
579
- return;
783
+ return newReport;
580
784
  }
581
785
  // Merge partial results into any existing report
582
786
  let existingReport = null;
@@ -591,6 +795,7 @@ class RuntimeUtils {
591
795
  ? RuntimeUtils.mergeTestReports(existingReport, newReport)
592
796
  : newReport;
593
797
  await promises_1.default.writeFile(destPath, JSON.stringify(merged, null, 2), "utf8");
798
+ return merged;
594
799
  }
595
800
  static mergeTestReports(existing, partial) {
596
801
  const updatedTests = [...existing.Tests];