@veducio/keja 0.0.0-stage → 0.3.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.
Files changed (96) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +39 -2
  3. package/examples/starter/.env.example +6 -0
  4. package/examples/starter/keja.config.ts +7 -0
  5. package/examples/starter/keja.versioning.ts +5 -0
  6. package/examples/starter/migrations/0001_init.sql +1 -0
  7. package/examples/starter/package.json +14 -0
  8. package/examples/starter/tsconfig.json +9 -0
  9. package/package.json +62 -4
  10. package/src/access/cloudflare.test.ts +131 -0
  11. package/src/access/cloudflare.ts +193 -0
  12. package/src/argus/call.test.ts +98 -0
  13. package/src/argus/call.ts +153 -0
  14. package/src/argus/heartbeat.ts +65 -0
  15. package/src/argus/register.test.ts +120 -0
  16. package/src/argus/register.ts +145 -0
  17. package/src/cli/banner.test.ts +98 -0
  18. package/src/cli/banner.ts +209 -0
  19. package/src/cli/commands/build.ts +12 -0
  20. package/src/cli/commands/dev.ts +12 -0
  21. package/src/cli/commands/generate.ts +12 -0
  22. package/src/cli/commands/init.ts +57 -0
  23. package/src/cli/commands/migrate.ts +12 -0
  24. package/src/cli/commands/seed.ts +12 -0
  25. package/src/cli/commands/start.ts +12 -0
  26. package/src/cli/dev-reload.test.ts +87 -0
  27. package/src/cli/dev-reload.ts +182 -0
  28. package/src/cli/generate-project.ts +39 -0
  29. package/src/cli/index.ts +29 -0
  30. package/src/cli/init-project.test.ts +312 -0
  31. package/src/cli/init-project.ts +416 -0
  32. package/src/cli/migrate-project.ts +31 -0
  33. package/src/cli/run-project.test.ts +50 -0
  34. package/src/cli/run-project.ts +93 -0
  35. package/src/cli/seed-project.ts +46 -0
  36. package/src/db/client.test.ts +49 -0
  37. package/src/db/client.ts +61 -0
  38. package/src/db/migrate.test.ts +141 -0
  39. package/src/db/migrate.ts +146 -0
  40. package/src/db/owned-sql.ts +202 -0
  41. package/src/db/postgres.ts +90 -0
  42. package/src/env/required-env.test.ts +64 -0
  43. package/src/env/required-env.ts +120 -0
  44. package/src/errors/framework-errors.ts +295 -0
  45. package/src/grant/grant.test.ts +130 -0
  46. package/src/grant/grant.ts +268 -0
  47. package/src/http/create-app.test.ts +340 -0
  48. package/src/http/create-app.ts +196 -0
  49. package/src/http/openapi.ts +111 -0
  50. package/src/process/slot.ts +18 -0
  51. package/src/project/config.test.ts +143 -0
  52. package/src/project/config.ts +450 -0
  53. package/src/project/load.test.ts +60 -0
  54. package/src/project/load.ts +158 -0
  55. package/src/project/reload-graph.test.ts +52 -0
  56. package/src/project/reload-graph.ts +175 -0
  57. package/src/repository/define.ts +123 -0
  58. package/src/repository/generate.test.ts +58 -0
  59. package/src/repository/generate.ts +117 -0
  60. package/src/repository/query.test.ts +99 -0
  61. package/src/repository/query.ts +287 -0
  62. package/src/repository/runtime.test.ts +154 -0
  63. package/src/repository/runtime.ts +434 -0
  64. package/src/repository/sql.test.ts +78 -0
  65. package/src/repository/sql.ts +282 -0
  66. package/src/route/define.test.ts +65 -0
  67. package/src/route/define.ts +307 -0
  68. package/src/route/middleware.test.ts +63 -0
  69. package/src/route/middleware.ts +144 -0
  70. package/src/seed/apply.test.ts +65 -0
  71. package/src/seed/apply.ts +11 -0
  72. package/src/seed/define.test.ts +51 -0
  73. package/src/seed/define.ts +55 -0
  74. package/src/seed/load.test.ts +54 -0
  75. package/src/seed/load.ts +67 -0
  76. package/src/seed/report.test.ts +16 -0
  77. package/src/seed/report.ts +15 -0
  78. package/src/self-test/checks.ts +88 -0
  79. package/src/self-test/run.test.ts +128 -0
  80. package/src/self-test/run.ts +105 -0
  81. package/src/server/identity.test.ts +57 -0
  82. package/src/server/identity.ts +116 -0
  83. package/src/server/index.test.ts +15 -0
  84. package/src/server/index.ts +171 -0
  85. package/src/server/swap.test.ts +89 -0
  86. package/src/server/swap.ts +29 -0
  87. package/src/typeid/id.test.ts +46 -0
  88. package/src/typeid/id.ts +136 -0
  89. package/src/typeid/owned.test.ts +73 -0
  90. package/src/typeid/owned.ts +193 -0
  91. package/src/vault/client.test.ts +128 -0
  92. package/src/vault/client.ts +282 -0
  93. package/src/versioning/define.test.ts +79 -0
  94. package/src/versioning/define.ts +173 -0
  95. package/src/versioning/select.test.ts +79 -0
  96. package/src/versioning/select.ts +62 -0
@@ -0,0 +1,209 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { Chalk } from "chalk";
4
+
5
+ export const veducioAnnotation =
6
+ "Veducio Keja is part of Veducio and its Labs program, with a mission to contribute quality software to open source.";
7
+
8
+ export function projectLabel(
9
+ pkg: { name?: string; version?: string },
10
+ fallbackName: string,
11
+ ) {
12
+ const name = pkg.name?.trim() || fallbackName;
13
+ const version = pkg.version?.trim();
14
+ return version ? `${name} ${version}` : name;
15
+ }
16
+
17
+ export function readProjectLabel(root = process.cwd()) {
18
+ const project = readProject(root);
19
+ return projectLabel(project, project.name);
20
+ }
21
+
22
+ export function formatBanner(options: {
23
+ name: string;
24
+ version?: string;
25
+ color?: boolean;
26
+ }) {
27
+ const style = ink(options.color === true);
28
+ const version = options.version?.trim();
29
+ const title = version
30
+ ? `${style.bold(options.name)} ${style.dim(version)}`
31
+ : style.bold(options.name);
32
+ const note = wrap(veducioAnnotation, 72)
33
+ .map((line) => ` ${style.dim(line)}`)
34
+ .join("\n");
35
+ return `\n ${title}\n\n${note}\n`;
36
+ }
37
+
38
+ export function formatAddresses(
39
+ publicUrl: string,
40
+ serviceUrl: string,
41
+ color = false,
42
+ ) {
43
+ const style = ink(color);
44
+ const row = (label: string, url: string) =>
45
+ ` ${style.dim(`- ${label.padEnd(8)}`)} ${url}`;
46
+ return `\n${row("Public", publicUrl)}\n${row("Service", serviceUrl)}\n`;
47
+ }
48
+
49
+ export function printStartup(root = process.cwd()) {
50
+ const project = readProject(root);
51
+ console.log(
52
+ formatBanner({
53
+ name: project.name,
54
+ version: project.version,
55
+ color: process.stdout.isTTY,
56
+ }),
57
+ );
58
+ }
59
+
60
+ export function formatProjectPaths(
61
+ paths: {
62
+ config: string | null;
63
+ versioning: string;
64
+ public: string;
65
+ service: string;
66
+ },
67
+ color = false,
68
+ ) {
69
+ const style = ink(color);
70
+ const row = (label: string, value: string) =>
71
+ ` ${style.dim(`- ${label.padEnd(10)}`)} ${value}`;
72
+ return `\n${row("Config", paths.config ?? "defaults")}\n${row("Versioning", paths.versioning)}\n${row("Public", paths.public)}\n${row("Service", paths.service)}\n`;
73
+ }
74
+
75
+ export function printProjectPaths(paths: {
76
+ config: string | null;
77
+ versioning: string;
78
+ public: string;
79
+ service: string;
80
+ }) {
81
+ console.log(formatProjectPaths(paths, process.stdout.isTTY === true));
82
+ }
83
+
84
+ export function formatIdentity(
85
+ identity: { name: string; public: string[]; service: string[] },
86
+ argus?: { url: string; env: string } | null,
87
+ color = false,
88
+ vault?: { address: string; mount: string } | null,
89
+ ) {
90
+ const style = ink(color);
91
+ const row = (label: string, value: string) =>
92
+ ` ${style.dim(`- ${label.padEnd(10)}`)} ${value}`;
93
+ const lines = [
94
+ row("Name", identity.name),
95
+ row("Public", identity.public.join(", ")),
96
+ row("Service", identity.service.join(", ")),
97
+ ];
98
+ if (argus) {
99
+ lines.push(row("Argus", `${argus.url} (${argus.env})`));
100
+ }
101
+ if (vault) {
102
+ lines.push(row("Vault", `${vault.address} (${vault.mount})`));
103
+ }
104
+ return `\n${lines.join("\n")}\n`;
105
+ }
106
+
107
+ export function printIdentity(
108
+ identity: { name: string; public: string[]; service: string[] },
109
+ argus?: { url: string; env: string } | null,
110
+ vault?: { address: string; mount: string } | null,
111
+ ) {
112
+ console.log(
113
+ formatIdentity(identity, argus, process.stdout.isTTY === true, vault),
114
+ );
115
+ }
116
+
117
+ export function formatDatabase(
118
+ database:
119
+ | { migrations: string; applied: string[] }
120
+ | { connected: true }
121
+ | null,
122
+ color = false,
123
+ ) {
124
+ const style = ink(color);
125
+ const row = (label: string, value: string) =>
126
+ ` ${style.dim(`- ${label.padEnd(10)}`)} ${value}`;
127
+ if (!database) {
128
+ return `\n${row("Database", "disabled")}\n`;
129
+ }
130
+ if (!("applied" in database)) {
131
+ return `\n${row("Database", "postgres")}\n`;
132
+ }
133
+ const applied =
134
+ database.applied.length > 0 ? database.applied.join(", ") : "up to date";
135
+ return `\n${row("Database", "postgres")}\n${row("Migrations", database.migrations)}\n${row("Applied", applied)}\n`;
136
+ }
137
+
138
+ export function printDatabase(
139
+ database:
140
+ | { migrations: string; applied: string[] }
141
+ | { connected: true }
142
+ | null,
143
+ ) {
144
+ console.log(formatDatabase(database, process.stdout.isTTY === true));
145
+ }
146
+
147
+ export function printAddresses(publicUrl: string, serviceUrl: string) {
148
+ console.log(
149
+ formatAddresses(publicUrl, serviceUrl, process.stdout.isTTY === true),
150
+ );
151
+ }
152
+
153
+ export function formatReloading(color = false) {
154
+ const style = ink(color);
155
+ return `\n ${style.dim("reloading...")}\n`;
156
+ }
157
+
158
+ export function formatProjectReady(color = false) {
159
+ const style = ink(color);
160
+ return `\n ${style.green("no issues found")}\n`;
161
+ }
162
+
163
+ export function printReloading() {
164
+ console.log(formatReloading(process.stdout.isTTY === true));
165
+ }
166
+
167
+ export function printProjectReady() {
168
+ console.log(formatProjectReady(process.stdout.isTTY === true));
169
+ }
170
+
171
+ function readProject(root: string) {
172
+ const fallbackName = path.basename(root);
173
+ try {
174
+ const parsed = JSON.parse(
175
+ readFileSync(path.join(root, "package.json"), "utf8"),
176
+ ) as { name?: unknown; version?: unknown };
177
+ return {
178
+ name:
179
+ typeof parsed.name === "string" && parsed.name.trim()
180
+ ? parsed.name.trim()
181
+ : fallbackName,
182
+ version: typeof parsed.version === "string" ? parsed.version : undefined,
183
+ };
184
+ } catch {
185
+ return { name: fallbackName, version: undefined };
186
+ }
187
+ }
188
+
189
+ function ink(color: boolean) {
190
+ return new Chalk({ level: color ? 1 : 0 });
191
+ }
192
+
193
+ function wrap(text: string, width: number) {
194
+ const lines: string[] = [];
195
+ let line = "";
196
+ for (const word of text.split(" ")) {
197
+ const next = line ? `${line} ${word}` : word;
198
+ if (next.length > width && line) {
199
+ lines.push(line);
200
+ line = word;
201
+ continue;
202
+ }
203
+ line = next;
204
+ }
205
+ if (line) {
206
+ lines.push(line);
207
+ }
208
+ return lines;
209
+ }
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+
3
+ export const build = defineCommand({
4
+ meta: {
5
+ name: "build",
6
+ description: "Build the server",
7
+ },
8
+ run() {
9
+ console.error("build is not implemented");
10
+ process.exit(1);
11
+ },
12
+ });
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+ import { runDev } from "../dev-reload.ts";
3
+
4
+ export const dev = defineCommand({
5
+ meta: {
6
+ name: "dev",
7
+ description: "Start the development server",
8
+ },
9
+ async run() {
10
+ await runDev();
11
+ },
12
+ });
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+ import { runGenerate } from "../generate-project.ts";
3
+
4
+ export const generate = defineCommand({
5
+ meta: {
6
+ name: "generate",
7
+ description: "Generate repositories from migrations",
8
+ },
9
+ async run() {
10
+ await runGenerate();
11
+ },
12
+ });
@@ -0,0 +1,57 @@
1
+ import { defineCommand } from "citty";
2
+ import {
3
+ askOnTerminal,
4
+ runCommand,
5
+ runInit,
6
+ starterPaths,
7
+ } from "../init-project.ts";
8
+
9
+ export const init = defineCommand({
10
+ meta: {
11
+ name: "init",
12
+ description: "Create a new project",
13
+ },
14
+ args: {
15
+ name: {
16
+ type: "string",
17
+ description: "Name of the project",
18
+ },
19
+ dir: {
20
+ type: "string",
21
+ description: "Where to create the project",
22
+ },
23
+ public: {
24
+ type: "string",
25
+ description: "Public route folder",
26
+ },
27
+ service: {
28
+ type: "string",
29
+ description: "Service route folder",
30
+ },
31
+ },
32
+ async run({ args }) {
33
+ const paths = starterPaths();
34
+ try {
35
+ await runInit({
36
+ input: {
37
+ name: args.name,
38
+ dir: args.dir,
39
+ public: args.public,
40
+ service: args.service,
41
+ },
42
+ cwd: process.cwd(),
43
+ templateDir: paths.templateDir,
44
+ packageRoot: paths.packageRoot,
45
+ interactive: process.stdin.isTTY === true,
46
+ ask: askOnTerminal,
47
+ run: runCommand,
48
+ log(message) {
49
+ console.log(message);
50
+ },
51
+ });
52
+ } catch (error) {
53
+ console.error(error instanceof Error ? error.message : "init failed");
54
+ process.exit(1);
55
+ }
56
+ },
57
+ });
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+ import { runMigrate } from "../migrate-project.ts";
3
+
4
+ export const migrate = defineCommand({
5
+ meta: {
6
+ name: "migrate",
7
+ description: "Apply project migrations",
8
+ },
9
+ async run() {
10
+ await runMigrate();
11
+ },
12
+ });
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+ import { runSeed } from "../seed-project.ts";
3
+
4
+ export const seed = defineCommand({
5
+ meta: {
6
+ name: "seed",
7
+ description: "Insert project seeders",
8
+ },
9
+ async run() {
10
+ await runSeed();
11
+ },
12
+ });
@@ -0,0 +1,12 @@
1
+ import { defineCommand } from "citty";
2
+ import { runProject } from "./../run-project.ts";
3
+
4
+ export const start = defineCommand({
5
+ meta: {
6
+ name: "start",
7
+ description: "Start the server",
8
+ },
9
+ async run() {
10
+ await runProject();
11
+ },
12
+ });
@@ -0,0 +1,87 @@
1
+ import { readFileSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { describe, expect, it } from "vitest";
4
+ import {
5
+ isIgnoredWatch,
6
+ refreshDevProject,
7
+ subscribeDirectories,
8
+ watchDirectories,
9
+ } from "./dev-reload.ts";
10
+
11
+ describe("dev reload", () => {
12
+ it("does not relaunch the process under bun --watch", () => {
13
+ const entry = readFileSync(new URL("./index.ts", import.meta.url), "utf8");
14
+ const dev = readFileSync(
15
+ new URL("./commands/dev.ts", import.meta.url),
16
+ "utf8",
17
+ );
18
+ expect(entry.includes("--watch")).toBe(false);
19
+ expect(dev.includes("--watch")).toBe(false);
20
+ });
21
+
22
+ it("watches the project and route dependencies outside it", () => {
23
+ const root = path.resolve("app");
24
+ expect(isIgnoredWatch("node_modules/pkg/index.ts")).toBe(true);
25
+ expect(isIgnoredWatch("src/data.ts")).toBe(false);
26
+ expect(
27
+ watchDirectories(
28
+ root,
29
+ {
30
+ config: path.join(root, "keja.config.ts"),
31
+ versioning: path.join(root, "keja.versioning.ts"),
32
+ public: path.join(root, "src", "public"),
33
+ service: path.join(root, "src", "service"),
34
+ },
35
+ ["D:/shared/schema.ts"],
36
+ ),
37
+ ).toContain(path.resolve("D:/shared"));
38
+ });
39
+
40
+ it("keeps the running project when a reload fails", async () => {
41
+ let swapped = false;
42
+ const result = await refreshDevProject({
43
+ running: true,
44
+ load: () => Promise.reject(new Error("bad route")),
45
+ boot: () => Promise.resolve(),
46
+ swap: () => {
47
+ swapped = true;
48
+ },
49
+ });
50
+ expect(swapped).toBe(false);
51
+ expect(result).toEqual({
52
+ error: expect.any(Error),
53
+ waiting: false,
54
+ });
55
+ });
56
+
57
+ it("boots when the servers are not up yet", async () => {
58
+ let booted = false;
59
+ const result = await refreshDevProject({
60
+ running: false,
61
+ load: () => Promise.reject(new Error("unused")),
62
+ boot: () => {
63
+ booted = true;
64
+ return Promise.resolve();
65
+ },
66
+ swap: () => {
67
+ throw new Error("swap");
68
+ },
69
+ });
70
+ expect(booted).toBe(true);
71
+ expect(result).toBe("booted");
72
+ });
73
+
74
+ it("subscribes through the injected watcher", () => {
75
+ const opened: string[] = [];
76
+ const subscription = subscribeDirectories(
77
+ [process.cwd()],
78
+ () => {},
79
+ (directory) => {
80
+ opened.push(directory);
81
+ return { close() {} };
82
+ },
83
+ );
84
+ expect(opened).toEqual([path.resolve(process.cwd())]);
85
+ subscription.close();
86
+ });
87
+ });
@@ -0,0 +1,182 @@
1
+ import { existsSync, watch as watchDirectory } from "node:fs";
2
+ import path from "node:path";
3
+ import type { LoadedProject } from "../project/load.ts";
4
+ import { loadProject } from "../project/load.ts";
5
+ import { projectDependenciesOutsideRoot } from "../project/reload-graph.ts";
6
+ import { listen } from "../server/index.ts";
7
+ import { printProjectReady, printReloading } from "./banner.ts";
8
+ import {
9
+ formatProjectFailure,
10
+ printServers,
11
+ readProjectFailure,
12
+ } from "./run-project.ts";
13
+
14
+ export const reloadDebounceMs = 50;
15
+
16
+ export function isIgnoredWatch(file: string) {
17
+ return file
18
+ .split(/[\\/]/)
19
+ .some(
20
+ (part) => part === "node_modules" || part === ".git" || part === "dist",
21
+ );
22
+ }
23
+
24
+ export function watchDirectories(
25
+ root: string,
26
+ paths: LoadedProject["paths"],
27
+ outside: readonly string[],
28
+ ) {
29
+ const dirs = [
30
+ root,
31
+ paths.public,
32
+ paths.service,
33
+ path.dirname(paths.versioning),
34
+ ];
35
+ if (paths.config) {
36
+ dirs.push(path.dirname(paths.config));
37
+ }
38
+ for (const file of outside) {
39
+ dirs.push(path.dirname(file));
40
+ }
41
+ return [...new Set(dirs.map((dir) => path.resolve(dir)))];
42
+ }
43
+
44
+ export async function refreshDevProject(input: {
45
+ running: boolean;
46
+ load: () => Promise<LoadedProject>;
47
+ boot: () => Promise<void>;
48
+ swap: (project: LoadedProject) => void;
49
+ }) {
50
+ if (!input.running) {
51
+ try {
52
+ await input.boot();
53
+ return "booted" as const;
54
+ } catch (error) {
55
+ return { error, waiting: true as const };
56
+ }
57
+ }
58
+ try {
59
+ const project = await input.load();
60
+ input.swap(project);
61
+ return "reloaded" as const;
62
+ } catch (error) {
63
+ return { error, waiting: false as const };
64
+ }
65
+ }
66
+
67
+ export function subscribeDirectories(
68
+ directories: readonly string[],
69
+ onFile: (file: string | null) => void,
70
+ open: (
71
+ directory: string,
72
+ onFile: (file: string | null) => void,
73
+ ) => { close: () => void } = defaultWatch,
74
+ ) {
75
+ const watchers = new Map<string, { close: () => void }>();
76
+ const add = (directory: string) => {
77
+ const resolved = path.resolve(directory);
78
+ if (watchers.has(resolved) || !existsSync(resolved)) {
79
+ return;
80
+ }
81
+ watchers.set(resolved, open(resolved, onFile));
82
+ };
83
+ for (const directory of directories) {
84
+ add(directory);
85
+ }
86
+ return {
87
+ add,
88
+ close() {
89
+ for (const watcher of watchers.values()) {
90
+ watcher.close();
91
+ }
92
+ watchers.clear();
93
+ },
94
+ };
95
+ }
96
+
97
+ export async function runDev(root = process.cwd()) {
98
+ let running: Awaited<ReturnType<typeof listen>> | null = null;
99
+ let generation = 0;
100
+ let busy = false;
101
+ let queued = false;
102
+ let timer: ReturnType<typeof setTimeout> | null = null;
103
+ const refresh = async () => {
104
+ if (busy) {
105
+ queued = true;
106
+ return;
107
+ }
108
+ busy = true;
109
+ try {
110
+ generation += 1;
111
+ const token = String(generation);
112
+ if (running) {
113
+ printReloading();
114
+ }
115
+ const result = await refreshDevProject({
116
+ running: running !== null,
117
+ load: () => loadProject(root, { generation: token }),
118
+ boot: async () => {
119
+ running = await listen(root, { generation: token });
120
+ printServers(running);
121
+ },
122
+ swap(project) {
123
+ running?.reload(project);
124
+ },
125
+ });
126
+ if (result === "reloaded") {
127
+ printProjectReady();
128
+ } else if (typeof result === "object") {
129
+ console.error(
130
+ formatProjectFailure(readProjectFailure(result.error), {
131
+ wait: result.waiting,
132
+ color: process.stderr.isTTY === true,
133
+ }),
134
+ );
135
+ }
136
+ if (running) {
137
+ for (const directory of watchDirectories(
138
+ root,
139
+ running.paths,
140
+ projectDependenciesOutsideRoot(),
141
+ )) {
142
+ subscription.add(directory);
143
+ }
144
+ }
145
+ } finally {
146
+ busy = false;
147
+ if (queued) {
148
+ queued = false;
149
+ await refresh();
150
+ }
151
+ }
152
+ };
153
+ const subscription = subscribeDirectories([root], (file) => {
154
+ if (file && isIgnoredWatch(file)) {
155
+ return;
156
+ }
157
+ if (timer) {
158
+ clearTimeout(timer);
159
+ }
160
+ timer = setTimeout(() => {
161
+ timer = null;
162
+ void refresh();
163
+ }, reloadDebounceMs);
164
+ });
165
+ await refresh();
166
+ await new Promise(() => {});
167
+ }
168
+
169
+ function defaultWatch(
170
+ directory: string,
171
+ onFile: (file: string | null) => void,
172
+ ) {
173
+ const watcher = watchDirectory(
174
+ directory,
175
+ { recursive: true },
176
+ (_event, filename) => {
177
+ onFile(filename === null ? null : String(filename));
178
+ },
179
+ );
180
+ watcher.on("error", () => {});
181
+ return { close: () => watcher.close() };
182
+ }
@@ -0,0 +1,39 @@
1
+ import { Chalk } from "chalk";
2
+ import { generateRepositories } from "../repository/generate.ts";
3
+ import { formatProjectFailure, readProjectFailure } from "./run-project.ts";
4
+
5
+ export function formatGenerate(
6
+ output: string,
7
+ tables: readonly string[],
8
+ color = false,
9
+ ) {
10
+ const style = new Chalk({ level: color ? 1 : 0 });
11
+ const row = (label: string, value: string) =>
12
+ ` ${style.dim(`- ${label.padEnd(10)}`)} ${value}`;
13
+ const names = tables.length > 0 ? tables.join(", ") : "none";
14
+ return `\n ${style.dim("GENERATE")}\n\n${row("File", output)}\n${row("Tables", names)}\n`;
15
+ }
16
+
17
+ export async function runGenerate(root = process.cwd()) {
18
+ try {
19
+ const generated = await generateRepositories(root);
20
+ console.log(
21
+ formatGenerate(
22
+ generated.output,
23
+ generated.tables,
24
+ process.stdout.isTTY === true,
25
+ ),
26
+ );
27
+ } catch (error) {
28
+ console.error(
29
+ formatProjectFailure(readProjectFailure(error), {
30
+ wait: false,
31
+ color: process.stderr.isTTY === true,
32
+ }),
33
+ );
34
+ process.exitCode = 1;
35
+ }
36
+ if (process.exitCode === 1) {
37
+ process.exit(1);
38
+ }
39
+ }
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env bun
2
+ import { defineCommand, runMain } from "citty";
3
+ import { printStartup } from "./banner.ts";
4
+ import { build } from "./commands/build.ts";
5
+ import { dev } from "./commands/dev.ts";
6
+ import { generate } from "./commands/generate.ts";
7
+ import { init } from "./commands/init.ts";
8
+ import { migrate } from "./commands/migrate.ts";
9
+ import { seed } from "./commands/seed.ts";
10
+ import { start } from "./commands/start.ts";
11
+
12
+ const main = defineCommand({
13
+ meta: {
14
+ name: "keja",
15
+ description: "Run Keja",
16
+ },
17
+ subCommands: {
18
+ dev,
19
+ start,
20
+ migrate,
21
+ generate,
22
+ seed,
23
+ build,
24
+ init,
25
+ },
26
+ });
27
+
28
+ printStartup();
29
+ runMain(main);