alepha 0.20.3 → 0.20.4

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 (217) hide show
  1. package/dist/api/audits/index.d.ts.map +1 -1
  2. package/dist/api/files/index.d.ts.map +1 -1
  3. package/dist/api/jobs/index.d.ts +14 -14
  4. package/dist/api/jobs/index.d.ts.map +1 -1
  5. package/dist/api/keys/index.d.ts +4 -4
  6. package/dist/api/organizations/index.d.ts.map +1 -1
  7. package/dist/api/parameters/index.d.ts +8 -3
  8. package/dist/api/parameters/index.d.ts.map +1 -1
  9. package/dist/api/parameters/index.js +20 -4
  10. package/dist/api/parameters/index.js.map +1 -1
  11. package/dist/api/payments/index.d.ts.map +1 -1
  12. package/dist/api/users/index.browser.js +6 -0
  13. package/dist/api/users/index.browser.js.map +1 -1
  14. package/dist/api/users/index.d.ts +5037 -139
  15. package/dist/api/users/index.d.ts.map +1 -1
  16. package/dist/api/users/index.js +58 -10
  17. package/dist/api/users/index.js.map +1 -1
  18. package/dist/bucket/index.d.ts +77 -107
  19. package/dist/bucket/index.d.ts.map +1 -1
  20. package/dist/bucket/index.js +148 -4
  21. package/dist/bucket/index.js.map +1 -1
  22. package/dist/bucket/index.workerd.js +7 -1
  23. package/dist/bucket/index.workerd.js.map +1 -1
  24. package/dist/cache/core/index.d.ts +26 -0
  25. package/dist/cache/core/index.d.ts.map +1 -1
  26. package/dist/cache/core/index.js +11 -1
  27. package/dist/cache/core/index.js.map +1 -1
  28. package/dist/cache/core/index.workerd.js +11 -1
  29. package/dist/cache/core/index.workerd.js.map +1 -1
  30. package/dist/cli/config/index.d.ts +7 -5
  31. package/dist/cli/config/index.d.ts.map +1 -1
  32. package/dist/cli/config/index.js +2 -3
  33. package/dist/cli/config/index.js.map +1 -1
  34. package/dist/cli/core/index.d.ts +420 -13
  35. package/dist/cli/core/index.d.ts.map +1 -1
  36. package/dist/cli/core/index.js +22 -511
  37. package/dist/cli/core/index.js.map +1 -1
  38. package/dist/cli/devtools/index.d.ts +4 -8
  39. package/dist/cli/devtools/index.d.ts.map +1 -1
  40. package/dist/cli/devtools/index.js +13 -15
  41. package/dist/cli/devtools/index.js.map +1 -1
  42. package/dist/cli/platform/index.d.ts +10 -13
  43. package/dist/cli/platform/index.d.ts.map +1 -1
  44. package/dist/cli/platform/index.js +18 -15
  45. package/dist/cli/platform/index.js.map +1 -1
  46. package/dist/cli/vendor/index.d.ts +10 -13
  47. package/dist/cli/vendor/index.d.ts.map +1 -1
  48. package/dist/cli/vendor/index.js +16 -13
  49. package/dist/cli/vendor/index.js.map +1 -1
  50. package/dist/core/index.browser.js +27 -3
  51. package/dist/core/index.browser.js.map +1 -1
  52. package/dist/core/index.d.ts +6 -3
  53. package/dist/core/index.d.ts.map +1 -1
  54. package/dist/core/index.js +27 -3
  55. package/dist/core/index.js.map +1 -1
  56. package/dist/core/index.native.js +27 -3
  57. package/dist/core/index.native.js.map +1 -1
  58. package/dist/core/index.workerd.js +27 -3
  59. package/dist/core/index.workerd.js.map +1 -1
  60. package/dist/datetime/index.d.ts +69 -10
  61. package/dist/datetime/index.d.ts.map +1 -1
  62. package/dist/datetime/index.js +135 -13
  63. package/dist/datetime/index.js.map +1 -1
  64. package/dist/email/smtp/index.js +10636 -2
  65. package/dist/email/smtp/index.js.map +1 -1
  66. package/dist/fake/index.d.ts +8085 -4
  67. package/dist/fake/index.d.ts.map +1 -1
  68. package/dist/fake/index.js +33554 -3
  69. package/dist/fake/index.js.map +1 -1
  70. package/dist/lock/core/index.d.ts +30 -2
  71. package/dist/lock/core/index.d.ts.map +1 -1
  72. package/dist/lock/core/index.js +35 -12
  73. package/dist/lock/core/index.js.map +1 -1
  74. package/dist/mcp/index.d.ts +238 -31
  75. package/dist/mcp/index.d.ts.map +1 -1
  76. package/dist/mcp/index.js +198 -71
  77. package/dist/mcp/index.js.map +1 -1
  78. package/dist/orm/core/index.browser.js +1 -1
  79. package/dist/orm/core/index.browser.js.map +1 -1
  80. package/dist/orm/core/index.bun.js +4 -3
  81. package/dist/orm/core/index.bun.js.map +1 -1
  82. package/dist/orm/core/index.d.ts +4877 -9
  83. package/dist/orm/core/index.d.ts.map +1 -1
  84. package/dist/orm/core/index.js +4 -3
  85. package/dist/orm/core/index.js.map +1 -1
  86. package/dist/orm/postgres/index.d.ts +608 -1
  87. package/dist/orm/postgres/index.d.ts.map +1 -1
  88. package/dist/react/core/index.d.ts +102 -1
  89. package/dist/react/core/index.d.ts.map +1 -1
  90. package/dist/react/core/index.js +65 -1
  91. package/dist/react/core/index.js.map +1 -1
  92. package/dist/react/form/index.d.ts +6 -0
  93. package/dist/react/form/index.d.ts.map +1 -1
  94. package/dist/react/form/index.js +7 -7
  95. package/dist/react/form/index.js.map +1 -1
  96. package/dist/react/i18n/index.d.ts +7 -1
  97. package/dist/react/i18n/index.d.ts.map +1 -1
  98. package/dist/react/i18n/index.js +6 -0
  99. package/dist/react/i18n/index.js.map +1 -1
  100. package/dist/react/router/index.browser.js +20 -2
  101. package/dist/react/router/index.browser.js.map +1 -1
  102. package/dist/react/router/index.d.ts +36 -4
  103. package/dist/react/router/index.d.ts.map +1 -1
  104. package/dist/react/router/index.js +20 -2
  105. package/dist/react/router/index.js.map +1 -1
  106. package/dist/react/testing/chunk-6Ep1yQYe.js +16 -0
  107. package/dist/react/testing/index.d.ts +411 -1
  108. package/dist/react/testing/index.d.ts.map +1 -1
  109. package/dist/react/testing/index.js +12293 -13
  110. package/dist/react/testing/index.js.map +1 -1
  111. package/dist/react/ui/index.d.ts +195 -1
  112. package/dist/react/ui/index.d.ts.map +1 -1
  113. package/dist/react/ui/index.js +61 -1
  114. package/dist/react/ui/index.js.map +1 -1
  115. package/dist/scheduler/index.d.ts +84 -3
  116. package/dist/scheduler/index.d.ts.map +1 -1
  117. package/dist/scheduler/index.js +390 -1
  118. package/dist/scheduler/index.js.map +1 -1
  119. package/dist/scheduler/index.workerd.js +390 -1
  120. package/dist/scheduler/index.workerd.js.map +1 -1
  121. package/dist/security/index.d.ts +325 -2
  122. package/dist/security/index.d.ts.map +1 -1
  123. package/dist/security/index.js +1361 -2
  124. package/dist/security/index.js.map +1 -1
  125. package/dist/server/auth/index.d.ts +1054 -1
  126. package/dist/server/auth/index.d.ts.map +1 -1
  127. package/dist/server/auth/index.js +1223 -1
  128. package/dist/server/auth/index.js.map +1 -1
  129. package/dist/server/core/index.browser.js +10 -3
  130. package/dist/server/core/index.browser.js.map +1 -1
  131. package/dist/server/core/index.d.ts.map +1 -1
  132. package/dist/server/core/index.js +28 -5
  133. package/dist/server/core/index.js.map +1 -1
  134. package/dist/server/metrics/index.d.ts +514 -1
  135. package/dist/server/metrics/index.d.ts.map +1 -1
  136. package/dist/server/metrics/index.js +4374 -4
  137. package/dist/server/metrics/index.js.map +1 -1
  138. package/dist/server/swagger/index.d.ts.map +1 -1
  139. package/dist/server/swagger/index.js +3 -4
  140. package/dist/server/swagger/index.js.map +1 -1
  141. package/dist/websocket/index.browser.js +11 -5
  142. package/dist/websocket/index.browser.js.map +1 -1
  143. package/dist/websocket/index.d.ts +3 -1
  144. package/dist/websocket/index.d.ts.map +1 -1
  145. package/dist/websocket/index.js +21 -6
  146. package/dist/websocket/index.js.map +1 -1
  147. package/package.json +671 -263
  148. package/src/api/parameters/services/ParameterProvider.ts +21 -4
  149. package/src/api/users/__tests__/SessionService.spec.ts +99 -0
  150. package/src/api/users/__tests__/UserJobs.spec.ts +67 -0
  151. package/src/api/users/atoms/realmAuthSettingsAtom.ts +15 -0
  152. package/src/api/users/entities/sessions.ts +6 -0
  153. package/src/api/users/jobs/UserJobs.ts +44 -17
  154. package/src/api/users/providers/RealmProvider.ts +4 -0
  155. package/src/api/users/services/SessionService.ts +27 -0
  156. package/src/bucket/__tests__/NodeS3BucketProvider.spec.ts +74 -0
  157. package/src/bucket/index.ts +19 -2
  158. package/src/bucket/primitives/$bucket.ts +9 -1
  159. package/src/bucket/providers/CloudflareR2Provider.ts +2 -137
  160. package/src/bucket/providers/NodeS3BucketProvider.ts +218 -0
  161. package/src/cache/core/index.ts +29 -0
  162. package/src/cache/core/primitives/$cache.ts +14 -1
  163. package/src/cli/config/defineConfig.ts +13 -15
  164. package/src/cli/core/__tests__/init.spec.ts +6 -7
  165. package/src/cli/core/services/ProjectScaffolder.ts +18 -14
  166. package/src/cli/core/tasks/BuildCloudflareTask.ts +5 -0
  167. package/src/cli/core/templates/agentMd.ts +2 -10
  168. package/src/cli/core/templates/saasAdminLayoutTsx.ts +3 -3
  169. package/src/cli/devtools/index.ts +12 -26
  170. package/src/cli/platform/index.ts +15 -24
  171. package/src/cli/vendor/atoms/vendorOptions.ts +1 -1
  172. package/src/cli/vendor/index.ts +14 -23
  173. package/src/core/Alepha.ts +11 -1
  174. package/src/core/helpers/ref.ts +18 -0
  175. package/src/core/index.shared.ts +1 -0
  176. package/src/core/providers/SchemaValidator.ts +9 -1
  177. package/src/core/providers/TypeProvider.ts +1 -2
  178. package/src/datetime/REFACTORING.md +118 -0
  179. package/src/datetime/providers/DateTimeProvider.ts +203 -24
  180. package/src/lock/core/index.ts +31 -0
  181. package/src/lock/core/primitives/$lock.ts +14 -1
  182. package/src/mcp/__tests__/jsonrpc.spec.ts +1 -1
  183. package/src/mcp/helpers/jsonrpc.ts +26 -1
  184. package/src/mcp/index.ts +10 -5
  185. package/src/mcp/interfaces/McpTypes.ts +83 -6
  186. package/src/mcp/primitives/$prompt.ts +18 -1
  187. package/src/mcp/primitives/$resource.ts +18 -1
  188. package/src/mcp/primitives/$tool.ts +83 -7
  189. package/src/mcp/providers/McpServerProvider.ts +74 -16
  190. package/src/mcp/transports/StreamableHttpMcpTransport.ts +226 -0
  191. package/src/orm/REFACTORING.md +330 -0
  192. package/src/orm/core/primitives/$transactional.ts +11 -0
  193. package/src/orm/core/schemas/updateSchema.ts +1 -1
  194. package/src/orm/core/services/PgRelationManager.ts +4 -2
  195. package/src/react/core/__tests__/useQuery.browser.spec.tsx +86 -0
  196. package/src/react/core/hooks/useQuery.ts +153 -0
  197. package/src/react/core/index.ts +1 -0
  198. package/src/react/form/services/FormModel.ts +15 -6
  199. package/src/react/form/services/parseField.ts +8 -0
  200. package/src/react/i18n/providers/I18nProvider.ts +8 -2
  201. package/src/react/router/__tests__/$page.spec.tsx +0 -16
  202. package/src/react/router/__tests__/ssr.spec.tsx +339 -0
  203. package/src/react/router/primitives/$page.ts +28 -4
  204. package/src/react/router/providers/ReactPageProvider.ts +27 -9
  205. package/src/react/ui/atoms/uiThemeListAtom.ts +36 -0
  206. package/src/react/ui/index.ts +6 -0
  207. package/src/react/ui/services/SchemaControl.ts +209 -0
  208. package/src/security/primitives/$issuer.ts +6 -3
  209. package/src/server/core/__tests__/ServerRouterProvider-serializationError.spec.ts +75 -0
  210. package/src/server/core/__tests__/ServerRouterProvider-validationError.spec.ts +306 -0
  211. package/src/server/core/errors/ValidationError.ts +13 -1
  212. package/src/server/core/primitives/$action.ts +16 -5
  213. package/src/server/core/providers/ServerRouterProvider.ts +26 -4
  214. package/src/server/swagger/providers/ServerSwaggerProvider.ts +5 -7
  215. package/src/websocket/providers/NodeWebSocketServerProvider.ts +10 -4
  216. package/src/websocket/services/WebSocketClient.ts +11 -5
  217. package/src/mcp/transports/SseMcpTransport.ts +0 -182
@@ -1,3 +1,4 @@
1
+ import type { R2Bucket } from "@cloudflare/workers-types";
1
2
  import {
2
3
  $env,
3
4
  $hook,
@@ -14,142 +15,6 @@ import type { FileStorageProvider } from "./FileStorageProvider.ts";
14
15
 
15
16
  // ---------------------------------------------------------------------------------------------------------------------
16
17
 
17
- /**
18
- * R2Bucket interface matching Cloudflare's R2 API.
19
- */
20
- export interface R2Bucket {
21
- put(
22
- key: string,
23
- value:
24
- | ReadableStream
25
- | ArrayBuffer
26
- | ArrayBufferView
27
- | string
28
- | Blob
29
- | null,
30
- options?: R2PutOptions,
31
- ): Promise<R2Object | null>;
32
- get(key: string, options?: R2GetOptions): Promise<R2ObjectBody | null>;
33
- head(key: string): Promise<R2Object | null>;
34
- delete(keys: string | string[]): Promise<void>;
35
- list(options?: R2ListOptions): Promise<R2Objects>;
36
- createMultipartUpload(
37
- key: string,
38
- options?: R2MultipartOptions,
39
- ): Promise<R2MultipartUpload>;
40
- }
41
-
42
- export interface R2Object {
43
- key: string;
44
- version: string;
45
- size: number;
46
- etag: string;
47
- httpEtag: string;
48
- checksums: R2Checksums;
49
- uploaded: Date;
50
- httpMetadata?: R2HTTPMetadata;
51
- customMetadata?: Record<string, string>;
52
- range?: R2Range;
53
- storageClass: string;
54
- }
55
-
56
- export interface R2ObjectBody extends R2Object {
57
- body: ReadableStream;
58
- bodyUsed: boolean;
59
- arrayBuffer(): Promise<ArrayBuffer>;
60
- text(): Promise<string>;
61
- json<T>(): Promise<T>;
62
- blob(): Promise<Blob>;
63
- }
64
-
65
- export interface R2PutOptions {
66
- onlyIf?: R2Conditional;
67
- httpMetadata?: R2HTTPMetadata;
68
- customMetadata?: Record<string, string>;
69
- md5?: ArrayBuffer | string;
70
- sha1?: ArrayBuffer | string;
71
- sha256?: ArrayBuffer | string;
72
- sha384?: ArrayBuffer | string;
73
- sha512?: ArrayBuffer | string;
74
- storageClass?: string;
75
- }
76
-
77
- export interface R2GetOptions {
78
- onlyIf?: R2Conditional;
79
- range?: R2Range;
80
- }
81
-
82
- export interface R2ListOptions {
83
- limit?: number;
84
- prefix?: string;
85
- cursor?: string;
86
- delimiter?: string;
87
- startAfter?: string;
88
- include?: ("httpMetadata" | "customMetadata")[];
89
- }
90
-
91
- export interface R2Objects {
92
- objects: R2Object[];
93
- truncated: boolean;
94
- cursor?: string;
95
- delimitedPrefixes: string[];
96
- }
97
-
98
- export interface R2Checksums {
99
- md5?: ArrayBuffer;
100
- sha1?: ArrayBuffer;
101
- sha256?: ArrayBuffer;
102
- sha384?: ArrayBuffer;
103
- sha512?: ArrayBuffer;
104
- }
105
-
106
- export interface R2HTTPMetadata {
107
- contentType?: string;
108
- contentLanguage?: string;
109
- contentDisposition?: string;
110
- contentEncoding?: string;
111
- cacheControl?: string;
112
- cacheExpiry?: Date;
113
- }
114
-
115
- export interface R2Conditional {
116
- etagMatches?: string;
117
- etagDoesNotMatch?: string;
118
- uploadedBefore?: Date;
119
- uploadedAfter?: Date;
120
- secondsGranularity?: boolean;
121
- }
122
-
123
- export interface R2Range {
124
- offset?: number;
125
- length?: number;
126
- suffix?: number;
127
- }
128
-
129
- export interface R2MultipartOptions {
130
- httpMetadata?: R2HTTPMetadata;
131
- customMetadata?: Record<string, string>;
132
- storageClass?: string;
133
- }
134
-
135
- export interface R2MultipartUpload {
136
- key: string;
137
- uploadId: string;
138
- uploadPart(
139
- partNumber: number,
140
- value: ReadableStream | ArrayBuffer | ArrayBufferView | string | Blob,
141
- ): Promise<R2UploadedPart>;
142
- abort(): Promise<void>;
143
- complete(uploadedParts: R2UploadedPart[]): Promise<R2Object>;
144
- }
145
-
146
- export interface R2UploadedPart {
147
- partNumber: number;
148
- etag: string;
149
- }
150
-
151
- // ---------------------------------------------------------------------------------------------------------------------
152
-
153
18
  /**
154
19
  * Cloudflare R2 storage provider.
155
20
  *
@@ -311,7 +176,7 @@ export class CloudflareR2Provider implements FileStorageProvider {
311
176
  type: contentType,
312
177
  size: object.size,
313
178
  lastModified: object.uploaded.getTime(),
314
- stream: () => object.body,
179
+ stream: () => object.body as unknown as ReadableStream,
315
180
  arrayBuffer: () => object.arrayBuffer(),
316
181
  text: () => object.text(),
317
182
  };
@@ -0,0 +1,218 @@
1
+ import { Buffer } from "node:buffer";
2
+ import {
3
+ $env,
4
+ $hook,
5
+ $inject,
6
+ Alepha,
7
+ AlephaError,
8
+ type FileLike,
9
+ type Static,
10
+ t,
11
+ } from "alepha";
12
+ import { $logger } from "alepha/logger";
13
+ import { FileDetector, FileSystemProvider } from "alepha/system";
14
+ import { S3mini } from "s3mini";
15
+ import { FileNotFoundError } from "../errors/FileNotFoundError.ts";
16
+ import { $bucket } from "../primitives/$bucket.ts";
17
+ import type { FileStorageProvider } from "./FileStorageProvider.ts";
18
+
19
+ const envSchema = t.object({
20
+ /**
21
+ * S3 endpoint URL. The bucket name is appended (path-style) per request.
22
+ *
23
+ * Examples:
24
+ * - AWS S3: `https://s3.us-east-1.amazonaws.com`
25
+ * - Cloudflare R2: `https://<account-id>.r2.cloudflarestorage.com`
26
+ * - MinIO: `http://localhost:9000`
27
+ * - DigitalOcean Spaces: `https://<region>.digitaloceanspaces.com`
28
+ */
29
+ S3_ENDPOINT: t.string(),
30
+
31
+ /**
32
+ * AWS region or "auto" for R2.
33
+ *
34
+ * @default "auto"
35
+ */
36
+ S3_REGION: t.optional(t.string()),
37
+
38
+ /**
39
+ * Access key ID for S3 authentication.
40
+ */
41
+ S3_ACCESS_KEY_ID: t.string(),
42
+
43
+ /**
44
+ * Secret access key for S3 authentication.
45
+ */
46
+ S3_SECRET_ACCESS_KEY: t.string(),
47
+ });
48
+
49
+ declare module "alepha" {
50
+ interface Env extends Partial<Static<typeof envSchema>> {}
51
+ }
52
+
53
+ /**
54
+ * S3-compatible file storage provider for Node.js.
55
+ *
56
+ * Backed by `s3mini` (zero-dep, ~20 KB). Works with AWS S3, Cloudflare R2,
57
+ * MinIO, DigitalOcean Spaces, Backblaze B2, and any other S3-compatible service.
58
+ *
59
+ * Uses path-style addressing (`<endpoint>/<bucket>`).
60
+ */
61
+ export class NodeS3BucketProvider implements FileStorageProvider {
62
+ protected readonly log = $logger();
63
+ protected readonly env = $env(envSchema);
64
+ protected readonly alepha = $inject(Alepha);
65
+ protected readonly fileSystem = $inject(FileSystemProvider);
66
+ protected readonly fileDetector = $inject(FileDetector);
67
+ protected readonly clients: Map<string, S3mini> = new Map();
68
+
69
+ /**
70
+ * Convert bucket name to S3-compatible format.
71
+ * S3 bucket names must be lowercase, 3-63 characters, no underscores.
72
+ */
73
+ public convertName(name: string): string {
74
+ return name.replaceAll("/", "-").replaceAll("_", "-").toLowerCase();
75
+ }
76
+
77
+ protected getClient(bucketName: string): S3mini {
78
+ const name = this.convertName(bucketName);
79
+ let client = this.clients.get(name);
80
+ if (!client) {
81
+ const endpoint = this.env.S3_ENDPOINT.replace(/\/+$/, "");
82
+ client = new S3mini({
83
+ accessKeyId: this.env.S3_ACCESS_KEY_ID,
84
+ secretAccessKey: this.env.S3_SECRET_ACCESS_KEY,
85
+ region: this.env.S3_REGION || "auto",
86
+ endpoint: `${endpoint}/${name}`,
87
+ });
88
+ this.clients.set(name, client);
89
+ }
90
+ return client;
91
+ }
92
+
93
+ protected readonly onStart = $hook({
94
+ on: "start",
95
+ handler: async () => {
96
+ for (const bucket of this.alepha.primitives($bucket)) {
97
+ if (bucket.provider !== this) {
98
+ continue;
99
+ }
100
+
101
+ const name = this.convertName(bucket.name);
102
+ const client = this.getClient(bucket.name);
103
+
104
+ this.log.debug(`Preparing S3 bucket '${name}'...`);
105
+
106
+ const exists = await client.bucketExists();
107
+ if (!exists) {
108
+ this.log.debug(`Creating S3 bucket '${name}'...`);
109
+ const created = await client.createBucket();
110
+ if (!created) {
111
+ throw new AlephaError(`Failed to create S3 bucket '${name}'`);
112
+ }
113
+ }
114
+
115
+ this.log.info(`S3 bucket '${bucket.name}' OK`);
116
+ }
117
+ },
118
+ });
119
+
120
+ protected createId(mimeType: string): string {
121
+ const ext = this.fileDetector.getExtensionFromMimeType(mimeType);
122
+ return `${crypto.randomUUID()}.${ext}`;
123
+ }
124
+
125
+ public async upload(
126
+ bucketName: string,
127
+ file: FileLike,
128
+ fileId?: string,
129
+ ): Promise<string> {
130
+ fileId ??= this.createId(file.type);
131
+
132
+ this.log.trace(
133
+ `Uploading file '${file.name}' to bucket '${bucketName}' with id '${fileId}'...`,
134
+ );
135
+
136
+ const client = this.getClient(bucketName);
137
+
138
+ try {
139
+ const buffer = new Uint8Array(await file.arrayBuffer());
140
+ await client.putObject(
141
+ fileId,
142
+ buffer,
143
+ file.type || "application/octet-stream",
144
+ undefined,
145
+ { "x-amz-meta-name": encodeURIComponent(file.name) },
146
+ file.size,
147
+ );
148
+
149
+ this.log.trace(`File uploaded successfully: ${fileId}`);
150
+ return fileId;
151
+ } catch (error) {
152
+ this.log.error(`Failed to upload file: ${error}`);
153
+ if (error instanceof Error) {
154
+ throw new AlephaError(`Upload failed: ${error.message}`, {
155
+ cause: error,
156
+ });
157
+ }
158
+ throw error;
159
+ }
160
+ }
161
+
162
+ public async download(bucketName: string, fileId: string): Promise<FileLike> {
163
+ this.log.trace(
164
+ `Downloading file '${fileId}' from bucket '${bucketName}'...`,
165
+ );
166
+
167
+ const client = this.getClient(bucketName);
168
+ const response = await client.getObjectResponse(fileId);
169
+
170
+ if (!response) {
171
+ throw new FileNotFoundError(
172
+ `File '${fileId}' not found in bucket '${bucketName}'`,
173
+ );
174
+ }
175
+
176
+ const buffer = Buffer.from(await response.arrayBuffer());
177
+
178
+ const mimeType =
179
+ response.headers.get("content-type") ||
180
+ this.fileDetector.getContentType(fileId);
181
+
182
+ const metaName = response.headers.get("x-amz-meta-name");
183
+ const name = metaName ? decodeURIComponent(metaName) : fileId;
184
+
185
+ return this.fileSystem.createFile({
186
+ buffer,
187
+ name,
188
+ type: mimeType,
189
+ size: buffer.length,
190
+ });
191
+ }
192
+
193
+ public async exists(bucketName: string, fileId: string): Promise<boolean> {
194
+ this.log.trace(
195
+ `Checking existence of file '${fileId}' in bucket '${bucketName}'...`,
196
+ );
197
+
198
+ const client = this.getClient(bucketName);
199
+ const result = await client.objectExists(fileId);
200
+ return result === true;
201
+ }
202
+
203
+ public async delete(bucketName: string, fileId: string): Promise<void> {
204
+ this.log.trace(`Deleting file '${fileId}' from bucket '${bucketName}'...`);
205
+
206
+ const client = this.getClient(bucketName);
207
+
208
+ try {
209
+ await client.deleteObject(fileId);
210
+ } catch (error) {
211
+ this.log.error(`Failed to delete file: ${error}`);
212
+ if (error instanceof Error) {
213
+ throw new FileNotFoundError("Error deleting file", { cause: error });
214
+ }
215
+ throw error;
216
+ }
217
+ }
218
+ }
@@ -12,6 +12,35 @@ export * from "./providers/MemoryCacheProvider.ts";
12
12
 
13
13
  // ---------------------------------------------------------------------------------------------------------------------
14
14
 
15
+ declare module "alepha" {
16
+ interface Hooks {
17
+ /**
18
+ * Fires when a cache lookup finds a value.
19
+ */
20
+ "cache:hit": {
21
+ container: string;
22
+ key: string;
23
+ };
24
+ /**
25
+ * Fires when a cache lookup does not find a value.
26
+ */
27
+ "cache:miss": {
28
+ container: string;
29
+ key: string;
30
+ };
31
+ /**
32
+ * Fires when a value is written to the cache.
33
+ */
34
+ "cache:set": {
35
+ container: string;
36
+ key: string;
37
+ ttlMs?: number;
38
+ };
39
+ }
40
+ }
41
+
42
+ // ---------------------------------------------------------------------------------------------------------------------
43
+
15
44
  /**
16
45
  * Type-safe caching with TTL support.
17
46
  *
@@ -239,6 +239,12 @@ export class CachePrimitive<
239
239
  ttl: px > 0 ? px : undefined,
240
240
  compress: this.options.compress,
241
241
  });
242
+
243
+ await this.alepha.events.emit("cache:set", {
244
+ container: this.container,
245
+ key,
246
+ ttlMs: px > 0 ? px : undefined,
247
+ });
242
248
  }
243
249
 
244
250
  public async get(key: string): Promise<TReturn | undefined> {
@@ -250,7 +256,14 @@ export class CachePrimitive<
250
256
  return undefined;
251
257
  }
252
258
 
253
- return this.provider.getTyped<TReturn>(this.container, key);
259
+ const value = await this.provider.getTyped<TReturn>(this.container, key);
260
+
261
+ await this.alepha.events.emit(
262
+ value === undefined ? "cache:miss" : "cache:hit",
263
+ { container: this.container, key },
264
+ );
265
+
266
+ return value;
254
267
  }
255
268
 
256
269
  protected $provider(): CacheProvider {
@@ -1,4 +1,4 @@
1
- import type { Alepha } from "alepha";
1
+ import type { Alepha, Service } from "alepha";
2
2
  import {
3
3
  type AppEntryOptions,
4
4
  appEntryOptions,
@@ -10,15 +10,6 @@ import {
10
10
 
11
11
  // ---------------------------------------------------------------------------------------------------------------------
12
12
 
13
- export type AlephaCliConfigPlugin = (
14
- config: AlephaCliConfig,
15
- alepha: Alepha,
16
- ) => void;
17
-
18
- export const cliConfigPlugins: AlephaCliConfigPlugin[] = [];
19
-
20
- // ---------------------------------------------------------------------------------------------------------------------
21
-
22
13
  export interface AlephaCliConfig {
23
14
  /**
24
15
  * Override entry paths.
@@ -28,7 +19,12 @@ export interface AlephaCliConfig {
28
19
  /**
29
20
  * Register more services to the Alepha CLI (enhancements, commands, etc.).
30
21
  */
31
- services?: Array<any>;
22
+ services?: Array<Service>;
23
+
24
+ /**
25
+ * @alias services Register more services to the Alepha CLI (enhancements, commands, etc.).
26
+ */
27
+ plugins?: Array<Service>;
32
28
 
33
29
  /**
34
30
  * Configure Alepha build command.
@@ -58,6 +54,12 @@ export const defineConfig = (config: AlephaCliConfig) => {
58
54
  }
59
55
  }
60
56
 
57
+ if (config.plugins) {
58
+ for (const it of config.plugins) {
59
+ alepha.with(it);
60
+ }
61
+ }
62
+
61
63
  if (config.env) {
62
64
  for (const [key, value] of Object.entries(config.env)) {
63
65
  process.env[key] = String(value);
@@ -76,10 +78,6 @@ export const defineConfig = (config: AlephaCliConfig) => {
76
78
  alepha.set(appEntryOptions, config.entry);
77
79
  }
78
80
 
79
- for (const plugin of cliConfigPlugins) {
80
- plugin(config, alepha);
81
- }
82
-
83
81
  return {};
84
82
  };
85
83
  };
@@ -98,25 +98,24 @@ describe("alepha init", () => {
98
98
  // ─────────────────────────────────────────────────────────────────────────────
99
99
 
100
100
  describe("AI agent files", () => {
101
- it("should create CLAUDE.md when claude CLI is installed", async () => {
102
- const { fs, shell, cli, cmd, json } = createTestEnv();
101
+ it("should create both AGENTS.md and CLAUDE.md", async () => {
102
+ const { fs, cli, cmd, json } = createTestEnv();
103
103
  await setupProject(fs, json);
104
- shell.installedCommands.add("claude");
105
104
 
106
105
  await cli.run(cmd.init, { root: "/project" });
107
106
 
107
+ expect(fs.wasWritten("/project/AGENTS.md")).toBe(true);
108
108
  expect(fs.wasWritten("/project/CLAUDE.md")).toBe(true);
109
- expect(fs.wasWritten("/project/AGENTS.md")).toBe(false);
110
109
  });
111
110
 
112
- it("should create AGENTS.md when claude CLI is not installed", async () => {
111
+ it("should write CLAUDE.md as a stub importing AGENTS.md", async () => {
113
112
  const { fs, cli, cmd, json } = createTestEnv();
114
113
  await setupProject(fs, json);
115
114
 
116
115
  await cli.run(cmd.init, { root: "/project" });
117
116
 
118
- expect(fs.wasWritten("/project/AGENTS.md")).toBe(true);
119
- expect(fs.wasWritten("/project/CLAUDE.md")).toBe(false);
117
+ const claude = await fs.readTextFile("/project/CLAUDE.md");
118
+ expect(claude.trim()).toBe("@AGENTS.md");
120
119
  });
121
120
 
122
121
  it("should include Alepha instructions in agent file", async () => {
@@ -3,7 +3,7 @@ import { $inject, AlephaError } from "alepha";
3
3
  import type { RunnerMethod } from "alepha/command";
4
4
  import { $logger, ConsoleColorProvider } from "alepha/logger";
5
5
  import { FileSystemProvider, ShellProvider } from "alepha/system";
6
- import { type AgentMdOptions, agentMd } from "../templates/agentMd.ts";
6
+ import { agentMd } from "../templates/agentMd.ts";
7
7
  import { alephaConfigTs } from "../templates/alephaConfigTs.ts";
8
8
  import { apiHelloControllerTs } from "../templates/apiHelloControllerTs.ts";
9
9
  import { apiHelloResponseSchemaTs } from "../templates/apiHelloResponseSchemaTs.ts";
@@ -94,7 +94,7 @@ export class ProjectScaffolder {
94
94
  tsconfigJson?: boolean | "local";
95
95
  biomeJson?: boolean;
96
96
  editorconfig?: boolean;
97
- agentMd?: false | AgentMdOptions;
97
+ agentMd?: boolean;
98
98
  },
99
99
  ): Promise<void> {
100
100
  const tasks: Promise<void>[] = [];
@@ -126,7 +126,7 @@ export class ProjectScaffolder {
126
126
  tasks.push(this.ensureEditorConfig(root, { force, checkWorkspace }));
127
127
  }
128
128
  if (opts.agentMd) {
129
- tasks.push(this.ensureAgentMd(root, { ...opts.agentMd, force }));
129
+ tasks.push(this.ensureAgentMd(root, { force }));
130
130
  }
131
131
 
132
132
  await Promise.all(tasks);
@@ -214,12 +214,19 @@ export class ProjectScaffolder {
214
214
  return true;
215
215
  }
216
216
 
217
+ /**
218
+ * Ensure AGENTS.md (cross-tool standard, canonical source) exists, with a
219
+ * CLAUDE.md stub that imports it via Claude Code's `@` syntax. Single
220
+ * source of truth, cross-platform, no symlink needed.
221
+ */
217
222
  public async ensureAgentMd(
218
223
  root: string,
219
- options: AgentMdOptions & { force?: boolean },
224
+ options: { force?: boolean } = {},
220
225
  ): Promise<void> {
221
- const filename = options.type === "claude" ? "CLAUDE.md" : "AGENTS.md";
222
- await this.ensureFile(root, filename, agentMd(options), options.force);
226
+ await Promise.all([
227
+ this.ensureFile(root, "AGENTS.md", agentMd(), options.force),
228
+ this.ensureFile(root, "CLAUDE.md", "@AGENTS.md\n", options.force),
229
+ ]);
223
230
  }
224
231
 
225
232
  /**
@@ -602,12 +609,9 @@ export class ProjectScaffolder {
602
609
  // Detect workspace context (are we inside packages/ or apps/ of a monorepo?)
603
610
  const workspace = await this.pm.getWorkspaceContext(root);
604
611
 
605
- // Detect agent type: claude CLI CLAUDE.md, else AGENTS.md
606
- let agentType: "claude" | "agents" | false = false;
607
- if (!workspace.isPackage) {
608
- const hasClaudeCli = await this.utils.isInstalledAsync("claude");
609
- agentType = hasClaudeCli ? "claude" : "agents";
610
- }
612
+ // Always emit both AGENTS.md and CLAUDE.md at project roots (skip for
613
+ // monorepo sub-packages where agent files live at workspace root).
614
+ const writeAgentMd = !workspace.isPackage;
611
615
 
612
616
  const isExpo = await this.pm.hasExpo(root);
613
617
 
@@ -626,7 +630,7 @@ export class ProjectScaffolder {
626
630
  tsconfigJson: f.shadcn ? "local" : !workspace.config.tsconfigJson,
627
631
  biomeJson: true,
628
632
  editorconfig: !workspace.config.editorconfig,
629
- agentMd: agentType ? { type: agentType } : false,
633
+ agentMd: writeAgentMd,
630
634
  });
631
635
 
632
636
  // Create alepha.config.ts with documented options
@@ -729,7 +733,7 @@ export class ProjectScaffolder {
729
733
  // pulls its peer primitives + dependencies (sonner, etc.).
730
734
  if (flags.saas) {
731
735
  // Pull the public SaaS bundle in one shot — it aggregates control,
732
- // auto-form, alepha-table, use-confirm, app-shell, every auth-*, and
736
+ // auto-form, alepha-table, use-dialog, app-shell, every auth-*, and
733
737
  // every admin-* block. Definition lives at
734
738
  // https://alepha.dev/r/saas.json (see @alepha/ui-registry).
735
739
  // `--yes --overwrite` is the only combo that works non-interactively
@@ -73,6 +73,11 @@ export class BuildCloudflareTask extends BuildTask {
73
73
  };
74
74
  }
75
75
 
76
+ wrangler.observability ??= {
77
+ enabled: true,
78
+ head_sampling_rate: 1,
79
+ };
80
+
76
81
  this.enhanceDomain(wrangler);
77
82
  this.enhanceCron(ctx, wrangler);
78
83
  this.enhanceDatabase(wrangler);
@@ -1,13 +1,5 @@
1
- export type AgentMdType = "claude" | "agents";
2
-
3
- export interface AgentMdOptions {
4
- type: AgentMdType;
5
- }
6
-
7
- export const agentMd = (options: AgentMdOptions): string => {
8
- const header = options.type === "claude" ? `# CLAUDE.md` : `# AGENTS.md`;
9
-
10
- return `${header}
1
+ export const agentMd = (): string => {
2
+ return `# AGENTS.md
11
3
 
12
4
  This is an **Alepha** project.
13
5
 
@@ -11,7 +11,7 @@ export const saasAdminLayoutTsx = () =>
11
11
  `import { AppShell } from "@/components/app-shell";
12
12
  import { Toaster } from "@/components/ui/sonner";
13
13
  import { TooltipProvider } from "@/components/ui/tooltip";
14
- import { ConfirmProvider } from "@/components/use-confirm";
14
+ import { DialogProvider } from "@/components/use-dialog";
15
15
  import { NestedView, useRouterState } from "alepha/react/router";
16
16
  import { ShieldCheck, Users } from "lucide-react";
17
17
 
@@ -39,7 +39,7 @@ const AdminLayout = () => {
39
39
 
40
40
  return (
41
41
  <TooltipProvider>
42
- <ConfirmProvider>
42
+ <DialogProvider>
43
43
  <AppShell
44
44
  brand={
45
45
  <a
@@ -68,7 +68,7 @@ const AdminLayout = () => {
68
68
  <NestedView />
69
69
  </AppShell>
70
70
  <Toaster />
71
- </ConfirmProvider>
71
+ </DialogProvider>
72
72
  </TooltipProvider>
73
73
  );
74
74
  };