keryx 0.3.0 → 0.3.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.
@@ -31,7 +31,7 @@ export class DB extends Initializer {
31
31
  async initialize() {
32
32
  const dbContainer = {} as {
33
33
  db: ReturnType<typeof drizzle>;
34
- pool: Pool;
34
+ pool: InstanceType<typeof Pool>;
35
35
  };
36
36
  return Object.assign(
37
37
  {
@@ -104,7 +104,7 @@ export class DB extends Initializer {
104
104
  const migrationConfig = {
105
105
  schema: path.join("models", "*"),
106
106
  dbCredentials: {
107
- uri: config.database.connectionString,
107
+ url: config.database.connectionString,
108
108
  },
109
109
  out: path.join("drizzle"),
110
110
  } satisfies DrizzleMigrateConfig;
package/keryx.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  scaffoldProject,
12
12
  type ScaffoldOptions,
13
13
  } from "./util/scaffold";
14
+ import { upgradeProject } from "./util/upgrade";
14
15
 
15
16
  const program = new Command();
16
17
  program.name(pkg.name).description(pkg.description).version(pkg.version);
@@ -57,6 +58,25 @@ Done! To get started:
57
58
  process.exit(0);
58
59
  });
59
60
 
61
+ program
62
+ .command("upgrade")
63
+ .summary("Update framework-owned files to match the installed keryx version")
64
+ .option("--dry-run", "Show what would change without writing files")
65
+ .option("--force", "Overwrite all framework files without confirmation")
66
+ .option("-y, --yes", "Overwrite all framework files without confirmation")
67
+ .action(async (opts) => {
68
+ try {
69
+ await upgradeProject(process.cwd(), {
70
+ dryRun: opts.dryRun || false,
71
+ force: opts.force || opts.yes || false,
72
+ });
73
+ process.exit(0);
74
+ } catch (e) {
75
+ console.error((e as Error).message);
76
+ process.exit(1);
77
+ }
78
+ });
79
+
60
80
  program
61
81
  .command("start")
62
82
  .summary("Run the server")
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "keryx",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/util/scaffold.ts CHANGED
@@ -51,6 +51,120 @@ export async function interactiveScaffold(
51
51
  return { projectName, options: { includeDb, includeExample } };
52
52
  }
53
53
 
54
+ /**
55
+ * Generate config file contents from the framework config directory,
56
+ * with imports rewritten for user projects.
57
+ * Returns a Map of relativePath → content (e.g., "config/index.ts" → "...").
58
+ */
59
+ export async function generateConfigFileContents(): Promise<
60
+ Map<string, string>
61
+ > {
62
+ const result = new Map<string, string>();
63
+ const configDir = path.join(import.meta.dir, "..", "config");
64
+ const glob = new Glob("**/*.ts");
65
+
66
+ for await (const file of glob.scan(configDir)) {
67
+ let content = await Bun.file(path.join(configDir, file)).text();
68
+
69
+ // Rewrite relative imports to package imports
70
+ content = content.replace(
71
+ /from ["']\.\.\/\.\.\/util\/config["']/g,
72
+ 'from "keryx"',
73
+ );
74
+ content = content.replace(
75
+ /from ["']\.\.\/util\/config["']/g,
76
+ 'from "keryx"',
77
+ );
78
+ content = content.replace(
79
+ /from ["']\.\.\/classes\/Logger["']/g,
80
+ 'from "keryx/classes/Logger.ts"',
81
+ );
82
+
83
+ // In index.ts, change `export const config` to `export default`
84
+ // and remove the KeryxConfig type export (it comes from the package)
85
+ if (file === "index.ts") {
86
+ content = content.replace("export const config =", "export default");
87
+ content = content.replace(
88
+ /\nexport type KeryxConfig = typeof config;\n/,
89
+ "\n",
90
+ );
91
+ }
92
+
93
+ result.set(`config/${file}`, content);
94
+ }
95
+
96
+ return result;
97
+ }
98
+
99
+ /**
100
+ * Generate built-in action file contents (status.ts, swagger.ts)
101
+ * with imports rewritten for user projects.
102
+ * Returns a Map of relativePath → content (e.g., "actions/status.ts" → "...").
103
+ */
104
+ export async function generateBuiltinActionContents(): Promise<
105
+ Map<string, string>
106
+ > {
107
+ const result = new Map<string, string>();
108
+ const builtinActions = ["status.ts", "swagger.ts"];
109
+ const actionsDir = path.join(import.meta.dir, "..", "actions");
110
+
111
+ for (const file of builtinActions) {
112
+ let content = await Bun.file(path.join(actionsDir, file)).text();
113
+
114
+ // Rewrite relative imports to package imports
115
+ content = content.replace(/from ["']\.\.\/api["']/g, 'from "keryx"');
116
+ content = content.replace(
117
+ /from ["']\.\.\/classes\/Action["']/g,
118
+ 'from "keryx/classes/Action.ts"',
119
+ );
120
+ content = content.replace(
121
+ /from ["']\.\.\/package\.json["']/g,
122
+ 'from "../package.json"',
123
+ );
124
+
125
+ result.set(`actions/${file}`, content);
126
+ }
127
+
128
+ return result;
129
+ }
130
+
131
+ /**
132
+ * Generate the tsconfig.json content for scaffolded projects.
133
+ */
134
+ export function generateTsconfigContents(): string {
135
+ return (
136
+ JSON.stringify(
137
+ {
138
+ compilerOptions: {
139
+ lib: ["ESNext"],
140
+ target: "ESNext",
141
+ module: "ESNext",
142
+ moduleResolution: "bundler",
143
+ types: ["bun-types"],
144
+ strict: true,
145
+ skipLibCheck: true,
146
+ noEmit: true,
147
+ esModuleInterop: true,
148
+ resolveJsonModule: true,
149
+ isolatedModules: true,
150
+ verbatimModuleSyntax: true,
151
+ noImplicitAny: true,
152
+ noImplicitReturns: true,
153
+ noUnusedLocals: true,
154
+ noUnusedParameters: true,
155
+ noFallthroughCasesInSwitch: true,
156
+ forceConsistentCasingInFileNames: true,
157
+ allowImportingTsExtensions: true,
158
+ },
159
+ include: ["**/*.ts"],
160
+ exclude: ["node_modules", "drizzle"],
161
+ },
162
+ null,
163
+ 2,
164
+ ) + "\n"
165
+ );
166
+ }
167
+
54
168
  export async function scaffoldProject(
55
169
  projectName: string,
56
170
  targetDir: string,
@@ -115,75 +229,17 @@ export async function scaffoldProject(
115
229
  ) + "\n",
116
230
  );
117
231
 
118
- // tsconfig.json is static JSON (no interpolation needed)
119
- await write(
120
- "tsconfig.json",
121
- JSON.stringify(
122
- {
123
- compilerOptions: {
124
- lib: ["ESNext"],
125
- target: "ESNext",
126
- module: "ESNext",
127
- moduleResolution: "bundler",
128
- types: ["bun-types"],
129
- strict: true,
130
- skipLibCheck: true,
131
- noEmit: true,
132
- esModuleInterop: true,
133
- resolveJsonModule: true,
134
- isolatedModules: true,
135
- verbatimModuleSyntax: true,
136
- noImplicitAny: true,
137
- noImplicitReturns: true,
138
- noUnusedLocals: true,
139
- noUnusedParameters: true,
140
- noFallthroughCasesInSwitch: true,
141
- forceConsistentCasingInFileNames: true,
142
- allowImportingTsExtensions: true,
143
- },
144
- include: ["**/*.ts"],
145
- exclude: ["node_modules", "drizzle"],
146
- },
147
- null,
148
- 2,
149
- ) + "\n",
150
- );
232
+ await write("tsconfig.json", generateTsconfigContents());
151
233
 
152
234
  await writeTemplate("index.ts", "index.ts.mustache");
153
235
  await writeTemplate("keryx.ts", "keryx.ts.mustache");
154
236
  await writeTemplate(".env.example", "env.example.mustache");
155
237
  await writeTemplate(".gitignore", "gitignore.mustache");
156
- // Copy config files from the framework, adjusting imports for user projects
157
- const configDir = path.join(import.meta.dir, "..", "config");
158
- const glob = new Glob("**/*.ts");
159
- for await (const file of glob.scan(configDir)) {
160
- let content = await Bun.file(path.join(configDir, file)).text();
161
238
 
162
- // Rewrite relative imports to package imports
163
- content = content.replace(
164
- /from ["']\.\.\/\.\.\/util\/config["']/g,
165
- 'from "keryx"',
166
- );
167
- content = content.replace(
168
- /from ["']\.\.\/util\/config["']/g,
169
- 'from "keryx"',
170
- );
171
- content = content.replace(
172
- /from ["']\.\.\/classes\/Logger["']/g,
173
- 'from "keryx/classes/Logger.ts"',
174
- );
175
-
176
- // In index.ts, change `export const config` to `export default`
177
- // and remove the KeryxConfig type export (it comes from the package)
178
- if (file === "index.ts") {
179
- content = content.replace("export const config =", "export default");
180
- content = content.replace(
181
- /\nexport type KeryxConfig = typeof config;\n/,
182
- "\n",
183
- );
184
- }
185
-
186
- await write(`config/${file}`, content);
239
+ // Copy config files from the framework, adjusting imports for user projects
240
+ const configFiles = await generateConfigFileContents();
241
+ for (const [filePath, content] of configFiles) {
242
+ await write(filePath, content);
187
243
  }
188
244
 
189
245
  // Create empty directories with .gitkeep
@@ -200,24 +256,9 @@ export async function scaffoldProject(
200
256
  }
201
257
 
202
258
  // --- Built-in actions (always included) ---
203
- // Copy status and swagger actions from the framework, adjusting imports
204
- const builtinActions = ["status.ts", "swagger.ts"];
205
- const actionsDir = path.join(import.meta.dir, "..", "actions");
206
- for (const file of builtinActions) {
207
- let content = await Bun.file(path.join(actionsDir, file)).text();
208
-
209
- // Rewrite relative imports to package imports
210
- content = content.replace(/from ["']\.\.\/api["']/g, 'from "keryx"');
211
- content = content.replace(
212
- /from ["']\.\.\/classes\/Action["']/g,
213
- 'from "keryx/classes/Action.ts"',
214
- );
215
- content = content.replace(
216
- /from ["']\.\.\/package\.json["']/g,
217
- 'from "../package.json"',
218
- );
219
-
220
- await write(`actions/${file}`, content);
259
+ const actionFiles = await generateBuiltinActionContents();
260
+ for (const [filePath, content] of actionFiles) {
261
+ await write(filePath, content);
221
262
  }
222
263
 
223
264
  // --- Example action ---
@@ -0,0 +1,163 @@
1
+ import fs from "fs";
2
+ import os from "os";
3
+ import path from "path";
4
+ import * as readline from "readline";
5
+ import pkg from "../package.json";
6
+ import {
7
+ generateBuiltinActionContents,
8
+ generateConfigFileContents,
9
+ generateTsconfigContents,
10
+ } from "./scaffold";
11
+
12
+ export interface UpgradeOptions {
13
+ dryRun: boolean;
14
+ force: boolean;
15
+ }
16
+
17
+ interface UpgradeSummary {
18
+ updated: number;
19
+ created: number;
20
+ skipped: number;
21
+ upToDate: number;
22
+ }
23
+
24
+ async function promptOverwrite(filePath: string): Promise<"y" | "n" | "d"> {
25
+ const rl = readline.createInterface({
26
+ input: process.stdin,
27
+ output: process.stdout,
28
+ });
29
+ return new Promise((resolve) => {
30
+ rl.question(` Overwrite ${filePath}? (y/n/d for diff) `, (answer) => {
31
+ rl.close();
32
+ const a = answer.trim().toLowerCase();
33
+ if (a === "d") resolve("d");
34
+ else if (a === "y") resolve("y");
35
+ else resolve("n");
36
+ });
37
+ });
38
+ }
39
+
40
+ async function showDiff(
41
+ existingPath: string,
42
+ newContent: string,
43
+ ): Promise<void> {
44
+ const tmpFile = path.join(
45
+ os.tmpdir(),
46
+ `keryx-upgrade-${Date.now()}-${Math.random().toString(36).slice(2)}`,
47
+ );
48
+ await Bun.write(tmpFile, newContent);
49
+ try {
50
+ const proc = Bun.spawn(["diff", "-u", existingPath, tmpFile], {
51
+ stdout: "pipe",
52
+ stderr: "pipe",
53
+ });
54
+ const output = await new Response(proc.stdout).text();
55
+ await proc.exited;
56
+ if (output) {
57
+ console.log(output);
58
+ }
59
+ } finally {
60
+ fs.unlinkSync(tmpFile);
61
+ }
62
+ }
63
+
64
+ export async function upgradeProject(
65
+ targetDir: string,
66
+ options: UpgradeOptions,
67
+ ): Promise<void> {
68
+ // Validate this is a keryx project
69
+ const pkgPath = path.join(targetDir, "package.json");
70
+ if (!fs.existsSync(pkgPath)) {
71
+ throw new Error(
72
+ "No package.json found. Run this command from a Keryx project directory.",
73
+ );
74
+ }
75
+
76
+ const projectPkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
77
+ const deps = {
78
+ ...projectPkg.dependencies,
79
+ ...projectPkg.devDependencies,
80
+ };
81
+ if (!deps.keryx) {
82
+ throw new Error(
83
+ 'This project does not have "keryx" as a dependency. Run this command from a Keryx project directory.',
84
+ );
85
+ }
86
+
87
+ console.log(`Upgrading project files to match keryx v${pkg.version}\n`);
88
+
89
+ // Generate all framework-owned file contents
90
+ const files = new Map<string, string>();
91
+
92
+ const configFiles = await generateConfigFileContents();
93
+ for (const [p, content] of configFiles) files.set(p, content);
94
+
95
+ const actionFiles = await generateBuiltinActionContents();
96
+ for (const [p, content] of actionFiles) files.set(p, content);
97
+
98
+ files.set("tsconfig.json", generateTsconfigContents());
99
+
100
+ const summary: UpgradeSummary = {
101
+ updated: 0,
102
+ created: 0,
103
+ skipped: 0,
104
+ upToDate: 0,
105
+ };
106
+
107
+ for (const [relativePath, newContent] of files) {
108
+ const fullPath = path.join(targetDir, relativePath);
109
+
110
+ if (!fs.existsSync(fullPath)) {
111
+ // New file — create it
112
+ if (!options.dryRun) {
113
+ fs.mkdirSync(path.dirname(fullPath), { recursive: true });
114
+ await Bun.write(fullPath, newContent);
115
+ }
116
+ console.log(` + created ${relativePath}`);
117
+ summary.created++;
118
+ continue;
119
+ }
120
+
121
+ const existingContent = await Bun.file(fullPath).text();
122
+ if (existingContent === newContent) {
123
+ console.log(` ✓ up to date ${relativePath}`);
124
+ summary.upToDate++;
125
+ continue;
126
+ }
127
+
128
+ // File differs
129
+ if (options.dryRun) {
130
+ console.log(` ⚡ would update ${relativePath}`);
131
+ summary.updated++;
132
+ continue;
133
+ }
134
+
135
+ if (options.force) {
136
+ await Bun.write(fullPath, newContent);
137
+ console.log(` ⚡ updated ${relativePath}`);
138
+ summary.updated++;
139
+ continue;
140
+ }
141
+
142
+ // Interactive prompt
143
+ let answer = await promptOverwrite(relativePath);
144
+ while (answer === "d") {
145
+ await showDiff(fullPath, newContent);
146
+ answer = await promptOverwrite(relativePath);
147
+ }
148
+
149
+ if (answer === "y") {
150
+ await Bun.write(fullPath, newContent);
151
+ console.log(` ⚡ updated ${relativePath}`);
152
+ summary.updated++;
153
+ } else {
154
+ console.log(` ⊘ skipped ${relativePath}`);
155
+ summary.skipped++;
156
+ }
157
+ }
158
+
159
+ console.log(
160
+ `\nUpdated ${summary.updated} file(s), created ${summary.created} file(s), ${summary.upToDate} already up to date` +
161
+ (summary.skipped > 0 ? `, ${summary.skipped} skipped` : ""),
162
+ );
163
+ }