anhedral 0.4.2 → 0.6.0

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 (89) hide show
  1. package/README.md +29 -269
  2. package/assets/anhedral-cli-init-technical.svg +322 -0
  3. package/assets/anhedral-cli-init.svg +257 -173
  4. package/dist/bin.js +28 -191
  5. package/dist/dependencies.d.ts +7 -57
  6. package/dist/dependencies.js +96 -308
  7. package/dist/index.d.ts +2 -7
  8. package/dist/index.js +1 -4
  9. package/dist/platforms/desktop.js +401 -0
  10. package/dist/platforms/extension.js +401 -0
  11. package/dist/platforms/mobile.js +232 -0
  12. package/dist/project.d.ts +0 -26
  13. package/dist/standard-products.d.ts +2 -0
  14. package/dist/standard-products.js +6 -0
  15. package/dist/standard.d.ts +18 -0
  16. package/dist/standard.js +578 -0
  17. package/dist/version.d.ts +1 -1
  18. package/dist/version.js +1 -1
  19. package/docs/application-stack-standard.md +700 -0
  20. package/package.json +34 -51
  21. package/CHANGELOG.md +0 -178
  22. package/CONTRIBUTING.md +0 -77
  23. package/SECURITY.md +0 -37
  24. package/SKILL.md +0 -339
  25. package/assets/images/svg/ably-symbol-color.svg +0 -8
  26. package/bin/anhedral.js +0 -2
  27. package/dist/architecture/contributions.d.ts +0 -28
  28. package/dist/architecture/contributions.js +0 -125
  29. package/dist/architecture/index.d.ts +0 -12
  30. package/dist/architecture/index.js +0 -6
  31. package/dist/architecture/manifest.d.ts +0 -53
  32. package/dist/architecture/manifest.js +0 -283
  33. package/dist/architecture/modules.d.ts +0 -41
  34. package/dist/architecture/modules.js +0 -187
  35. package/dist/architecture/plan.d.ts +0 -58
  36. package/dist/architecture/plan.js +0 -145
  37. package/dist/architecture/templates.d.ts +0 -10
  38. package/dist/architecture/templates.js +0 -23
  39. package/dist/cli.d.ts +0 -32
  40. package/dist/cli.js +0 -281
  41. package/dist/composers/root.d.ts +0 -6
  42. package/dist/composers/root.js +0 -35
  43. package/dist/print.d.ts +0 -8
  44. package/dist/print.js +0 -48
  45. package/dist/prompts.d.ts +0 -6
  46. package/dist/prompts.js +0 -41
  47. package/dist/scaffold.d.ts +0 -64
  48. package/dist/scaffold.js +0 -2102
  49. package/dist/template-source.d.ts +0 -20
  50. package/dist/template-source.js +0 -180
  51. package/dist/templates/api.d.ts +0 -2
  52. package/dist/templates/api.js +0 -3187
  53. package/dist/templates/assets-private-proxy.d.ts +0 -3
  54. package/dist/templates/assets-private-proxy.js +0 -277
  55. package/dist/templates/desktop.js +0 -792
  56. package/dist/templates/electron-updater.d.ts +0 -3
  57. package/dist/templates/electron-updater.js +0 -358
  58. package/dist/templates/extension.js +0 -856
  59. package/dist/templates/first-run.d.ts +0 -2
  60. package/dist/templates/first-run.js +0 -167
  61. package/dist/templates/mobile.js +0 -861
  62. package/dist/templates/shared.d.ts +0 -2
  63. package/dist/templates/shared.js +0 -577
  64. package/dist/templates/web.d.ts +0 -2
  65. package/dist/templates/web.js +0 -569
  66. package/dist/toolchain.d.ts +0 -3
  67. package/dist/toolchain.js +0 -8
  68. package/dist/ui.d.ts +0 -33
  69. package/dist/ui.js +0 -199
  70. package/dist/workspace-config.d.ts +0 -11
  71. package/dist/workspace-config.js +0 -372
  72. package/docs/NORTHSTAR.md +0 -114
  73. package/docs/conventions.md +0 -94
  74. package/docs/engineering-principles.md +0 -88
  75. package/docs/master-stack-map.md +0 -199
  76. package/docs/output-tree-contract.md +0 -493
  77. package/docs/references/manual-scaffolding.md +0 -421
  78. package/docs/references/provisioning.md +0 -577
  79. package/scripts/render-master-stack-map.mjs +0 -510
  80. package/templates/api-fastify/apps/api/tsconfig.json +0 -20
  81. package/templates/catalog.json +0 -23
  82. package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
  83. package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
  84. package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
  85. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
  86. package/templates/web-next/apps/web/next-env.d.ts +0 -6
  87. /package/dist/{templates → platforms}/desktop.d.ts +0 -0
  88. /package/dist/{templates → platforms}/extension.d.ts +0 -0
  89. /package/dist/{templates → platforms}/mobile.d.ts +0 -0
@@ -1,3 +0,0 @@
1
- import type { ProjectOptions } from '../project.js';
2
- export declare function r2BucketName(projectName: string): string;
3
- export declare function scaffoldAssetsPrivateProxy(root: string, options: ProjectOptions): void;
@@ -1,277 +0,0 @@
1
- import path from 'node:path';
2
- import { anhedralPrint } from '../print.js';
3
- import { childPackageName } from '../render.js';
4
- import { TOOLCHAIN_DEPENDENCIES } from '../dependencies.js';
5
- import { writeFile } from '../util.js';
6
- export function r2BucketName(projectName) {
7
- const unscoped = projectName.replace(/^@[^/]+\//, '');
8
- const normalized = unscoped.toLowerCase()
9
- .replace(/[^a-z0-9-]+/g, '-')
10
- .replace(/-+/g, '-')
11
- .replace(/^-+|-+$/g, '') || 'anhedral';
12
- const base = normalized.slice(0, 56).replace(/-+$/g, '') || 'anhedral';
13
- return `${base}-assets`;
14
- }
15
- export function scaffoldAssetsPrivateProxy(root, options) {
16
- const dir = path.join(root, 'apps/assets-private-proxy');
17
- const wrangler = `pnpm dlx wrangler@${TOOLCHAIN_DEPENDENCIES.wrangler}`;
18
- const bucketName = r2BucketName(options.projectName);
19
- anhedralPrint.section('Private asset proxy (Cloudflare Worker + R2)');
20
- anhedralPrint.step('Writing private-bucket delivery Worker');
21
- writeFile(path.join(dir, 'package.json'), JSON.stringify({
22
- name: childPackageName(options.projectName, 'assets-private-proxy'),
23
- version: '0.1.0',
24
- private: true,
25
- type: 'module',
26
- scripts: {
27
- check: 'node --check src/index.js',
28
- dev: `${wrangler} dev`,
29
- deploy: `${wrangler} deploy`,
30
- types: `${wrangler} types`,
31
- },
32
- }, null, 2) + '\n');
33
- writeFile(path.join(dir, 'wrangler.jsonc'), `{
34
- // Replace both assets.example.com values with a hostname in your active Cloudflare zone before deployment.
35
- "name": "assets-private-proxy",
36
- "main": "src/index.js",
37
- "compatibility_date": "2026-07-16",
38
- "compatibility_flags": ["nodejs_compat"],
39
- "workers_dev": false,
40
- "preview_urls": false,
41
- "routes": [{ "pattern": "assets.example.com", "custom_domain": true }],
42
- "vars": { "ASSET_HOSTNAME": "assets.example.com", "R2_PREFIX": "storage" },
43
- "r2_buckets": [{ "binding": "ASSETS", "bucket_name": "${bucketName}" }],
44
- "observability": {
45
- "enabled": true,
46
- "logs": { "enabled": true, "head_sampling_rate": 1 }
47
- }
48
- }
49
- `);
50
- writeFile(path.join(dir, 'src/index.js'), `/**
51
- * Public asset delivery for a private Anhedral R2 bucket.
52
- *
53
- * Cloudflare configuration:
54
- * - Worker name: assets-private-proxy
55
- * - Custom domain: replace assets.example.com in wrangler.jsonc
56
- * - R2 binding name: ASSETS
57
- * - Public object prefix: storage/confirmed/
58
- *
59
- * Uploads remain authenticated through the Anhedral application. This Worker
60
- * only allows public GET and HEAD access to objects whose unguessable key is
61
- * known. Use authenticated or application-signed requests instead when object
62
- * keys are not sufficient authorization for your product.
63
- */
64
- const CACHE_CONTROL = "public, max-age=86400, s-maxage=2592000, immutable";
65
- const PRIVATE_GENERATION_INPUT_SEGMENT = "/generation-inputs/";
66
-
67
- const worker = {
68
- async fetch(request, env, ctx) {
69
- const method = request.method.toUpperCase();
70
- if (method !== "GET" && method !== "HEAD") {
71
- return textNoStore("Method Not Allowed", 405, { allow: "GET, HEAD" });
72
- }
73
-
74
- const url = new URL(request.url);
75
- if (url.hostname !== env.ASSET_HOSTNAME) return textNoStore("Forbidden", 403);
76
- const key = decodeObjectKey(url.pathname);
77
- const publicPrefix = env.R2_PREFIX + "/confirmed/";
78
- if (!key || !key.startsWith(publicPrefix) || key.includes(PRIVATE_GENERATION_INPUT_SEGMENT)) {
79
- return textNoStore("Not Found", 404);
80
- }
81
-
82
- try {
83
- const cache = caches.default;
84
- const cacheKey = buildCacheKey(url, key);
85
- const canUseBodyCache = isCacheableBodyRequest(request, method);
86
-
87
- if (method === "HEAD") {
88
- const cached = await cache.match(cacheKey);
89
- if (cached) return headFromResponse(cached, "HIT");
90
- const object = await env.ASSETS.head(key);
91
- if (!object) return textNoStore("Not Found", 404);
92
- return new Response(null, {
93
- status: 200,
94
- headers: withAssetDebugHeaders(buildObjectHeaders(object), "BYPASS"),
95
- });
96
- }
97
-
98
- if (canUseBodyCache) {
99
- const cached = await cache.match(cacheKey);
100
- if (cached) return withCacheStatus(cached, "HIT");
101
- }
102
-
103
- const object = await env.ASSETS.get(key, {
104
- onlyIf: request.headers,
105
- range: request.headers,
106
- });
107
- if (!object) return textNoStore("Not Found", 404);
108
-
109
- const hasBody = "body" in object;
110
- const headers = buildObjectHeaders(object);
111
- const isRangeResponse = hasBody && request.headers.has("range");
112
- if (isRangeResponse && object.range) {
113
- const returnedRange = normalizeReturnedRange(object.range, object.size);
114
- headers.set("content-range", "bytes " + returnedRange.offset + "-" + (returnedRange.offset + returnedRange.length - 1) + "/" + object.size);
115
- headers.set("content-length", String(returnedRange.length));
116
- }
117
-
118
- if (!hasBody) {
119
- const status = getFailedConditionalRequestStatus(request.headers);
120
- headers.delete("content-length");
121
- if (status === 412) headers.set("cache-control", "no-store");
122
- return new Response(null, {
123
- status,
124
- headers: withAssetDebugHeaders(headers, "BYPASS"),
125
- });
126
- }
127
-
128
- const response = new Response(object.body, {
129
- status: isRangeResponse ? 206 : 200,
130
- headers,
131
- });
132
- if (canUseBodyCache && response.status === 200) {
133
- ctx.waitUntil(cache.put(cacheKey, response.clone()).catch((error) => {
134
- console.error("Asset cache put failed", { key, error });
135
- }));
136
- return withCacheStatus(response, "MISS");
137
- }
138
- return withCacheStatus(response, "BYPASS");
139
- } catch (error) {
140
- console.error("Asset delivery failed", { key, error });
141
- return textNoStore("Asset delivery failed", 500);
142
- }
143
- },
144
- };
145
-
146
- export default worker;
147
-
148
- function isCacheableBodyRequest(request, method) {
149
- if (method !== "GET") return false;
150
- return !request.headers.has("range")
151
- && !request.headers.has("if-none-match")
152
- && !request.headers.has("if-match")
153
- && !request.headers.has("if-modified-since")
154
- && !request.headers.has("if-unmodified-since");
155
- }
156
-
157
- function buildCacheKey(url, key) {
158
- return new Request(url.origin + "/" + encodeObjectKey(key), { method: "GET" });
159
- }
160
-
161
- function decodeObjectKey(pathname) {
162
- const rawKey = pathname.replace(/^\\/+/, "");
163
- if (!rawKey) return null;
164
- const parts = [];
165
- for (const rawPart of rawKey.split("/")) {
166
- if (!rawPart) continue;
167
- let part;
168
- try {
169
- part = decodeURIComponent(rawPart);
170
- } catch {
171
- return null;
172
- }
173
- if (!part || part === "." || part === ".." || part.includes("/")) return null;
174
- parts.push(part);
175
- }
176
- return parts.join("/") || null;
177
- }
178
-
179
- function encodeObjectKey(key) {
180
- return key.split("/").map(encodeURIComponent).join("/");
181
- }
182
-
183
- function buildObjectHeaders(object) {
184
- const headers = new Headers();
185
- object.writeHttpMetadata(headers);
186
- headers.set("accept-ranges", "bytes");
187
- headers.set("cache-control", CACHE_CONTROL);
188
- headers.set("etag", object.httpEtag);
189
- headers.set("content-length", String(object.size));
190
- headers.set("x-assets-proxy", "anhedral-r2-direct-v1");
191
- if (object.uploaded instanceof Date) headers.set("last-modified", object.uploaded.toUTCString());
192
- headers.delete("set-cookie");
193
- if (headers.get("vary") === "*") headers.delete("vary");
194
- return headers;
195
- }
196
-
197
- function headFromResponse(response, cacheStatus) {
198
- const headers = new Headers(response.headers);
199
- headers.set("x-assets-cache", cacheStatus);
200
- return new Response(null, { status: response.status, headers });
201
- }
202
-
203
- function withCacheStatus(response, cacheStatus) {
204
- const headers = new Headers(response.headers);
205
- headers.set("x-assets-cache", cacheStatus);
206
- return new Response(response.body, {
207
- status: response.status,
208
- statusText: response.statusText,
209
- headers,
210
- });
211
- }
212
-
213
- function withAssetDebugHeaders(headers, cacheStatus) {
214
- const nextHeaders = new Headers(headers);
215
- nextHeaders.set("x-assets-cache", cacheStatus);
216
- return nextHeaders;
217
- }
218
-
219
- function textNoStore(body, status, extraHeaders = {}) {
220
- return new Response(body, {
221
- status,
222
- headers: { "cache-control": "no-store", ...extraHeaders },
223
- });
224
- }
225
-
226
- function getFailedConditionalRequestStatus(headers) {
227
- if (headers.has("if-none-match")) return 304;
228
- if (!headers.has("if-match") && !headers.has("if-unmodified-since") && headers.has("if-modified-since")) return 304;
229
- return 412;
230
- }
231
-
232
- function normalizeReturnedRange(range, objectSize) {
233
- if (typeof range.suffix === "number") {
234
- const length = Math.min(Math.max(0, range.suffix), objectSize);
235
- return { offset: objectSize - length, length };
236
- }
237
- const offset = Math.min(Math.max(0, range.offset ?? 0), objectSize);
238
- const availableLength = objectSize - offset;
239
- const length = typeof range.length === "number"
240
- ? Math.min(Math.max(0, range.length), availableLength)
241
- : availableLength;
242
- return { offset, length };
243
- }
244
- `);
245
- writeFile(path.join(root, 'cloudflare/r2-cors.template.json'), JSON.stringify({
246
- rules: [
247
- {
248
- id: 'anhedral-browser-uploads',
249
- allowed: {
250
- origins: ['https://app.example.com', 'http://localhost:3000'],
251
- methods: ['GET', 'HEAD', 'PUT'],
252
- headers: ['Content-Type'],
253
- },
254
- exposeHeaders: ['ETag'],
255
- maxAgeSeconds: 3600,
256
- },
257
- ],
258
- }, null, 2) + '\n');
259
- writeFile(path.join(root, 'cloudflare/README.md'), `# Anhedral Cloudflare resources
260
-
261
- The R2 bucket remains private. Browser uploads use short-lived, authenticated
262
- presigned URLs on the R2 S3 API hostname. Public downloads pass through the
263
- \`assets-private-proxy\` Worker, which exposes only keys below
264
- \`storage/confirmed/\` by default. Authenticated reads use the API's
265
- \`/api/storage/uploads/:uploadId/read-url\` endpoint instead.
266
-
267
- Before applying \`r2-cors.template.json\`, replace \`app.example.com\` and merge
268
- every exact web or extension origin that uploads to this bucket. Wrangler's
269
- \`r2 bucket cors set\` command replaces the complete live policy, so list and
270
- review the deployed policy before every update.
271
-
272
- \`CLOUDFLARE_API_TOKEN\` is an operations/CI credential used by Wrangler. It is
273
- not an application storage credential and must never be exposed to a browser,
274
- mobile build, desktop renderer, extension bundle, or Worker variable.
275
- `);
276
- anhedralPrint.done('Private R2 asset proxy written');
277
- }