@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.
- package/LICENSE.md +102 -0
- package/dist/commands/add-endpoint.js +104 -0
- package/dist/commands/add-migration.js +80 -0
- package/dist/commands/add-nav-item.js +72 -0
- package/dist/commands/add-page.js +64 -0
- package/dist/commands/build-frontend.js +126 -0
- package/dist/commands/compile-pages.js +120 -0
- package/dist/commands/connect.js +122 -0
- package/dist/commands/deploy.js +74 -0
- package/dist/commands/dev.js +13 -0
- package/dist/commands/init-core.js +900 -0
- package/dist/commands/init.js +124 -0
- package/dist/commands/init.test.js +84 -0
- package/dist/commands/migrate.js +22 -0
- package/dist/commands/proxy.js +157 -0
- package/dist/commands/publish.js +81 -0
- package/dist/commands/replay.js +142 -0
- package/dist/commands/serve-frontend.js +92 -0
- package/dist/config.js +110 -0
- package/dist/config.test.js +48 -0
- package/dist/index.js +81 -0
- package/dist/integration/cli-api.test.js +116 -0
- package/dist/utils/ejs-renderer.js +46 -0
- package/dist/utils/ejs-renderer.test.js +93 -0
- package/dist/utils/formatting.js +65 -0
- package/dist/utils/generateTimestamp.js +17 -0
- package/dist/utils/logger.js +33 -0
- package/dist/utils/validation.js +15 -0
- package/package.json +41 -0
- package/src/commands/add-endpoint.ts +157 -0
- package/src/commands/add-migration.ts +102 -0
- package/src/commands/add-nav-item.ts +106 -0
- package/src/commands/add-page.ts +100 -0
- package/src/commands/build-frontend.ts +148 -0
- package/src/commands/connect.ts +98 -0
- package/src/commands/deploy.ts +85 -0
- package/src/commands/dev.ts +12 -0
- package/src/commands/init-core.ts +1019 -0
- package/src/commands/init.test.ts +92 -0
- package/src/commands/init.ts +171 -0
- package/src/commands/migrate.ts +20 -0
- package/src/commands/proxy.ts +206 -0
- package/src/commands/publish.ts +106 -0
- package/src/commands/serve-frontend.ts +103 -0
- package/src/config.test.ts +50 -0
- package/src/config.ts +125 -0
- package/src/index.ts +100 -0
- package/src/integration/cli-api.test.ts +143 -0
- package/src/utils/ejs-renderer.ts +55 -0
- package/src/utils/formatting.ts +62 -0
- package/src/utils/generateTimestamp.ts +16 -0
- package/src/utils/logger.ts +27 -0
- package/src/utils/validation.ts +13 -0
- package/templates/endpoint/handler.js.ejs +23 -0
- package/templates/endpoint/handler.py.ejs +23 -0
- package/templates/migration/down.sql.ejs +6 -0
- package/templates/migration/up.sql.ejs +11 -0
- package/templates/page/page.vue.ejs +63 -0
- package/templates/plugin/Dockerfile.ejs +13 -0
- package/templates/plugin/Dockerfile.node.ejs +14 -0
- package/templates/plugin/README.md.ejs +19 -0
- package/templates/plugin/gitignore.ejs +6 -0
- package/templates/plugin/manifest.json.ejs +18 -0
- package/templates/plugin/migrations/.gitkeep +0 -0
- package/templates/plugin/pages/.gitkeep +0 -0
- package/templates/plugin/public/.gitkeep +0 -0
- package/templates/plugin/server.js.ejs +27 -0
- package/templates/plugin/server.py.ejs +32 -0
- package/tsconfig.json +16 -0
- package/vitest.config.ts +14 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.initCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const logger_1 = require("../utils/logger");
|
|
11
|
+
const ejs_renderer_1 = require("../utils/ejs-renderer");
|
|
12
|
+
const config_1 = require("../config");
|
|
13
|
+
const node_readline_1 = __importDefault(require("node:readline"));
|
|
14
|
+
exports.initCommand = new commander_1.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(async (name, options, cmd) => {
|
|
19
|
+
const config = (0, config_1.loadConfig)(cmd.optsWithGlobals());
|
|
20
|
+
const pluginDir = config.pluginDir || process.cwd();
|
|
21
|
+
const targetDir = node_path_1.default.resolve(pluginDir, name);
|
|
22
|
+
const slug = slugify(name);
|
|
23
|
+
if (node_fs_1.default.existsSync(targetDir)) {
|
|
24
|
+
(0, logger_1.error)(`Directory already exists: ${targetDir}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const language = options.language
|
|
28
|
+
? parseLanguage(options.language)
|
|
29
|
+
: await promptLanguage();
|
|
30
|
+
const templatesDir = node_path_1.default.resolve(__dirname, "../../templates");
|
|
31
|
+
const registryUrl = config.registryUrl || "localhost:5000";
|
|
32
|
+
const data = {
|
|
33
|
+
name,
|
|
34
|
+
slug,
|
|
35
|
+
version: "1.0.0",
|
|
36
|
+
description: `A new AlcedoCore plugin`,
|
|
37
|
+
language,
|
|
38
|
+
registryUrl,
|
|
39
|
+
};
|
|
40
|
+
const spinner = (0, logger_1.createSpinner)(`Scaffolding plugin: ${name}`);
|
|
41
|
+
try {
|
|
42
|
+
// Create target directory
|
|
43
|
+
node_fs_1.default.mkdirSync(targetDir, { recursive: true });
|
|
44
|
+
// Generate files from templates
|
|
45
|
+
// manifest.json
|
|
46
|
+
(0, ejs_renderer_1.renderAndWrite)(node_path_1.default.join(templatesDir, "plugin", "manifest.json.ejs"), node_path_1.default.join(targetDir, "manifest.json"), data);
|
|
47
|
+
// Dockerfile (language-specific)
|
|
48
|
+
const dockerTemplate = language === "python"
|
|
49
|
+
? "Dockerfile.ejs"
|
|
50
|
+
: "Dockerfile.node.ejs";
|
|
51
|
+
(0, ejs_renderer_1.renderAndWrite)(node_path_1.default.join(templatesDir, "plugin", dockerTemplate), node_path_1.default.join(targetDir, "Dockerfile"), data);
|
|
52
|
+
// Server stub (language-specific)
|
|
53
|
+
const serverTemplate = language === "python" ? "server.py.ejs" : "server.js.ejs";
|
|
54
|
+
(0, ejs_renderer_1.renderAndWrite)(node_path_1.default.join(templatesDir, "plugin", serverTemplate), node_path_1.default.join(targetDir, `server.${language === "python" ? "py" : "js"}`), data);
|
|
55
|
+
// .gitignore
|
|
56
|
+
(0, ejs_renderer_1.renderAndWrite)(node_path_1.default.join(templatesDir, "plugin", "gitignore.ejs"), node_path_1.default.join(targetDir, ".gitignore"), data);
|
|
57
|
+
// README.md
|
|
58
|
+
(0, ejs_renderer_1.renderAndWrite)(node_path_1.default.join(templatesDir, "plugin", "README.md.ejs"), node_path_1.default.join(targetDir, "README.md"), data);
|
|
59
|
+
copyGitkeep(templatesDir, targetDir, "migrations");
|
|
60
|
+
copyGitkeep(templatesDir, targetDir, "pages");
|
|
61
|
+
copyGitkeep(templatesDir, targetDir, "public");
|
|
62
|
+
spinner.succeed();
|
|
63
|
+
(0, logger_1.success)(`Plugin scaffolded: ${targetDir}`);
|
|
64
|
+
(0, logger_1.info)(`Next steps:
|
|
65
|
+
cd ${name}
|
|
66
|
+
# Edit server.${language === "python" ? "py" : "js"} and manifest.json
|
|
67
|
+
# Build with: docker build -t ${registryUrl}/${slug}:1.0.0 .`);
|
|
68
|
+
}
|
|
69
|
+
catch (err) {
|
|
70
|
+
spinner.fail();
|
|
71
|
+
(0, logger_1.error)(`Failed to scaffold plugin: ${err.message}`);
|
|
72
|
+
process.exit(1);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
function promptLanguage() {
|
|
76
|
+
const rl = node_readline_1.default.createInterface({
|
|
77
|
+
input: process.stdin,
|
|
78
|
+
output: process.stdout,
|
|
79
|
+
});
|
|
80
|
+
return new Promise((resolve) => {
|
|
81
|
+
rl.question("Select plugin language (python/node): ", (answer) => {
|
|
82
|
+
rl.close();
|
|
83
|
+
const lang = answer.trim().toLowerCase();
|
|
84
|
+
if (lang === "python" || lang === "py") {
|
|
85
|
+
resolve("python");
|
|
86
|
+
}
|
|
87
|
+
else if (lang === "node" ||
|
|
88
|
+
lang === "nodejs" ||
|
|
89
|
+
lang === "javascript" ||
|
|
90
|
+
lang === "js") {
|
|
91
|
+
resolve("node");
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// Default to python on invalid input, matching hello-world
|
|
95
|
+
(0, logger_1.info)(`Unknown language "${lang}", defaulting to Python`);
|
|
96
|
+
resolve("python");
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
function slugify(name) {
|
|
102
|
+
return name
|
|
103
|
+
.toLowerCase()
|
|
104
|
+
.replace(/[^a-z0-9-]/g, "-")
|
|
105
|
+
.replace(/-+/g, "-")
|
|
106
|
+
.replace(/^-|-$/g, "");
|
|
107
|
+
}
|
|
108
|
+
function copyGitkeep(templateDir, targetDir, subDir) {
|
|
109
|
+
const src = node_path_1.default.join(templateDir, "plugin", subDir, ".gitkeep");
|
|
110
|
+
const dest = node_path_1.default.join(targetDir, subDir, ".gitkeep");
|
|
111
|
+
if (node_fs_1.default.existsSync(src)) {
|
|
112
|
+
node_fs_1.default.mkdirSync(node_path_1.default.dirname(dest), { recursive: true });
|
|
113
|
+
node_fs_1.default.writeFileSync(dest, "");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
function parseLanguage(lang) {
|
|
117
|
+
const v = lang.trim().toLowerCase();
|
|
118
|
+
if (v === "python" || v === "py")
|
|
119
|
+
return "python";
|
|
120
|
+
if (v === "node" || v === "nodejs" || v === "javascript" || v === "js")
|
|
121
|
+
return "node";
|
|
122
|
+
throw new Error(`Unknown language "${lang}". Use "python" or "node".`);
|
|
123
|
+
}
|
|
124
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const vitest_1 = require("vitest");
|
|
7
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
8
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
9
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
10
|
+
(0, vitest_1.describe)("Init command", () => {
|
|
11
|
+
const testDir = node_path_1.default.join(node_os_1.default.tmpdir(), `alcedo-test-init-${Date.now()}`);
|
|
12
|
+
(0, vitest_1.beforeAll)(() => {
|
|
13
|
+
node_fs_1.default.mkdirSync(testDir, { recursive: true });
|
|
14
|
+
});
|
|
15
|
+
(0, vitest_1.afterAll)(() => {
|
|
16
|
+
node_fs_1.default.rmSync(testDir, { recursive: true, force: true });
|
|
17
|
+
});
|
|
18
|
+
(0, vitest_1.it)("validates slug format correctly", () => {
|
|
19
|
+
// Test the validation logic used by init command
|
|
20
|
+
const validSlugs = ["my-plugin", "hello-world", "test123"];
|
|
21
|
+
const invalidSlugs = ["My Plugin", "my_plugin", "", "UPPERCASE"];
|
|
22
|
+
// The validation function requires lowercase alphanumeric + hyphens
|
|
23
|
+
const isValid = (slug) => slug.length > 0 && /^[a-z0-9]+(-[a-z0-9]+)*$/.test(slug);
|
|
24
|
+
for (const slug of validSlugs) {
|
|
25
|
+
(0, vitest_1.expect)(isValid(slug)).toBe(true);
|
|
26
|
+
}
|
|
27
|
+
for (const slug of invalidSlugs) {
|
|
28
|
+
(0, vitest_1.expect)(isValid(slug)).toBe(false);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
(0, vitest_1.it)("generates valid plugin directory structure", () => {
|
|
32
|
+
// Verify the expected directory structure matches what init would create
|
|
33
|
+
const expectedDirs = ["migrations", "pages"];
|
|
34
|
+
for (const dir of expectedDirs) {
|
|
35
|
+
const dirPath = node_path_1.default.join(testDir, dir);
|
|
36
|
+
node_fs_1.default.mkdirSync(dirPath, { recursive: true });
|
|
37
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(dirPath)).toBe(true);
|
|
38
|
+
}
|
|
39
|
+
// Verify key files that init should generate
|
|
40
|
+
const expectedFiles = ["manifest.json", "Dockerfile", "server.py"];
|
|
41
|
+
for (const file of expectedFiles) {
|
|
42
|
+
const filePath = node_path_1.default.join(testDir, file);
|
|
43
|
+
// Only test structure, not content — init command generates these via templates
|
|
44
|
+
if (file === "manifest.json") {
|
|
45
|
+
node_fs_1.default.writeFileSync(filePath, JSON.stringify({
|
|
46
|
+
slug: "test-plugin",
|
|
47
|
+
version: "1.0.0",
|
|
48
|
+
plugin_type: "dynamic",
|
|
49
|
+
}));
|
|
50
|
+
}
|
|
51
|
+
if (file === "Dockerfile") {
|
|
52
|
+
node_fs_1.default.writeFileSync(filePath, "FROM python:3.11-slim\n");
|
|
53
|
+
}
|
|
54
|
+
if (file === "server.py") {
|
|
55
|
+
node_fs_1.default.writeFileSync(filePath, 'print("hello")\n');
|
|
56
|
+
}
|
|
57
|
+
(0, vitest_1.expect)(node_fs_1.default.existsSync(filePath)).toBe(true);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
(0, vitest_1.it)("generates versioned migration filenames correctly", () => {
|
|
61
|
+
// Migration filenames follow pattern: YYYYMMDD_<name>.up.sql
|
|
62
|
+
const date = "20260527";
|
|
63
|
+
const name = "create_users_table";
|
|
64
|
+
const upFilename = `${date}_${name}.up.sql`;
|
|
65
|
+
const downFilename = `${date}_${name}.down.sql`;
|
|
66
|
+
(0, vitest_1.expect)(upFilename).toMatch(/^\d{8}_.+\.up\.sql$/);
|
|
67
|
+
(0, vitest_1.expect)(downFilename).toMatch(/^\d{8}_.+\.down\.sql$/);
|
|
68
|
+
});
|
|
69
|
+
(0, vitest_1.it)("generates endpoint handler with correct structure", () => {
|
|
70
|
+
// Endpoint template generates: slug-safe endpoint path + handler stub
|
|
71
|
+
const endpointName = "user-data";
|
|
72
|
+
const expectedMethod = "GET";
|
|
73
|
+
// Simulate the manifest endpoint entry format
|
|
74
|
+
const endpointEntry = {
|
|
75
|
+
method: expectedMethod,
|
|
76
|
+
path: `/${endpointName}`,
|
|
77
|
+
handler: `${endpointName}_handler`,
|
|
78
|
+
};
|
|
79
|
+
(0, vitest_1.expect)(endpointEntry.path).toBe(`/${endpointName}`);
|
|
80
|
+
(0, vitest_1.expect)(endpointEntry.method).toBe(expectedMethod);
|
|
81
|
+
(0, vitest_1.expect)(endpointEntry.handler).toBeTruthy();
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
//# sourceMappingURL=init.test.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.migrateCommand = void 0;
|
|
4
|
+
const commander_1 = require("commander");
|
|
5
|
+
const logger_1 = require("../utils/logger");
|
|
6
|
+
const add_migration_1 = require("./add-migration");
|
|
7
|
+
const generateCommand = new commander_1.Command("generate")
|
|
8
|
+
.argument("<name>", "Migration name (e.g., create_users_table)")
|
|
9
|
+
.description("Alias for `alcedo add migration` — generate a versioned SQL migration pair")
|
|
10
|
+
.action(async (name) => {
|
|
11
|
+
try {
|
|
12
|
+
await (0, add_migration_1.addMigrationAction)(name);
|
|
13
|
+
}
|
|
14
|
+
catch (err) {
|
|
15
|
+
(0, logger_1.error)(`Generate failed: ${err.message}`);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
});
|
|
19
|
+
exports.migrateCommand = new commander_1.Command("migrate")
|
|
20
|
+
.description("Alias for `alcedo add migration`");
|
|
21
|
+
exports.migrateCommand.addCommand(generateCommand);
|
|
22
|
+
//# sourceMappingURL=migrate.js.map
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.proxyCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const node_http_1 = __importDefault(require("node:http"));
|
|
9
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const logger_1 = require("../utils/logger");
|
|
12
|
+
exports.proxyCommand = new commander_1.Command("proxy")
|
|
13
|
+
.description("Start a dev proxy that forwards requests to a local dev server")
|
|
14
|
+
.option("-p, --port <port>", "Proxy listen port", "3099")
|
|
15
|
+
.option("-t, --target <url>", "Target dev server URL (e.g., localhost:8080)", "localhost:3000")
|
|
16
|
+
.option("-k, --api-key <key>", "Core API key (or ALCEDO_API_KEY env var)")
|
|
17
|
+
.action(async (opts, cmd) => {
|
|
18
|
+
const config = (0, config_1.loadConfig)(cmd.optsWithGlobals());
|
|
19
|
+
const coreUrl = config.coreUrl || "http://localhost:8080";
|
|
20
|
+
const pluginDir = node_path_1.default.resolve(config.pluginDir || process.cwd());
|
|
21
|
+
const slug = node_path_1.default.basename(pluginDir);
|
|
22
|
+
const apiKey = opts.apiKey || config.apiKey || process.env.ALCEDO_API_KEY || "";
|
|
23
|
+
const proxyPort = parseInt(opts.port || "3099", 10);
|
|
24
|
+
const target = opts.target || "localhost:3000";
|
|
25
|
+
if (isNaN(proxyPort) || proxyPort < 1 || proxyPort > 65535) {
|
|
26
|
+
(0, logger_1.error)(`Invalid proxy port: ${opts.port}`);
|
|
27
|
+
process.exit(1);
|
|
28
|
+
}
|
|
29
|
+
const parsedTarget = parseTarget(target);
|
|
30
|
+
const validateSpinner = (0, logger_1.createSpinner)("Validating API key...");
|
|
31
|
+
const testId = await registerRequest(coreUrl, apiKey || undefined, slug);
|
|
32
|
+
if (!testId) {
|
|
33
|
+
validateSpinner.fail();
|
|
34
|
+
(0, logger_1.error)(`Failed to connect to core at ${coreUrl}`);
|
|
35
|
+
(0, logger_1.error)("Make sure the AlcedoCore instance is running and API key is correct");
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
validateSpinner.succeed();
|
|
39
|
+
const server = node_http_1.default.createServer(async (clientReq, clientRes) => {
|
|
40
|
+
const startTime = Date.now();
|
|
41
|
+
const requestId = await registerRequest(coreUrl, apiKey || undefined, slug);
|
|
42
|
+
if (!requestId) {
|
|
43
|
+
(0, logger_1.error)("Failed to register request ID with core");
|
|
44
|
+
clientRes.statusCode = 502;
|
|
45
|
+
clientRes.setHeader("Content-Type", "text/plain");
|
|
46
|
+
clientRes.end("Bad Gateway: core unavailable");
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
// Clone headers and inject X-Request-ID
|
|
50
|
+
const headers = {};
|
|
51
|
+
for (const [key, value] of Object.entries(clientReq.headers)) {
|
|
52
|
+
if (value !== undefined) {
|
|
53
|
+
headers[key] = Array.isArray(value)
|
|
54
|
+
? value.join(", ")
|
|
55
|
+
: value;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
headers["X-Request-ID"] = requestId;
|
|
59
|
+
headers["host"] = `${parsedTarget.hostname}:${parsedTarget.port}`;
|
|
60
|
+
const options = {
|
|
61
|
+
hostname: parsedTarget.hostname,
|
|
62
|
+
port: parsedTarget.port,
|
|
63
|
+
path: clientReq.url,
|
|
64
|
+
method: clientReq.method,
|
|
65
|
+
headers,
|
|
66
|
+
};
|
|
67
|
+
const proxyReq = node_http_1.default.request(options, (proxyRes) => {
|
|
68
|
+
const chunks = [];
|
|
69
|
+
proxyRes.on("data", (chunk) => chunks.push(chunk));
|
|
70
|
+
proxyRes.on("end", () => {
|
|
71
|
+
const duration = Date.now() - startTime;
|
|
72
|
+
const statusCode = proxyRes.statusCode || 0;
|
|
73
|
+
(0, logger_1.info)(` ${statusCode} ${clientReq.method} ${duration}ms ${clientReq.url}`);
|
|
74
|
+
const responseHeaders = { ...proxyRes.headers };
|
|
75
|
+
clientRes.writeHead(statusCode, responseHeaders);
|
|
76
|
+
clientRes.end(Buffer.concat(chunks));
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
proxyReq.on("error", (err) => {
|
|
80
|
+
(0, logger_1.error)(`Proxy request error: ${err.message}`);
|
|
81
|
+
if (!clientRes.headersSent) {
|
|
82
|
+
clientRes.statusCode = 502;
|
|
83
|
+
clientRes.setHeader("Content-Type", "text/plain");
|
|
84
|
+
clientRes.end(`Bad Gateway: ${err.message}`);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
clientReq.pipe(proxyReq);
|
|
88
|
+
});
|
|
89
|
+
let shuttingDown = false;
|
|
90
|
+
function handleShutdown() {
|
|
91
|
+
if (shuttingDown)
|
|
92
|
+
return;
|
|
93
|
+
shuttingDown = true;
|
|
94
|
+
console.log("");
|
|
95
|
+
(0, logger_1.info)("Shutting down proxy...");
|
|
96
|
+
server.close(() => {
|
|
97
|
+
(0, logger_1.success)("Proxy stopped");
|
|
98
|
+
process.exit(0);
|
|
99
|
+
});
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
(0, logger_1.warn)("Proxy did not close gracefully, forcing exit");
|
|
102
|
+
process.exit(0);
|
|
103
|
+
}, 3000);
|
|
104
|
+
}
|
|
105
|
+
process.on("SIGINT", handleShutdown);
|
|
106
|
+
process.on("SIGTERM", handleShutdown);
|
|
107
|
+
process.on("SIGHUP", handleShutdown);
|
|
108
|
+
server.listen(proxyPort, () => {
|
|
109
|
+
(0, logger_1.success)(`Dev proxy listening on http://localhost:${proxyPort}`);
|
|
110
|
+
(0, logger_1.info)(`Forwarding to http://${target}`);
|
|
111
|
+
(0, logger_1.info)(`Plugin slug: ${slug}`);
|
|
112
|
+
(0, logger_1.info)(`Core URL: ${coreUrl}`);
|
|
113
|
+
if (apiKey) {
|
|
114
|
+
(0, logger_1.info)("API key authentication enabled");
|
|
115
|
+
}
|
|
116
|
+
(0, logger_1.info)("Press Ctrl+C to stop");
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
async function coreFetch(coreUrl, apiKey, endpoint, body) {
|
|
120
|
+
try {
|
|
121
|
+
const headers = {
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
};
|
|
124
|
+
if (apiKey) {
|
|
125
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
126
|
+
}
|
|
127
|
+
return await fetch(`${coreUrl.replace(/\/$/, "")}${endpoint}`, {
|
|
128
|
+
method: "POST",
|
|
129
|
+
headers,
|
|
130
|
+
body: JSON.stringify(body),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
async function registerRequest(coreUrl, apiKey, slug) {
|
|
138
|
+
const res = await coreFetch(coreUrl, apiKey, "/api/dev/request-id", {
|
|
139
|
+
slug,
|
|
140
|
+
});
|
|
141
|
+
if (!res || !res.ok)
|
|
142
|
+
return null;
|
|
143
|
+
try {
|
|
144
|
+
const data = (await res.json());
|
|
145
|
+
return data.request_id;
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
function parseTarget(target) {
|
|
152
|
+
let cleaned = target.replace(/^https?:\/\//, "");
|
|
153
|
+
const [hostname, portStr] = cleaned.split(":");
|
|
154
|
+
const port = portStr ? parseInt(portStr, 10) : 3000;
|
|
155
|
+
return { hostname, port };
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=proxy.js.map
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.publishCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
const path_1 = __importDefault(require("path"));
|
|
10
|
+
const config_1 = require("../config");
|
|
11
|
+
const fs_1 = require("fs");
|
|
12
|
+
const child_process_1 = require("child_process");
|
|
13
|
+
exports.publishCommand = new commander_1.Command("publish")
|
|
14
|
+
.description("Build and push a plugin to an image registry")
|
|
15
|
+
.action(async (slug, options, cmd) => {
|
|
16
|
+
const spinner = (0, logger_1.createSpinner)(`Preparing build...`);
|
|
17
|
+
const config = (0, config_1.loadConfig)();
|
|
18
|
+
const pluginDir = config.pluginDir || process.cwd();
|
|
19
|
+
const manifestPath = path_1.default.resolve(pluginDir, "manifest.json");
|
|
20
|
+
const dockerFilePath = path_1.default.resolve(pluginDir, "Dockerfile");
|
|
21
|
+
if (!(0, fs_1.existsSync)(manifestPath)) {
|
|
22
|
+
spinner.fail();
|
|
23
|
+
(0, logger_1.error)(`Could not build, manifest.json is missing!`);
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
26
|
+
if (!config.registryUrl) {
|
|
27
|
+
spinner.fail();
|
|
28
|
+
(0, logger_1.error)(`Registery URL not set`);
|
|
29
|
+
process.exit(1);
|
|
30
|
+
}
|
|
31
|
+
if (!(0, fs_1.existsSync)(dockerFilePath)) {
|
|
32
|
+
spinner.fail();
|
|
33
|
+
(0, logger_1.error)(`Could not build, Dockerfile is missing!`);
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
if (!checkDocker) {
|
|
37
|
+
spinner.fail();
|
|
38
|
+
(0, logger_1.error)(`Could not build, Docker is unreachable!`);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
const manifest = JSON.parse((0, fs_1.readFileSync)(manifestPath, "utf-8"));
|
|
42
|
+
try {
|
|
43
|
+
spinner.text = "Building Docker image...";
|
|
44
|
+
await buildDockerImage(pluginDir, config.registryUrl, manifest.name, manifest.version);
|
|
45
|
+
(0, logger_1.success)(`Plugin has been build`);
|
|
46
|
+
spinner.text = "Pushing Docker image...";
|
|
47
|
+
await pushDockerImage(config.registryUrl, manifest.name, manifest.version);
|
|
48
|
+
spinner.succeed();
|
|
49
|
+
(0, logger_1.success)(`Plugin has been build and pushed under ${manifest.name}:${manifest.version}`);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
spinner.fail();
|
|
53
|
+
(0, logger_1.error)(`Failed to deploy: ${err.message}`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
function buildDockerImage(pluginDir, registryURL, image, version) {
|
|
58
|
+
(0, child_process_1.execSync)(`docker build -t ${registryURL}/${image}:${version} ${pluginDir}`, {
|
|
59
|
+
stdio: "pipe",
|
|
60
|
+
});
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
function pushDockerImage(registryURL, image, version) {
|
|
64
|
+
(0, child_process_1.execSync)(`docker push ${registryURL}/${image}:${version}`, {
|
|
65
|
+
stdio: "pipe",
|
|
66
|
+
});
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
function checkDocker() {
|
|
70
|
+
try {
|
|
71
|
+
(0, child_process_1.execSync)("docker info --format '{{.ServerVersion}}'", {
|
|
72
|
+
stdio: "pipe",
|
|
73
|
+
timeout: 5000,
|
|
74
|
+
});
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
//# sourceMappingURL=publish.js.map
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.replayCommand = void 0;
|
|
7
|
+
const commander_1 = require("commander");
|
|
8
|
+
const config_js_1 = require("../config.js");
|
|
9
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
10
|
+
const logger_js_1 = require("../utils/logger.js");
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// API helper — fetch request details from core
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
async function fetchRequestDetail(coreUrl, slug, requestId, apiKey) {
|
|
15
|
+
const url = `${coreUrl.replace(/\/$/, "")}/api/plugins/${slug}/logs/${requestId}`;
|
|
16
|
+
const headers = { "Content-Type": "application/json" };
|
|
17
|
+
if (apiKey) {
|
|
18
|
+
headers["Authorization"] = `Bearer ${apiKey}`;
|
|
19
|
+
}
|
|
20
|
+
const res = await fetch(url, { headers });
|
|
21
|
+
if (!res.ok) {
|
|
22
|
+
const errBody = await res.json().catch(() => ({ error: res.statusText }));
|
|
23
|
+
throw new Error(errBody.error || `HTTP ${res.status}: ${res.statusText}`);
|
|
24
|
+
}
|
|
25
|
+
const body = await res.json();
|
|
26
|
+
if (!body.data) {
|
|
27
|
+
throw new Error(body.error || "Empty response from detail endpoint");
|
|
28
|
+
}
|
|
29
|
+
return body.data.request;
|
|
30
|
+
}
|
|
31
|
+
async function replayRequest(devUrl, original, timeoutMs) {
|
|
32
|
+
const path = original.path || "/";
|
|
33
|
+
const targetUrl = `${devUrl.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
|
|
34
|
+
const headers = {};
|
|
35
|
+
if (original.request_headers) {
|
|
36
|
+
const skipHeaders = new Set(["host", "content-length", "transfer-encoding", "x-request-id"]);
|
|
37
|
+
for (const [key, value] of Object.entries(original.request_headers)) {
|
|
38
|
+
if (!skipHeaders.has(key.toLowerCase())) {
|
|
39
|
+
headers[key] = value;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const controller = new AbortController();
|
|
44
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
45
|
+
try {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const res = await fetch(targetUrl, {
|
|
48
|
+
method: original.method,
|
|
49
|
+
headers,
|
|
50
|
+
body: original.request_body || undefined,
|
|
51
|
+
signal: controller.signal,
|
|
52
|
+
});
|
|
53
|
+
const duration = Date.now() - startTime;
|
|
54
|
+
const resBody = await res.text();
|
|
55
|
+
return {
|
|
56
|
+
original: { status_code: original.status_code, duration_ms: original.duration_ms },
|
|
57
|
+
replayed: { status_code: res.status, duration_ms: duration, body: resBody.length > 500 ? resBody.slice(0, 500) + "\n... (truncated)" : resBody },
|
|
58
|
+
statusMatch: original.status_code === res.status,
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
finally {
|
|
62
|
+
clearTimeout(timeout);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
// Display helpers
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
function displayComparison(result) {
|
|
69
|
+
const statusIcon = result.statusMatch ? chalk_1.default.green("✓") : chalk_1.default.red("✗");
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(chalk_1.default.bold(" Response Comparison:"));
|
|
72
|
+
console.log(chalk_1.default.bold(" ─────────────────────"));
|
|
73
|
+
console.log(` ${chalk_1.default.bold("Status Code:")} Original: ${chalk_1.default.cyan(String(result.original.status_code))} → Replayed: ${chalk_1.default.cyan(String(result.replayed.status_code))} ${statusIcon}`);
|
|
74
|
+
console.log(` ${chalk_1.default.bold("Duration:")} Original: ${chalk_1.default.cyan(`${result.original.duration_ms}ms`)} → Replayed: ${chalk_1.default.cyan(`${result.replayed.duration_ms}ms`)}`);
|
|
75
|
+
if (!result.statusMatch) {
|
|
76
|
+
(0, logger_js_1.warn)(` Status code mismatch: expected ${result.original.status_code}, got ${result.replayed.status_code}`);
|
|
77
|
+
}
|
|
78
|
+
if (result.replayed.body) {
|
|
79
|
+
console.log("");
|
|
80
|
+
console.log(chalk_1.default.bold(" Replayed Response Body:"));
|
|
81
|
+
console.log(` ${result.replayed.body}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Main replay command
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
exports.replayCommand = new commander_1.Command("replay")
|
|
88
|
+
.description("Replay a historical request against the local dev server")
|
|
89
|
+
.argument("<requestId>", "Request UUID from the plugin logs")
|
|
90
|
+
.option("--dev-port <port>", "Local dev server port", "3000")
|
|
91
|
+
.option("-t, --timeout <ms>", "Request timeout in milliseconds", "30000")
|
|
92
|
+
.option("--api-key <key>", "API key for core authentication")
|
|
93
|
+
.action(async (requestId, opts, cmd) => {
|
|
94
|
+
const config = (0, config_js_1.loadConfig)(cmd.optsWithGlobals());
|
|
95
|
+
const coreUrl = config.coreUrl || "http://localhost:8080";
|
|
96
|
+
const pluginDir = config.pluginDir || process.cwd();
|
|
97
|
+
const slug = require("node:path").basename(pluginDir);
|
|
98
|
+
const port = parseInt(opts.devPort || "3000", 10);
|
|
99
|
+
const timeout = parseInt(opts.timeout || "30000", 10);
|
|
100
|
+
const apiKey = opts.apiKey || process.env.ALCEDO_API_KEY || "";
|
|
101
|
+
if (isNaN(port) || port < 1 || port > 65535) {
|
|
102
|
+
(0, logger_js_1.error)(`Invalid port: ${opts.devPort}`);
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
const devUrl = `http://localhost:${port}`;
|
|
106
|
+
// Step 1: Fetch original request details
|
|
107
|
+
const fetchSpinner = (0, logger_js_1.createSpinner)(`Fetching request ${requestId}...`);
|
|
108
|
+
let requestDetail;
|
|
109
|
+
try {
|
|
110
|
+
requestDetail = await fetchRequestDetail(coreUrl, slug, requestId, apiKey || undefined);
|
|
111
|
+
fetchSpinner.succeed();
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
fetchSpinner.fail();
|
|
115
|
+
(0, logger_js_1.error)(`Failed to fetch request details: ${err.message}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
(0, logger_js_1.info)(`Original request: ${requestDetail.method} ${requestDetail.path}`);
|
|
119
|
+
(0, logger_js_1.info)(` Status: ${requestDetail.status_code} (${requestDetail.duration_ms}ms)`);
|
|
120
|
+
(0, logger_js_1.info)(` Timestamp: ${requestDetail.created_at}`);
|
|
121
|
+
// Step 2: Replay against dev server
|
|
122
|
+
const replaySpinner = (0, logger_js_1.createSpinner)(`Replaying against ${devUrl}...`);
|
|
123
|
+
try {
|
|
124
|
+
const result = await replayRequest(devUrl, requestDetail, timeout);
|
|
125
|
+
replaySpinner.succeed();
|
|
126
|
+
displayComparison(result);
|
|
127
|
+
if (result.statusMatch) {
|
|
128
|
+
(0, logger_js_1.success)("Response status matches original");
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
replaySpinner.fail();
|
|
133
|
+
if (err.name === "AbortError") {
|
|
134
|
+
(0, logger_js_1.error)(`Request timed out after ${timeout}ms`);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
(0, logger_js_1.error)(`Replay failed: ${err.message}`);
|
|
138
|
+
}
|
|
139
|
+
process.exit(1);
|
|
140
|
+
}
|
|
141
|
+
});
|
|
142
|
+
//# sourceMappingURL=replay.js.map
|