alepha 0.13.8 → 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 (160) hide show
  1. package/dist/api/audits/index.d.ts +418 -338
  2. package/dist/api/audits/index.d.ts.map +1 -0
  3. package/dist/api/files/index.d.ts +81 -1
  4. package/dist/api/files/index.d.ts.map +1 -0
  5. package/dist/api/jobs/index.d.ts +107 -27
  6. package/dist/api/jobs/index.d.ts.map +1 -0
  7. package/dist/api/notifications/index.d.ts +21 -1
  8. package/dist/api/notifications/index.d.ts.map +1 -0
  9. package/dist/api/parameters/index.d.ts +455 -8
  10. package/dist/api/parameters/index.d.ts.map +1 -0
  11. package/dist/api/users/index.d.ts +844 -840
  12. package/dist/api/users/index.d.ts.map +1 -0
  13. package/dist/api/verifications/index.d.ts.map +1 -0
  14. package/dist/batch/index.d.ts.map +1 -0
  15. package/dist/bucket/index.d.ts.map +1 -0
  16. package/dist/cache/core/index.d.ts.map +1 -0
  17. package/dist/cache/redis/index.d.ts.map +1 -0
  18. package/dist/cli/index.d.ts +254 -59
  19. package/dist/cli/index.d.ts.map +1 -0
  20. package/dist/cli/index.js +499 -127
  21. package/dist/cli/index.js.map +1 -1
  22. package/dist/command/index.d.ts +217 -10
  23. package/dist/command/index.d.ts.map +1 -0
  24. package/dist/command/index.js +350 -74
  25. package/dist/command/index.js.map +1 -1
  26. package/dist/core/index.browser.js +1334 -1318
  27. package/dist/core/index.browser.js.map +1 -1
  28. package/dist/core/index.d.ts +76 -72
  29. package/dist/core/index.d.ts.map +1 -0
  30. package/dist/core/index.js +1337 -1321
  31. package/dist/core/index.js.map +1 -1
  32. package/dist/core/index.native.js +1337 -1321
  33. package/dist/core/index.native.js.map +1 -1
  34. package/dist/datetime/index.d.ts.map +1 -0
  35. package/dist/email/index.d.ts.map +1 -0
  36. package/dist/fake/index.d.ts.map +1 -0
  37. package/dist/file/index.d.ts.map +1 -0
  38. package/dist/file/index.js.map +1 -1
  39. package/dist/lock/core/index.d.ts.map +1 -0
  40. package/dist/lock/redis/index.d.ts.map +1 -0
  41. package/dist/logger/index.d.ts +1 -0
  42. package/dist/logger/index.d.ts.map +1 -0
  43. package/dist/mcp/index.d.ts +820 -0
  44. package/dist/mcp/index.d.ts.map +1 -0
  45. package/dist/mcp/index.js +978 -0
  46. package/dist/mcp/index.js.map +1 -0
  47. package/dist/orm/index.d.ts +234 -107
  48. package/dist/orm/index.d.ts.map +1 -0
  49. package/dist/orm/index.js +376 -316
  50. package/dist/orm/index.js.map +1 -1
  51. package/dist/queue/core/index.d.ts +4 -4
  52. package/dist/queue/core/index.d.ts.map +1 -0
  53. package/dist/queue/redis/index.d.ts.map +1 -0
  54. package/dist/queue/redis/index.js +2 -4
  55. package/dist/queue/redis/index.js.map +1 -1
  56. package/dist/redis/index.d.ts +400 -29
  57. package/dist/redis/index.d.ts.map +1 -0
  58. package/dist/redis/index.js +412 -21
  59. package/dist/redis/index.js.map +1 -1
  60. package/dist/retry/index.d.ts.map +1 -0
  61. package/dist/router/index.d.ts.map +1 -0
  62. package/dist/scheduler/index.d.ts +6 -6
  63. package/dist/scheduler/index.d.ts.map +1 -0
  64. package/dist/security/index.d.ts +28 -28
  65. package/dist/security/index.d.ts.map +1 -0
  66. package/dist/server/auth/index.d.ts +155 -155
  67. package/dist/server/auth/index.d.ts.map +1 -0
  68. package/dist/server/cache/index.d.ts.map +1 -0
  69. package/dist/server/compress/index.d.ts.map +1 -0
  70. package/dist/server/cookies/index.d.ts.map +1 -0
  71. package/dist/server/core/index.d.ts +0 -1
  72. package/dist/server/core/index.d.ts.map +1 -0
  73. package/dist/server/core/index.js.map +1 -1
  74. package/dist/server/cors/index.d.ts.map +1 -0
  75. package/dist/server/health/index.d.ts +17 -17
  76. package/dist/server/health/index.d.ts.map +1 -0
  77. package/dist/server/helmet/index.d.ts +4 -1
  78. package/dist/server/helmet/index.d.ts.map +1 -0
  79. package/dist/server/links/index.d.ts +33 -33
  80. package/dist/server/links/index.d.ts.map +1 -0
  81. package/dist/server/metrics/index.d.ts.map +1 -0
  82. package/dist/server/multipart/index.d.ts.map +1 -0
  83. package/dist/server/multipart/index.js.map +1 -1
  84. package/dist/server/proxy/index.d.ts.map +1 -0
  85. package/dist/server/proxy/index.js.map +1 -1
  86. package/dist/server/rate-limit/index.d.ts.map +1 -0
  87. package/dist/server/security/index.d.ts +9 -9
  88. package/dist/server/security/index.d.ts.map +1 -0
  89. package/dist/server/static/index.d.ts.map +1 -0
  90. package/dist/server/swagger/index.d.ts.map +1 -0
  91. package/dist/sms/index.d.ts.map +1 -0
  92. package/dist/thread/index.d.ts.map +1 -0
  93. package/dist/topic/core/index.d.ts.map +1 -0
  94. package/dist/topic/redis/index.d.ts.map +1 -0
  95. package/dist/topic/redis/index.js +3 -3
  96. package/dist/topic/redis/index.js.map +1 -1
  97. package/dist/vite/index.d.ts +10 -2
  98. package/dist/vite/index.d.ts.map +1 -0
  99. package/dist/vite/index.js +45 -20
  100. package/dist/vite/index.js.map +1 -1
  101. package/dist/websocket/index.d.ts.map +1 -0
  102. package/package.json +9 -4
  103. package/src/cli/apps/AlephaCli.ts +10 -3
  104. package/src/cli/apps/AlephaPackageBuilderCli.ts +15 -8
  105. package/src/cli/assets/mainTs.ts +9 -10
  106. package/src/cli/atoms/changelogOptions.ts +45 -0
  107. package/src/cli/commands/ChangelogCommands.ts +259 -0
  108. package/src/cli/commands/DeployCommands.ts +118 -0
  109. package/src/cli/commands/DrizzleCommands.ts +230 -10
  110. package/src/cli/commands/ViteCommands.ts +47 -23
  111. package/src/cli/defineConfig.ts +15 -0
  112. package/src/cli/index.ts +3 -0
  113. package/src/cli/services/AlephaCliUtils.ts +10 -154
  114. package/src/cli/services/GitMessageParser.ts +77 -0
  115. package/src/command/helpers/EnvUtils.ts +37 -0
  116. package/src/command/index.ts +3 -1
  117. package/src/command/primitives/$command.ts +172 -6
  118. package/src/command/providers/CliProvider.ts +499 -95
  119. package/src/core/Alepha.ts +1 -1
  120. package/src/core/providers/SchemaValidator.ts +23 -1
  121. package/src/file/providers/NodeFileSystemProvider.ts +3 -1
  122. package/src/mcp/errors/McpError.ts +72 -0
  123. package/src/mcp/helpers/jsonrpc.ts +163 -0
  124. package/src/mcp/index.ts +132 -0
  125. package/src/mcp/interfaces/McpTypes.ts +248 -0
  126. package/src/mcp/primitives/$prompt.ts +188 -0
  127. package/src/mcp/primitives/$resource.ts +171 -0
  128. package/src/mcp/primitives/$tool.ts +285 -0
  129. package/src/mcp/providers/McpServerProvider.ts +382 -0
  130. package/src/mcp/transports/SseMcpTransport.ts +172 -0
  131. package/src/mcp/transports/StdioMcpTransport.ts +126 -0
  132. package/src/orm/index.ts +20 -4
  133. package/src/orm/interfaces/PgQueryWhere.ts +1 -26
  134. package/src/orm/providers/drivers/BunPostgresProvider.ts +225 -0
  135. package/src/orm/providers/drivers/BunSqliteProvider.ts +180 -0
  136. package/src/orm/providers/drivers/CloudflareD1Provider.ts +164 -0
  137. package/src/orm/providers/drivers/DatabaseProvider.ts +25 -0
  138. package/src/orm/providers/drivers/NodePostgresProvider.ts +0 -25
  139. package/src/orm/providers/drivers/NodeSqliteProvider.ts +3 -1
  140. package/src/orm/services/QueryManager.ts +10 -125
  141. package/src/queue/redis/providers/RedisQueueProvider.ts +2 -7
  142. package/src/redis/index.ts +65 -3
  143. package/src/redis/providers/BunRedisProvider.ts +304 -0
  144. package/src/redis/providers/BunRedisSubscriberProvider.ts +94 -0
  145. package/src/redis/providers/NodeRedisProvider.ts +280 -0
  146. package/src/redis/providers/NodeRedisSubscriberProvider.ts +94 -0
  147. package/src/redis/providers/RedisProvider.ts +134 -140
  148. package/src/redis/providers/RedisSubscriberProvider.ts +58 -49
  149. package/src/server/core/providers/BunHttpServerProvider.ts +0 -3
  150. package/src/server/core/providers/ServerBodyParserProvider.ts +3 -1
  151. package/src/server/core/providers/ServerProvider.ts +7 -4
  152. package/src/server/multipart/providers/ServerMultipartProvider.ts +3 -1
  153. package/src/server/proxy/providers/ServerProxyProvider.ts +1 -1
  154. package/src/topic/redis/providers/RedisTopicProvider.ts +3 -3
  155. package/src/vite/plugins/viteAlephaBuild.ts +8 -2
  156. package/src/vite/plugins/viteAlephaDev.ts +6 -2
  157. package/src/vite/tasks/buildServer.ts +2 -1
  158. package/src/vite/tasks/generateCloudflare.ts +43 -15
  159. package/src/vite/tasks/runAlepha.ts +1 -0
  160. package/src/orm/services/PgJsonQueryManager.ts +0 -511
@@ -2,10 +2,9 @@ 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
- import type { DrizzleKitProvider, RepositoryProvider } from "alepha/orm";
9
8
  import { boot } from "alepha/vite";
10
9
  import { tsImport } from "tsx/esm/api";
11
10
  import { appRouterTs } from "../assets/appRouterTs.ts";
@@ -32,6 +31,7 @@ import { version } from "../version.ts";
32
31
  export class AlephaCliUtils {
33
32
  protected readonly log = $logger();
34
33
  protected readonly fs = $inject(FileSystemProvider);
34
+ protected readonly envUtils = $inject(EnvUtils);
35
35
 
36
36
  /**
37
37
  * Execute a command using npx with inherited stdio.
@@ -495,160 +495,16 @@ ${models.map((it: string) => `export const ${it} = models["${it}"];`).join("\n")
495
495
  }
496
496
 
497
497
  /**
498
- * Prepare Drizzle configuration files for a database provider.
498
+ * Load environment variables from a .env file.
499
499
  *
500
- * Creates temporary entities.js and drizzle.config.js files needed
501
- * for Drizzle Kit commands to run properly.
502
- *
503
- * @param options - Configuration options including kit, provider info, and paths
504
- * @returns Path to the generated drizzle.config.js file
500
+ * Reads the .env file in the specified root directory and sets
501
+ * the environment variables in process.env.
505
502
  */
506
- public async prepareDrizzleConfig(options: {
507
- kit: any;
508
- provider: any;
509
- providerName: string;
510
- providerUrl: string;
511
- dialect: string;
512
- entry: string;
513
- rootDir: string;
514
- }): Promise<string> {
515
- const models = Object.keys(options.kit.getModels(options.provider));
516
- const entitiesJs = this.generateEntitiesJs(
517
- options.entry,
518
- options.providerName,
519
- models,
520
- );
521
-
522
- const entitiesJsPath = await this.writeConfigFile(
523
- "entities.js",
524
- entitiesJs,
525
- options.rootDir,
526
- );
527
-
528
- const config: Record<string, any> = {
529
- schema: entitiesJsPath,
530
- out: `./migrations/${options.providerName}`,
531
- dialect: options.dialect,
532
- dbCredentials: {
533
- url: options.providerUrl,
534
- },
535
- };
536
-
537
- if (options.dialect === "sqlite") {
538
- let url = options.providerUrl;
539
- url = url.replace("sqlite://", "").replace("file://", "");
540
- url = join(options.rootDir, url);
541
-
542
- config.dbCredentials = {
543
- url,
544
- };
545
- }
546
-
547
- if (options.providerName === "pglite") {
548
- config.driver = "pglite";
549
- }
550
-
551
- const drizzleConfigJs = `export default ${JSON.stringify(config, null, 2)}`;
552
-
553
- return await this.writeConfigFile(
554
- "drizzle.config.js",
555
- drizzleConfigJs,
556
- options.rootDir,
557
- );
558
- }
559
-
560
- public async loadEnvFile(root: string): Promise<void> {
561
- const envPath = join(root, ".env");
562
- try {
563
- const envContent = await readFile(envPath, "utf8");
564
- const lines = envContent.split("\n");
565
- for (const line of lines) {
566
- const [key, ...rest] = line.split("=");
567
- if (key) {
568
- const value = rest.join("=");
569
- process.env[key.trim()] = value.trim();
570
- }
571
- }
572
- this.log.debug(`Loaded environment variables from ${envPath}`);
573
- } catch {
574
- this.log.debug(`No .env file found at ${envPath}, skipping load.`);
575
- }
576
- }
577
-
578
- /**
579
- * Run a drizzle-kit command for all database providers in an Alepha instance.
580
- *
581
- * Iterates through all repository providers, prepares Drizzle config for each,
582
- * and executes the specified drizzle-kit command.
583
- *
584
- * @param options - Configuration including command to run, flags, and logging
585
- */
586
- public async runDrizzleKitCommand(options: {
587
- root: string;
588
- args?: string;
589
- command: string;
590
- commandFlags?: string;
591
- provider?: string;
592
- logMessage: (providerName: string, dialect: string) => string;
593
- }): Promise<void> {
594
- const rootDir = options.root;
595
-
596
- await this.loadEnvFile(rootDir);
597
-
598
- this.log.debug(`Using project root: ${rootDir}`);
599
-
600
- const { alepha, entry } = await this.loadAlephaFromServerEntryFile(
601
- rootDir,
602
- options.args,
603
- );
604
-
605
- const drizzleKitProvider =
606
- alepha.inject<DrizzleKitProvider>("DrizzleKitProvider");
607
- const repositoryProvider =
608
- alepha.inject<RepositoryProvider>("RepositoryProvider");
609
- const accepted = new Set<string>([]);
610
-
611
- for (const primitive of repositoryProvider.getRepositories()) {
612
- const provider = primitive.provider;
613
- const providerName = provider.name;
614
- const dialect = provider.dialect;
615
-
616
- if (accepted.has(providerName)) {
617
- continue;
618
- }
619
- accepted.add(providerName);
620
-
621
- // Skip if provider filter is set and doesn't match
622
- if (options.provider && options.provider !== providerName) {
623
- this.log.debug(
624
- `Skipping provider '${providerName}' (filter: ${options.provider})`,
625
- );
626
- continue;
627
- }
628
-
629
- this.log.info("");
630
- this.log.info(options.logMessage(providerName, dialect));
631
-
632
- const drizzleConfigJsPath = await this.prepareDrizzleConfig({
633
- kit: drizzleKitProvider,
634
- provider,
635
- providerName,
636
- providerUrl: provider.url,
637
- dialect,
638
- entry,
639
- rootDir,
640
- });
641
-
642
- const flags = options.commandFlags ? ` ${options.commandFlags}` : "";
643
- await this.exec(
644
- `drizzle-kit ${options.command} --config=${drizzleConfigJsPath}${flags}`,
645
- {
646
- env: {
647
- NODE_OPTIONS: "--import tsx",
648
- },
649
- },
650
- );
651
- }
503
+ public async loadEnv(
504
+ root: string,
505
+ files: string[] = [".env"],
506
+ ): Promise<void> {
507
+ await this.envUtils.loadEnv(root, files);
652
508
  }
653
509
 
654
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
  }