keryx 0.0.1 → 0.1.2
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/keryx.ts +46 -0
- package/package.json +2 -2
- package/templates/scaffold/env.example.mustache +38 -0
- package/templates/scaffold/gitignore.mustache +3 -0
- package/templates/scaffold/hello-action.ts.mustache +16 -0
- package/templates/scaffold/index.ts.mustache +10 -0
- package/templates/scaffold/keryx.ts.mustache +62 -0
- package/templates/scaffold/migrations.ts.mustache +8 -0
- package/util/scaffold.ts +178 -0
package/keryx.ts
CHANGED
|
@@ -6,10 +6,56 @@ import { Action, api } from "./api";
|
|
|
6
6
|
import pkg from "./package.json";
|
|
7
7
|
import { addActionToProgram } from "./util/cli";
|
|
8
8
|
import { globLoader } from "./util/glob";
|
|
9
|
+
import {
|
|
10
|
+
interactiveScaffold,
|
|
11
|
+
scaffoldProject,
|
|
12
|
+
type ScaffoldOptions,
|
|
13
|
+
} from "./util/scaffold";
|
|
9
14
|
|
|
10
15
|
const program = new Command();
|
|
11
16
|
program.name(pkg.name).description(pkg.description).version(pkg.version);
|
|
12
17
|
|
|
18
|
+
program
|
|
19
|
+
.command("new [project-name]")
|
|
20
|
+
.summary("Create a new Keryx project")
|
|
21
|
+
.description("Scaffold a new Keryx application with project boilerplate")
|
|
22
|
+
.option("--no-interactive", "Skip prompts and use defaults")
|
|
23
|
+
.option("--no-db", "Skip database setup files")
|
|
24
|
+
.option("--no-example", "Skip example action")
|
|
25
|
+
.action(async (projectName: string | undefined, opts) => {
|
|
26
|
+
let options: ScaffoldOptions;
|
|
27
|
+
|
|
28
|
+
if (opts.interactive === false) {
|
|
29
|
+
// --no-interactive: use defaults
|
|
30
|
+
projectName = projectName || "my-keryx-app";
|
|
31
|
+
options = {
|
|
32
|
+
includeDb: opts.db !== false,
|
|
33
|
+
includeExample: opts.example !== false,
|
|
34
|
+
};
|
|
35
|
+
} else {
|
|
36
|
+
const result = await interactiveScaffold(projectName);
|
|
37
|
+
projectName = result.projectName;
|
|
38
|
+
options = result.options;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const targetDir = path.resolve(process.cwd(), projectName);
|
|
42
|
+
|
|
43
|
+
console.log(`\nCreating new Keryx project: ${projectName}\n`);
|
|
44
|
+
|
|
45
|
+
const files = await scaffoldProject(projectName, targetDir, options);
|
|
46
|
+
files.forEach((f) => console.log(` ${f}`));
|
|
47
|
+
|
|
48
|
+
console.log(`
|
|
49
|
+
Done! To get started:
|
|
50
|
+
|
|
51
|
+
cd ${projectName}
|
|
52
|
+
cp .env.example .env
|
|
53
|
+
bun install
|
|
54
|
+
bun dev
|
|
55
|
+
`);
|
|
56
|
+
process.exit(0);
|
|
57
|
+
});
|
|
58
|
+
|
|
13
59
|
program
|
|
14
60
|
.command("start")
|
|
15
61
|
.summary("Run the server")
|
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "keryx",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"description": "Keryx - the messenger of the gods. The greatest framework for building realtime AI, CLI, and web applications, and other applications too!",
|
|
8
|
-
"author": "Evan Tahler <evantahler
|
|
8
|
+
"author": "Evan Tahler <evan@evantahler.com>",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
11
|
"url": "git+https://github.com/evantahler/keryx.git",
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
PROCESS_NAME={{projectName}}
|
|
2
|
+
PROCESS_NAME_TEST=test-server
|
|
3
|
+
PROCESS_SHUTDOWN_TIMEOUT=30000
|
|
4
|
+
|
|
5
|
+
LOG_LEVEL=info
|
|
6
|
+
LOG_LEVEL_TEST=fatal
|
|
7
|
+
LOG_INCLUDE_TIMESTAMPS=false
|
|
8
|
+
LOG_COLORIZE=true
|
|
9
|
+
|
|
10
|
+
WEB_SERVER_ENABLED=true
|
|
11
|
+
WEB_SERVER_PORT=8080
|
|
12
|
+
WEB_SERVER_PORT_TEST=0
|
|
13
|
+
WEB_SERVER_HOST=localhost
|
|
14
|
+
WEB_SERVER_API_ROUTE="/api"
|
|
15
|
+
WEB_SERVER_ALLOWED_ORIGINS="http://localhost:3000"
|
|
16
|
+
WEB_SERVER_ALLOWED_METHODS="GET, POST, PUT, DELETE, OPTIONS"
|
|
17
|
+
|
|
18
|
+
MCP_SERVER_ENABLED=true
|
|
19
|
+
|
|
20
|
+
SESSION_TTL=86400000
|
|
21
|
+
SESSION_COOKIE_NAME="__session"
|
|
22
|
+
|
|
23
|
+
DATABASE_URL="postgres://$USER@localhost:5432/{{projectName}}"
|
|
24
|
+
DATABASE_URL_TEST="postgres://$USER@localhost:5432/{{projectName}}-test"
|
|
25
|
+
DATABASE_AUTO_MIGRATE=true
|
|
26
|
+
|
|
27
|
+
REDIS_URL="redis://localhost:6379/0"
|
|
28
|
+
REDIS_URL_TEST="redis://localhost:6379/1"
|
|
29
|
+
|
|
30
|
+
RATE_LIMIT_ENABLED=true
|
|
31
|
+
RATE_LIMIT_WINDOW_MS=60000
|
|
32
|
+
RATE_LIMIT_UNAUTH_LIMIT=20
|
|
33
|
+
RATE_LIMIT_AUTH_LIMIT=200
|
|
34
|
+
|
|
35
|
+
TASKS_ENABLED=true
|
|
36
|
+
TASK_PROCESSORS=1
|
|
37
|
+
TASK_TIMEOUT=5000
|
|
38
|
+
TASK_TIMEOUT_TEST=100
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { Action, type ActionParams } from "keryx";
|
|
3
|
+
import { HTTP_METHOD } from "keryx/classes/Action.ts";
|
|
4
|
+
|
|
5
|
+
export class Hello implements Action {
|
|
6
|
+
name = "hello";
|
|
7
|
+
description = "Say hello";
|
|
8
|
+
inputs = z.object({
|
|
9
|
+
name: z.string().default("World"),
|
|
10
|
+
});
|
|
11
|
+
web = { route: "/hello", method: HTTP_METHOD.GET };
|
|
12
|
+
|
|
13
|
+
async run(params: ActionParams<Hello>) {
|
|
14
|
+
return { message: `Hello, ${params.name}!` };
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { api } from "keryx";
|
|
2
|
+
|
|
3
|
+
// Tell the framework where this project lives so it can auto-discover
|
|
4
|
+
// actions, initializers, channels, etc. from this directory.
|
|
5
|
+
// Every entry point (keryx.ts, migrations.ts, test setup) should
|
|
6
|
+
// `import "./index"` to ensure rootDir is set before anything runs.
|
|
7
|
+
api.rootDir = import.meta.dir;
|
|
8
|
+
|
|
9
|
+
// Re-export everything from keryx for convenience
|
|
10
|
+
export * from "keryx";
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
#! /usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
// Set rootDir before any framework code loads actions
|
|
4
|
+
import { api } from "./index";
|
|
5
|
+
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { Action, globLoader } from "keryx";
|
|
8
|
+
import { addActionToProgram } from "keryx/util/cli.ts";
|
|
9
|
+
import path from "path";
|
|
10
|
+
import pkg from "./package.json";
|
|
11
|
+
|
|
12
|
+
const program = new Command();
|
|
13
|
+
program.name(pkg.name).description(pkg.description).version(pkg.version);
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.command("start")
|
|
17
|
+
.summary("Run the server")
|
|
18
|
+
.description("Start the Keryx server")
|
|
19
|
+
.action(async () => {
|
|
20
|
+
await api.start();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// Load framework actions from the package directory
|
|
24
|
+
const frameworkActions = await globLoader<Action>(
|
|
25
|
+
path.join(api.packageDir, "actions"),
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
// Load user project actions
|
|
29
|
+
let userActions: Action[] = [];
|
|
30
|
+
try {
|
|
31
|
+
userActions = await globLoader<Action>(path.join(api.rootDir, "actions"));
|
|
32
|
+
} catch {
|
|
33
|
+
// user project may not have actions, that's fine
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const actions = [...frameworkActions, ...userActions];
|
|
37
|
+
actions.forEach((action) => addActionToProgram(program, action));
|
|
38
|
+
|
|
39
|
+
program
|
|
40
|
+
.command("actions")
|
|
41
|
+
.summary("List all actions")
|
|
42
|
+
.action(async () => {
|
|
43
|
+
const actionSpacing =
|
|
44
|
+
actions.map((a) => a.name.length).reduce((a, b) => Math.max(a, b), 0) +
|
|
45
|
+
2;
|
|
46
|
+
const routeSpacing =
|
|
47
|
+
actions
|
|
48
|
+
.map((a) =>
|
|
49
|
+
a.web ? a.web.route.toString().length + a.web.method.length : 0,
|
|
50
|
+
)
|
|
51
|
+
.reduce((a, b) => Math.max(a, b), 0) + 2;
|
|
52
|
+
|
|
53
|
+
actions
|
|
54
|
+
.sort((a, b) => a.name.localeCompare(b.name))
|
|
55
|
+
.forEach((action) => {
|
|
56
|
+
console.log(
|
|
57
|
+
`${action.name}${" ".repeat(actionSpacing - action.name.length)} ${action.web ? `[${action.web.method}] ${action.web.route}` : " "}${" ".repeat(routeSpacing - (action.web ? action.web.method.length + action.web.route.toString().length + 2 : 0))} ${action.description ?? ""}`,
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
program.parse();
|
package/util/scaffold.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import Mustache from "mustache";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import * as readline from "readline";
|
|
5
|
+
import pkg from "../package.json";
|
|
6
|
+
|
|
7
|
+
export interface ScaffoldOptions {
|
|
8
|
+
includeDb: boolean;
|
|
9
|
+
includeExample: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const templatesDir = path.join(import.meta.dir, "..", "templates", "scaffold");
|
|
13
|
+
|
|
14
|
+
async function loadTemplate(name: string): Promise<string> {
|
|
15
|
+
return Bun.file(path.join(templatesDir, name)).text();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function prompt(question: string, defaultValue: string): Promise<string> {
|
|
19
|
+
const rl = readline.createInterface({
|
|
20
|
+
input: process.stdin,
|
|
21
|
+
output: process.stdout,
|
|
22
|
+
});
|
|
23
|
+
return new Promise((resolve) => {
|
|
24
|
+
rl.question(`${question} `, (answer) => {
|
|
25
|
+
rl.close();
|
|
26
|
+
resolve(answer.trim() || defaultValue);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function promptYesNo(
|
|
32
|
+
question: string,
|
|
33
|
+
defaultYes: boolean,
|
|
34
|
+
): Promise<boolean> {
|
|
35
|
+
const hint = defaultYes ? "Y/n" : "y/N";
|
|
36
|
+
const answer = await prompt(`${question} (${hint})`, defaultYes ? "y" : "n");
|
|
37
|
+
return answer.toLowerCase().startsWith("y");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function interactiveScaffold(
|
|
41
|
+
projectName?: string,
|
|
42
|
+
): Promise<{ projectName: string; options: ScaffoldOptions }> {
|
|
43
|
+
if (!projectName) {
|
|
44
|
+
projectName = await prompt("Project name:", "my-keryx-app");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const includeDb = await promptYesNo("Include database setup?", true);
|
|
48
|
+
const includeExample = await promptYesNo("Include example action?", true);
|
|
49
|
+
|
|
50
|
+
return { projectName, options: { includeDb, includeExample } };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function scaffoldProject(
|
|
54
|
+
projectName: string,
|
|
55
|
+
targetDir: string,
|
|
56
|
+
options: ScaffoldOptions,
|
|
57
|
+
): Promise<string[]> {
|
|
58
|
+
const keryxVersion = pkg.version;
|
|
59
|
+
const createdFiles: string[] = [];
|
|
60
|
+
|
|
61
|
+
if (fs.existsSync(targetDir)) {
|
|
62
|
+
throw new Error(`Directory "${projectName}" already exists`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
66
|
+
|
|
67
|
+
const view = { projectName, keryxVersion };
|
|
68
|
+
|
|
69
|
+
const write = async (filePath: string, content: string) => {
|
|
70
|
+
const fullPath = path.join(targetDir, filePath);
|
|
71
|
+
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
|
72
|
+
await Bun.write(fullPath, content);
|
|
73
|
+
createdFiles.push(filePath);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const writeTemplate = async (filePath: string, templateName: string) => {
|
|
77
|
+
const template = await loadTemplate(templateName);
|
|
78
|
+
await write(filePath, Mustache.render(template, view));
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// --- Always generated ---
|
|
82
|
+
|
|
83
|
+
// package.json is built programmatically (conditional deps/scripts)
|
|
84
|
+
await write(
|
|
85
|
+
"package.json",
|
|
86
|
+
JSON.stringify(
|
|
87
|
+
{
|
|
88
|
+
name: projectName,
|
|
89
|
+
version: "0.0.1",
|
|
90
|
+
module: "index.ts",
|
|
91
|
+
type: "module",
|
|
92
|
+
private: true,
|
|
93
|
+
license: "MIT",
|
|
94
|
+
bin: { keryx: "keryx.ts" },
|
|
95
|
+
scripts: {
|
|
96
|
+
start: "bun keryx.ts start",
|
|
97
|
+
dev: "bun --watch keryx.ts start",
|
|
98
|
+
...(options.includeDb ? { migrations: "bun run migrations.ts" } : {}),
|
|
99
|
+
lint: "tsc && prettier --check .",
|
|
100
|
+
format: "tsc && prettier --write .",
|
|
101
|
+
},
|
|
102
|
+
dependencies: {
|
|
103
|
+
keryx: `^${keryxVersion}`,
|
|
104
|
+
...(options.includeDb ? { "drizzle-zod": "^0.8.3" } : {}),
|
|
105
|
+
},
|
|
106
|
+
devDependencies: {
|
|
107
|
+
"@types/bun": "latest",
|
|
108
|
+
prettier: "^3.8.1",
|
|
109
|
+
...(options.includeDb ? { "drizzle-kit": "^0.20.18" } : {}),
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
null,
|
|
113
|
+
2,
|
|
114
|
+
) + "\n",
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
// tsconfig.json is static JSON (no interpolation needed)
|
|
118
|
+
await write(
|
|
119
|
+
"tsconfig.json",
|
|
120
|
+
JSON.stringify(
|
|
121
|
+
{
|
|
122
|
+
compilerOptions: {
|
|
123
|
+
lib: ["ESNext"],
|
|
124
|
+
target: "ESNext",
|
|
125
|
+
module: "ESNext",
|
|
126
|
+
moduleResolution: "bundler",
|
|
127
|
+
types: ["bun-types"],
|
|
128
|
+
strict: true,
|
|
129
|
+
skipLibCheck: true,
|
|
130
|
+
noEmit: true,
|
|
131
|
+
esModuleInterop: true,
|
|
132
|
+
resolveJsonModule: true,
|
|
133
|
+
isolatedModules: true,
|
|
134
|
+
verbatimModuleSyntax: true,
|
|
135
|
+
noImplicitAny: true,
|
|
136
|
+
noImplicitReturns: true,
|
|
137
|
+
noUnusedLocals: true,
|
|
138
|
+
noUnusedParameters: true,
|
|
139
|
+
noFallthroughCasesInSwitch: true,
|
|
140
|
+
forceConsistentCasingInFileNames: true,
|
|
141
|
+
allowImportingTsExtensions: true,
|
|
142
|
+
},
|
|
143
|
+
include: ["**/*.ts"],
|
|
144
|
+
exclude: ["node_modules", "drizzle"],
|
|
145
|
+
},
|
|
146
|
+
null,
|
|
147
|
+
2,
|
|
148
|
+
) + "\n",
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
await writeTemplate("index.ts", "index.ts.mustache");
|
|
152
|
+
await writeTemplate("keryx.ts", "keryx.ts.mustache");
|
|
153
|
+
await writeTemplate(".env.example", "env.example.mustache");
|
|
154
|
+
await writeTemplate(".gitignore", "gitignore.mustache");
|
|
155
|
+
|
|
156
|
+
// Create empty directories with .gitkeep
|
|
157
|
+
await write("initializers/.gitkeep", "");
|
|
158
|
+
await write("middleware/.gitkeep", "");
|
|
159
|
+
await write("channels/.gitkeep", "");
|
|
160
|
+
|
|
161
|
+
// --- Database setup ---
|
|
162
|
+
|
|
163
|
+
if (options.includeDb) {
|
|
164
|
+
await writeTemplate("migrations.ts", "migrations.ts.mustache");
|
|
165
|
+
await write("schema/.gitkeep", "");
|
|
166
|
+
await write("drizzle/.gitkeep", "");
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// --- Example action ---
|
|
170
|
+
|
|
171
|
+
if (options.includeExample) {
|
|
172
|
+
await writeTemplate("actions/hello.ts", "hello-action.ts.mustache");
|
|
173
|
+
} else {
|
|
174
|
+
await write("actions/.gitkeep", "");
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
return createdFiles;
|
|
178
|
+
}
|