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.
- package/initializers/db.ts +2 -2
- package/keryx.ts +20 -0
- package/package.json +1 -1
- package/util/scaffold.ts +122 -81
- package/util/upgrade.ts +163 -0
package/initializers/db.ts
CHANGED
|
@@ -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
|
-
|
|
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
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
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
-
|
|
204
|
-
const
|
|
205
|
-
|
|
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 ---
|
package/util/upgrade.ts
ADDED
|
@@ -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
|
+
}
|