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
@@ -1,13 +1,40 @@
1
1
  import { exec } from "node:child_process";
2
- import { readFile, writeFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
2
  import { promisify } from "node:util";
5
- import { t } from "alepha";
3
+ import { $inject, $use, t } from "alepha";
6
4
  import { $command } from "alepha/command";
5
+ import { $logger } from "alepha/logger";
6
+ import { changelogOptions } from "../atoms/changelogOptions.ts";
7
+ import { GitMessageParser } from "../services/GitMessageParser.ts";
8
+
9
+ export {
10
+ type ChangelogOptions,
11
+ changelogOptions,
12
+ DEFAULT_IGNORE,
13
+ } from "../atoms/changelogOptions.ts";
14
+ export { GitMessageParser } from "../services/GitMessageParser.ts";
7
15
 
8
16
  const execAsync = promisify(exec);
9
17
 
10
- interface Commit {
18
+ // =============================================================================
19
+ // GIT PROVIDER
20
+ // =============================================================================
21
+
22
+ /**
23
+ * Git provider for executing git commands.
24
+ * Can be substituted in tests with a mock implementation.
25
+ */
26
+ export class GitProvider {
27
+ async exec(cmd: string, cwd: string): Promise<string> {
28
+ const { stdout } = await execAsync(`git ${cmd}`, { cwd });
29
+ return stdout;
30
+ }
31
+ }
32
+
33
+ // =============================================================================
34
+ // TYPES
35
+ // =============================================================================
36
+
37
+ export interface Commit {
11
38
  hash: string;
12
39
  type: string;
13
40
  scope: string | null;
@@ -16,374 +43,217 @@ interface Commit {
16
43
  }
17
44
 
18
45
  interface ChangelogEntry {
19
- version: string;
20
- date: string;
21
46
  features: Commit[];
22
47
  fixes: Commit[];
23
- docs: Commit[];
24
- improvements: Commit[];
25
48
  breaking: Commit[];
26
49
  }
27
50
 
28
- interface ChangelogConfig {
51
+ // =============================================================================
52
+ // CHANGELOG COMMANDS
53
+ // =============================================================================
54
+
55
+ /**
56
+ * Changelog command for generating release notes from git commits.
57
+ *
58
+ * Usage:
59
+ * - `alepha changelog` - Show unreleased changes since latest tag to HEAD
60
+ * - `alepha changelog --from=1.0.0` - Show changes from version to HEAD
61
+ * - `alepha changelog --from=1.0.0 --to=1.1.0` - Show changes between two refs
62
+ * - `alepha changelog | tee -a CHANGELOG.md` - Append to file
63
+ */
64
+ export class ChangelogCommands {
65
+ protected readonly log = $logger();
66
+ protected readonly git = $inject(GitProvider);
67
+ protected readonly parser = $inject(GitMessageParser);
68
+ protected readonly config = $use(changelogOptions);
69
+
70
+ // ---------------------------------------------------------------------------
71
+ // FORMATTING
72
+ // ---------------------------------------------------------------------------
73
+
29
74
  /**
30
- * Commit prefixes to ignore (e.g., "project", "release", "chore")
75
+ * Format a single commit line.
76
+ * Example: `- **cli**: add new command (\`abc1234\`)`
31
77
  */
32
- ignore?: string[];
78
+ protected formatCommit(commit: Commit): string {
79
+ return `- **${commit.scope}**: ${commit.description} (\`${commit.hash}\`)`;
80
+ }
81
+
33
82
  /**
34
- * Module scopes to recognize (e.g., "ui", "cli", "core")
35
- * If not provided, all non-ignored scopes are included
83
+ * Format the changelog entry with sections.
36
84
  */
37
- scopes?: string[];
38
- }
85
+ protected formatEntry(entry: ChangelogEntry): string {
86
+ const sections: string[] = [];
39
87
 
40
- const DEFAULT_IGNORE = [
41
- "project",
42
- "release",
43
- "starter",
44
- "example",
45
- "chore",
46
- "ci",
47
- "build",
48
- "test",
49
- "style",
50
- ];
51
-
52
- function parseCommit(line: string, config: ChangelogConfig): Commit | null {
53
- const match = line.match(/^([a-f0-9]+)\s+(.+)$/);
54
- if (!match) return null;
55
-
56
- const [, hash, message] = match;
57
- const breaking =
58
- message.includes("!:") || message.toLowerCase().includes("breaking");
59
- const ignore = config.ignore ?? DEFAULT_IGNORE;
60
-
61
- // Conventional commit: feat(scope): description or feat!: description
62
- const conventionalMatch = message.match(
63
- /^(feat|fix|docs|refactor|perf|revert)(?:\(([^)]+)\))?!?:\s*(.+)$/i,
64
- );
65
- if (conventionalMatch) {
66
- const [, type, scope, description] = conventionalMatch;
67
-
68
- // Skip if scope matches ignore list
69
- if (scope) {
70
- const baseScope = scope.split("/")[0];
71
- if (ignore.includes(baseScope) || ignore.includes(scope)) {
72
- return null;
88
+ if (entry.breaking.length > 0) {
89
+ sections.push("### Breaking Changes\n");
90
+ for (const commit of entry.breaking) {
91
+ sections.push(this.formatCommit(commit));
73
92
  }
93
+ sections.push("");
74
94
  }
75
95
 
76
- // Skip if type (without scope) matches ignore list (e.g., "docs" in ignore)
77
- if (!scope && ignore.includes(type.toLowerCase())) {
78
- return null;
79
- }
80
-
81
- return {
82
- hash: hash.substring(0, 8),
83
- type: type.toLowerCase(),
84
- scope: scope || null,
85
- description: description.trim(),
86
- breaking,
87
- };
88
- }
89
-
90
- // Module-specific commit: module: description or module/submodule: description
91
- const moduleMatch = message.match(
92
- /^([a-z][a-z0-9-]*(?:\/[a-z][a-z0-9-]*)?):\s*(.+)$/i,
93
- );
94
- if (moduleMatch) {
95
- const [, module, description] = moduleMatch;
96
- const baseModule = module.split("/")[0];
97
-
98
- // Skip ignored prefixes
99
- if (ignore.includes(baseModule)) {
100
- return null;
101
- }
102
-
103
- // If scopes are defined, check if this is a known scope
104
- if (config.scopes && !config.scopes.includes(baseModule)) {
105
- return null;
96
+ if (entry.features.length > 0) {
97
+ sections.push("### Features\n");
98
+ for (const commit of entry.features) {
99
+ sections.push(this.formatCommit(commit));
100
+ }
101
+ sections.push("");
106
102
  }
107
103
 
108
- // Determine type based on description keywords
109
- const desc = description.toLowerCase();
110
- let type = "improve";
111
- if (
112
- desc.includes("fix") ||
113
- desc.includes("bug") ||
114
- desc.includes("issue")
115
- ) {
116
- type = "fix";
117
- } else if (
118
- desc.includes("add") ||
119
- desc.includes("new") ||
120
- desc.includes("implement")
121
- ) {
122
- type = "feat";
104
+ if (entry.fixes.length > 0) {
105
+ sections.push("### Bug Fixes\n");
106
+ for (const commit of entry.fixes) {
107
+ sections.push(this.formatCommit(commit));
108
+ }
109
+ sections.push("");
123
110
  }
124
111
 
125
- return {
126
- hash: hash.substring(0, 8),
127
- type,
128
- scope: module,
129
- description: description.trim(),
130
- breaking,
131
- };
112
+ return sections.join("\n");
132
113
  }
133
114
 
134
- return null;
135
- }
115
+ // ---------------------------------------------------------------------------
116
+ // PARSING
117
+ // ---------------------------------------------------------------------------
136
118
 
137
- function formatCommit(commit: Commit): string {
138
- const scope = commit.scope ? `**${commit.scope}**: ` : "";
139
- return `- ${scope}${commit.description} (\`${commit.hash}\`)`;
140
- }
141
-
142
- function generateChangelog(entries: ChangelogEntry[]): string {
143
- let output = "# Changelog\n\n";
144
- output +=
145
- "All notable changes to this project will be documented in this file.\n\n";
119
+ /**
120
+ * Parse git log output into a changelog entry.
121
+ */
122
+ protected parseCommits(commitsOutput: string): ChangelogEntry {
123
+ const entry: ChangelogEntry = {
124
+ features: [],
125
+ fixes: [],
126
+ breaking: [],
127
+ };
146
128
 
147
- for (const entry of entries) {
148
- output += `## [${entry.version}] - ${entry.date}\n\n`;
149
- output += formatEntry(entry);
150
- }
129
+ for (const line of commitsOutput.trim().split("\n")) {
130
+ if (!line.trim()) continue;
151
131
 
152
- return output;
153
- }
132
+ const commit = this.parser.parseCommit(line, this.config);
133
+ if (!commit) {
134
+ this.log.trace("Skipping commit", { line });
135
+ continue;
136
+ }
154
137
 
155
- function formatEntry(entry: ChangelogEntry): string {
156
- let output = "";
138
+ this.log.trace("Parsed commit", { commit });
157
139
 
158
- if (entry.breaking.length > 0) {
159
- output += "### Breaking Changes\n\n";
160
- for (const commit of entry.breaking) {
161
- output += `${formatCommit(commit)}\n`;
140
+ // Categorize commit
141
+ if (commit.breaking) {
142
+ entry.breaking.push(commit);
143
+ }
144
+ if (commit.type === "feat") {
145
+ entry.features.push(commit);
146
+ } else if (commit.type === "fix") {
147
+ entry.fixes.push(commit);
148
+ }
162
149
  }
163
- output += "\n";
164
- }
165
150
 
166
- if (entry.features.length > 0) {
167
- output += "### Features\n\n";
168
- for (const commit of entry.features) {
169
- output += `${formatCommit(commit)}\n`;
170
- }
171
- output += "\n";
151
+ return entry;
172
152
  }
173
153
 
174
- if (entry.fixes.length > 0) {
175
- output += "### Bug Fixes\n\n";
176
- for (const commit of entry.fixes) {
177
- output += `${formatCommit(commit)}\n`;
178
- }
179
- output += "\n";
154
+ /**
155
+ * Check if entry has any public commits.
156
+ */
157
+ protected hasChanges(entry: ChangelogEntry): boolean {
158
+ return (
159
+ entry.features.length > 0 ||
160
+ entry.fixes.length > 0 ||
161
+ entry.breaking.length > 0
162
+ );
180
163
  }
181
164
 
182
- return output;
183
- }
184
-
185
- async function loadConfig(root: string): Promise<ChangelogConfig> {
186
- try {
187
- const pkgPath = join(root, "package.json");
188
- const pkg = JSON.parse(await readFile(pkgPath, "utf8"));
189
- return pkg.changelog ?? {};
190
- } catch {
191
- return {};
165
+ /**
166
+ * Get the latest version tag.
167
+ */
168
+ protected async getLatestTag(
169
+ git: (cmd: string) => Promise<string>,
170
+ ): Promise<string | null> {
171
+ const tagsOutput = await git("tag --sort=-version:refname");
172
+ const tags = tagsOutput
173
+ .trim()
174
+ .split("\n")
175
+ .filter((tag) => tag.match(/^\d+\.\d+\.\d+$/));
176
+
177
+ return tags[0] || null;
192
178
  }
193
- }
194
179
 
195
- export class ChangelogCommands {
180
+ // ---------------------------------------------------------------------------
181
+ // COMMAND
182
+ // ---------------------------------------------------------------------------
183
+
196
184
  public readonly changelog = $command({
197
185
  name: "changelog",
198
- description: "Generate CHANGELOG.md from git commits",
186
+ description:
187
+ "Generate changelog from conventional commits (outputs to stdout)",
199
188
  flags: t.object({
200
- release: t.optional(
201
- t.boolean({
202
- when: ["--release", "-r"],
203
- description:
204
- "Output release notes for the latest version only (for GitHub Release)",
205
- }),
206
- ),
207
- preview: t.optional(
208
- t.boolean({
209
- when: ["--preview", "-p"],
210
- description: "Preview unreleased changes (commits since last tag)",
211
- }),
212
- ),
213
- output: t.optional(
189
+ /**
190
+ * Show changes from this ref (tag, commit, branch).
191
+ * Defaults to the latest version tag.
192
+ * Example: --from=1.0.0
193
+ */
194
+ from: t.optional(
214
195
  t.string({
215
- when: ["--output", "-o"],
216
- description:
217
- "Output file path (defaults to CHANGELOG.md, use - for stdout)",
196
+ aliases: ["f"],
197
+ description: "Starting ref (default: latest tag)",
218
198
  }),
219
199
  ),
220
- limit: t.optional(
221
- t.number({
222
- when: ["--limit", "-l"],
223
- description: "Limit number of versions to include",
200
+ /**
201
+ * Show changes up to this ref (tag, commit, branch).
202
+ * Defaults to HEAD.
203
+ * Example: --to=main
204
+ */
205
+ to: t.optional(
206
+ t.string({
207
+ aliases: ["t"],
208
+ description: "Ending ref (default: HEAD)",
224
209
  }),
225
210
  ),
226
211
  }),
227
- handler: async ({ flags, run, root }) => {
228
- const config = await loadConfig(root);
229
-
230
- const git = async (cmd: string) => {
231
- const { stdout } = await execAsync(`git ${cmd}`, { cwd: root });
232
- return stdout;
233
- };
234
-
235
- // Get all tags sorted by version
236
- const tagsOutput = await git("tag --sort=-version:refname");
237
- const tags = tagsOutput
238
- .trim()
239
- .split("\n")
240
- .filter((tag) => tag.match(/^\d+\.\d+\.\d+$/));
241
-
242
- if (tags.length === 0) {
243
- throw new Error("No version tags found");
244
- }
245
-
246
- // Preview mode: show unreleased changes
247
- if (flags.preview) {
248
- const latestTag = tags[0];
249
- const commitsOutput = await git(`log ${latestTag}..HEAD --oneline`);
212
+ handler: async ({ flags, root }) => {
213
+ const git = (cmd: string) => this.git.exec(cmd, root);
250
214
 
251
- if (!commitsOutput.trim()) {
252
- console.log("No unreleased changes since", latestTag);
253
- return;
254
- }
255
-
256
- const entry: ChangelogEntry = {
257
- version: "Unreleased",
258
- date: new Date().toISOString().split("T")[0],
259
- features: [],
260
- fixes: [],
261
- docs: [],
262
- improvements: [],
263
- breaking: [],
264
- };
265
-
266
- for (const line of commitsOutput.trim().split("\n")) {
267
- if (!line.trim()) continue;
268
- const commit = parseCommit(line, config);
269
- if (!commit) continue;
270
-
271
- if (commit.breaking) entry.breaking.push(commit);
272
-
273
- switch (commit.type) {
274
- case "feat":
275
- entry.features.push(commit);
276
- break;
277
- case "fix":
278
- entry.fixes.push(commit);
279
- break;
280
- case "docs":
281
- entry.docs.push(commit);
282
- break;
283
- case "refactor":
284
- case "perf":
285
- case "improve":
286
- entry.improvements.push(commit);
287
- break;
288
- }
289
- }
290
-
291
- const hasCommits =
292
- entry.features.length > 0 ||
293
- entry.fixes.length > 0 ||
294
- entry.breaking.length > 0;
215
+ // Determine the starting point
216
+ let fromRef: string;
295
217
 
296
- if (!hasCommits) {
297
- console.log("No public changes since", latestTag);
218
+ if (flags.from) {
219
+ // User specified a ref
220
+ fromRef = flags.from;
221
+ this.log.debug("Using specified from ref", { from: fromRef });
222
+ } else {
223
+ // Use latest tag
224
+ const latestTag = await this.getLatestTag(git);
225
+ if (!latestTag) {
226
+ process.stdout.write("No version tags found in repository\n");
298
227
  return;
299
228
  }
300
-
301
- console.log(`## [Unreleased] - since ${latestTag}\n`);
302
- console.log(formatEntry(entry));
303
- return;
229
+ fromRef = latestTag;
230
+ this.log.debug("Using latest tag", { from: fromRef });
304
231
  }
305
232
 
306
- const entries: ChangelogEntry[] = [];
307
- const limit = flags.limit || (flags.release ? 1 : tags.length);
308
-
309
- for (let i = 0; i < Math.min(limit, tags.length); i++) {
310
- const tag = tags[i];
311
- const prevTag = tags[i + 1];
312
-
313
- // Get tag date
314
- const dateOutput = await git(`log -1 --format=%ci ${tag}`);
315
- const date = dateOutput.trim().split(" ")[0];
316
-
317
- // Get commits between tags
318
- const range = prevTag ? `${prevTag}..${tag}` : tag;
319
- const commitsOutput = await git(`log ${range} --oneline`);
320
-
321
- const entry: ChangelogEntry = {
322
- version: tag,
323
- date,
324
- features: [],
325
- fixes: [],
326
- docs: [],
327
- improvements: [],
328
- breaking: [],
329
- };
330
-
331
- for (const line of commitsOutput.trim().split("\n")) {
332
- if (!line.trim()) continue;
333
- const commit = parseCommit(line, config);
334
- if (!commit) continue;
335
-
336
- if (commit.breaking) entry.breaking.push(commit);
337
-
338
- switch (commit.type) {
339
- case "feat":
340
- entry.features.push(commit);
341
- break;
342
- case "fix":
343
- entry.fixes.push(commit);
344
- break;
345
- case "docs":
346
- entry.docs.push(commit);
347
- break;
348
- case "refactor":
349
- case "perf":
350
- case "improve":
351
- entry.improvements.push(commit);
352
- break;
353
- }
354
- }
233
+ // Determine the ending point
234
+ const toRef = flags.to || "HEAD";
235
+ this.log.debug("Using to ref", { to: toRef });
355
236
 
356
- // Only add entry if it has any commits
357
- const hasCommits =
358
- entry.features.length > 0 ||
359
- entry.fixes.length > 0 ||
360
- entry.breaking.length > 0;
237
+ // Get commits in range
238
+ const commitsOutput = await git(`log ${fromRef}..${toRef} --oneline`);
361
239
 
362
- if (hasCommits) {
363
- entries.push(entry);
364
- }
365
- }
366
-
367
- if (entries.length === 0) {
368
- console.log("No public commits found");
240
+ if (!commitsOutput.trim()) {
241
+ process.stdout.write(`No changes in range ${fromRef}..${toRef}\n`);
369
242
  return;
370
243
  }
371
244
 
372
- let output: string;
373
- if (flags.release) {
374
- output = formatEntry(entries[0]);
375
- } else {
376
- output = generateChangelog(entries);
377
- }
245
+ // Parse and format
246
+ const entry = this.parseCommits(commitsOutput);
378
247
 
379
- const outputPath = flags.output ?? "CHANGELOG.md";
380
- if (outputPath === "-") {
381
- console.log(output);
382
- } else {
383
- await run(`Writing ${outputPath}`, () =>
384
- writeFile(join(root, outputPath), output, "utf8"),
248
+ if (!this.hasChanges(entry)) {
249
+ process.stdout.write(
250
+ `No public changes in range ${fromRef}..${toRef}\n`,
385
251
  );
252
+ return;
386
253
  }
254
+
255
+ // Output the formatted changelog (no header - caller adds it if needed)
256
+ process.stdout.write(this.formatEntry(entry));
387
257
  },
388
258
  });
389
259
  }
@@ -0,0 +1,118 @@
1
+ import { join } from "node:path";
2
+ import { $inject, AlephaError, t } from "alepha";
3
+ import { $command } from "alepha/command";
4
+ import { $logger } from "alepha/logger";
5
+ import { AlephaCliUtils } from "../services/AlephaCliUtils.ts";
6
+
7
+ export class DeployCommands {
8
+ protected readonly log = $logger();
9
+ protected readonly utils = $inject(AlephaCliUtils);
10
+
11
+ /**
12
+ * Deploy the project to a hosting platform (e.g., Vercel, Cloudflare, Surge)
13
+ *
14
+ * Deploy command can be overridden by creating a alepha.config.ts in the project root:
15
+ *
16
+ * ```ts
17
+ * import { defineConfig } from "alepha/cli";
18
+ *
19
+ * export default defineConfig({
20
+ * commands: {
21
+ * deploy: {
22
+ * handler: async ({ root, mode, flags }) => {
23
+ * // Custom deployment logic here
24
+ * },
25
+ * },
26
+ * },
27
+ * });
28
+ * ```
29
+ */
30
+ public readonly deploy = $command({
31
+ name: "deploy",
32
+ description:
33
+ "Deploy the project to a hosting platform (e.g., Vercel, Cloudflare, Surge)",
34
+ mode: true,
35
+ flags: t.object({
36
+ build: t.boolean({
37
+ description: "Build the project before deployment",
38
+ default: false,
39
+ }),
40
+ migrate: t.boolean({
41
+ description:
42
+ "Run database migrations before deployment (if applicable)",
43
+ default: false,
44
+ }),
45
+ }),
46
+ env: t.object({
47
+ VERCEL_TOKEN: t.optional(
48
+ t.text({
49
+ description: "Vercel API token (e.g., xxxxxxxxxxxxxxxxxxxx)",
50
+ }),
51
+ ),
52
+ VERCEL_ORG_ID: t.optional(
53
+ t.text({
54
+ description: "Vercel organization ID (e.g., team_abc123...)",
55
+ }),
56
+ ),
57
+ VERCEL_PROJECT_ID: t.optional(
58
+ t.text({ description: "Vercel project ID (e.g., prj_abc123...)" }),
59
+ ),
60
+ CLOUDFLARE_API_TOKEN: t.optional(
61
+ t.text({
62
+ description: "Cloudflare API token (e.g., xxxx-xxxx-xxxx-xxxx)",
63
+ }),
64
+ ),
65
+ CLOUDFLARE_ACCOUNT_ID: t.optional(
66
+ t.text({
67
+ description: "Cloudflare account ID (e.g., abc123def456...)",
68
+ }),
69
+ ),
70
+ }),
71
+ handler: async ({ root, mode, flags }) => {
72
+ if (flags.build) {
73
+ await this.utils.exec("alepha build");
74
+ }
75
+
76
+ // Vercel deployment
77
+ if (await this.utils.exists(root, "dist/vercel.json")) {
78
+ if (flags.migrate) {
79
+ this.log.debug("Running database migrations before deployment...");
80
+ await this.utils.exec(`alepha db migrate --mode=${mode}`);
81
+ }
82
+ await this.utils.ensureDependency(root, "vercel", { dev: true });
83
+ const command =
84
+ `vercel . --cwd=dist ${mode === "production" ? "--prod" : ""}`.trim();
85
+ this.log.debug(`Deploying to Vercel with command: ${command}`);
86
+ await this.utils.exec(command);
87
+ return;
88
+ }
89
+
90
+ // Cloudflare deployment
91
+ if (await this.utils.exists(root, "dist/wrangler.jsonc")) {
92
+ if (flags.migrate) {
93
+ this.log.debug("Running database migrations before deployment...");
94
+ await this.utils.exec(`alepha db migrate --mode=${mode}`);
95
+ }
96
+ await this.utils.ensureDependency(root, "wrangler", { dev: true });
97
+ const command =
98
+ `wrangler deploy ${mode === "production" ? "" : "--env preview"} --config=dist/wrangler.jsonc`.trim();
99
+ this.log.info(`Deploying to Cloudflare with command: ${command}`);
100
+ await this.utils.exec(command);
101
+ return;
102
+ }
103
+
104
+ // Surge deployment
105
+ if (await this.utils.exists(root, "dist/public/404.html")) {
106
+ await this.utils.ensureDependency(root, "surge", { dev: true });
107
+ const distPath = join(root, "dist/public");
108
+ this.log.debug(`Deploying to Surge from directory: ${distPath}`);
109
+ await this.utils.exec(`surge ${distPath}`);
110
+ return;
111
+ }
112
+
113
+ throw new AlephaError(
114
+ "No deployment configuration found in the dist folder.",
115
+ );
116
+ },
117
+ });
118
+ }