@zerotal/core 1.0.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 (201) hide show
  1. package/CHANGELOG.md +79 -0
  2. package/LICENSE +21 -0
  3. package/README.md +128 -0
  4. package/package.json +72 -0
  5. package/src/application/Application.ts +1671 -0
  6. package/src/application/BootDoctor.ts +108 -0
  7. package/src/application/DevErrorPage.ts +567 -0
  8. package/src/application/ExceptionHandler.ts +183 -0
  9. package/src/application/currentApp.ts +73 -0
  10. package/src/assets/assets.ts +79 -0
  11. package/src/assets/index.ts +16 -0
  12. package/src/auth/AuthenticatedUser.ts +18 -0
  13. package/src/build/PackageLinter.ts +146 -0
  14. package/src/build/PackageScaffold.ts +127 -0
  15. package/src/build/codemod.ts +64 -0
  16. package/src/build/index.ts +12 -0
  17. package/src/command/Command.ts +254 -0
  18. package/src/command/CommandRunner.ts +593 -0
  19. package/src/command/OutputWriter.ts +61 -0
  20. package/src/command/builtin/CompileCommand.ts +46 -0
  21. package/src/command/builtin/CssBuildCommand.ts +71 -0
  22. package/src/command/builtin/KeyGenerateCommand.ts +58 -0
  23. package/src/command/builtin/LintPackagesCommand.ts +72 -0
  24. package/src/command/builtin/MakeCommandCommand.ts +85 -0
  25. package/src/command/builtin/MakeControllerCommand.ts +95 -0
  26. package/src/command/builtin/MakeEventCommand.ts +85 -0
  27. package/src/command/builtin/MakeJobCommand.ts +53 -0
  28. package/src/command/builtin/MakeListenerCommand.ts +35 -0
  29. package/src/command/builtin/MakeMiddlewareCommand.ts +63 -0
  30. package/src/command/builtin/MakeNotificationCommand.ts +48 -0
  31. package/src/command/builtin/MakeObserverCommand.ts +78 -0
  32. package/src/command/builtin/MakePackageCommand.ts +45 -0
  33. package/src/command/builtin/MakePolicyCommand.ts +66 -0
  34. package/src/command/builtin/MakeProviderCommand.ts +75 -0
  35. package/src/command/builtin/MakeRequestCommand.ts +47 -0
  36. package/src/command/builtin/MakeResourceCommand.ts +61 -0
  37. package/src/command/builtin/MakeTestCommand.ts +120 -0
  38. package/src/command/builtin/ReloadCommand.ts +52 -0
  39. package/src/command/builtin/ReplCommand.ts +174 -0
  40. package/src/command/builtin/RouteListCommand.ts +188 -0
  41. package/src/command/builtin/ServeCommand.ts +321 -0
  42. package/src/command/builtin/StartCommand.ts +3 -0
  43. package/src/command/builtin/StatusCommand.ts +71 -0
  44. package/src/command/builtin/TestCommand.ts +172 -0
  45. package/src/command/builtin/WorkerCommand.ts +27 -0
  46. package/src/command/builtin/index.ts +53 -0
  47. package/src/command/scaffold/worker.ts.txt +12 -0
  48. package/src/command/scaffold/zerotal.ts.txt +26 -0
  49. package/src/command/startZerotal.ts +55 -0
  50. package/src/config/AppConfig.ts +253 -0
  51. package/src/config/ConfigLoader.ts +117 -0
  52. package/src/config/ConfigManager.ts +169 -0
  53. package/src/config/index.ts +46 -0
  54. package/src/config/registry.ts +59 -0
  55. package/src/config/validation.ts +117 -0
  56. package/src/container/Container.ts +606 -0
  57. package/src/container/ContextualBindingBuilder.ts +57 -0
  58. package/src/container/ScopedResolver.ts +117 -0
  59. package/src/container/index.ts +32 -0
  60. package/src/container/inject.ts +55 -0
  61. package/src/container/types.ts +71 -0
  62. package/src/context/RequestContext.ts +91 -0
  63. package/src/contracts/auth.ts +24 -0
  64. package/src/contracts/index.ts +23 -0
  65. package/src/contracts/session.ts +70 -0
  66. package/src/contracts/transaction.ts +26 -0
  67. package/src/conventions/ConventionLoader.ts +128 -0
  68. package/src/conventions/builtinConcerns.ts +131 -0
  69. package/src/crypt/Crypt.ts +141 -0
  70. package/src/crypt/URLSigner.ts +96 -0
  71. package/src/datetime/Carbon.ts +1396 -0
  72. package/src/datetime/CarbonInterval.ts +421 -0
  73. package/src/datetime/clock.ts +28 -0
  74. package/src/datetime/index.ts +23 -0
  75. package/src/datetime/temporal-shim.ts +1 -0
  76. package/src/dev/BuildOutput.ts +131 -0
  77. package/src/dev/CssPlugins.ts +184 -0
  78. package/src/dev/DevBuildHook.ts +74 -0
  79. package/src/dev/DevOrchestrator.ts +213 -0
  80. package/src/dev/DevReloadMiddleware.ts +101 -0
  81. package/src/dev/DevReloadServer.ts +85 -0
  82. package/src/dev/DevWsServer.ts +45 -0
  83. package/src/dev/index.ts +19 -0
  84. package/src/dev/reloadClient.ts +39 -0
  85. package/src/env/Def.ts +232 -0
  86. package/src/env/EnvSchema.ts +105 -0
  87. package/src/env/index.ts +34 -0
  88. package/src/env/t.ts +128 -0
  89. package/src/errors/ConfigError.ts +12 -0
  90. package/src/errors/ContainerErrors.ts +143 -0
  91. package/src/errors/HttpError.ts +127 -0
  92. package/src/errors/ValidationError.ts +19 -0
  93. package/src/errors/ZerotalError.ts +25 -0
  94. package/src/errors/index.ts +46 -0
  95. package/src/events/CallQueuedListener.ts +66 -0
  96. package/src/events/Emitter.ts +280 -0
  97. package/src/events/EventFake.ts +160 -0
  98. package/src/events/FrameworkEvents.ts +252 -0
  99. package/src/facade/Facade.ts +101 -0
  100. package/src/facade/facades/App.ts +155 -0
  101. package/src/facade/facades/Artisan.ts +63 -0
  102. package/src/facade/facades/Config.ts +21 -0
  103. package/src/facade/facades/Events.ts +19 -0
  104. package/src/facade/facades/index.ts +28 -0
  105. package/src/global.d.ts +9 -0
  106. package/src/hash/Hash.ts +60 -0
  107. package/src/health/Health.ts +221 -0
  108. package/src/health/index.ts +27 -0
  109. package/src/helpers/Collection.ts +435 -0
  110. package/src/helpers/config.ts +59 -0
  111. package/src/helpers/fluent.ts +52 -0
  112. package/src/helpers/html.ts +11 -0
  113. package/src/helpers/index.ts +266 -0
  114. package/src/helpers/make.ts +35 -0
  115. package/src/helpers/markdown.ts +73 -0
  116. package/src/helpers/pageElements.ts +27 -0
  117. package/src/helpers/request.ts +62 -0
  118. package/src/helpers/response.ts +411 -0
  119. package/src/helpers/str.ts +208 -0
  120. package/src/http/Http.ts +298 -0
  121. package/src/http/HttpClient.ts +289 -0
  122. package/src/http/Resource.ts +171 -0
  123. package/src/http/UploadedFile.ts +204 -0
  124. package/src/http/Uri.ts +490 -0
  125. package/src/http/index.ts +46 -0
  126. package/src/http/negotiate.ts +213 -0
  127. package/src/http/originGuard.ts +76 -0
  128. package/src/http/sniffContentType.ts +105 -0
  129. package/src/http/url.ts +204 -0
  130. package/src/http/withHeaders.ts +24 -0
  131. package/src/index.ts +250 -0
  132. package/src/lock/LockManager.ts +228 -0
  133. package/src/lock/config.ts +49 -0
  134. package/src/lock/drivers/LockDriver.ts +32 -0
  135. package/src/lock/drivers/MemoryLockDriver.ts +52 -0
  136. package/src/lock/drivers/RedisLockDriver.ts +58 -0
  137. package/src/lock/drivers/SqliteLockDriver.ts +85 -0
  138. package/src/lock/errors.ts +20 -0
  139. package/src/lock/facades/Lock.ts +114 -0
  140. package/src/lock/index.ts +53 -0
  141. package/src/logger/Log.ts +35 -0
  142. package/src/logger/LogManager.ts +430 -0
  143. package/src/logger/LoggerMiddleware.ts +125 -0
  144. package/src/logger/channels/ConsoleChannel.ts +139 -0
  145. package/src/logger/channels/DailyChannel.ts +74 -0
  146. package/src/logger/channels/NullChannel.ts +17 -0
  147. package/src/logger/channels/SingleChannel.ts +34 -0
  148. package/src/logger/channels/StackChannel.ts +29 -0
  149. package/src/logger/config.ts +90 -0
  150. package/src/logger/format.ts +96 -0
  151. package/src/logger/frameworkLog.ts +93 -0
  152. package/src/logger/index.ts +68 -0
  153. package/src/logger/renderTable.ts +111 -0
  154. package/src/logger/types.ts +212 -0
  155. package/src/macros/config.macro.ts +50 -0
  156. package/src/metrics/HttpMetrics.ts +114 -0
  157. package/src/metrics/index.ts +18 -0
  158. package/src/middleware/BaseMiddleware.ts +72 -0
  159. package/src/middleware/CorsMiddleware.ts +152 -0
  160. package/src/middleware/RateLimiter.ts +255 -0
  161. package/src/middleware/SecureHeadersMiddleware.ts +127 -0
  162. package/src/middleware/ThrottleMiddleware.ts +252 -0
  163. package/src/middleware/WebhookMiddleware.ts +204 -0
  164. package/src/pipeline/ContextRegistry.ts +42 -0
  165. package/src/pipeline/HttpContext.ts +865 -0
  166. package/src/pipeline/Pipeline.ts +150 -0
  167. package/src/pipeline/currentPage.ts +46 -0
  168. package/src/pipeline/types.ts +80 -0
  169. package/src/provider/LockProvider.ts +64 -0
  170. package/src/provider/LogProvider.ts +137 -0
  171. package/src/provider/ServiceProvider.ts +84 -0
  172. package/src/provider/StorageProvider.ts +45 -0
  173. package/src/router/FileRouter.ts +526 -0
  174. package/src/router/Route.ts +76 -0
  175. package/src/router/RouteHandler.ts +335 -0
  176. package/src/router/Router.ts +1247 -0
  177. package/src/router/domain.ts +65 -0
  178. package/src/security/index.ts +22 -0
  179. package/src/storage/FakeDisk.ts +233 -0
  180. package/src/storage/StorageFilesMiddleware.ts +150 -0
  181. package/src/storage/StorageManager.ts +173 -0
  182. package/src/storage/config.ts +47 -0
  183. package/src/storage/drivers/LocalDriver.ts +138 -0
  184. package/src/storage/drivers/S3Driver.ts +169 -0
  185. package/src/storage/errors.ts +135 -0
  186. package/src/storage/facades/Storage.ts +3 -0
  187. package/src/storage/global.d.ts +7 -0
  188. package/src/storage/index.ts +22 -0
  189. package/src/storage/root.ts +59 -0
  190. package/src/storage/types.ts +104 -0
  191. package/src/support/appKey.ts +38 -0
  192. package/src/support/cookie.ts +72 -0
  193. package/src/support/crypto.ts +52 -0
  194. package/src/support/deepMerge.ts +117 -0
  195. package/src/support/env.ts +71 -0
  196. package/src/support/network.ts +79 -0
  197. package/src/support/port.ts +197 -0
  198. package/src/support/str.ts +122 -0
  199. package/src/view/FileRouteResolver.ts +59 -0
  200. package/src/view/index.ts +144 -0
  201. package/src/view/jsx-runtime.ts +233 -0
@@ -0,0 +1,71 @@
1
+ import { Command } from "../Command.ts";
2
+ import { buildCssBundle } from "../../dev/CssPlugins.ts";
3
+
4
+ /**
5
+ * `bun zt css:build` — builds the Tailwind CSS bundle for production.
6
+ *
7
+ * Used by Pulse and View apps (Inertia uses `inertia:build` instead).
8
+ *
9
+ * Input: resources/css/app.css
10
+ * Output: public/css/app.css
11
+ *
12
+ * @category Build & assets
13
+ */
14
+ export class CssBuildCommand extends Command {
15
+ static commandName = "css:build";
16
+ static description = "Build the Tailwind CSS bundle for production";
17
+ static needsApp = false;
18
+
19
+ static flags = [
20
+ {
21
+ name: "input",
22
+ short: "i",
23
+ type: "string" as const,
24
+ description: "Path to the CSS entry point",
25
+ default: "resources/css/app.css",
26
+ },
27
+ {
28
+ name: "output",
29
+ short: "o",
30
+ type: "string" as const,
31
+ description: "Output directory",
32
+ default: "public/css",
33
+ },
34
+ {
35
+ name: "minify",
36
+ short: "m",
37
+ type: "boolean" as const,
38
+ description: "Minify the output",
39
+ default: true,
40
+ },
41
+ ];
42
+
43
+ async run(): Promise<void> {
44
+ const cwd = process.cwd();
45
+ const input = this.flags["input"] as string;
46
+ const output = this.flags["output"] as string;
47
+ const minify = this.flags["minify"] as boolean;
48
+
49
+ const absoluteInput = input.startsWith("/") ? input : `${cwd}/${input}`;
50
+ const absoluteOutput = output.startsWith("/") ? output : `${cwd}/${output}`;
51
+
52
+ const cssExists = await Bun.file(absoluteInput).exists();
53
+ if (!cssExists) {
54
+ this.error(`CSS entry point not found: ${input}`);
55
+ return;
56
+ }
57
+
58
+ this.info(`Building CSS: ${input} → ${output}/`);
59
+
60
+ const result = await buildCssBundle(absoluteInput, absoluteOutput, minify);
61
+
62
+ if (!result.success) {
63
+ for (const log of result.logs) {
64
+ this.error(String(log));
65
+ }
66
+ throw new Error("CSS build failed.");
67
+ }
68
+
69
+ this.info("CSS build complete.");
70
+ }
71
+ }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * The `key:generate` command and the helpers it uses to mint an APP_KEY and
3
+ * write it into the project's `.env` file.
4
+ */
5
+ import { Command } from "../Command.ts";
6
+
7
+ /** Generate a random 32-byte base64 application key. */
8
+ export function generateKey(): string {
9
+ const bytes = new Uint8Array(32);
10
+ crypto.getRandomValues(bytes);
11
+ return Buffer.from(bytes).toString("base64");
12
+ }
13
+
14
+ /** Return true if the given .env content already has an APP_KEY line. */
15
+ function hasAppKey(content: string): boolean {
16
+ return content.includes("APP_KEY=");
17
+ }
18
+
19
+ /** Insert or replace the APP_KEY line in .env file content. */
20
+ export function updateEnvContent(content: string, key: string): string {
21
+ return hasAppKey(content)
22
+ ? content.replace(/^APP_KEY=.*$/m, `APP_KEY=${key}`)
23
+ : content + `\nAPP_KEY=${key}\n`;
24
+ }
25
+
26
+ /**
27
+ * `bun zt key:generate` — generates a new random APP_KEY and writes it to the
28
+ * project's `.env` file.
29
+ *
30
+ * @category App setup
31
+ */
32
+ export class KeyGenerateCommand extends Command {
33
+ static commandName = "key:generate";
34
+ static description = "Generate a new APP_KEY and write it to .env";
35
+ static needsApp = false;
36
+
37
+ static get args() {
38
+ return [];
39
+ }
40
+ static get flags() {
41
+ return [];
42
+ }
43
+
44
+ run(): Promise<void> {
45
+ const key = generateKey();
46
+ const envPath = ".env";
47
+
48
+ return Bun.file(envPath)
49
+ .text()
50
+ .catch(() => "")
51
+ .then((content) => {
52
+ return Bun.write(envPath, updateEnvContent(content, key)).then(() => {
53
+ this.info("APP_KEY generated and written to .env");
54
+ this.dim(` APP_KEY=${key}`);
55
+ });
56
+ });
57
+ }
58
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * The `lint:packages` command, which checks every workspace package against the
3
+ * documented conventions and prints a conformance report.
4
+ */
5
+ import { Command } from "../Command.ts";
6
+ import { lintPackages, countViolations, type Severity } from "../../build/PackageLinter.ts";
7
+
8
+ const SEVERITY_COLOR: Record<Severity, string> = {
9
+ high: "\x1b[31m",
10
+ medium: "\x1b[33m",
11
+ low: "\x1b[2m",
12
+ };
13
+ const RESET = "\x1b[0m";
14
+
15
+ /**
16
+ * `bun zt lint:packages` — checks every workspace package under a directory
17
+ * against the documented framework conventions and prints a conformance report.
18
+ *
19
+ * @category Diagnostics
20
+ */
21
+ export class LintPackagesCommand extends Command {
22
+ static commandName = "lint:packages";
23
+ static description = "Check every package against the documented conventions";
24
+ static needsApp = false;
25
+ static args = [{ name: "dir", required: false, default: "./packages" }];
26
+ static flags = [
27
+ {
28
+ name: "quiet",
29
+ short: "q",
30
+ type: "boolean" as const,
31
+ description: "Only print the summary",
32
+ default: false,
33
+ },
34
+ ];
35
+
36
+ async run(): Promise<void> {
37
+ const dir = (this.args["dir"] as string | undefined) || "./packages";
38
+ const quiet = this.flags["quiet"] === true;
39
+ const reports = await lintPackages(dir);
40
+ if (reports.length === 0) {
41
+ this.error(`No packages found under ${dir}`);
42
+ throw new Error(`lint:packages: no packages found under ${dir}`);
43
+ }
44
+ const total = countViolations(reports);
45
+ const clean = reports.filter((report) => report.violations.length === 0).length;
46
+ if (!quiet) {
47
+ this.section(`Package conformance — ${reports.length} packages`);
48
+ for (const report of reports) {
49
+ if (report.violations.length === 0) {
50
+ this.info(` ✓ ${report.package}`);
51
+ continue;
52
+ }
53
+ this.write(` \x1b[31m✗\x1b[0m ${report.package}\n`);
54
+ for (const violation of report.violations) {
55
+ const color = SEVERITY_COLOR[violation.severity];
56
+ this.write(
57
+ ` ${color}${violation.severity.padEnd(6)}${RESET} ${violation.rule.padEnd(22)} ${violation.message}\n`,
58
+ );
59
+ }
60
+ }
61
+ this.newLine();
62
+ }
63
+ if (total === 0) {
64
+ this.info(`✓ All ${reports.length} packages conform.`);
65
+ return;
66
+ }
67
+ this.error(
68
+ `✖ ${total} violation${total === 1 ? "" : "s"} across ${reports.length - clean} package${reports.length - clean === 1 ? "" : "s"} (${clean} clean).`,
69
+ );
70
+ throw new Error(`lint:packages: ${total} violation(s)`);
71
+ }
72
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `make:command` command and the CLI-command source stub it writes.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /** Convert a name to kebab-case (e.g. `SendDailyReport` → `send-daily-report`). */
7
+ export function toKebab(name: string): string {
8
+ return name
9
+ .replace(/([a-z0-9])([A-Z])/g, "$1-$2")
10
+ .replace(/[_\s]+/g, "-")
11
+ .replace(/-+/g, "-")
12
+ .toLowerCase();
13
+ }
14
+
15
+ /** Source for a new CLI command class extending `Command`. */
16
+ export function commandStub(name: string): string {
17
+ return `import { Command } from '@zerotal/core';
18
+ import type { ArgDef, FlagDef } from '@zerotal/core';
19
+
20
+ export class ${name} extends Command {
21
+ static commandName = '${toKebab(name)}';
22
+ static description = 'Describe what this command does';
23
+ static needsApp = true;
24
+
25
+ static args: ArgDef[] = [
26
+ // { name: 'target', description: 'The target', required: true },
27
+ ];
28
+
29
+ static flags: FlagDef[] = [
30
+ // { name: 'dry-run', short: 'd', type: 'boolean' as const,
31
+ // description: 'Preview without making changes', default: false },
32
+ ];
33
+
34
+ async run(): Promise<void> {
35
+ this.section('${name}');
36
+ // your implementation here
37
+ this.info('Done.');
38
+ }
39
+ }
40
+ `;
41
+ }
42
+
43
+ function commandPath(name: string): string {
44
+ return `app/commands/${name}.ts`;
45
+ }
46
+
47
+ /**
48
+ * `bun zt make:command <name>` — scaffolds a new CLI command class (extending
49
+ * {@link Command}) under `app/commands/`.
50
+ *
51
+ * @category Scaffolding (make:*)
52
+ */
53
+ export class MakeCommandCommand extends Command {
54
+ static commandName = "make:command";
55
+ static description = "Create a new CLI command class";
56
+ static needsApp = false;
57
+
58
+ static get args() {
59
+ return [
60
+ { name: "name", required: true, description: "Command class name (e.g. SendDailyReport)" },
61
+ ];
62
+ }
63
+
64
+ static get flags() {
65
+ return [];
66
+ }
67
+
68
+ run(): Promise<void> {
69
+ const name = this.args["name"]!;
70
+ const path = commandPath(name);
71
+
72
+ return Bun.file(path)
73
+ .exists()
74
+ .then((exists) => {
75
+ if (exists) {
76
+ this.error(`File already exists: ${path}`);
77
+ return;
78
+ }
79
+ // Bun.write() creates any missing parent directories, so no mkdir is needed.
80
+ return Bun.write(path, commandStub(name)).then(() => {
81
+ this.info(`Created: ${path}`);
82
+ });
83
+ });
84
+ }
85
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * The `make:controller` command and the controller source stubs it writes.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /** Source for a minimal controller with a single `index` action. */
7
+ export function basicStub(name: string): string {
8
+ return `import type { HttpContext } from '@zerotal/core';
9
+
10
+ export class ${name} {
11
+ async index(ctx: HttpContext): Promise<void> {
12
+ ctx.response = Response.json({ message: 'ok' });
13
+ }
14
+ }
15
+ `;
16
+ }
17
+
18
+ /** Source for a resourceful controller with full CRUD action stubs. */
19
+ export function resourceStub(name: string): string {
20
+ return `import type { HttpContext } from '@zerotal/core';
21
+
22
+ export class ${name} {
23
+ async index(ctx: HttpContext): Promise<void> {
24
+ ctx.response = Response.json([]);
25
+ }
26
+
27
+ async show(ctx: HttpContext): Promise<void> {
28
+ const { id } = ctx.params;
29
+ ctx.response = Response.json({ id });
30
+ }
31
+
32
+ async store(ctx: HttpContext): Promise<void> {
33
+ const body = await ctx.request.json();
34
+ ctx.response = Response.json(body, { status: 201 });
35
+ }
36
+
37
+ async update(ctx: HttpContext): Promise<void> {
38
+ const body = await ctx.request.json();
39
+ ctx.response = Response.json(body);
40
+ }
41
+
42
+ async destroy(ctx: HttpContext): Promise<void> {
43
+ ctx.response = new Response(null, { status: 204 });
44
+ }
45
+ }
46
+ `;
47
+ }
48
+
49
+ /**
50
+ * `bun zt make:controller <name>` — scaffolds a new controller class under
51
+ * `app/controllers/` (pass `--resource` for a full CRUD controller).
52
+ *
53
+ * @category Scaffolding (make:*)
54
+ */
55
+ export class MakeControllerCommand extends Command {
56
+ static commandName = "make:controller";
57
+ static description = "Create a new controller class";
58
+ static needsApp = false;
59
+
60
+ static get args() {
61
+ return [
62
+ { name: "name", required: true, description: "Controller class name (e.g. UserController)" },
63
+ ];
64
+ }
65
+
66
+ static get flags() {
67
+ return [
68
+ {
69
+ name: "resource",
70
+ type: "boolean" as const,
71
+ description: "Include CRUD action stubs",
72
+ default: false,
73
+ },
74
+ ];
75
+ }
76
+
77
+ run(): Promise<void> {
78
+ const name = this.args["name"]!;
79
+ const resource = this.flags["resource"] as boolean;
80
+ const path = `app/controllers/${name}.ts`;
81
+
82
+ return Bun.file(path)
83
+ .exists()
84
+ .then((exists) => {
85
+ if (exists) {
86
+ this.error(`File already exists: ${path}`);
87
+ return;
88
+ }
89
+ // Bun.write() creates any missing parent directories, so no mkdir is needed.
90
+ return Bun.write(path, resource ? resourceStub(name) : basicStub(name)).then(() => {
91
+ this.info(`Created: ${path}`);
92
+ });
93
+ });
94
+ }
95
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * The `make:event` command and the event source stubs it writes (plain and
3
+ * broadcastable).
4
+ */
5
+ import { Command } from "../Command.ts";
6
+
7
+ /**
8
+ * `bun zt make:event <name>` — scaffolds a new event class under `app/events/`
9
+ * (pass `--broadcast`/`-b` for a broadcastable event extending `BroadcastingEvent`).
10
+ *
11
+ * @category Scaffolding (make:*)
12
+ */
13
+ export class MakeEventCommand extends Command {
14
+ static commandName = "make:event";
15
+ static description = "Create a new event class";
16
+ static needsApp = false;
17
+ static override args = [{ name: "name", required: true, default: "" }];
18
+ static override flags = [
19
+ {
20
+ name: "broadcast",
21
+ short: "b",
22
+ type: "boolean" as const,
23
+ description: "Generate a broadcastable event (extends BroadcastingEvent)",
24
+ default: false,
25
+ },
26
+ ];
27
+
28
+ async run(): Promise<void> {
29
+ const name = this.args["name"];
30
+ if (!name) {
31
+ this.error("Name is required.");
32
+ return;
33
+ }
34
+ const path = `app/events/${name}.ts`;
35
+ if (await Bun.file(path).exists()) {
36
+ this.error(`File already exists: ${path}`);
37
+ return;
38
+ }
39
+ const broadcast = this.flags["broadcast"] as boolean;
40
+ await Bun.write(path, broadcast ? broadcastEventStub(name) : plainEventStub(name));
41
+ this.info(`Created: ${path}`);
42
+ }
43
+ }
44
+
45
+ function plainEventStub(name: string): string {
46
+ return `/**\n * ${name} event.\n */\nexport class ${name} {\n constructor(\n // public readonly userId: number,\n ) {}\n}\n`;
47
+ }
48
+
49
+ /** Source for a broadcastable event that extends `BroadcastingEvent`. */
50
+ export function broadcastEventStub(name: string): string {
51
+ return `import { BroadcastingEvent, privateChannel } from "@zerotal/broadcasting";
52
+
53
+ /**
54
+ * ${name} - a broadcastable event.
55
+ *
56
+ * Dispatch it (broadcasts + runs any listeners):
57
+ * ${name}.dispatch(order);
58
+ *
59
+ * Or broadcast to everyone but the current user:
60
+ * broadcast(new ${name}(order)).toOthers();
61
+ */
62
+ export class ${name} extends BroadcastingEvent {
63
+ constructor(
64
+ // public readonly order: Order,
65
+ ) {
66
+ super();
67
+ }
68
+
69
+ /** The channel(s) this event broadcasts on. Use [param] placeholders in channel auth rules. */
70
+ broadcastOn() {
71
+ return privateChannel("channel-name");
72
+ }
73
+
74
+ /** Optional: the wire payload (defaults to this event's own public properties). */
75
+ broadcastWith() {
76
+ return {};
77
+ }
78
+
79
+ /** Optional: only broadcast when this returns true. */
80
+ // broadcastWhen() {
81
+ // return true;
82
+ // }
83
+ }
84
+ `;
85
+ }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * The `make:job` command, which scaffolds a queue job class.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /**
7
+ * `bun zt make:job <name>` — scaffolds a new queue job class under `app/jobs/`.
8
+ *
9
+ * @category Scaffolding (make:*)
10
+ */
11
+ export class MakeJobCommand extends Command {
12
+ static commandName = "make:job";
13
+ static description = "Create a new queue job class";
14
+ static needsApp = false;
15
+ static override args = [{ name: "name", required: true, default: "" }];
16
+
17
+ async run(): Promise<void> {
18
+ const name = this.args["name"];
19
+ if (!name) {
20
+ this.error("Name is required.");
21
+ return;
22
+ }
23
+ const path = `app/jobs/${name}.ts`;
24
+ if (await Bun.file(path).exists()) {
25
+ this.error(`File already exists: ${path}`);
26
+ return;
27
+ }
28
+ await Bun.write(
29
+ path,
30
+ `import { Job, JobRegistry } from '@zerotal/queue';
31
+
32
+ export class ${name} extends Job {
33
+ readonly queue = 'default';
34
+
35
+ constructor(
36
+ // public readonly id: number,
37
+ ) { super(); }
38
+
39
+ payload(): Record<string, unknown> {
40
+ return {};
41
+ }
42
+
43
+ async handle(): Promise<void> {
44
+ // Implement the job logic here
45
+ }
46
+ }
47
+
48
+ JobRegistry.register(${name} as never);
49
+ `,
50
+ );
51
+ this.info(`Created: ${path}`);
52
+ }
53
+ }
@@ -0,0 +1,35 @@
1
+ /**
2
+ * The `make:listener` command, which scaffolds an event listener class.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /**
7
+ * `bun zt make:listener <name>` — scaffolds a new event listener class under
8
+ * `app/listeners/`.
9
+ *
10
+ * @category Scaffolding (make:*)
11
+ */
12
+ export class MakeListenerCommand extends Command {
13
+ static commandName = "make:listener";
14
+ static description = "Create a new event listener class";
15
+ static needsApp = false;
16
+ static override args = [{ name: "name", required: true, default: "" }];
17
+
18
+ async run(): Promise<void> {
19
+ const name = this.args["name"];
20
+ if (!name) {
21
+ this.error("Name is required.");
22
+ return;
23
+ }
24
+ const path = `app/listeners/${name}.ts`;
25
+ if (await Bun.file(path).exists()) {
26
+ this.error(`File already exists: ${path}`);
27
+ return;
28
+ }
29
+ await Bun.write(
30
+ path,
31
+ `export class ${name} {\n async handle(event: unknown): Promise<void> {\n // Handle the event here\n void event;\n }\n}\n`,
32
+ );
33
+ this.info(`Created: ${path}`);
34
+ }
35
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * The `make:middleware` command and the middleware source stub it writes.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /** Source for a pass-through middleware class implementing `Pipe<HttpContext>`. */
7
+ export function middlewareStub(name: string): string {
8
+ return `import type { HttpContext, Pipe, NextFn } from '@zerotal/core';
9
+
10
+ export class ${name} implements Pipe<HttpContext> {
11
+ async handle(ctx: HttpContext, next: NextFn): Promise<Response | void> {
12
+ // Continue down the pipeline:
13
+ return next();
14
+ // ...or short-circuit by returning a Response instead of calling next():
15
+ // return Response.json({ message: 'Forbidden' }, { status: 403 });
16
+ }
17
+ }
18
+ `;
19
+ }
20
+
21
+ function middlewarePath(name: string): string {
22
+ return `app/middleware/${name}.ts`;
23
+ }
24
+
25
+ /**
26
+ * `bun zt make:middleware <name>` — scaffolds a new middleware class
27
+ * (implementing `Pipe<HttpContext>`) under `app/middleware/`.
28
+ *
29
+ * @category Scaffolding (make:*)
30
+ */
31
+ export class MakeMiddlewareCommand extends Command {
32
+ static commandName = "make:middleware";
33
+ static description = "Create a new middleware class";
34
+ static needsApp = false;
35
+
36
+ static get args() {
37
+ return [
38
+ { name: "name", required: true, description: "Middleware class name (e.g. AuthMiddleware)" },
39
+ ];
40
+ }
41
+
42
+ static get flags() {
43
+ return [];
44
+ }
45
+
46
+ run(): Promise<void> {
47
+ const name = this.args["name"]!;
48
+ const path = middlewarePath(name);
49
+
50
+ return Bun.file(path)
51
+ .exists()
52
+ .then((exists) => {
53
+ if (exists) {
54
+ this.error(`File already exists: ${path}`);
55
+ return;
56
+ }
57
+ // Bun.write() creates any missing parent directories, so no mkdir is needed.
58
+ return Bun.write(path, middlewareStub(name)).then(() => {
59
+ this.info(`Created: ${path}`);
60
+ });
61
+ });
62
+ }
63
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * The `make:notification` command and the notification source stub it writes.
3
+ */
4
+ import { Command } from "../Command.ts";
5
+
6
+ /**
7
+ * `bun zt make:notification <name>` — scaffolds a new notification class under
8
+ * `app/notifications/`.
9
+ *
10
+ * @category Scaffolding (make:*)
11
+ */
12
+ export class MakeNotificationCommand extends Command {
13
+ static commandName = "make:notification";
14
+ static description = "Create a new notification class";
15
+ static needsApp = false;
16
+ static args = [
17
+ { name: "name", required: true, description: "Notification name (e.g. OrderShipped)" },
18
+ ];
19
+
20
+ async run(): Promise<void> {
21
+ const name = this.args["name"]!;
22
+ const path = `app/notifications/${name}.ts`;
23
+
24
+ if (await Bun.file(path).exists()) {
25
+ this.error(`File already exists: ${path}`);
26
+ return;
27
+ }
28
+
29
+ await Bun.write(path, notificationStub(name));
30
+ this.info(`Created: ${path}`);
31
+ }
32
+ }
33
+
34
+ /** Source for a new notification class extending `Notification`. */
35
+ export function notificationStub(name: string): string {
36
+ return `import { Notification } from '@zerotal/notifications';
37
+
38
+ export class ${name} extends Notification {
39
+ channels(): string[] {
40
+ return ['database'];
41
+ }
42
+
43
+ toDatabase(): Record<string, unknown> {
44
+ return {};
45
+ }
46
+ }
47
+ `;
48
+ }