@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,148 @@
|
|
|
1
|
+
import esbuild from "esbuild";
|
|
2
|
+
// @ts-ignore - unplugin-vue exports ./esbuild but types are not fully resolved
|
|
3
|
+
import Vue from "unplugin-vue/esbuild";
|
|
4
|
+
import postcssPlugin from "@chialab/esbuild-plugin-postcss";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "path";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
|
|
9
|
+
export const buildFrontendCommand = new Command("build-frontend")
|
|
10
|
+
.description("Compile the frontend code into a dist directory")
|
|
11
|
+
.action(async (opts: null, cmd: Command) => {
|
|
12
|
+
buildProdFiles().catch((err) => {
|
|
13
|
+
console.error(`[page-compiler] Failed:`, err);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
});
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const compilerDir = path.dirname(process.cwd());
|
|
19
|
+
const compilerRootDir = path.join(compilerDir, "..");
|
|
20
|
+
const compilerNodeModules = path.join(compilerRootDir, "node_modules");
|
|
21
|
+
|
|
22
|
+
interface CliArgs {
|
|
23
|
+
plugin: string;
|
|
24
|
+
output: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseArgs(): CliArgs {
|
|
28
|
+
let plugin = "./pages";
|
|
29
|
+
let output = "./pages/dist";
|
|
30
|
+
|
|
31
|
+
for (let i = 0; i < process.argv.length; i++) {
|
|
32
|
+
const arg = process.argv[i];
|
|
33
|
+
if (arg === "--plugin" && i + 1 < process.argv.length) {
|
|
34
|
+
plugin = process.argv[i + 1];
|
|
35
|
+
} else if (arg.startsWith("--plugin=")) {
|
|
36
|
+
plugin = arg.split("=")[1];
|
|
37
|
+
}
|
|
38
|
+
if (arg === "--output" && i + 1 < process.argv.length) {
|
|
39
|
+
output = process.argv[i + 1];
|
|
40
|
+
} else if (arg.startsWith("--output=")) {
|
|
41
|
+
output = arg.split("=")[1];
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return { plugin, output };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export let currentImportsGlobalVue = new Set<string>();
|
|
49
|
+
let importingModules =
|
|
50
|
+
"createTextVNode,defineComponent,toDisplayString,createElementVNode,openBlock,createElementBlock,Fragment,renderSlot,createCommentVNode,useModel,vModelText,vModelSelect,withDirectives,withKeys,normalizeClass,renderList,computed,ref,onMounted,normalizeStyle,createComment,resolveComponent,resolveDirective,withCtx,toHandlers,mergeProps,createSlots,withScopeId,createStaticVNode,createBlock,popScopeId,pushScopeId,isRef,unref,isReactive,toRef,toRefs,isProxy,isReadonly,shallowRef,triggerRef,customRef,markRaw,toRaw,reactive,readonly,watch,watchEffect,watchPostEffect,watchSyncEffect,provide,inject,getCurrentInstance,h,nextTick,onBeforeMount,onBeforeUpdate,onBeforeUnmount,onUpdated,onUnmounted,onActivated,onDeactivated,onErrorCaptured,onRenderTracked,onRenderTriggered,isVNode,cloneVNode,createVNode,Transition,TransitionGroup,Teleport,Suspense,KeepAlive,defineAsyncComponent,defineEmits,defineExpose,defineProps,withDefaults,useAttrs,useSlots,useCssModule,useCssVars,EffectScope,effectScope,getCurrentScope,onScopeDispose,useId,resolveDynamicComponent,normalizeProps,withModifiers,vShow";
|
|
51
|
+
currentImportsGlobalVue = new Set(importingModules.split(","));
|
|
52
|
+
const vueGlobalPlugin = {
|
|
53
|
+
name: "vue-global",
|
|
54
|
+
setup(build: any) {
|
|
55
|
+
build.onResolve({ filter: /^vue$/ }, async (args: any) => {
|
|
56
|
+
let source = await fs.promises.readFile(args.importer, "utf8");
|
|
57
|
+
|
|
58
|
+
const matchingImports =
|
|
59
|
+
source.match(/import\s*{\s*([^}]*)\s*}\s*from\s*["']vue["']/) ||
|
|
60
|
+
[];
|
|
61
|
+
// console.log(matchingImports);
|
|
62
|
+
|
|
63
|
+
if (matchingImports[1]) {
|
|
64
|
+
importingModules = matchingImports[1] + "," + importingModules;
|
|
65
|
+
for (const imp of importingModules.split(",")) {
|
|
66
|
+
currentImportsGlobalVue.add(imp.trim());
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
path: args.path,
|
|
71
|
+
namespace: "vue-global",
|
|
72
|
+
};
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
build.onLoad({ filter: /.*/, namespace: "vue-global" }, () => {
|
|
76
|
+
return {
|
|
77
|
+
contents: `
|
|
78
|
+
export const { ${[...currentImportsGlobalVue].join(",")} } = window.vue;
|
|
79
|
+
export default window.vue;
|
|
80
|
+
`,
|
|
81
|
+
loader: "js",
|
|
82
|
+
};
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
async function buildProdFiles(): Promise<void> {
|
|
88
|
+
const args = parseArgs();
|
|
89
|
+
const outputDir = path.resolve(process.cwd(), args.output);
|
|
90
|
+
const pluginPath = path.resolve(process.cwd(), args.plugin);
|
|
91
|
+
const mainPath = path.join(pluginPath, "main.ts");
|
|
92
|
+
|
|
93
|
+
await buildFrontendFiles(outputDir);
|
|
94
|
+
|
|
95
|
+
console.log(`[page-compiler] Compiling pages from: ${pluginPath}`);
|
|
96
|
+
console.log(`[page-compiler] Output directory: ${outputDir}`);
|
|
97
|
+
|
|
98
|
+
if (!fs.existsSync(mainPath)) {
|
|
99
|
+
console.error(`[page-compiler] Entry point not found: ${mainPath}`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await fs.promises.mkdir(outputDir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
const mainJsPath = path.join(outputDir, "main.js");
|
|
106
|
+
const pluginPagesJsPath = path.join(outputDir, "plugin-pages.js");
|
|
107
|
+
|
|
108
|
+
if (fs.existsSync(mainJsPath)) {
|
|
109
|
+
await fs.promises.rename(mainJsPath, pluginPagesJsPath);
|
|
110
|
+
console.log(`[page-compiler] Renamed main.js -> plugin-pages.js`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
console.log(`[page-compiler] Build complete`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export async function buildFrontendFiles(outputDir: string | boolean) {
|
|
117
|
+
const args = parseArgs();
|
|
118
|
+
const pluginPath = path.resolve(process.cwd(), args.plugin);
|
|
119
|
+
const mainPath = path.join(pluginPath, "main.ts");
|
|
120
|
+
|
|
121
|
+
const buildOptions: esbuild.BuildOptions = {
|
|
122
|
+
entryPoints: [mainPath],
|
|
123
|
+
bundle: true,
|
|
124
|
+
format: "esm",
|
|
125
|
+
target: "es2020",
|
|
126
|
+
platform: "browser",
|
|
127
|
+
sourcemap: outputDir == false ? undefined : false,
|
|
128
|
+
minify: outputDir == false ? false : true,
|
|
129
|
+
write: outputDir == false ? false : undefined,
|
|
130
|
+
outfile: outputDir == false ? ".temp/bundle.js" : undefined,
|
|
131
|
+
outdir: typeof outputDir == "string" ? outputDir : undefined,
|
|
132
|
+
plugins: [postcssPlugin(), vueGlobalPlugin, Vue({ sourceMap: false })],
|
|
133
|
+
alias: {
|
|
134
|
+
vue: path.join(
|
|
135
|
+
compilerNodeModules,
|
|
136
|
+
"vue",
|
|
137
|
+
"dist",
|
|
138
|
+
"vue.esm-bundler.js",
|
|
139
|
+
),
|
|
140
|
+
},
|
|
141
|
+
loader: {
|
|
142
|
+
".css": "css",
|
|
143
|
+
},
|
|
144
|
+
external: ["vue"],
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return await esbuild.build(buildOptions);
|
|
148
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { loadConfig } from "../config";
|
|
5
|
+
import {
|
|
6
|
+
success,
|
|
7
|
+
error as logError,
|
|
8
|
+
info,
|
|
9
|
+
createSpinner,
|
|
10
|
+
} from "../utils/logger";
|
|
11
|
+
|
|
12
|
+
interface ConnectOptions {
|
|
13
|
+
url?: string;
|
|
14
|
+
apiKey?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const connectCommand = new Command("connect")
|
|
18
|
+
.description("Connect to a core instance and save credentials")
|
|
19
|
+
.option("-u, --url <url>", "Core URL")
|
|
20
|
+
.option("-k, --api-key <key>", "API key")
|
|
21
|
+
.action(async (opts: ConnectOptions, cmd: Command) => {
|
|
22
|
+
const config = loadConfig(cmd.optsWithGlobals() as any);
|
|
23
|
+
|
|
24
|
+
let coreUrl = opts.url || config.coreUrl || "http://localhost:8080";
|
|
25
|
+
let apiKey = opts.apiKey || "";
|
|
26
|
+
|
|
27
|
+
// If no flags, prompt interactively
|
|
28
|
+
if (!opts.url || !opts.apiKey) {
|
|
29
|
+
const inquirer = (await import("inquirer")).default;
|
|
30
|
+
const prompts: any[] = [];
|
|
31
|
+
|
|
32
|
+
if (!opts.url) {
|
|
33
|
+
prompts.push({
|
|
34
|
+
type: "input",
|
|
35
|
+
name: "coreUrl",
|
|
36
|
+
message: "Core URL:",
|
|
37
|
+
default: coreUrl,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
if (!opts.apiKey) {
|
|
41
|
+
prompts.push({
|
|
42
|
+
type: "password",
|
|
43
|
+
name: "apiKey",
|
|
44
|
+
message: "API Key:",
|
|
45
|
+
mask: "*",
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const answers = await inquirer.prompt(prompts);
|
|
50
|
+
if (answers.coreUrl) coreUrl = answers.coreUrl;
|
|
51
|
+
if (answers.apiKey) apiKey = answers.apiKey;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!coreUrl) {
|
|
55
|
+
logError("Core URL is required");
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
if (!apiKey) {
|
|
59
|
+
logError("API key is required");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Validate by registering a test request ID
|
|
64
|
+
const validateSpinner = createSpinner("Validating connection...");
|
|
65
|
+
try {
|
|
66
|
+
const res = await fetch(
|
|
67
|
+
`${coreUrl.replace(/\/$/, "")}/api/dev/request-id`,
|
|
68
|
+
{
|
|
69
|
+
method: "POST",
|
|
70
|
+
headers: {
|
|
71
|
+
"Content-Type": "application/json",
|
|
72
|
+
Authorization: `Bearer ${apiKey}`,
|
|
73
|
+
},
|
|
74
|
+
body: JSON.stringify({ slug: "alcedo-connect" }),
|
|
75
|
+
},
|
|
76
|
+
);
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
let errMsg = `HTTP ${res.status}`;
|
|
79
|
+
try {
|
|
80
|
+
const errBody = (await res.json()) as { error?: string };
|
|
81
|
+
if (errBody.error) errMsg = errBody.error;
|
|
82
|
+
} catch {}
|
|
83
|
+
throw new Error(errMsg);
|
|
84
|
+
}
|
|
85
|
+
validateSpinner.succeed();
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
validateSpinner.fail();
|
|
88
|
+
logError(`Failed to connect: ${err.message}`);
|
|
89
|
+
process.exit(1);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Write to .alcedocore.dev.env
|
|
93
|
+
const envPath = path.join(process.cwd(), ".alcedocore.dev.env");
|
|
94
|
+
fs.writeFileSync(envPath, `CORE_URL=${coreUrl}\nAPI_KEY=${apiKey}\n`);
|
|
95
|
+
success(`Credentials saved to ${envPath}`);
|
|
96
|
+
info(` CORE_URL=${coreUrl}`);
|
|
97
|
+
info(" API_KEY=********");
|
|
98
|
+
});
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { createSpinner, success, error as logError, info } from "../utils/logger";
|
|
3
|
+
import { loadConfig } from "../config";
|
|
4
|
+
|
|
5
|
+
interface DeployOptions {
|
|
6
|
+
tag?: string;
|
|
7
|
+
image?: string;
|
|
8
|
+
start?: boolean;
|
|
9
|
+
env?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function parseEnvVars(env: string[] = []): Record<string, string> {
|
|
13
|
+
const result: Record<string, string> = {};
|
|
14
|
+
for (const e of env) {
|
|
15
|
+
const eqIdx = e.indexOf("=");
|
|
16
|
+
if (eqIdx === -1) {
|
|
17
|
+
result[e] = "";
|
|
18
|
+
} else {
|
|
19
|
+
result[e.slice(0, eqIdx)] = e.slice(eqIdx + 1);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export const deployCommand = new Command("deploy")
|
|
26
|
+
.argument("<slug>", "Plugin slug (e.g., my-plugin)")
|
|
27
|
+
.option("-t, --tag <version>", "Plugin version tag", "1.0.0")
|
|
28
|
+
.option("-i, --image <image>", "Docker image (default: {registry}/{slug}:{tag})")
|
|
29
|
+
.option("--no-start", "Register only, don't start the container")
|
|
30
|
+
.option("-e, --env <key=value>", "Environment variables (repeatable)", collectEnv, [])
|
|
31
|
+
.description("Deploy a plugin to Alcedo Core")
|
|
32
|
+
.action(async (slug: string, options: DeployOptions, cmd: Command) => {
|
|
33
|
+
const config = loadConfig(cmd.optsWithGlobals() as any);
|
|
34
|
+
const registryUrl = (config.registryUrl || "localhost:5000").replace(/^https?:\/\//, "");
|
|
35
|
+
const coreUrl = config.coreUrl;
|
|
36
|
+
const tag = options.tag || "1.0.0";
|
|
37
|
+
const image = options.image || `${registryUrl}/${slug}:${tag}`;
|
|
38
|
+
|
|
39
|
+
const spinner = createSpinner(`Deploying plugin: ${slug} v${tag}`);
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const body = JSON.stringify({
|
|
43
|
+
slug,
|
|
44
|
+
version: tag,
|
|
45
|
+
image,
|
|
46
|
+
env: parseEnvVars(options.env),
|
|
47
|
+
start_container: options.start !== false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
const res = await fetch(`${coreUrl}/api/plugins/deploy`, {
|
|
51
|
+
method: "POST",
|
|
52
|
+
headers: { "Content-Type": "application/json" },
|
|
53
|
+
body,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
if (!res.ok) {
|
|
57
|
+
const errBody = await res.text().catch(() => "Unknown error");
|
|
58
|
+
throw new Error(`Deploy failed (${res.status}): ${errBody}`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const contentType = res.headers.get("content-type") || "";
|
|
62
|
+
if (!contentType.includes("json")) {
|
|
63
|
+
const text = await res.text();
|
|
64
|
+
throw new Error(`Expected JSON but got ${contentType}: ${text.slice(0, 200)}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const result: any = await res.json();
|
|
68
|
+
const data = result.data || result;
|
|
69
|
+
|
|
70
|
+
spinner.succeed();
|
|
71
|
+
success(`Plugin "${slug}" v${tag} deployed`);
|
|
72
|
+
if (data.container_id) {
|
|
73
|
+
info(`Container: ${data.container_id}`);
|
|
74
|
+
}
|
|
75
|
+
} catch (err: any) {
|
|
76
|
+
spinner.fail();
|
|
77
|
+
logError(`Failed to deploy: ${err.message}`);
|
|
78
|
+
process.exit(1);
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
function collectEnv(val: string, prev: string[]): string[] {
|
|
83
|
+
prev.push(val);
|
|
84
|
+
return prev;
|
|
85
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import { proxyCommand } from "./proxy";
|
|
3
|
+
import { buildFrontendCommand } from "./build-frontend";
|
|
4
|
+
import { serveFrontendCommand } from "./serve-frontend";
|
|
5
|
+
|
|
6
|
+
export const devCommand = new Command("dev")
|
|
7
|
+
.description(
|
|
8
|
+
"Plugin development commands (use 'alcedo dev proxy' to start the dev proxy)",
|
|
9
|
+
)
|
|
10
|
+
.addCommand(proxyCommand)
|
|
11
|
+
.addCommand(buildFrontendCommand)
|
|
12
|
+
.addCommand(serveFrontendCommand);
|