alepha 0.13.6 → 0.13.7

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 (134) hide show
  1. package/dist/api-audits/index.browser.js +116 -0
  2. package/dist/api-audits/index.browser.js.map +1 -0
  3. package/dist/api-audits/index.d.ts +1194 -0
  4. package/dist/api-audits/index.js +674 -0
  5. package/dist/api-audits/index.js.map +1 -0
  6. package/dist/api-notifications/index.d.ts +147 -147
  7. package/dist/api-parameters/index.browser.js +36 -5
  8. package/dist/api-parameters/index.browser.js.map +1 -1
  9. package/dist/api-parameters/index.d.ts +711 -33
  10. package/dist/api-parameters/index.js +831 -17
  11. package/dist/api-parameters/index.js.map +1 -1
  12. package/dist/api-users/index.d.ts +793 -780
  13. package/dist/api-users/index.js +699 -19
  14. package/dist/api-users/index.js.map +1 -1
  15. package/dist/api-verifications/index.js +2 -1
  16. package/dist/api-verifications/index.js.map +1 -1
  17. package/dist/bin/index.js +1 -0
  18. package/dist/bin/index.js.map +1 -1
  19. package/dist/cli/index.d.ts +85 -31
  20. package/dist/cli/index.js +205 -33
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/command/index.d.ts +67 -6
  23. package/dist/command/index.js +30 -3
  24. package/dist/command/index.js.map +1 -1
  25. package/dist/core/index.browser.js +241 -61
  26. package/dist/core/index.browser.js.map +1 -1
  27. package/dist/core/index.d.ts +170 -90
  28. package/dist/core/index.js +264 -67
  29. package/dist/core/index.js.map +1 -1
  30. package/dist/core/index.native.js +248 -65
  31. package/dist/core/index.native.js.map +1 -1
  32. package/dist/email/index.js +15 -10554
  33. package/dist/email/index.js.map +1 -1
  34. package/dist/logger/index.d.ts +4 -4
  35. package/dist/logger/index.js +77 -72
  36. package/dist/logger/index.js.map +1 -1
  37. package/dist/orm/index.d.ts +5 -1
  38. package/dist/orm/index.js +24 -7
  39. package/dist/orm/index.js.map +1 -1
  40. package/dist/queue/index.d.ts +4 -4
  41. package/dist/scheduler/index.d.ts +6 -6
  42. package/dist/server/index.d.ts +10 -1
  43. package/dist/server/index.js +20 -6
  44. package/dist/server/index.js.map +1 -1
  45. package/dist/server-auth/index.d.ts +163 -152
  46. package/dist/server-auth/index.js +40 -10
  47. package/dist/server-auth/index.js.map +1 -1
  48. package/dist/server-cookies/index.js +5 -1
  49. package/dist/server-cookies/index.js.map +1 -1
  50. package/dist/server-links/index.d.ts +33 -33
  51. package/dist/server-security/index.d.ts +9 -9
  52. package/dist/thread/index.js +2 -2
  53. package/dist/thread/index.js.map +1 -1
  54. package/dist/vite/index.d.ts +2 -2
  55. package/dist/vite/index.js +102 -45
  56. package/dist/vite/index.js.map +1 -1
  57. package/dist/websocket/index.browser.js +3 -3
  58. package/dist/websocket/index.browser.js.map +1 -1
  59. package/dist/websocket/index.js +4 -4
  60. package/dist/websocket/index.js.map +1 -1
  61. package/package.json +14 -9
  62. package/src/api-audits/controllers/AuditController.ts +186 -0
  63. package/src/api-audits/entities/audits.ts +132 -0
  64. package/src/api-audits/index.browser.ts +18 -0
  65. package/src/api-audits/index.ts +58 -0
  66. package/src/api-audits/primitives/$audit.ts +159 -0
  67. package/src/api-audits/schemas/auditQuerySchema.ts +23 -0
  68. package/src/api-audits/schemas/auditResourceSchema.ts +9 -0
  69. package/src/api-audits/schemas/createAuditSchema.ts +27 -0
  70. package/src/api-audits/services/AuditService.ts +412 -0
  71. package/src/api-parameters/controllers/ConfigController.ts +324 -0
  72. package/src/api-parameters/entities/parameters.ts +93 -10
  73. package/src/api-parameters/index.ts +43 -4
  74. package/src/api-parameters/primitives/$config.ts +291 -19
  75. package/src/api-parameters/schedulers/ConfigActivationScheduler.ts +30 -0
  76. package/src/api-parameters/services/ConfigStore.ts +491 -0
  77. package/src/api-users/atoms/realmAuthSettingsAtom.ts +19 -0
  78. package/src/api-users/controllers/UserRealmController.ts +0 -2
  79. package/src/api-users/index.ts +2 -0
  80. package/src/api-users/primitives/$userRealm.ts +18 -3
  81. package/src/api-users/providers/UserRealmProvider.ts +6 -3
  82. package/src/api-users/services/RegistrationService.ts +2 -1
  83. package/src/api-users/services/SessionService.ts +4 -0
  84. package/src/api-users/services/UserService.ts +3 -0
  85. package/src/api-verifications/index.ts +7 -1
  86. package/src/bin/index.ts +1 -0
  87. package/src/cli/assets/biomeJson.ts +1 -1
  88. package/src/cli/assets/dummySpecTs.ts +7 -0
  89. package/src/cli/assets/editorconfig.ts +13 -0
  90. package/src/cli/assets/mainTs.ts +14 -0
  91. package/src/cli/commands/BiomeCommands.ts +2 -0
  92. package/src/cli/commands/CoreCommands.ts +28 -9
  93. package/src/cli/commands/VerifyCommands.ts +2 -1
  94. package/src/cli/commands/ViteCommands.ts +8 -9
  95. package/src/cli/services/AlephaCliUtils.ts +214 -23
  96. package/src/command/helpers/Asker.ts +0 -1
  97. package/src/command/primitives/$command.ts +67 -0
  98. package/src/command/providers/CliProvider.ts +39 -8
  99. package/src/core/Alepha.ts +40 -30
  100. package/src/core/helpers/jsonSchemaToTypeBox.ts +307 -0
  101. package/src/core/index.shared.ts +1 -0
  102. package/src/core/index.ts +30 -3
  103. package/src/core/providers/EventManager.ts +1 -1
  104. package/src/core/providers/StateManager.ts +23 -12
  105. package/src/core/providers/TypeProvider.ts +26 -34
  106. package/src/logger/index.ts +8 -6
  107. package/src/logger/primitives/$logger.ts +1 -1
  108. package/src/logger/providers/{SimpleFormatterProvider.ts → PrettyFormatterProvider.ts} +10 -1
  109. package/src/orm/index.ts +6 -0
  110. package/src/orm/services/PgRelationManager.ts +2 -2
  111. package/src/orm/services/PostgresModelBuilder.ts +11 -7
  112. package/src/orm/services/Repository.ts +16 -7
  113. package/src/orm/services/SqliteModelBuilder.ts +10 -0
  114. package/src/server/index.ts +6 -0
  115. package/src/server/primitives/$action.ts +10 -1
  116. package/src/server/providers/ServerBodyParserProvider.ts +11 -5
  117. package/src/server/providers/ServerRouterProvider.ts +13 -7
  118. package/src/server-auth/primitives/$auth.ts +7 -0
  119. package/src/server-auth/providers/ServerAuthProvider.ts +51 -8
  120. package/src/server-cookies/index.ts +2 -1
  121. package/src/thread/primitives/$thread.ts +2 -2
  122. package/src/vite/index.ts +0 -2
  123. package/src/vite/tasks/buildServer.ts +3 -4
  124. package/src/vite/tasks/generateCloudflare.ts +35 -19
  125. package/src/vite/tasks/generateDocker.ts +18 -4
  126. package/src/vite/tasks/generateSitemap.ts +5 -7
  127. package/src/vite/tasks/generateVercel.ts +76 -41
  128. package/src/vite/tasks/runAlepha.ts +16 -1
  129. package/src/websocket/providers/NodeWebSocketServerProvider.ts +3 -11
  130. package/src/websocket/services/WebSocketClient.ts +3 -3
  131. package/dist/cli/dist-BlfFtOk2.js +0 -2770
  132. package/dist/cli/dist-BlfFtOk2.js.map +0 -1
  133. package/src/api-parameters/controllers/ParameterController.ts +0 -45
  134. package/src/api-parameters/services/ParameterStore.ts +0 -23
@@ -1,6 +1,7 @@
1
1
  import { $module } from "alepha";
2
2
  import { VerificationController } from "./controllers/VerificationController.ts";
3
3
  import { VerificationJobs } from "./jobs/VerificationJobs.ts";
4
+ import { VerificationParameters } from "./parameters/VerificationParameters.ts";
4
5
  import { VerificationService } from "./services/VerificationService.ts";
5
6
 
6
7
  // ---------------------------------------------------------------------------------------------------------------------
@@ -25,5 +26,10 @@ export * from "./services/VerificationService.ts";
25
26
  */
26
27
  export const AlephaApiVerification = $module({
27
28
  name: "alepha.api.verifications",
28
- services: [VerificationController, VerificationJobs, VerificationService],
29
+ services: [
30
+ VerificationController,
31
+ VerificationJobs,
32
+ VerificationService,
33
+ VerificationParameters,
34
+ ],
29
35
  });
package/src/bin/index.ts CHANGED
@@ -5,6 +5,7 @@ import { AlephaCli, version } from "alepha/cli";
5
5
 
6
6
  const alepha = Alepha.create({
7
7
  env: {
8
+ APP_NAME: "CLI",
8
9
  LOG_LEVEL: "alepha.core:warn,info",
9
10
  LOG_FORMAT: "raw",
10
11
  CLI_NAME: "alepha",
@@ -11,7 +11,7 @@ export const biomeJson = `
11
11
  },
12
12
  "formatter": {
13
13
  "enabled": true,
14
- "indentStyle": "space"
14
+ "useEditorconfig": true
15
15
  },
16
16
  "linter": {
17
17
  "enabled": true,
@@ -0,0 +1,7 @@
1
+ export const dummySpecTs = () => `
2
+ import { test, expect } from "vitest";
3
+
4
+ test("dummy test", () => {
5
+ expect(1 + 1).toBe(2);
6
+ });
7
+ `.trim();
@@ -0,0 +1,13 @@
1
+ export const editorconfig = `
2
+ # https://editorconfig.org
3
+
4
+ root = true
5
+
6
+ [*]
7
+ charset = utf-8
8
+ end_of_line = lf
9
+ insert_final_newline = true
10
+ trim_trailing_whitespace = true
11
+ indent_style = space
12
+ indent_size = 2
13
+ `.trim();
@@ -0,0 +1,14 @@
1
+ export const mainTs = () => `
2
+ import { Alepha, run } from "alepha";
3
+ import { $logger } from "alepha/logger";
4
+
5
+ const alepha = Alepha.create();
6
+
7
+ alepha.with(() => {
8
+ const log = $logger();
9
+
10
+ log.info("Hello from Alepha!");
11
+ });
12
+
13
+ run(alepha);
14
+ `.trim();
@@ -12,6 +12,7 @@ export class BiomeCommands {
12
12
  description: "Format the codebase using Biome",
13
13
  handler: async ({ root }) => {
14
14
  await this.utils.ensureConfig(root, { biomeJson: true });
15
+ await this.utils.ensureDependency(root, "@biomejs/biome");
15
16
  await this.utils.exec(`biome format --fix`);
16
17
  },
17
18
  });
@@ -21,6 +22,7 @@ export class BiomeCommands {
21
22
  description: "Run linter across the codebase using Biome",
22
23
  handler: async ({ root }) => {
23
24
  await this.utils.ensureConfig(root, { biomeJson: true });
25
+ await this.utils.ensureDependency(root, "@biomejs/biome");
24
26
  await this.utils.exec(`biome check --formatter-enabled=false --fix`);
25
27
  },
26
28
  });
@@ -58,6 +58,8 @@ export class CoreCommands {
58
58
  // choose package manager
59
59
  yarn: t.optional(t.boolean({ description: "Use Yarn package manager" })),
60
60
  pnpm: t.optional(t.boolean({ description: "Use pnpm package manager" })),
61
+ npm: t.optional(t.boolean({ description: "Use npm package manager" })),
62
+ bun: t.optional(t.boolean({ description: "Use Bun package manager" })),
61
63
  // choose which dependencies to add
62
64
  react: t.optional(
63
65
  t.boolean({ description: "Include Alepha React dependencies" }),
@@ -65,6 +67,9 @@ export class CoreCommands {
65
67
  ui: t.optional(
66
68
  t.boolean({ description: "Include Alepha UI dependencies" }),
67
69
  ),
70
+ test: t.optional(
71
+ t.boolean({ description: "Include Vitest and create test directory" }),
72
+ ),
68
73
  }),
69
74
  handler: async ({ run, flags, root }) => {
70
75
  if (flags.ui) {
@@ -72,31 +77,45 @@ export class CoreCommands {
72
77
  }
73
78
 
74
79
  await run({
75
- name: "Ensuring configuration files",
80
+ name: "ensuring configuration files",
76
81
  handler: async () => {
77
82
  await this.utils.ensureConfig(root, {
78
83
  tsconfigJson: true,
79
84
  packageJson: flags,
80
85
  biomeJson: true,
81
86
  viteConfigTs: true,
87
+ editorconfig: true,
82
88
  indexHtml: !!flags.react,
83
89
  });
90
+
91
+ // Create src/main.ts if src directory is empty or doesn't exist
92
+ if (!flags.react) {
93
+ await this.utils.ensureSrcMain(root);
94
+ }
84
95
  },
85
96
  });
86
97
 
87
98
  // TODO: check if all alepha dependencies are same version
88
99
 
89
- const guessedPm = await this.utils.getPackageManager(root);
90
-
91
- if (flags.yarn || guessedPm === "yarn") {
100
+ const pm = await this.utils.getPackageManager(root, flags);
101
+ if (pm === "yarn") {
92
102
  await this.utils.ensureYarn(root);
93
103
  await run("yarn set version stable");
94
- await run("yarn install", {
95
- alias: "Installing dependencies with Yarn",
96
- });
104
+ } else if (pm === "pnpm") {
105
+ await this.utils.ensurePnpm(root);
97
106
  } else {
98
- await run("npm install", {
99
- alias: "Installing dependencies with npm",
107
+ await this.utils.ensureNpm(root);
108
+ }
109
+
110
+ await run(`${pm} install`, {
111
+ alias: `installing dependencies with ${pm}`,
112
+ });
113
+
114
+ // Install vitest and create test directory if --test flag is set
115
+ if (flags.test) {
116
+ await this.utils.ensureTestDir(root);
117
+ await run(`${pm} ${pm === "yarn" ? "add" : "install"} -D vitest`, {
118
+ alias: "setup testing with Vitest",
100
119
  });
101
120
  }
102
121
  },
@@ -48,7 +48,8 @@ export class VerifyCommands {
48
48
  public readonly typecheck = $command({
49
49
  name: "typecheck",
50
50
  description: "Check TypeScript types across the codebase",
51
- handler: async () => {
51
+ handler: async ({ root }) => {
52
+ await this.utils.ensureDependency(root, "typescript");
52
53
  await this.utils.exec("tsc --noEmit");
53
54
  },
54
55
  });
@@ -75,6 +75,8 @@ export class ViteCommands {
75
75
  return;
76
76
  }
77
77
 
78
+ // Ensure vite is installed before running
79
+ await this.utils.ensureDependency(root, "vite");
78
80
  await this.utils.exec(`vite`);
79
81
  },
80
82
  });
@@ -132,6 +134,10 @@ export class ViteCommands {
132
134
  const distDir = "dist";
133
135
  const clientDir = "public";
134
136
 
137
+ await this.utils.ensureDependency(root, "vite", {
138
+ run,
139
+ });
140
+
135
141
  await run.rm("dist", {
136
142
  alias: "clean dist",
137
143
  });
@@ -301,15 +307,8 @@ export class ViteCommands {
301
307
  viteConfigTs: true,
302
308
  });
303
309
 
304
- // check if vitest is installed
305
- try {
306
- await import("vitest");
307
- } catch {
308
- this.log.error(
309
- "Vitest is not installed. Please install it with `npm install -D vitest` or `yarn add -D vitest`.",
310
- );
311
- process.exit(1);
312
- }
310
+ // Ensure vitest is installed before running
311
+ await this.utils.ensureDependency(root, "vitest");
313
312
 
314
313
  await this.utils.exec(`vitest run ${this.env.VITEST_ARGS}`);
315
314
  },
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
2
2
  import { access, mkdir, readFile, writeFile } from "node:fs/promises";
3
3
  import { join } from "node:path";
4
4
  import { $inject, Alepha, AlephaError } from "alepha";
5
+ import type { RunnerMethod } from "alepha/command";
5
6
  import { FileSystemProvider } from "alepha/file";
6
7
  import { $logger } from "alepha/logger";
7
8
  import type { DrizzleKitProvider, RepositoryProvider } from "alepha/orm";
@@ -9,8 +10,11 @@ import { boot } from "alepha/vite";
9
10
  import { tsImport } from "tsx/esm/api";
10
11
  import { appRouterTs } from "../assets/appRouterTs.ts";
11
12
  import { biomeJson } from "../assets/biomeJson.ts";
13
+ import { dummySpecTs } from "../assets/dummySpecTs.ts";
14
+ import { editorconfig } from "../assets/editorconfig.ts";
12
15
  import { indexHtml } from "../assets/indexHtml.ts";
13
16
  import { mainBrowserTs } from "../assets/mainBrowserTs.ts";
17
+ import { mainTs } from "../assets/mainTs.ts";
14
18
  import { tsconfigJson } from "../assets/tsconfigJson.ts";
15
19
  import { viteConfigTs } from "../assets/viteConfigTs.ts";
16
20
  import { version } from "../version.ts";
@@ -32,10 +36,6 @@ export class AlephaCliUtils {
32
36
  /**
33
37
  * Execute a command using npx with inherited stdio.
34
38
  *
35
- * @param command - The command to execute (will be passed to npx)
36
- * @param env - Optional environment variables to set for the command
37
- * @returns Promise that resolves when the process exits
38
- *
39
39
  * @example
40
40
  * ```ts
41
41
  * const runner = alepha.inject(ProcessRunner);
@@ -44,40 +44,63 @@ export class AlephaCliUtils {
44
44
  */
45
45
  public async exec(
46
46
  command: string,
47
- env: Record<string, string> = {},
47
+ options: {
48
+ env?: Record<string, string>;
49
+ global?: boolean;
50
+ } = {},
48
51
  ): Promise<void> {
49
52
  const root = process.cwd();
50
53
  this.log.debug(`Executing command: ${command}`, { cwd: root });
51
54
 
55
+ const runExec = async (app: string, args: string[]) => {
56
+ const prog = spawn(app, args, {
57
+ stdio: "inherit",
58
+ cwd: root,
59
+ env: {
60
+ ...process.env,
61
+ ...options.env,
62
+ },
63
+ });
64
+
65
+ await new Promise<void>((resolve) =>
66
+ prog.on("exit", () => {
67
+ resolve();
68
+ }),
69
+ );
70
+ };
71
+
72
+ if (options.global) {
73
+ const [app, ...args] = command.split(" ");
74
+ await runExec(app, args);
75
+ return;
76
+ }
77
+
52
78
  const suffix = process.platform === "win32" ? ".cmd" : "";
53
79
  const [app, ...args] = command.split(" ");
54
- const execPath = await this.checkFileExists(
80
+
81
+ // find executable inside project node_modules
82
+ let execPath = await this.checkFileExists(
55
83
  root,
56
84
  `node_modules/.bin/${app}${suffix}`,
57
85
  true,
58
86
  );
59
87
 
88
+ // or, find executable inside alepha package node_modules (pnpm style)
89
+ if (!execPath) {
90
+ execPath = await this.checkFileExists(
91
+ root,
92
+ `node_modules/alepha/node_modules/.bin/${app}${suffix}`,
93
+ true,
94
+ );
95
+ }
96
+
60
97
  if (!execPath) {
61
98
  throw new AlephaError(
62
99
  `Could not find executable for command '${app}'. Make sure the package is installed.`,
63
100
  );
64
101
  }
65
102
 
66
- const prog = spawn(execPath, args, {
67
- stdio: "inherit",
68
- cwd: root,
69
- env: {
70
- ...process.env,
71
- ...env,
72
- // NODE_OPTIONS: "--import tsx",
73
- },
74
- });
75
-
76
- await new Promise<void>((resolve) =>
77
- prog.on("exit", () => {
78
- resolve();
79
- }),
80
- );
103
+ await runExec(execPath, args);
81
104
  }
82
105
 
83
106
  /**
@@ -139,6 +162,22 @@ export class AlephaCliUtils {
139
162
  await this.fs.rm(join(root, "pnpm-lock.yaml"), { force: true });
140
163
  }
141
164
 
165
+ public async ensurePnpm(root: string): Promise<void> {
166
+ // remove lock files from other package managers
167
+ await this.fs.rm(join(root, "package-lock.json"), { force: true });
168
+ await this.fs.rm(join(root, "yarn.lock"), { force: true });
169
+ await this.fs.rm(join(root, ".yarn"), { force: true, recursive: true });
170
+ await this.fs.rm(join(root, ".yarnrc.yml"), { force: true });
171
+ }
172
+
173
+ public async ensureNpm(root: string): Promise<void> {
174
+ // remove lock files from other package managers
175
+ await this.fs.rm(join(root, "pnpm-lock.yaml"), { force: true });
176
+ await this.fs.rm(join(root, "yarn.lock"), { force: true });
177
+ await this.fs.rm(join(root, ".yarn"), { force: true, recursive: true });
178
+ await this.fs.rm(join(root, ".yarnrc.yml"), { force: true });
179
+ }
180
+
142
181
  /**
143
182
  * Generate package.json content with Alepha dependencies.
144
183
  *
@@ -232,6 +271,7 @@ export class AlephaCliUtils {
232
271
  viteConfigTs?: boolean;
233
272
  indexHtml?: boolean;
234
273
  biomeJson?: boolean;
274
+ editorconfig?: boolean;
235
275
  },
236
276
  ) {
237
277
  const tasks: Promise<void>[] = [];
@@ -256,6 +296,9 @@ export class AlephaCliUtils {
256
296
  if (opts.biomeJson) {
257
297
  tasks.push(this.ensureBiomeConfig(root));
258
298
  }
299
+ if (opts.editorconfig) {
300
+ tasks.push(this.ensureEditorConfig(root));
301
+ }
259
302
 
260
303
  await Promise.all(tasks);
261
304
  }
@@ -353,6 +396,17 @@ export class AlephaCliUtils {
353
396
  await this.ensureFileExists(root, "biome.json", biomeJson, true);
354
397
  }
355
398
 
399
+ /**
400
+ * Ensure .editorconfig exists in the project.
401
+ *
402
+ * Creates a standard .editorconfig if none exists.
403
+ *
404
+ * @param root - The root directory of the project
405
+ */
406
+ public async ensureEditorConfig(root: string): Promise<void> {
407
+ await this.ensureFileExists(root, ".editorconfig", editorconfig, true);
408
+ }
409
+
356
410
  // ===================================================================================================================
357
411
  // Drizzle ORM & Kit Utilities
358
412
  // ===================================================================================================================
@@ -585,7 +639,9 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
585
639
  await this.exec(
586
640
  `drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`,
587
641
  {
588
- NODE_OPTIONS: "--import tsx",
642
+ env: {
643
+ NODE_OPTIONS: "--import tsx",
644
+ },
589
645
  },
590
646
  );
591
647
  }
@@ -593,7 +649,20 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
593
649
 
594
650
  public async getPackageManager(
595
651
  root: string,
596
- ): Promise<"yarn" | "pnpm" | "npm"> {
652
+ flags?: { yarn?: boolean; pnpm?: boolean; npm?: boolean; bun?: boolean },
653
+ ): Promise<"yarn" | "pnpm" | "npm" | "bun"> {
654
+ if (flags?.yarn) {
655
+ return "yarn";
656
+ }
657
+ if (flags?.pnpm) {
658
+ return "pnpm";
659
+ }
660
+ if (flags?.npm) {
661
+ return "npm";
662
+ }
663
+ if (flags?.bun) {
664
+ return "bun";
665
+ }
597
666
  if (await this.checkFileExists(root, "yarn.lock", true)) {
598
667
  return "yarn";
599
668
  }
@@ -635,6 +704,64 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
635
704
  return this.fs.exists(join(root, dirName));
636
705
  }
637
706
 
707
+ /**
708
+ * Ensure src/main.ts exists with a minimal Alepha bootstrap.
709
+ *
710
+ * Creates the src directory and main.ts file if the src directory
711
+ * doesn't exist or is empty.
712
+ *
713
+ * @param root - The root directory of the project
714
+ */
715
+ public async ensureSrcMain(root: string): Promise<void> {
716
+ const srcDir = join(root, "src");
717
+ const mainPath = join(srcDir, "main.ts");
718
+
719
+ // Check if src directory exists
720
+ const srcExists = await this.fs.exists(srcDir);
721
+
722
+ if (!srcExists) {
723
+ // Create src directory and main.ts
724
+ await this.fs.mkdir(srcDir, { recursive: true });
725
+ await this.fs.writeFile(mainPath, mainTs());
726
+ return;
727
+ }
728
+
729
+ // Check if src directory is empty
730
+ const files = await this.fs.ls(srcDir);
731
+ if (files.length === 0) {
732
+ await this.fs.writeFile(mainPath, mainTs());
733
+ }
734
+ }
735
+
736
+ /**
737
+ * Ensure test directory exists with a dummy test file.
738
+ *
739
+ * Creates the test directory and a dummy.spec.ts file if the test directory
740
+ * doesn't exist or is empty.
741
+ *
742
+ * @param root - The root directory of the project
743
+ */
744
+ public async ensureTestDir(root: string): Promise<void> {
745
+ const testDir = join(root, "test");
746
+ const dummyPath = join(testDir, "dummy.spec.ts");
747
+
748
+ // Check if test directory exists
749
+ const testExists = await this.fs.exists(testDir);
750
+
751
+ if (!testExists) {
752
+ // Create test directory and dummy.spec.ts
753
+ await this.fs.mkdir(testDir, { recursive: true });
754
+ await this.fs.writeFile(dummyPath, dummySpecTs());
755
+ return;
756
+ }
757
+
758
+ // Check if test directory is empty
759
+ const files = await this.fs.ls(testDir);
760
+ if (files.length === 0) {
761
+ await this.fs.writeFile(dummyPath, dummySpecTs());
762
+ }
763
+ }
764
+
638
765
  async readPackageJson(root: string): Promise<Record<string, any>> {
639
766
  const packageJson = await this.fs
640
767
  .createFile({
@@ -643,6 +770,70 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
643
770
  .text();
644
771
  return JSON.parse(packageJson);
645
772
  }
773
+
774
+ /**
775
+ * Check if a dependency is installed in the project.
776
+ *
777
+ * @param root - The root directory of the project
778
+ * @param packageName - The name of the package to check
779
+ * @returns True if the package is in dependencies or devDependencies
780
+ */
781
+ async hasDependency(root: string, packageName: string): Promise<boolean> {
782
+ try {
783
+ const pkg = await this.readPackageJson(root);
784
+ return !!(
785
+ pkg.dependencies?.[packageName] || pkg.devDependencies?.[packageName]
786
+ );
787
+ } catch {
788
+ return false;
789
+ }
790
+ }
791
+
792
+ /**
793
+ * Install a dependency if it's missing from the project.
794
+ *
795
+ * Automatically detects the package manager (yarn, pnpm, npm) and installs
796
+ * the package as a dev dependency if not already present.
797
+ */
798
+ async ensureDependency(
799
+ root: string,
800
+ packageName: string,
801
+ options: { dev?: boolean; run?: RunnerMethod } = {},
802
+ ): Promise<void> {
803
+ const { dev = true } = options;
804
+
805
+ if (await this.hasDependency(root, packageName)) {
806
+ this.log.debug(`Dependency '${packageName}' is already installed`);
807
+ return;
808
+ }
809
+
810
+ const pm = await this.getPackageManager(root);
811
+ let cmd: string;
812
+
813
+ switch (pm) {
814
+ case "yarn":
815
+ cmd = `yarn add ${dev ? "-D" : ""} ${packageName}`;
816
+ break;
817
+ case "pnpm":
818
+ cmd = `pnpm add ${dev ? "-D" : ""} ${packageName}`;
819
+ break;
820
+ default:
821
+ cmd = `npm install ${dev ? "--save-dev" : ""} ${packageName}`;
822
+ }
823
+
824
+ cmd = cmd.replace(/\s+/g, " ").trim();
825
+
826
+ if (options.run) {
827
+ // if it's during a Runner flow, just use the runner's run method
828
+ await options.run(cmd, {
829
+ alias: `installing ${packageName}`,
830
+ });
831
+ } else {
832
+ // else, run directly with our util exec method
833
+ this.log.debug(`Installing ${packageName}`);
834
+ await this.exec(cmd, { global: true });
835
+ }
836
+ }
646
837
  }
647
838
 
648
839
  export interface DependencyModes {
@@ -40,7 +40,6 @@ export interface AskOptions<T extends TSchema = TString> {
40
40
  }
41
41
 
42
42
  export interface AskMethod {
43
- // biome-ignore lint/style/useShorthandFunctionType: .
44
43
  <T extends TSchema = TString>(
45
44
  question: string,
46
45
  options?: AskOptions<T>,
@@ -82,6 +82,61 @@ export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
82
82
  * Equivalent to setting name to an empty string "".
83
83
  */
84
84
  root?: boolean;
85
+
86
+ /**
87
+ * Run this command's handler BEFORE the specified target command.
88
+ *
89
+ * Pre-hooks are not listed in help and cannot be called directly.
90
+ * They receive the same parsed flags and args as the target command.
91
+ *
92
+ * @example
93
+ * ```ts
94
+ * class BuildCommands {
95
+ * prebuild = $command({
96
+ * pre: "build",
97
+ * handler: async ({ run }) => {
98
+ * await run("cleaning dist folder...", () => fs.rm("dist"));
99
+ * }
100
+ * });
101
+ *
102
+ * build = $command({
103
+ * name: "build",
104
+ * handler: async () => { ... }
105
+ * });
106
+ * }
107
+ * ```
108
+ */
109
+ pre?: string;
110
+
111
+ /**
112
+ * Run this command's handler AFTER the specified target command.
113
+ *
114
+ * Post-hooks are not listed in help and cannot be called directly.
115
+ * They receive the same parsed flags and args as the target command.
116
+ *
117
+ * @example
118
+ * ```ts
119
+ * class BuildCommands {
120
+ * build = $command({
121
+ * name: "build",
122
+ * handler: async () => { ... }
123
+ * });
124
+ *
125
+ * postbuild = $command({
126
+ * post: "build",
127
+ * handler: async ({ run }) => {
128
+ * await run("generating checksums...", generateChecksums);
129
+ * }
130
+ * });
131
+ * }
132
+ * ```
133
+ */
134
+ post?: string;
135
+
136
+ /**
137
+ * If true, this command will be hidden from the help output.
138
+ */
139
+ hide?: boolean;
85
140
  }
86
141
 
87
142
  // ---------------------------------------------------------------------------------------------------------------------
@@ -93,10 +148,22 @@ export class CommandPrimitive<
93
148
  public readonly flags = this.options.flags ?? t.object({});
94
149
  public readonly aliases = this.options.aliases ?? [];
95
150
 
151
+ protected onInit() {
152
+ if (this.options.pre || this.options.post) {
153
+ this.options.hide ??= true;
154
+ }
155
+ }
156
+
96
157
  public get name(): string {
97
158
  if (this.options.root) {
98
159
  return "";
99
160
  }
161
+ if (this.options.pre) {
162
+ return `pre${this.options.pre}`;
163
+ }
164
+ if (this.options.post) {
165
+ return `post${this.options.post}`;
166
+ }
100
167
  return this.options.name ?? `${this.config.propertyKey}`;
101
168
  }
102
169
  }