alepha 0.14.0 → 0.14.1

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 (94) hide show
  1. package/dist/api/audits/index.d.ts +417 -338
  2. package/dist/api/audits/index.d.ts.map +1 -1
  3. package/dist/api/files/index.d.ts +80 -1
  4. package/dist/api/files/index.d.ts.map +1 -1
  5. package/dist/api/jobs/index.d.ts +236 -157
  6. package/dist/api/jobs/index.d.ts.map +1 -1
  7. package/dist/api/notifications/index.d.ts +21 -1
  8. package/dist/api/notifications/index.d.ts.map +1 -1
  9. package/dist/api/parameters/index.d.ts +451 -4
  10. package/dist/api/parameters/index.d.ts.map +1 -1
  11. package/dist/api/users/index.d.ts +833 -830
  12. package/dist/api/users/index.d.ts.map +1 -1
  13. package/dist/cli/index.d.ts +212 -29
  14. package/dist/cli/index.d.ts.map +1 -1
  15. package/dist/cli/index.js +320 -219
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/command/index.d.ts +206 -9
  18. package/dist/command/index.d.ts.map +1 -1
  19. package/dist/command/index.js +306 -69
  20. package/dist/command/index.js.map +1 -1
  21. package/dist/core/index.browser.js.map +1 -1
  22. package/dist/core/index.d.ts +1 -1
  23. package/dist/core/index.js.map +1 -1
  24. package/dist/core/index.native.js.map +1 -1
  25. package/dist/file/index.d.ts.map +1 -1
  26. package/dist/file/index.js.map +1 -1
  27. package/dist/orm/index.d.ts +180 -126
  28. package/dist/orm/index.d.ts.map +1 -1
  29. package/dist/orm/index.js +486 -512
  30. package/dist/orm/index.js.map +1 -1
  31. package/dist/queue/redis/index.js +2 -4
  32. package/dist/queue/redis/index.js.map +1 -1
  33. package/dist/redis/index.d.ts +400 -29
  34. package/dist/redis/index.d.ts.map +1 -1
  35. package/dist/redis/index.js +412 -21
  36. package/dist/redis/index.js.map +1 -1
  37. package/dist/scheduler/index.d.ts +6 -6
  38. package/dist/security/index.d.ts +28 -28
  39. package/dist/security/index.d.ts.map +1 -1
  40. package/dist/server/auth/index.d.ts +155 -155
  41. package/dist/server/core/index.d.ts +0 -1
  42. package/dist/server/core/index.d.ts.map +1 -1
  43. package/dist/server/core/index.js.map +1 -1
  44. package/dist/server/health/index.d.ts +17 -17
  45. package/dist/server/helmet/index.d.ts +4 -1
  46. package/dist/server/helmet/index.d.ts.map +1 -1
  47. package/dist/server/multipart/index.d.ts.map +1 -1
  48. package/dist/server/multipart/index.js.map +1 -1
  49. package/dist/server/proxy/index.js.map +1 -1
  50. package/dist/topic/redis/index.js +3 -3
  51. package/dist/topic/redis/index.js.map +1 -1
  52. package/dist/vite/index.js +9 -6
  53. package/dist/vite/index.js.map +1 -1
  54. package/package.json +3 -3
  55. package/src/cli/apps/AlephaCli.ts +8 -3
  56. package/src/cli/apps/AlephaPackageBuilderCli.ts +3 -0
  57. package/src/cli/atoms/changelogOptions.ts +45 -0
  58. package/src/cli/commands/ChangelogCommands.ts +187 -317
  59. package/src/cli/commands/DeployCommands.ts +118 -0
  60. package/src/cli/commands/DrizzleCommands.ts +28 -8
  61. package/src/cli/commands/ViteCommands.ts +23 -9
  62. package/src/cli/defineConfig.ts +15 -0
  63. package/src/cli/index.ts +3 -0
  64. package/src/cli/services/AlephaCliUtils.ts +4 -21
  65. package/src/cli/services/GitMessageParser.ts +77 -0
  66. package/src/command/helpers/EnvUtils.ts +37 -0
  67. package/src/command/index.ts +3 -1
  68. package/src/command/primitives/$command.ts +172 -6
  69. package/src/command/providers/CliProvider.ts +424 -91
  70. package/src/core/Alepha.ts +1 -1
  71. package/src/file/providers/NodeFileSystemProvider.ts +3 -1
  72. package/src/orm/index.ts +8 -4
  73. package/src/orm/interfaces/PgQueryWhere.ts +1 -26
  74. package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
  75. package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
  76. package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
  77. package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
  78. package/src/orm/services/QueryManager.ts +10 -125
  79. package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
  80. package/src/redis/index.ts +65 -3
  81. package/src/redis/providers/BunRedisProvider.ts +304 -0
  82. package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
  83. package/src/redis/providers/NodeRedisProvider.ts +280 -0
  84. package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
  85. package/src/redis/providers/RedisProvider.ts +134 -140
  86. package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
  87. package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
  88. package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
  89. package/src/server/core/providers/ServerProvider.ts +7 -4
  90. package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
  91. package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
  92. package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
  93. package/src/vite/tasks/buildServer.ts +1 -0
  94. package/src/orm/services/PgJsonQueryManager.ts +0 -511
@@ -3,7 +3,11 @@ import { join } from "node:path";
3
3
  import { $inject, AlephaError, t } from "alepha";
4
4
  import { $command } from "alepha/command";
5
5
  import { $logger } from "alepha/logger";
6
- import type { DrizzleKitProvider, RepositoryProvider } from "alepha/orm";
6
+ import type {
7
+ DatabaseProvider,
8
+ DrizzleKitProvider,
9
+ RepositoryProvider,
10
+ } from "alepha/orm";
7
11
  import { AlephaCliUtils } from "../services/AlephaCliUtils.ts";
8
12
 
9
13
  const drizzleCommandFlags = t.object({
@@ -29,7 +33,7 @@ export class DrizzleCommands {
29
33
  * Check if database migrations are up to date.
30
34
  */
31
35
  check = $command({
32
- name: "db:check-migrations",
36
+ name: "check-migrations",
33
37
  description: "Check if database migration files are up to date",
34
38
  args: t.optional(
35
39
  t.text({
@@ -134,7 +138,7 @@ export class DrizzleCommands {
134
138
  * - Invokes Drizzle Kit's CLI to generate migration files based on the current schema.
135
139
  */
136
140
  generate = $command({
137
- name: "db:generate",
141
+ name: "generate",
138
142
  description: "Generate migration files based on current database schema",
139
143
  summary: false,
140
144
  args: t.optional(
@@ -178,7 +182,7 @@ export class DrizzleCommands {
178
182
  * - Invokes Drizzle Kit's push command to apply schema changes directly.
179
183
  */
180
184
  push = $command({
181
- name: "db:push",
185
+ name: "push",
182
186
  description: "Push database schema changes directly to the database",
183
187
  summary: false,
184
188
  args: t.optional(
@@ -210,7 +214,7 @@ export class DrizzleCommands {
210
214
  * - Invokes Drizzle Kit's migrate command to apply pending migrations.
211
215
  */
212
216
  migrate = $command({
213
- name: "db:migrate",
217
+ name: "migrate",
214
218
  description: "Apply pending database migrations",
215
219
  summary: false,
216
220
  args: t.optional(
@@ -242,7 +246,7 @@ export class DrizzleCommands {
242
246
  * - Invokes Drizzle Kit's studio command to launch the web-based database browser.
243
247
  */
244
248
  studio = $command({
245
- name: "db:studio",
249
+ name: "studio",
246
250
  description: "Launch Drizzle Studio database browser",
247
251
  summary: false,
248
252
  args: t.optional(
@@ -265,6 +269,18 @@ export class DrizzleCommands {
265
269
  },
266
270
  });
267
271
 
272
+ /**
273
+ * Parent command for database operations.
274
+ */
275
+ db = $command({
276
+ name: "db",
277
+ description: "Database management commands",
278
+ children: [this.check, this.generate, this.push, this.migrate, this.studio],
279
+ handler: async ({ help }) => {
280
+ help();
281
+ },
282
+ });
283
+
268
284
  /**
269
285
  * Run a drizzle-kit command for all database providers in an Alepha instance.
270
286
  *
@@ -289,7 +305,7 @@ export class DrizzleCommands {
289
305
  envFiles.push(`.env.${options.env}`);
290
306
  }
291
307
 
292
- await this.utils.loadEnvFile(rootDir, envFiles);
308
+ await this.utils.loadEnv(rootDir, envFiles);
293
309
 
294
310
  this.log.debug(`Using project root: ${rootDir}`);
295
311
 
@@ -358,7 +374,7 @@ export class DrizzleCommands {
358
374
  */
359
375
  public async prepareDrizzleConfig(options: {
360
376
  kit: any;
361
- provider: any;
377
+ provider: DatabaseProvider;
362
378
  providerName: string;
363
379
  providerUrl: string;
364
380
  dialect: string;
@@ -387,6 +403,10 @@ export class DrizzleCommands {
387
403
  },
388
404
  };
389
405
 
406
+ if (options.provider.schema) {
407
+ config.schemaFilter = options.provider.schema;
408
+ }
409
+
390
410
  if (options.providerName === "d1") {
391
411
  config.driver = "d1-http";
392
412
  }
@@ -122,11 +122,6 @@ export class ViteCommands {
122
122
  description: "Generate sitemap.xml with base URL",
123
123
  }),
124
124
  ),
125
- prerender: t.optional(
126
- t.boolean({
127
- description: "Pre-render static pages",
128
- }),
129
- ),
130
125
  }),
131
126
  handler: async ({ flags, args, run, root }) => {
132
127
  // Tell viteAlephaBuild plugin to skip - CLI handles all tasks
@@ -165,7 +160,7 @@ export class ViteCommands {
165
160
  );
166
161
  const viteAlephaBuildOptions = alephaPlugin?.[OPTIONS] || {};
167
162
 
168
- await this.utils.loadEnvFile(root, [".env", ".env.production"]);
163
+ await this.utils.loadEnv(root, [".env", ".env.production"]);
169
164
 
170
165
  const stats = flags.stats ?? viteAlephaBuildOptions.stats ?? false;
171
166
  const hasServer = viteAlephaBuildOptions.serverEntry !== false;
@@ -254,7 +249,7 @@ export class ViteCommands {
254
249
  }
255
250
 
256
251
  // Pre-render static pages
257
- const shouldPrerender = flags.prerender ?? clientOptions.prerender;
252
+ const shouldPrerender = clientOptions.prerender;
258
253
 
259
254
  if (shouldPrerender) {
260
255
  await run({
@@ -322,7 +317,24 @@ export class ViteCommands {
322
317
  public readonly test = $command({
323
318
  name: "test",
324
319
  description: "Run tests using Vitest",
325
- handler: async ({ root }) => {
320
+ flags: t.object({
321
+ config: t.optional(
322
+ t.string({
323
+ description: "Path to Vitest config file",
324
+ alias: "c",
325
+ }),
326
+ ),
327
+ }),
328
+ env: t.object({
329
+ VITEST_ARGS: t.optional(
330
+ t.string({
331
+ default: "",
332
+ description:
333
+ "Additional arguments to pass to Vitest. E.g., --coverage",
334
+ }),
335
+ ),
336
+ }),
337
+ handler: async ({ root, flags, env }) => {
326
338
  await this.utils.ensureConfig(root, {
327
339
  tsconfigJson: true,
328
340
  viteConfigTs: true,
@@ -331,7 +343,9 @@ export class ViteCommands {
331
343
  // Ensure vitest is installed before running
332
344
  await this.utils.ensureDependency(root, "vitest");
333
345
 
334
- await this.utils.exec(`vitest run ${this.env.VITEST_ARGS}`);
346
+ const config = flags.config ? `--config=${flags.config}` : "";
347
+
348
+ await this.utils.exec(`vitest run ${config} ${env.VITEST_ARGS}`);
335
349
  },
336
350
  });
337
351
  }
@@ -0,0 +1,15 @@
1
+ import type { Alepha } from "alepha";
2
+ import type { CommandPrimitive } from "alepha/command";
3
+
4
+ export type AlephaCliConfig = (alepha: Alepha) => {
5
+ commands?: Record<string, CommandPrimitive>;
6
+ };
7
+
8
+ export const defineConfig = (config: AlephaCliConfig) => {
9
+ return (alepha: Alepha) => {
10
+ const { commands } = config(alepha);
11
+ return {
12
+ ...commands,
13
+ };
14
+ };
15
+ };
package/src/cli/index.ts CHANGED
@@ -1,9 +1,12 @@
1
1
  export * from "./apps/AlephaCli.ts";
2
2
  export * from "./apps/AlephaPackageBuilderCli.ts";
3
3
  export * from "./commands/BiomeCommands.ts";
4
+ export * from "./commands/ChangelogCommands.ts";
4
5
  export * from "./commands/CoreCommands.ts";
6
+ export * from "./commands/DeployCommands.ts";
5
7
  export * from "./commands/DrizzleCommands.ts";
6
8
  export * from "./commands/VerifyCommands.ts";
7
9
  export * from "./commands/ViteCommands.ts";
10
+ export * from "./defineConfig.ts";
8
11
  export * from "./services/AlephaCliUtils.ts";
9
12
  export * from "./version.ts";
@@ -2,7 +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
+ import { EnvUtils, type RunnerMethod } from "alepha/command";
6
6
  import { FileSystemProvider } from "alepha/file";
7
7
  import { $logger } from "alepha/logger";
8
8
  import { boot } from "alepha/vite";
@@ -31,6 +31,7 @@ import { version } from "../version.ts";
31
31
  export class AlephaCliUtils {
32
32
  protected readonly log = $logger();
33
33
  protected readonly fs = $inject(FileSystemProvider);
34
+ protected readonly envUtils = $inject(EnvUtils);
34
35
 
35
36
  /**
36
37
  * Execute a command using npx with inherited stdio.
@@ -499,29 +500,11 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
499
500
  * Reads the .env file in the specified root directory and sets
500
501
  * the environment variables in process.env.
501
502
  */
502
- public async loadEnvFile(
503
+ public async loadEnv(
503
504
  root: string,
504
505
  files: string[] = [".env"],
505
506
  ): Promise<void> {
506
- for (const it of files) {
507
- for (const file of [it, `${it}.local`]) {
508
- const envPath = join(root, file);
509
- try {
510
- const envContent = await readFile(envPath, "utf8");
511
- const lines = envContent.split("\n");
512
- for (const line of lines) {
513
- const [key, ...rest] = line.split("=");
514
- if (key) {
515
- const value = rest.join("=");
516
- process.env[key.trim()] = value.trim();
517
- }
518
- }
519
- this.log.debug(`Loaded environment variables from ${envPath}`);
520
- } catch {
521
- this.log.debug(`No ${file} file found at ${envPath}, skipping load.`);
522
- }
523
- }
524
- }
507
+ await this.envUtils.loadEnv(root, files);
525
508
  }
526
509
 
527
510
  public async getPackageManager(
@@ -0,0 +1,77 @@
1
+ import { $logger } from "alepha/logger";
2
+ import {
3
+ type ChangelogOptions,
4
+ DEFAULT_IGNORE,
5
+ } from "../atoms/changelogOptions.ts";
6
+ import type { Commit } from "../commands/ChangelogCommands.ts";
7
+
8
+ /**
9
+ * Service for parsing git commit messages into structured format.
10
+ *
11
+ * Only parses **conventional commits with a scope**:
12
+ * - `feat(scope): description` → feature
13
+ * - `fix(scope): description` → bug fix
14
+ * - `feat(scope)!: description` → breaking change
15
+ *
16
+ * Commits without scope are ignored, allowing developers to commit
17
+ * work-in-progress changes without polluting release notes:
18
+ * - `cli: work in progress` → ignored (no type)
19
+ * - `fix: quick patch` → ignored (no scope)
20
+ * - `feat(cli): add command` → included
21
+ */
22
+ export class GitMessageParser {
23
+ protected readonly log = $logger();
24
+
25
+ /**
26
+ * Parse a git commit line into a structured Commit object.
27
+ *
28
+ * **Format:** `type(scope): description` or `type(scope)!: description`
29
+ *
30
+ * **Supported types:** feat, fix, docs, refactor, perf, revert
31
+ *
32
+ * **Breaking changes:** Use `!` before `:` (e.g., `feat(api)!: remove endpoint`)
33
+ *
34
+ * @returns Commit object or null if not matching/ignored
35
+ */
36
+ parseCommit(line: string, config: ChangelogOptions): Commit | null {
37
+ // Extract hash and message from git log --oneline format
38
+ const match = line.match(/^([a-f0-9]+)\s+(.+)$/);
39
+ if (!match) return null;
40
+
41
+ const [, hash, message] = match;
42
+ const ignore = config.ignore ?? DEFAULT_IGNORE;
43
+
44
+ // Conventional commit with REQUIRED scope: type(scope): description
45
+ // The `!` before `:` marks a breaking change
46
+ const conventionalMatch = message.match(
47
+ /^(feat|fix|docs|refactor|perf|revert)\(([^)]+)\)(!)?:\s*(.+)$/i,
48
+ );
49
+
50
+ if (!conventionalMatch) {
51
+ // No match - commit doesn't follow required format
52
+ return null;
53
+ }
54
+
55
+ const [, type, scope, breakingMark, description] = conventionalMatch;
56
+
57
+ // Check if scope should be ignored
58
+ const baseScope = scope.split("/")[0];
59
+ if (ignore.includes(baseScope) || ignore.includes(scope)) {
60
+ return null;
61
+ }
62
+
63
+ // Breaking change detection:
64
+ // 1. Explicit `!` marker: feat(api)!: change
65
+ // 2. Word "breaking" in description: feat(api): breaking change to auth
66
+ const breaking =
67
+ breakingMark === "!" || description.toLowerCase().includes("breaking");
68
+
69
+ return {
70
+ hash: hash.substring(0, 8),
71
+ type: type.toLowerCase(),
72
+ scope,
73
+ description: description.trim(),
74
+ breaking,
75
+ };
76
+ }
77
+ }
@@ -0,0 +1,37 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { $logger } from "alepha/logger";
4
+
5
+ export class EnvUtils {
6
+ protected readonly log = $logger();
7
+
8
+ /**
9
+ * Load environment variables from .env files into process.env.
10
+ * By default, it loads from ".env" and ".env.local".
11
+ * You can specify additional files to load, e.g. [".env", ".env.production"].
12
+ */
13
+ public async loadEnv(
14
+ root: string,
15
+ files: string[] = [".env"],
16
+ ): Promise<void> {
17
+ for (const it of files) {
18
+ for (const file of [it, `${it}.local`]) {
19
+ const envPath = join(root, file);
20
+ try {
21
+ const envContent = await readFile(envPath, "utf8");
22
+ const lines = envContent.split("\n");
23
+ for (const line of lines) {
24
+ const [key, ...rest] = line.split("=");
25
+ if (key) {
26
+ const value = rest.join("=");
27
+ process.env[key.trim()] = value.trim();
28
+ }
29
+ }
30
+ this.log.debug(`Loaded environment variables from ${envPath}`);
31
+ } catch {
32
+ this.log.debug(`No ${file} file found at ${envPath}, skipping load.`);
33
+ }
34
+ }
35
+ }
36
+ }
37
+ }
@@ -1,5 +1,6 @@
1
1
  import { $module } from "alepha";
2
2
  import { Asker } from "./helpers/Asker.ts";
3
+ import { EnvUtils } from "./helpers/EnvUtils.ts";
3
4
  import { PrettyPrint } from "./helpers/PrettyPrint.ts";
4
5
  import { Runner } from "./helpers/Runner.ts";
5
6
  import { $command } from "./primitives/$command.ts";
@@ -9,6 +10,7 @@ import { CliProvider } from "./providers/CliProvider.ts";
9
10
 
10
11
  export * from "./errors/CommandError.ts";
11
12
  export * from "./helpers/Asker.ts";
13
+ export * from "./helpers/EnvUtils.ts";
12
14
  export * from "./helpers/PrettyPrint.ts";
13
15
  export * from "./helpers/Runner.ts";
14
16
  export * from "./primitives/$command.ts";
@@ -28,7 +30,7 @@ export * from "./providers/CliProvider.ts";
28
30
  export const AlephaCommand = $module({
29
31
  name: "alepha.command",
30
32
  primitives: [$command],
31
- services: [CliProvider, Runner, Asker, PrettyPrint],
33
+ services: [CliProvider, Runner, Asker, PrettyPrint, EnvUtils],
32
34
  });
33
35
 
34
36
  // ---------------------------------------------------------------------------------------------------------------------
@@ -19,17 +19,29 @@ import type { RunnerMethod } from "../helpers/Runner.ts";
19
19
  * This primitive allows you to define a command, its flags, and its handler
20
20
  * within your Alepha application structure.
21
21
  */
22
- export const $command = <T extends TObject, A extends TSchema>(
23
- options: CommandPrimitiveOptions<T, A>,
24
- ) => createPrimitive(CommandPrimitive<T, A>, options);
22
+ export const $command = <
23
+ T extends TObject,
24
+ A extends TSchema,
25
+ E extends TObject,
26
+ >(
27
+ options: CommandPrimitiveOptions<T, A, E>,
28
+ ) => createPrimitive(CommandPrimitive<T, A, E>, options);
25
29
 
26
30
  // ---------------------------------------------------------------------------------------------------------------------
27
31
 
28
- export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
32
+ export interface CommandPrimitiveOptions<
33
+ T extends TObject,
34
+ A extends TSchema,
35
+ E extends TObject = TObject,
36
+ > {
29
37
  /**
30
38
  * The handler function to execute when the command is matched.
39
+ *
40
+ * For parent commands with children, the handler is called when:
41
+ * - The parent command is invoked without a subcommand
42
+ * - The parent command is invoked with --help (to show available subcommands)
31
43
  */
32
- handler: (args: CommandHandlerArgs<T, A>) => Async<void>;
44
+ handler: (args: CommandHandlerArgs<T, A, E>) => Async<void>;
33
45
 
34
46
  /**
35
47
  * The name of the command. If omitted, the property key is used.
@@ -53,6 +65,28 @@ export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
53
65
  */
54
66
  flags?: T;
55
67
 
68
+ /**
69
+ * A TypeBox object schema defining required environment variables.
70
+ *
71
+ * Environment variables are validated before the handler runs (fail fast).
72
+ * They are displayed in the help output under "Env:" section.
73
+ *
74
+ * @example
75
+ * ```ts
76
+ * $command({
77
+ * env: t.object({
78
+ * VERCEL_TOKEN: t.text({ description: "Vercel API token" }),
79
+ * VERCEL_ORG_ID: t.optional(t.text({ description: "Organization ID" })),
80
+ * }),
81
+ * handler: async ({ env }) => {
82
+ * // env.VERCEL_TOKEN is typed & guaranteed to exist
83
+ * console.log(env.VERCEL_TOKEN);
84
+ * }
85
+ * })
86
+ * ```
87
+ */
88
+ env?: E;
89
+
56
90
  /**
57
91
  * An optional TypeBox schema defining the arguments for the command.
58
92
  *
@@ -137,6 +171,86 @@ export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
137
171
  * If true, this command will be hidden from the help output.
138
172
  */
139
173
  hide?: boolean;
174
+
175
+ /**
176
+ * Adds a `--mode, -m` flag to load environment files.
177
+ *
178
+ * When enabled:
179
+ * - Loads `.env` and `.env.local` by default
180
+ * - With `--mode production`, also loads `.env.production` and `.env.production.local`
181
+ * - The mode value is exposed in the handler as `mode: string | undefined`
182
+ *
183
+ * Set to `true` to enable with no default, or a string to set a default mode.
184
+ *
185
+ * This follows Vite's environment loading convention.
186
+ * @see https://vite.dev/guide/env-and-mode
187
+ *
188
+ * @example
189
+ * ```ts
190
+ * // No default mode
191
+ * build = $command({
192
+ * mode: true,
193
+ * handler: async ({ mode }) => {
194
+ * console.log(`Building for ${mode ?? 'development'}...`);
195
+ * }
196
+ * });
197
+ *
198
+ * // Default mode "production"
199
+ * deploy = $command({
200
+ * mode: "production",
201
+ * handler: async ({ mode }) => {
202
+ * console.log(`Deploying for ${mode}...`); // always defined
203
+ * }
204
+ * });
205
+ * ```
206
+ *
207
+ * Usage:
208
+ * - `cli build` - loads .env (mode = undefined)
209
+ * - `cli build --mode production` - loads .env and .env.production
210
+ * - `cli deploy` - loads .env and .env.production (default mode)
211
+ * - `cli deploy --mode staging` - loads .env and .env.staging
212
+ */
213
+ mode?: boolean | string;
214
+
215
+ /**
216
+ * Child commands (subcommands) for this command.
217
+ *
218
+ * When children are defined, the command becomes a parent command that
219
+ * can be invoked with space-separated subcommands:
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * class DeployCommands {
224
+ * // Subcommands
225
+ * vercel = $command({
226
+ * description: "Deploy to Vercel",
227
+ * handler: async () => { ... }
228
+ * });
229
+ *
230
+ * cloudflare = $command({
231
+ * description: "Deploy to Cloudflare",
232
+ * handler: async () => { ... }
233
+ * });
234
+ *
235
+ * // Parent command with children
236
+ * deploy = $command({
237
+ * description: "Deploy the application",
238
+ * children: [this.vercel, this.cloudflare],
239
+ * handler: async () => {
240
+ * // Called when "deploy" is invoked without subcommand
241
+ * console.log("Available: deploy vercel, deploy cloudflare");
242
+ * }
243
+ * });
244
+ * }
245
+ * ```
246
+ *
247
+ * This allows CLI usage like:
248
+ * - `cli deploy vercel` - runs the vercel subcommand
249
+ * - `cli deploy cloudflare` - runs the cloudflare subcommand
250
+ * - `cli deploy` - runs the parent handler (shows available subcommands)
251
+ * - `cli deploy --help` - shows help with all available subcommands
252
+ */
253
+ children?: CommandPrimitive<any, any>[];
140
254
  }
141
255
 
142
256
  // ---------------------------------------------------------------------------------------------------------------------
@@ -144,8 +258,10 @@ export interface CommandPrimitiveOptions<T extends TObject, A extends TSchema> {
144
258
  export class CommandPrimitive<
145
259
  T extends TObject = TObject,
146
260
  A extends TSchema = TSchema,
147
- > extends Primitive<CommandPrimitiveOptions<T, A>> {
261
+ E extends TObject = TObject,
262
+ > extends Primitive<CommandPrimitiveOptions<T, A, E>> {
148
263
  public readonly flags = this.options.flags ?? t.object({});
264
+ public readonly env = this.options.env ?? t.object({});
149
265
  public readonly aliases = this.options.aliases ?? [];
150
266
 
151
267
  protected onInit() {
@@ -166,6 +282,29 @@ export class CommandPrimitive<
166
282
  }
167
283
  return this.options.name ?? `${this.config.propertyKey}`;
168
284
  }
285
+
286
+ /**
287
+ * Get the child commands (subcommands) for this command.
288
+ */
289
+ public get children(): CommandPrimitive<any, any>[] {
290
+ return this.options.children ?? [];
291
+ }
292
+
293
+ /**
294
+ * Check if this command has child commands (is a parent command).
295
+ */
296
+ public get hasChildren(): boolean {
297
+ return this.children.length > 0;
298
+ }
299
+
300
+ /**
301
+ * Find a child command by name or alias.
302
+ */
303
+ public findChild(name: string): CommandPrimitive<any, any> | undefined {
304
+ return this.children.find(
305
+ (child) => child.name === name || child.aliases.includes(name),
306
+ );
307
+ }
169
308
  }
170
309
 
171
310
  $command[KIND] = CommandPrimitive;
@@ -175,9 +314,11 @@ $command[KIND] = CommandPrimitive;
175
314
  export interface CommandHandlerArgs<
176
315
  T extends TObject,
177
316
  A extends TSchema = TSchema,
317
+ E extends TObject = TObject,
178
318
  > {
179
319
  flags: Static<T>;
180
320
  args: A extends TSchema ? Static<A> : Array<string>;
321
+ env: Static<E>;
181
322
  run: RunnerMethod;
182
323
  ask: AskMethod;
183
324
  glob: typeof glob;
@@ -187,4 +328,29 @@ export interface CommandHandlerArgs<
187
328
  * The root directory where the command is executed.
188
329
  */
189
330
  root: string;
331
+
332
+ /**
333
+ * Display help for the current command.
334
+ *
335
+ * Useful for parent commands with children to show available subcommands
336
+ * when invoked without a specific subcommand.
337
+ *
338
+ * @example
339
+ * ```ts
340
+ * deploy = $command({
341
+ * children: [this.vercel, this.cloudflare],
342
+ * handler: async ({ help }) => {
343
+ * help(); // Shows available subcommands
344
+ * }
345
+ * });
346
+ * ```
347
+ */
348
+ help: () => void;
349
+
350
+ /**
351
+ * The current execution mode (e.g., "development", "production", "staging").
352
+ *
353
+ * Use --mode flag to set this value when running the command.
354
+ */
355
+ mode?: string;
190
356
  }