@alcedocore/cli 0.0.1-rc.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/LICENSE.md +102 -0
  2. package/dist/commands/add-endpoint.js +104 -0
  3. package/dist/commands/add-migration.js +80 -0
  4. package/dist/commands/add-nav-item.js +72 -0
  5. package/dist/commands/add-page.js +64 -0
  6. package/dist/commands/build-frontend.js +126 -0
  7. package/dist/commands/compile-pages.js +120 -0
  8. package/dist/commands/connect.js +122 -0
  9. package/dist/commands/deploy.js +74 -0
  10. package/dist/commands/dev.js +13 -0
  11. package/dist/commands/init-core.js +900 -0
  12. package/dist/commands/init.js +124 -0
  13. package/dist/commands/init.test.js +84 -0
  14. package/dist/commands/migrate.js +22 -0
  15. package/dist/commands/proxy.js +157 -0
  16. package/dist/commands/publish.js +81 -0
  17. package/dist/commands/replay.js +142 -0
  18. package/dist/commands/serve-frontend.js +92 -0
  19. package/dist/config.js +110 -0
  20. package/dist/config.test.js +48 -0
  21. package/dist/index.js +81 -0
  22. package/dist/integration/cli-api.test.js +116 -0
  23. package/dist/utils/ejs-renderer.js +46 -0
  24. package/dist/utils/ejs-renderer.test.js +93 -0
  25. package/dist/utils/formatting.js +65 -0
  26. package/dist/utils/generateTimestamp.js +17 -0
  27. package/dist/utils/logger.js +33 -0
  28. package/dist/utils/validation.js +15 -0
  29. package/package.json +41 -0
  30. package/src/commands/add-endpoint.ts +157 -0
  31. package/src/commands/add-migration.ts +102 -0
  32. package/src/commands/add-nav-item.ts +106 -0
  33. package/src/commands/add-page.ts +100 -0
  34. package/src/commands/build-frontend.ts +148 -0
  35. package/src/commands/connect.ts +98 -0
  36. package/src/commands/deploy.ts +85 -0
  37. package/src/commands/dev.ts +12 -0
  38. package/src/commands/init-core.ts +1019 -0
  39. package/src/commands/init.test.ts +92 -0
  40. package/src/commands/init.ts +171 -0
  41. package/src/commands/migrate.ts +20 -0
  42. package/src/commands/proxy.ts +206 -0
  43. package/src/commands/publish.ts +106 -0
  44. package/src/commands/serve-frontend.ts +103 -0
  45. package/src/config.test.ts +50 -0
  46. package/src/config.ts +125 -0
  47. package/src/index.ts +100 -0
  48. package/src/integration/cli-api.test.ts +143 -0
  49. package/src/utils/ejs-renderer.ts +55 -0
  50. package/src/utils/formatting.ts +62 -0
  51. package/src/utils/generateTimestamp.ts +16 -0
  52. package/src/utils/logger.ts +27 -0
  53. package/src/utils/validation.ts +13 -0
  54. package/templates/endpoint/handler.js.ejs +23 -0
  55. package/templates/endpoint/handler.py.ejs +23 -0
  56. package/templates/migration/down.sql.ejs +6 -0
  57. package/templates/migration/up.sql.ejs +11 -0
  58. package/templates/page/page.vue.ejs +63 -0
  59. package/templates/plugin/Dockerfile.ejs +13 -0
  60. package/templates/plugin/Dockerfile.node.ejs +14 -0
  61. package/templates/plugin/README.md.ejs +19 -0
  62. package/templates/plugin/gitignore.ejs +6 -0
  63. package/templates/plugin/manifest.json.ejs +18 -0
  64. package/templates/plugin/migrations/.gitkeep +0 -0
  65. package/templates/plugin/pages/.gitkeep +0 -0
  66. package/templates/plugin/public/.gitkeep +0 -0
  67. package/templates/plugin/server.js.ejs +27 -0
  68. package/templates/plugin/server.py.ejs +32 -0
  69. package/tsconfig.json +16 -0
  70. package/vitest.config.ts +14 -0
@@ -0,0 +1,92 @@
1
+ import { describe, it, expect, beforeAll, afterAll } from "vitest";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+
6
+ describe("Init command", () => {
7
+ const testDir = path.join(os.tmpdir(), `alcedo-test-init-${Date.now()}`);
8
+
9
+ beforeAll(() => {
10
+ fs.mkdirSync(testDir, { recursive: true });
11
+ });
12
+
13
+ afterAll(() => {
14
+ fs.rmSync(testDir, { recursive: true, force: true });
15
+ });
16
+
17
+ it("validates slug format correctly", () => {
18
+ // Test the validation logic used by init command
19
+ const validSlugs = ["my-plugin", "hello-world", "test123"];
20
+ const invalidSlugs = ["My Plugin", "my_plugin", "", "UPPERCASE"];
21
+
22
+ // The validation function requires lowercase alphanumeric + hyphens
23
+ const isValid = (slug: string): boolean =>
24
+ slug.length > 0 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);
25
+
26
+ for (const slug of validSlugs) {
27
+ expect(isValid(slug)).toBe(true);
28
+ }
29
+ for (const slug of invalidSlugs) {
30
+ expect(isValid(slug)).toBe(false);
31
+ }
32
+ });
33
+
34
+ it("generates valid plugin directory structure", () => {
35
+ // Verify the expected directory structure matches what init would create
36
+ const expectedDirs = ["migrations", "pages"];
37
+ for (const dir of expectedDirs) {
38
+ const dirPath = path.join(testDir, dir);
39
+ fs.mkdirSync(dirPath, { recursive: true });
40
+ expect(fs.existsSync(dirPath)).toBe(true);
41
+ }
42
+
43
+ // Verify key files that init should generate
44
+ const expectedFiles = ["manifest.json", "Dockerfile", "server.py"];
45
+ for (const file of expectedFiles) {
46
+ const filePath = path.join(testDir, file);
47
+ // Only test structure, not content — init command generates these via templates
48
+ if (file === "manifest.json") {
49
+ fs.writeFileSync(filePath, JSON.stringify({
50
+ slug: "test-plugin",
51
+ version: "1.0.0",
52
+ plugin_type: "dynamic",
53
+ }));
54
+ }
55
+ if (file === "Dockerfile") {
56
+ fs.writeFileSync(filePath, "FROM python:3.11-slim\n");
57
+ }
58
+ if (file === "server.py") {
59
+ fs.writeFileSync(filePath, 'print("hello")\n');
60
+ }
61
+ expect(fs.existsSync(filePath)).toBe(true);
62
+ }
63
+ });
64
+
65
+ it("generates versioned migration filenames correctly", () => {
66
+ // Migration filenames follow pattern: YYYYMMDD_<name>.up.sql
67
+ const date = "20260527";
68
+ const name = "create_users_table";
69
+ const upFilename = `${date}_${name}.up.sql`;
70
+ const downFilename = `${date}_${name}.down.sql`;
71
+
72
+ expect(upFilename).toMatch(/^\d{8}_.+\.up\.sql$/);
73
+ expect(downFilename).toMatch(/^\d{8}_.+\.down\.sql$/);
74
+ });
75
+
76
+ it("generates endpoint handler with correct structure", () => {
77
+ // Endpoint template generates: slug-safe endpoint path + handler stub
78
+ const endpointName = "user-data";
79
+ const expectedMethod = "GET";
80
+
81
+ // Simulate the manifest endpoint entry format
82
+ const endpointEntry = {
83
+ method: expectedMethod,
84
+ path: `/${endpointName}`,
85
+ handler: `${endpointName}_handler`,
86
+ };
87
+
88
+ expect(endpointEntry.path).toBe(`/${endpointName}`);
89
+ expect(endpointEntry.method).toBe(expectedMethod);
90
+ expect(endpointEntry.handler).toBeTruthy();
91
+ });
92
+ });
@@ -0,0 +1,171 @@
1
+ import { Command } from "commander";
2
+ import path from "node:path";
3
+ import fs from "node:fs";
4
+ import {
5
+ createSpinner,
6
+ success,
7
+ error as logError,
8
+ info,
9
+ } from "../utils/logger";
10
+ import { renderAndWrite } from "../utils/ejs-renderer";
11
+ import { loadConfig } from "../config";
12
+ import readline from "node:readline";
13
+
14
+ export const initCommand = new Command("init")
15
+ .argument("<name>", "Plugin project name (e.g., my-plugin)")
16
+ .option("-l, --language <language>", "Plugin language (python or node)")
17
+ .description("Scaffold a new plugin project")
18
+ .action(
19
+ async (name: string, options: { language?: string }, cmd: Command) => {
20
+ const config = loadConfig(cmd.optsWithGlobals() as any);
21
+ const pluginDir = config.pluginDir || process.cwd();
22
+ const targetDir = path.resolve(pluginDir, name);
23
+ const slug = slugify(name);
24
+
25
+ if (fs.existsSync(targetDir)) {
26
+ logError(`Directory already exists: ${targetDir}`);
27
+ process.exit(1);
28
+ }
29
+
30
+ const language = options.language
31
+ ? parseLanguage(options.language)
32
+ : await promptLanguage();
33
+
34
+ const templatesDir = path.resolve(__dirname, "../../templates");
35
+
36
+ const registryUrl = config.registryUrl || "localhost:5000";
37
+ const data = {
38
+ name,
39
+ slug,
40
+ version: "1.0.0",
41
+ description: `A new AlcedoCore plugin`,
42
+ language,
43
+ registryUrl,
44
+ };
45
+
46
+ const spinner = createSpinner(`Scaffolding plugin: ${name}`);
47
+
48
+ try {
49
+ // Create target directory
50
+ fs.mkdirSync(targetDir, { recursive: true });
51
+
52
+ // Generate files from templates
53
+ // manifest.json
54
+ renderAndWrite(
55
+ path.join(templatesDir, "plugin", "manifest.json.ejs"),
56
+ path.join(targetDir, "manifest.json"),
57
+ data,
58
+ );
59
+
60
+ // Dockerfile (language-specific)
61
+ const dockerTemplate =
62
+ language === "python"
63
+ ? "Dockerfile.ejs"
64
+ : "Dockerfile.node.ejs";
65
+ renderAndWrite(
66
+ path.join(templatesDir, "plugin", dockerTemplate),
67
+ path.join(targetDir, "Dockerfile"),
68
+ data,
69
+ );
70
+
71
+ // Server stub (language-specific)
72
+ const serverTemplate =
73
+ language === "python" ? "server.py.ejs" : "server.js.ejs";
74
+ renderAndWrite(
75
+ path.join(templatesDir, "plugin", serverTemplate),
76
+ path.join(
77
+ targetDir,
78
+ `server.${language === "python" ? "py" : "js"}`,
79
+ ),
80
+ data,
81
+ );
82
+
83
+ // .gitignore
84
+ renderAndWrite(
85
+ path.join(templatesDir, "plugin", "gitignore.ejs"),
86
+ path.join(targetDir, ".gitignore"),
87
+ data,
88
+ );
89
+
90
+ // README.md
91
+ renderAndWrite(
92
+ path.join(templatesDir, "plugin", "README.md.ejs"),
93
+ path.join(targetDir, "README.md"),
94
+ data,
95
+ );
96
+
97
+ copyGitkeep(templatesDir, targetDir, "migrations");
98
+ copyGitkeep(templatesDir, targetDir, "pages");
99
+ copyGitkeep(templatesDir, targetDir, "public");
100
+
101
+ spinner.succeed();
102
+
103
+ success(`Plugin scaffolded: ${targetDir}`);
104
+ info(`Next steps:
105
+ cd ${name}
106
+ # Edit server.${language === "python" ? "py" : "js"} and manifest.json
107
+ # Build with: docker build -t ${registryUrl}/${slug}:1.0.0 .`);
108
+ } catch (err: any) {
109
+ spinner.fail();
110
+ logError(`Failed to scaffold plugin: ${err.message}`);
111
+ process.exit(1);
112
+ }
113
+ },
114
+ );
115
+
116
+ function promptLanguage(): Promise<"python" | "node"> {
117
+ const rl = readline.createInterface({
118
+ input: process.stdin,
119
+ output: process.stdout,
120
+ });
121
+
122
+ return new Promise((resolve) => {
123
+ rl.question("Select plugin language (python/node): ", (answer) => {
124
+ rl.close();
125
+ const lang = answer.trim().toLowerCase();
126
+ if (lang === "python" || lang === "py") {
127
+ resolve("python");
128
+ } else if (
129
+ lang === "node" ||
130
+ lang === "nodejs" ||
131
+ lang === "javascript" ||
132
+ lang === "js"
133
+ ) {
134
+ resolve("node");
135
+ } else {
136
+ // Default to python on invalid input, matching hello-world
137
+ info(`Unknown language "${lang}", defaulting to Python`);
138
+ resolve("python");
139
+ }
140
+ });
141
+ });
142
+ }
143
+
144
+ function slugify(name: string): string {
145
+ return name
146
+ .toLowerCase()
147
+ .replace(/[^a-z0-9-]/g, "-")
148
+ .replace(/-+/g, "-")
149
+ .replace(/^-|-$/g, "");
150
+ }
151
+
152
+ function copyGitkeep(
153
+ templateDir: string,
154
+ targetDir: string,
155
+ subDir: string,
156
+ ): void {
157
+ const src = path.join(templateDir, "plugin", subDir, ".gitkeep");
158
+ const dest = path.join(targetDir, subDir, ".gitkeep");
159
+ if (fs.existsSync(src)) {
160
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
161
+ fs.writeFileSync(dest, "");
162
+ }
163
+ }
164
+
165
+ function parseLanguage(lang: string): "python" | "node" {
166
+ const v = lang.trim().toLowerCase();
167
+ if (v === "python" || v === "py") return "python";
168
+ if (v === "node" || v === "nodejs" || v === "javascript" || v === "js")
169
+ return "node";
170
+ throw new Error(`Unknown language "${lang}". Use "python" or "node".`);
171
+ }
@@ -0,0 +1,20 @@
1
+ import { Command } from "commander";
2
+ import { error as logError } from "../utils/logger";
3
+ import { addMigrationAction } from "./add-migration";
4
+
5
+ const generateCommand = new Command("generate")
6
+ .argument("<name>", "Migration name (e.g., create_users_table)")
7
+ .description("Alias for `alcedo add migration` — generate a versioned SQL migration pair")
8
+ .action(async (name: string) => {
9
+ try {
10
+ await addMigrationAction(name);
11
+ } catch (err: any) {
12
+ logError(`Generate failed: ${err.message}`);
13
+ process.exit(1);
14
+ }
15
+ });
16
+
17
+ export const migrateCommand = new Command("migrate")
18
+ .description("Alias for `alcedo add migration`");
19
+
20
+ migrateCommand.addCommand(generateCommand);
@@ -0,0 +1,206 @@
1
+ import { Command } from "commander";
2
+ import http from "node:http";
3
+ import path from "node:path";
4
+ import { loadConfig } from "../config";
5
+ import {
6
+ success,
7
+ error as logError,
8
+ info,
9
+ warn,
10
+ createSpinner,
11
+ } from "../utils/logger";
12
+
13
+ interface ProxyOptions {
14
+ port?: string;
15
+ target?: string;
16
+ apiKey?: string;
17
+ }
18
+
19
+ export const proxyCommand = new Command("proxy")
20
+ .description(
21
+ "Start a dev proxy that forwards requests to a local dev server",
22
+ )
23
+ .option("-p, --port <port>", "Proxy listen port", "3099")
24
+ .option(
25
+ "-t, --target <url>",
26
+ "Target dev server URL (e.g., localhost:8080)",
27
+ "localhost:3000",
28
+ )
29
+ .option("-k, --api-key <key>", "Core API key (or ALCEDO_API_KEY env var)")
30
+ .action(async (opts: ProxyOptions, cmd: Command) => {
31
+ const config = loadConfig(cmd.optsWithGlobals() as any);
32
+ const coreUrl = config.coreUrl || "http://localhost:8080";
33
+ const pluginDir = path.resolve(config.pluginDir || process.cwd());
34
+ const slug = path.basename(pluginDir);
35
+ const apiKey =
36
+ opts.apiKey || config.apiKey || process.env.ALCEDO_API_KEY || "";
37
+ const proxyPort = parseInt(opts.port || "3099", 10);
38
+ const target = opts.target || "localhost:3000";
39
+
40
+ if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
41
+ logError(`Invalid proxy port: ${opts.port}`);
42
+ process.exit(1);
43
+ }
44
+
45
+ const parsedTarget = parseTarget(target);
46
+
47
+ const validateSpinner = createSpinner("Validating API key...");
48
+ const testId = await registerRequest(
49
+ coreUrl,
50
+ apiKey || undefined,
51
+ slug,
52
+ );
53
+ if (!testId) {
54
+ validateSpinner.fail();
55
+ logError(`Failed to connect to core at ${coreUrl}`);
56
+ logError(
57
+ "Make sure the AlcedoCore instance is running and API key is correct",
58
+ );
59
+ process.exit(1);
60
+ }
61
+ validateSpinner.succeed();
62
+
63
+ const server = http.createServer(async (clientReq, clientRes) => {
64
+ const startTime = Date.now();
65
+
66
+ const requestId = await registerRequest(
67
+ coreUrl,
68
+ apiKey || undefined,
69
+ slug,
70
+ );
71
+ if (!requestId) {
72
+ logError("Failed to register request ID with core");
73
+ clientRes.statusCode = 502;
74
+ clientRes.setHeader("Content-Type", "text/plain");
75
+ clientRes.end("Bad Gateway: core unavailable");
76
+ return;
77
+ }
78
+
79
+ // Clone headers and inject X-Request-ID
80
+ const headers: Record<string, string> = {};
81
+ for (const [key, value] of Object.entries(clientReq.headers)) {
82
+ if (value !== undefined) {
83
+ headers[key] = Array.isArray(value)
84
+ ? value.join(", ")
85
+ : value;
86
+ }
87
+ }
88
+ headers["X-Request-ID"] = requestId;
89
+ headers["host"] = `${parsedTarget.hostname}:${parsedTarget.port}`;
90
+
91
+ const options: http.RequestOptions = {
92
+ hostname: parsedTarget.hostname,
93
+ port: parsedTarget.port,
94
+ path: clientReq.url,
95
+ method: clientReq.method,
96
+ headers,
97
+ };
98
+
99
+ const proxyReq = http.request(options, (proxyRes) => {
100
+ const chunks: Buffer[] = [];
101
+ proxyRes.on("data", (chunk: Buffer) => chunks.push(chunk));
102
+ proxyRes.on("end", () => {
103
+ const duration = Date.now() - startTime;
104
+ const statusCode = proxyRes.statusCode || 0;
105
+
106
+ info(
107
+ ` ${statusCode} ${clientReq.method} ${duration}ms ${clientReq.url}`,
108
+ );
109
+
110
+ const responseHeaders = { ...proxyRes.headers };
111
+ clientRes.writeHead(statusCode, responseHeaders);
112
+ clientRes.end(Buffer.concat(chunks));
113
+ });
114
+ });
115
+
116
+ proxyReq.on("error", (err) => {
117
+ logError(`Proxy request error: ${err.message}`);
118
+ if (!clientRes.headersSent) {
119
+ clientRes.statusCode = 502;
120
+ clientRes.setHeader("Content-Type", "text/plain");
121
+ clientRes.end(`Bad Gateway: ${err.message}`);
122
+ }
123
+ });
124
+
125
+ clientReq.pipe(proxyReq);
126
+ });
127
+
128
+ let shuttingDown = false;
129
+
130
+ function handleShutdown() {
131
+ if (shuttingDown) return;
132
+ shuttingDown = true;
133
+ console.log("");
134
+ info("Shutting down proxy...");
135
+ server.close(() => {
136
+ success("Proxy stopped");
137
+ process.exit(0);
138
+ });
139
+ setTimeout(() => {
140
+ warn("Proxy did not close gracefully, forcing exit");
141
+ process.exit(0);
142
+ }, 3000);
143
+ }
144
+
145
+ process.on("SIGINT", handleShutdown);
146
+ process.on("SIGTERM", handleShutdown);
147
+ process.on("SIGHUP", handleShutdown);
148
+
149
+ server.listen(proxyPort, () => {
150
+ success(`Dev proxy listening on http://localhost:${proxyPort}`);
151
+ info(`Forwarding to http://${target}`);
152
+ info(`Plugin slug: ${slug}`);
153
+ info(`Core URL: ${coreUrl}`);
154
+ if (apiKey) {
155
+ info("API key authentication enabled");
156
+ }
157
+ info("Press Ctrl+C to stop");
158
+ });
159
+ });
160
+
161
+ async function coreFetch(
162
+ coreUrl: string,
163
+ apiKey: string | undefined,
164
+ endpoint: string,
165
+ body: Record<string, unknown>,
166
+ ): Promise<Response | null> {
167
+ try {
168
+ const headers: Record<string, string> = {
169
+ "Content-Type": "application/json",
170
+ };
171
+ if (apiKey) {
172
+ headers["Authorization"] = `Bearer ${apiKey}`;
173
+ }
174
+ return await fetch(`${coreUrl.replace(/\/$/, "")}${endpoint}`, {
175
+ method: "POST",
176
+ headers,
177
+ body: JSON.stringify(body),
178
+ });
179
+ } catch {
180
+ return null;
181
+ }
182
+ }
183
+
184
+ async function registerRequest(
185
+ coreUrl: string,
186
+ apiKey: string | undefined,
187
+ slug: string,
188
+ ): Promise<string | null> {
189
+ const res = await coreFetch(coreUrl, apiKey, "/api/dev/request-id", {
190
+ slug,
191
+ });
192
+ if (!res || !res.ok) return null;
193
+ try {
194
+ const data = (await res.json()) as { request_id: string };
195
+ return data.request_id;
196
+ } catch {
197
+ return null;
198
+ }
199
+ }
200
+
201
+ function parseTarget(target: string): { hostname: string; port: number } {
202
+ let cleaned = target.replace(/^https?:\/\//, "");
203
+ const [hostname, portStr] = cleaned.split(":");
204
+ const port = portStr ? parseInt(portStr, 10) : 3000;
205
+ return { hostname, port };
206
+ }
@@ -0,0 +1,106 @@
1
+ import { Command } from "commander";
2
+ import { createSpinner, success, error as logError } from "../utils/logger";
3
+ import path from "path";
4
+ import { loadConfig } from "../config";
5
+ import { existsSync, readFileSync } from "fs";
6
+ import { execSync } from "child_process";
7
+
8
+ export const publishCommand = new Command("publish")
9
+ .description("Build and push a plugin to an image registry")
10
+ .action(async (slug: string, options: any, cmd: Command) => {
11
+ const spinner = createSpinner(`Preparing build...`);
12
+ const config = loadConfig();
13
+
14
+ const pluginDir = config.pluginDir || process.cwd();
15
+ const manifestPath = path.resolve(pluginDir, "manifest.json");
16
+ const dockerFilePath = path.resolve(pluginDir, "Dockerfile");
17
+
18
+ if (!existsSync(manifestPath)) {
19
+ spinner.fail();
20
+ logError(`Could not build, manifest.json is missing!`);
21
+ process.exit(1);
22
+ }
23
+
24
+ if (!config.registryUrl) {
25
+ spinner.fail();
26
+ logError(`Registery URL not set`);
27
+ process.exit(1);
28
+ }
29
+
30
+ if (!existsSync(dockerFilePath)) {
31
+ spinner.fail();
32
+ logError(`Could not build, Dockerfile is missing!`);
33
+ process.exit(1);
34
+ }
35
+
36
+ if (!checkDocker) {
37
+ spinner.fail();
38
+ logError(`Could not build, Docker is unreachable!`);
39
+ process.exit(1);
40
+ }
41
+
42
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf-8"));
43
+
44
+ try {
45
+ spinner.text = "Building Docker image...";
46
+ await buildDockerImage(
47
+ pluginDir,
48
+ config.registryUrl,
49
+ manifest.name,
50
+ manifest.version,
51
+ );
52
+ success(`Plugin has been build`);
53
+ spinner.text = "Pushing Docker image...";
54
+ await pushDockerImage(
55
+ config.registryUrl,
56
+ manifest.name,
57
+ manifest.version,
58
+ );
59
+ spinner.succeed();
60
+ success(
61
+ `Plugin has been build and pushed under ${manifest.name}:${manifest.version}`,
62
+ );
63
+ } catch (err: any) {
64
+ spinner.fail();
65
+ logError(`Failed to deploy: ${err.message}`);
66
+ process.exit(1);
67
+ }
68
+ });
69
+
70
+ function buildDockerImage(
71
+ pluginDir: string,
72
+ registryURL: string,
73
+ image: string,
74
+ version: string,
75
+ ): boolean {
76
+ execSync(
77
+ `docker build -t ${registryURL}/${image}:${version} ${pluginDir}`,
78
+ {
79
+ stdio: "pipe",
80
+ },
81
+ );
82
+ return true;
83
+ }
84
+
85
+ function pushDockerImage(
86
+ registryURL: string,
87
+ image: string,
88
+ version: string,
89
+ ): boolean {
90
+ execSync(`docker push ${registryURL}/${image}:${version}`, {
91
+ stdio: "pipe",
92
+ });
93
+ return true;
94
+ }
95
+
96
+ function checkDocker(): boolean {
97
+ try {
98
+ execSync("docker info --format '{{.ServerVersion}}'", {
99
+ stdio: "pipe",
100
+ timeout: 5000,
101
+ });
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
@@ -0,0 +1,103 @@
1
+ import { Command } from "commander";
2
+ import express from "express";
3
+ import EventEmitter from "node:events";
4
+ import { buildFrontendFiles, currentImportsGlobalVue } from "./build-frontend";
5
+ import chokidar from "chokidar";
6
+
7
+ export const serveFrontendCommand = new Command("serve-frontend")
8
+ .description("Compile all pages into a dist directory")
9
+ .action(async (opts: null, cmd: Command) => {
10
+ const app = express();
11
+
12
+ app.use((req, res, next) => {
13
+ res.header(`Access-Control-Allow-Origin`, `*`);
14
+ res.header(`Access-Control-Allow-Methods`, `GET`);
15
+ res.header(`Access-Control-Allow-Headers`, `Content-Type`);
16
+ next();
17
+ });
18
+
19
+ app.get("/dev/css", async (req, res) => {
20
+ res.end(lastCSS);
21
+ });
22
+ app.get("/dev/js", async (req, res) => {
23
+ res.json({
24
+ code: lastJS,
25
+ currentImportsGlobalVue: [...currentImportsGlobalVue].map((e) =>
26
+ e.trim(),
27
+ ),
28
+ });
29
+ });
30
+
31
+ const updateEvents = new EventEmitter();
32
+
33
+ const watchUpdates = chokidar.watch("./pages");
34
+
35
+ app.get("/streaming", (req, res) => {
36
+ res.setHeader("Cache-Control", "no-cache");
37
+ res.setHeader("Content-Type", "text/event-stream");
38
+ res.setHeader("Access-Control-Allow-Origin", "*");
39
+ res.setHeader("Connection", "keep-alive");
40
+ res.flushHeaders(); // flush the headers to establish SSE with client
41
+
42
+ updateEvents.on("jsUpdate", () => {
43
+ res.write(`id:0\nevent:reloadJS\ndata:js\n\n`);
44
+ });
45
+ updateEvents.on("cssUpdate", () => {
46
+ res.write(`id:0\nevent:reloadCSS\ndata:css\n\n`);
47
+ });
48
+ // If client closes connection, stop sending events
49
+ res.on("close", () => {
50
+ // watcher.
51
+ res.end();
52
+ });
53
+ });
54
+
55
+ buildInMemory().then(() => {
56
+ app.listen(3003, () => {
57
+ console.log("Dev server is running");
58
+
59
+ watchUpdates.on("all", (event, path) => {
60
+ let beforeTSJS = tsJS;
61
+ let beforeTSCSS = tsCSS;
62
+
63
+ buildInMemory().then(() => {
64
+ if (beforeTSCSS != tsCSS) {
65
+ updateEvents.emit("cssUpdate");
66
+ }
67
+ if (beforeTSJS != tsJS) {
68
+ updateEvents.emit("jsUpdate");
69
+ }
70
+ });
71
+ });
72
+ });
73
+ });
74
+ });
75
+
76
+ let lastCSS = "",
77
+ lastJS = "",
78
+ tsJS = new Date().getTime(),
79
+ tsCSS = new Date().getTime();
80
+
81
+ export async function buildInMemory() {
82
+ console.log("Building...");
83
+ const result = await buildFrontendFiles(false);
84
+
85
+ const js = result
86
+ .outputFiles!.filter((e) => e.path.endsWith(".js"))
87
+ .map((e) => e.text)
88
+ .join("\n");
89
+ const css = `${result
90
+ .outputFiles!.filter((e) => e.path.endsWith(".css"))
91
+ .map((e) => e.text)
92
+ .join("\n")}\`;`;
93
+
94
+ if (js != lastJS) {
95
+ tsJS = new Date().getTime();
96
+ lastJS = js;
97
+ }
98
+ if (css != lastCSS) {
99
+ tsCSS = new Date().getTime();
100
+ lastCSS = css;
101
+ }
102
+ console.log("Build done!");
103
+ }