@codenotch/codenotch.cli 1.0.33 → 1.0.34
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.
- package/dist/commands/index.js +2 -0
- package/dist/commands/index.js.map +1 -1
- package/dist/commands/validation/ValidationUtils.d.ts +28 -0
- package/dist/commands/validation/ValidationUtils.js +182 -0
- package/dist/commands/validation/ValidationUtils.js.map +1 -0
- package/dist/commands/validation/files/AppManifestValidator.d.ts +6 -0
- package/dist/commands/validation/files/AppManifestValidator.js +93 -0
- package/dist/commands/validation/files/AppManifestValidator.js.map +1 -0
- package/dist/commands/validation/files/BpmnValidator.d.ts +5 -0
- package/dist/commands/validation/files/BpmnValidator.js +32 -0
- package/dist/commands/validation/files/BpmnValidator.js.map +1 -0
- package/dist/commands/validation/files/DbSchemaValidator.d.ts +5 -0
- package/dist/commands/validation/files/DbSchemaValidator.js +16 -0
- package/dist/commands/validation/files/DbSchemaValidator.js.map +1 -0
- package/dist/commands/validation/files/DocumentValidator.d.ts +5 -0
- package/dist/commands/validation/files/DocumentValidator.js +12 -0
- package/dist/commands/validation/files/DocumentValidator.js.map +1 -0
- package/dist/commands/validation/files/I18nValidator.d.ts +5 -0
- package/dist/commands/validation/files/I18nValidator.js +13 -0
- package/dist/commands/validation/files/I18nValidator.js.map +1 -0
- package/dist/commands/validation/files/JsonValidator.d.ts +5 -0
- package/dist/commands/validation/files/JsonValidator.js +13 -0
- package/dist/commands/validation/files/JsonValidator.js.map +1 -0
- package/dist/commands/validation/files/ManifestCacheValidator.d.ts +5 -0
- package/dist/commands/validation/files/ManifestCacheValidator.js +121 -0
- package/dist/commands/validation/files/ManifestCacheValidator.js.map +1 -0
- package/dist/commands/validation/files/ManifestValidator.d.ts +5 -0
- package/dist/commands/validation/files/ManifestValidator.js +14 -0
- package/dist/commands/validation/files/ManifestValidator.js.map +1 -0
- package/dist/commands/validation/files/ScriptValidator.d.ts +12 -0
- package/dist/commands/validation/files/ScriptValidator.js +128 -0
- package/dist/commands/validation/files/ScriptValidator.js.map +1 -0
- package/dist/commands/validation/files/TableValidator.d.ts +5 -0
- package/dist/commands/validation/files/TableValidator.js +19 -0
- package/dist/commands/validation/files/TableValidator.js.map +1 -0
- package/dist/commands/validation/validate.d.ts +2 -0
- package/dist/commands/validation/validate.js +245 -0
- package/dist/commands/validation/validate.js.map +1 -0
- package/dist/utils/BuildUtils.d.ts +22 -4
- package/dist/utils/BuildUtils.js +45 -16
- package/dist/utils/BuildUtils.js.map +1 -1
- package/dist/utils/FileUtils.d.ts +13 -0
- package/dist/utils/FileUtils.js +30 -0
- package/dist/utils/FileUtils.js.map +1 -1
- package/dist/utils/I18nUtils.js +0 -2
- package/dist/utils/I18nUtils.js.map +1 -1
- package/package.json +14 -6
- package/.github/copilot-instructions.md +0 -3
- package/bin/cli.js +0 -3
- package/docs/ARCHITECTURE.md +0 -179
- package/docs/TODO.md +0 -18
- package/src/bin/cli.ts +0 -4
- package/src/commands/index.ts +0 -13
- package/src/commands/project/compile.ts +0 -20
- package/src/commands/project/deploy.ts +0 -11
- package/src/commands/project/index.ts +0 -28
- package/src/commands/project/inspect.ts +0 -12
- package/src/commands/project/new.ts +0 -11
- package/src/commands/project/open.ts +0 -21
- package/src/commands/project/package.ts +0 -20
- package/src/commands/project/refresh.ts +0 -13
- package/src/commands/project/remove.ts +0 -12
- package/src/commands/project/shared.ts +0 -9
- package/src/commands/project/start.ts +0 -24
- package/src/commands/project/test.ts +0 -15
- package/src/commands/react/build.ts +0 -12
- package/src/commands/react/index.ts +0 -12
- package/src/commands/react/serve.ts +0 -13
- package/src/commands/runtime/clear.ts +0 -11
- package/src/commands/runtime/index.ts +0 -24
- package/src/commands/runtime/info.ts +0 -47
- package/src/commands/runtime/latest.ts +0 -18
- package/src/commands/runtime/portal.ts +0 -20
- package/src/commands/runtime/start.ts +0 -16
- package/src/commands/runtime/stop.ts +0 -13
- package/src/commands/runtime/update.ts +0 -12
- package/src/commands/system/index.ts +0 -9
- package/src/commands/system/info.ts +0 -23
- package/src/commands/system/read.ts +0 -25
- package/src/index.ts +0 -22
- package/src/models/Codenotch.ts +0 -361
- package/src/models/DbModels.ts +0 -428
- package/src/models/OpenApiModels.ts +0 -145
- package/src/utils/BuildUtils.ts +0 -739
- package/src/utils/ConsoleUtils.ts +0 -22
- package/src/utils/FileUtils.ts +0 -266
- package/src/utils/GitHubAuthUtils.ts +0 -83
- package/src/utils/I18nUtils.ts +0 -58
- package/src/utils/OpenAPIUtils.ts +0 -285
- package/src/utils/OsUtils.ts +0 -32
- package/src/utils/ProjectCompilationUtils.ts +0 -1002
- package/src/utils/ProjectUtils.ts +0 -151
- package/src/utils/RuntimeUtils.ts +0 -743
- package/src/utils/ServeUtils.ts +0 -175
- package/src/utils/ShellUtils.ts +0 -26
- package/src/utils/StringUtils.ts +0 -80
- 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
|
-
}
|