anhedral 0.1.10 → 0.4.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 (87) hide show
  1. package/CHANGELOG.md +119 -0
  2. package/CONTRIBUTING.md +73 -0
  3. package/LICENSE +196 -17
  4. package/README.md +202 -145
  5. package/SECURITY.md +37 -0
  6. package/SKILL.md +244 -0
  7. package/bin/anhedral.js +1 -1
  8. package/dist/architecture/contributions.d.ts +28 -0
  9. package/dist/architecture/contributions.js +125 -0
  10. package/dist/architecture/index.d.ts +12 -0
  11. package/dist/architecture/index.js +6 -0
  12. package/dist/architecture/manifest.d.ts +53 -0
  13. package/dist/architecture/manifest.js +283 -0
  14. package/dist/architecture/modules.d.ts +37 -0
  15. package/dist/architecture/modules.js +184 -0
  16. package/dist/architecture/plan.d.ts +58 -0
  17. package/dist/architecture/plan.js +145 -0
  18. package/dist/architecture/templates.d.ts +10 -0
  19. package/dist/architecture/templates.js +23 -0
  20. package/dist/bin.d.ts +2 -0
  21. package/dist/bin.js +179 -0
  22. package/dist/cli.d.ts +24 -2
  23. package/dist/cli.js +189 -21
  24. package/dist/composers/root.d.ts +6 -0
  25. package/dist/composers/root.js +35 -0
  26. package/dist/dependencies.d.ts +31 -9
  27. package/dist/dependencies.js +227 -60
  28. package/dist/index.d.ts +8 -2
  29. package/dist/index.js +5 -31
  30. package/dist/print.js +15 -0
  31. package/dist/prompts.d.ts +5 -0
  32. package/dist/prompts.js +19 -0
  33. package/dist/render.d.ts +11 -0
  34. package/dist/render.js +74 -0
  35. package/dist/scaffold.d.ts +64 -12
  36. package/dist/scaffold.js +1726 -1150
  37. package/dist/template-source.d.ts +20 -0
  38. package/dist/template-source.js +180 -0
  39. package/dist/templates/api.d.ts +2 -0
  40. package/dist/templates/api.js +3122 -0
  41. package/dist/templates/assets-private-proxy.d.ts +3 -0
  42. package/dist/templates/assets-private-proxy.js +277 -0
  43. package/dist/templates/desktop.d.ts +2 -0
  44. package/dist/templates/desktop.js +639 -0
  45. package/dist/templates/electron-updater.d.ts +3 -0
  46. package/dist/templates/electron-updater.js +358 -0
  47. package/dist/templates/extension.d.ts +1 -1
  48. package/dist/templates/extension.js +322 -248
  49. package/dist/templates/mobile.d.ts +2 -0
  50. package/dist/templates/mobile.js +720 -0
  51. package/dist/templates/shared.d.ts +2 -0
  52. package/dist/templates/shared.js +520 -0
  53. package/dist/templates/web.d.ts +2 -0
  54. package/dist/templates/web.js +412 -0
  55. package/dist/toolchain.d.ts +0 -8
  56. package/dist/toolchain.js +0 -21
  57. package/dist/transaction.d.ts +22 -0
  58. package/dist/transaction.js +822 -0
  59. package/dist/ui.d.ts +33 -0
  60. package/dist/ui.js +199 -0
  61. package/dist/util.d.ts +1 -5
  62. package/dist/util.js +70 -159
  63. package/dist/version.d.ts +2 -0
  64. package/dist/version.js +2 -0
  65. package/dist/workspace-config.d.ts +11 -0
  66. package/dist/workspace-config.js +372 -0
  67. package/docs/conventions.md +92 -0
  68. package/docs/master-stack-map.md +192 -0
  69. package/docs/output-tree-contract.md +482 -0
  70. package/docs/references/manual-scaffolding.md +403 -0
  71. package/docs/references/provisioning.md +220 -0
  72. package/package.json +57 -13
  73. package/templates/api-fastify/apps/api/tsconfig.json +20 -0
  74. package/templates/catalog.json +23 -0
  75. package/templates/db-drizzle/packages/db/tsconfig.json +14 -0
  76. package/templates/desktop-electron/apps/desktop/tsconfig.json +37 -0
  77. package/templates/extension-wxt/apps/extension/tsconfig.json +41 -0
  78. package/templates/mobile-expo/apps/mobile/expo-env.d.ts +1 -0
  79. package/templates/web-next/apps/web/next-env.d.ts +6 -0
  80. package/dist/branding.d.ts +0 -5
  81. package/dist/branding.js +0 -17
  82. package/dist/commands.d.ts +0 -7
  83. package/dist/commands.js +0 -24
  84. package/dist/templates/backend.d.ts +0 -2
  85. package/dist/templates/backend.js +0 -2115
  86. package/dist/templates/frontend.d.ts +0 -2
  87. package/dist/templates/frontend.js +0 -1268
@@ -0,0 +1,372 @@
1
+ import { lstatSync, readFileSync } from 'node:fs';
2
+ import path from 'node:path';
3
+ import { SECURITY_OVERRIDES } from './dependencies.js';
4
+ import { writeFile } from './util.js';
5
+ function pathEntryExists(target) {
6
+ try {
7
+ lstatSync(target);
8
+ return true;
9
+ }
10
+ catch (error) {
11
+ if (error.code === 'ENOENT')
12
+ return false;
13
+ throw error;
14
+ }
15
+ }
16
+ export function desiredWorkspacePolicy() {
17
+ return {
18
+ overrides: { ...SECURITY_OVERRIDES },
19
+ onlyBuiltDependencies: ['electron', 'esbuild', 'sharp'],
20
+ ignoredBuiltDependencies: [
21
+ 'browser-tabs-lock',
22
+ 'bufferutil',
23
+ 'core-js',
24
+ 'electron-winstaller',
25
+ 'spawn-sync',
26
+ 'utf-8-validate',
27
+ ],
28
+ peerDependencyRules: {
29
+ ignoreMissing: ['@solana/web3.js', 'bs58', 'react-native'],
30
+ allowedVersions: {
31
+ esbuild: '>=0.25.0',
32
+ 'utf-8-validate': '>=5.0.2',
33
+ },
34
+ },
35
+ };
36
+ }
37
+ // Managed pnpm policy intentionally supports only scalar maps and string lists. More complex YAML
38
+ // is left untouched outside managed sections and rejected inside them instead of being guessed at.
39
+ function sourceLines(content) {
40
+ const lines = [];
41
+ let start = 0;
42
+ while (start < content.length) {
43
+ let cursor = start;
44
+ while (cursor < content.length && content[cursor] !== '\n' && content[cursor] !== '\r')
45
+ cursor += 1;
46
+ const text = content.slice(start, cursor);
47
+ if (content[cursor] === '\r' && content[cursor + 1] === '\n')
48
+ cursor += 2;
49
+ else if (cursor < content.length)
50
+ cursor += 1;
51
+ lines.push({ text, start, end: cursor });
52
+ start = cursor;
53
+ }
54
+ return lines;
55
+ }
56
+ function workspaceNewline(content) {
57
+ return content.includes('\r\n') ? '\r\n' : content.includes('\r') ? '\r' : '\n';
58
+ }
59
+ function parseWorkspaceStringScalar(raw) {
60
+ const leading = /^[ \t]*/.exec(raw)?.[0].length ?? 0;
61
+ const valueAndComment = raw.slice(leading);
62
+ if (!valueAndComment)
63
+ return null;
64
+ const singleQuoted = /^'((?:[^']|'')*)'([ \t]*(?:#.*)?)$/.exec(valueAndComment);
65
+ if (singleQuoted) {
66
+ return {
67
+ value: singleQuoted[1].replaceAll("''", "'"),
68
+ tokenStart: leading,
69
+ tokenEnd: leading + singleQuoted[0].length - singleQuoted[2].length,
70
+ };
71
+ }
72
+ if (valueAndComment.startsWith("'"))
73
+ return null;
74
+ const doubleQuoted = /^("(?:[^"\\]|\\.)*")([ \t]*(?:#.*)?)$/.exec(valueAndComment);
75
+ if (doubleQuoted) {
76
+ try {
77
+ const value = JSON.parse(doubleQuoted[1]);
78
+ return typeof value === 'string'
79
+ ? { value, tokenStart: leading, tokenEnd: leading + doubleQuoted[1].length }
80
+ : null;
81
+ }
82
+ catch {
83
+ return null;
84
+ }
85
+ }
86
+ if (valueAndComment.startsWith('"'))
87
+ return null;
88
+ const comment = /[ \t]+#/.exec(valueAndComment);
89
+ const withoutComment = valueAndComment.slice(0, comment?.index ?? valueAndComment.length).trimEnd();
90
+ if (!withoutComment || /^[\[{|>&*!]/.test(withoutComment))
91
+ return null;
92
+ if (/^(?:null|~|true|false|[-+]?(?:\.inf|\.nan|0x[\da-f]+|0o[0-7]+|\d+(?:\.\d+)?(?:e[-+]?\d+)?))$/i.test(withoutComment)) {
93
+ return null;
94
+ }
95
+ return { value: withoutComment, tokenStart: leading, tokenEnd: leading + withoutComment.length };
96
+ }
97
+ function parseWorkspaceMapLine(line) {
98
+ const indent = /^([ \t]*)/.exec(line)?.[1] ?? '';
99
+ const body = line.slice(indent.length);
100
+ let match = /^'((?:[^']|'')*)'[ \t]*:/.exec(body);
101
+ let key;
102
+ if (match)
103
+ key = match[1].replaceAll("''", "'");
104
+ else {
105
+ match = /^("(?:[^"\\]|\\.)*")[ \t]*:/.exec(body);
106
+ if (match) {
107
+ try {
108
+ const parsed = JSON.parse(match[1]);
109
+ if (typeof parsed !== 'string')
110
+ return null;
111
+ key = parsed;
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ }
117
+ else {
118
+ match = /^([^:#][^:]*?)[ \t]*:/.exec(body);
119
+ if (!match)
120
+ return null;
121
+ key = match[1].trim();
122
+ if (!key || key.includes('#'))
123
+ return null;
124
+ }
125
+ }
126
+ const restOffset = indent.length + match[0].length;
127
+ return { indent, key, rest: line.slice(restOffset), restOffset };
128
+ }
129
+ function parseWorkspaceListLine(line) {
130
+ const match = /^([ \t]+)-[ \t]+(.+)$/.exec(line);
131
+ if (!match)
132
+ return null;
133
+ const scalar = parseWorkspaceStringScalar(match[2]);
134
+ return scalar ? { indent: match[1], value: scalar.value } : null;
135
+ }
136
+ function quoteWorkspaceString(value) {
137
+ return `'${value.replaceAll("'", "''")}'`;
138
+ }
139
+ function workspaceLineIndent(line) {
140
+ if (!line.trim() || line.trimStart().startsWith('#'))
141
+ return null;
142
+ return /^([ \t]*)/.exec(line)?.[1] ?? '';
143
+ }
144
+ function workspaceBlockAt(lines, keyLineIndex) {
145
+ const keyLine = lines[keyLineIndex];
146
+ const key = parseWorkspaceMapLine(keyLine.text);
147
+ if (!key)
148
+ throw new Error('pnpm-workspace.yaml contains an unsupported mapping key.');
149
+ if (key.indent.includes('\t'))
150
+ throw new Error('pnpm-workspace.yaml managed sections must use spaces for indentation.');
151
+ let sectionEndIndex = lines.length;
152
+ for (let index = keyLineIndex + 1; index < lines.length; index += 1) {
153
+ const indent = workspaceLineIndent(lines[index].text);
154
+ if (indent !== null && indent.length <= key.indent.length) {
155
+ sectionEndIndex = index;
156
+ break;
157
+ }
158
+ }
159
+ return { lines, keyLineIndex, sectionEndIndex, keyLine, key };
160
+ }
161
+ function findTopLevelWorkspaceBlock(content, key) {
162
+ const lines = sourceLines(content);
163
+ const matches = lines.flatMap((line, index) => {
164
+ const parsed = parseWorkspaceMapLine(line.text);
165
+ return parsed?.indent === '' && parsed.key === key ? [index] : [];
166
+ });
167
+ if (matches.length > 1)
168
+ throw new Error(`pnpm-workspace.yaml contains duplicate top-level ${key} keys.`);
169
+ return matches.length === 1 ? workspaceBlockAt(lines, matches[0]) : null;
170
+ }
171
+ function appendWorkspaceLines(content, offset, additions) {
172
+ if (additions.length === 0)
173
+ return content;
174
+ const newline = workspaceNewline(content);
175
+ const leading = offset > 0 && !/(?:\r\n|\n|\r)$/.test(content.slice(0, offset)) ? newline : '';
176
+ return `${content.slice(0, offset)}${leading}${additions.join(newline)}${newline}${content.slice(offset)}`;
177
+ }
178
+ function appendTopLevelWorkspaceKey(content, key) {
179
+ return appendWorkspaceLines(content, content.length, [`${key}:`]);
180
+ }
181
+ function requireWorkspaceBlockValue(block, fieldPath) {
182
+ if (!/^[ \t]*(?:#.*)?$/.test(block.key.rest)) {
183
+ throw new Error(`Conflict in ${fieldPath}: expected a block value that can be merged safely.`);
184
+ }
185
+ }
186
+ function mergeWorkspaceListBlock(content, block, desiredValues, fieldPath) {
187
+ requireWorkspaceBlockValue(block, fieldPath);
188
+ const existing = new Set();
189
+ let indent = `${block.key.indent} `;
190
+ let insertionOffset = block.keyLine.end;
191
+ let foundItem = false;
192
+ for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
193
+ const line = block.lines[index];
194
+ if (workspaceLineIndent(line.text) === null)
195
+ continue;
196
+ const parsed = parseWorkspaceListLine(line.text);
197
+ if (!parsed || parsed.indent.includes('\t') || parsed.indent.length <= block.key.indent.length) {
198
+ throw new Error(`Conflict in ${fieldPath}: expected a simple block list of strings.`);
199
+ }
200
+ if (foundItem && parsed.indent !== indent) {
201
+ throw new Error(`Conflict in ${fieldPath}: list items must use consistent indentation.`);
202
+ }
203
+ foundItem = true;
204
+ indent = parsed.indent;
205
+ existing.add(parsed.value);
206
+ insertionOffset = line.end;
207
+ }
208
+ const additions = [...new Set(desiredValues)].filter((value) => !existing.has(value));
209
+ return appendWorkspaceLines(content, insertionOffset, additions.map((value) => `${indent}- ${quoteWorkspaceString(value)}`));
210
+ }
211
+ function ensureTopLevelWorkspaceBlock(content, key) {
212
+ const existing = findTopLevelWorkspaceBlock(content, key);
213
+ if (existing)
214
+ return { content, block: existing };
215
+ const appended = appendTopLevelWorkspaceKey(content, key);
216
+ return { content: appended, block: findTopLevelWorkspaceBlock(appended, key) };
217
+ }
218
+ function mergeTopLevelWorkspaceList(content, key, desiredValues) {
219
+ const ensured = ensureTopLevelWorkspaceBlock(content, key);
220
+ return mergeWorkspaceListBlock(ensured.content, ensured.block, desiredValues, `pnpm-workspace.yaml ${key}`);
221
+ }
222
+ function mergeWorkspaceMapBlock(content, block, desiredValues, fieldPath, allowGeneratedUpdates) {
223
+ requireWorkspaceBlockValue(block, fieldPath);
224
+ const entries = new Map();
225
+ let indent = `${block.key.indent} `;
226
+ let insertionOffset = block.keyLine.end;
227
+ let foundEntry = false;
228
+ for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
229
+ const line = block.lines[index];
230
+ if (workspaceLineIndent(line.text) === null)
231
+ continue;
232
+ const parsed = parseWorkspaceMapLine(line.text);
233
+ if (!parsed || parsed.indent.includes('\t') || parsed.indent.length <= block.key.indent.length) {
234
+ throw new Error(`Conflict in ${fieldPath}: expected a simple string-to-string mapping.`);
235
+ }
236
+ if (foundEntry && parsed.indent !== indent) {
237
+ throw new Error(`Conflict in ${fieldPath}: mapping entries must use consistent indentation.`);
238
+ }
239
+ const scalar = parseWorkspaceStringScalar(parsed.rest);
240
+ if (!scalar)
241
+ throw new Error(`Conflict in ${fieldPath}: mapping values must be strings.`);
242
+ if (entries.has(parsed.key))
243
+ throw new Error(`Conflict in ${fieldPath}: duplicate mapping entry ${parsed.key}.`);
244
+ foundEntry = true;
245
+ indent = parsed.indent;
246
+ insertionOffset = line.end;
247
+ entries.set(parsed.key, { line, parsed, scalar });
248
+ }
249
+ const additions = [];
250
+ const replacements = [];
251
+ for (const [key, desiredValue] of Object.entries(desiredValues)) {
252
+ const existing = entries.get(key);
253
+ if (!existing) {
254
+ additions.push(`${indent}${quoteWorkspaceString(key)}: ${quoteWorkspaceString(desiredValue)}`);
255
+ continue;
256
+ }
257
+ if (existing.scalar.value === desiredValue)
258
+ continue;
259
+ if (!allowGeneratedUpdates) {
260
+ throw new Error(`Conflict in ${fieldPath}: mapping entry ${key} differs from the generated value.`);
261
+ }
262
+ replacements.push({
263
+ start: existing.line.start + existing.parsed.restOffset + existing.scalar.tokenStart,
264
+ end: existing.line.start + existing.parsed.restOffset + existing.scalar.tokenEnd,
265
+ value: quoteWorkspaceString(desiredValue),
266
+ });
267
+ }
268
+ let merged = appendWorkspaceLines(content, insertionOffset, additions);
269
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
270
+ merged = `${merged.slice(0, replacement.start)}${replacement.value}${merged.slice(replacement.end)}`;
271
+ }
272
+ return merged;
273
+ }
274
+ function mergeTopLevelWorkspaceMap(content, key, desiredValues, allowGeneratedUpdates) {
275
+ const ensured = ensureTopLevelWorkspaceBlock(content, key);
276
+ return mergeWorkspaceMapBlock(ensured.content, ensured.block, desiredValues, `pnpm-workspace.yaml ${key}`, allowGeneratedUpdates);
277
+ }
278
+ function directChildIndent(block, fieldPath) {
279
+ let childIndent = null;
280
+ for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
281
+ const indent = workspaceLineIndent(block.lines[index].text);
282
+ if (indent === null)
283
+ continue;
284
+ if (indent.includes('\t') || indent.length <= block.key.indent.length) {
285
+ throw new Error(`Conflict in ${fieldPath}: expected an indented mapping.`);
286
+ }
287
+ if (childIndent === null || indent.length < childIndent.length)
288
+ childIndent = indent;
289
+ }
290
+ return childIndent ?? `${block.key.indent} `;
291
+ }
292
+ function findDirectWorkspaceBlock(parent, key, fieldPath) {
293
+ const childIndent = directChildIndent(parent, fieldPath);
294
+ const matches = [];
295
+ for (let index = parent.keyLineIndex + 1; index < parent.sectionEndIndex; index += 1) {
296
+ const line = parent.lines[index];
297
+ const indent = workspaceLineIndent(line.text);
298
+ if (indent === null || indent.length !== childIndent.length)
299
+ continue;
300
+ const parsed = parseWorkspaceMapLine(line.text);
301
+ if (!parsed || parsed.indent !== childIndent) {
302
+ throw new Error(`Conflict in ${fieldPath}: expected a mapping of named policies.`);
303
+ }
304
+ if (parsed.key === key)
305
+ matches.push(index);
306
+ }
307
+ if (matches.length > 1)
308
+ throw new Error(`Conflict in ${fieldPath}: duplicate ${key} keys.`);
309
+ return matches.length === 1 ? workspaceBlockAt(parent.lines, matches[0]) : null;
310
+ }
311
+ function ensureDirectWorkspaceBlock(content, parentKey, key, fieldPath) {
312
+ let parent = findTopLevelWorkspaceBlock(content, parentKey);
313
+ if (!parent) {
314
+ content = appendTopLevelWorkspaceKey(content, parentKey);
315
+ parent = findTopLevelWorkspaceBlock(content, parentKey);
316
+ }
317
+ requireWorkspaceBlockValue(parent, `pnpm-workspace.yaml ${parentKey}`);
318
+ const existing = findDirectWorkspaceBlock(parent, key, fieldPath);
319
+ if (existing)
320
+ return { content, block: existing };
321
+ const indent = directChildIndent(parent, fieldPath);
322
+ let insertionOffset = parent.keyLine.end;
323
+ for (let index = parent.keyLineIndex + 1; index < parent.sectionEndIndex; index += 1) {
324
+ const line = parent.lines[index];
325
+ if (workspaceLineIndent(line.text) !== null)
326
+ insertionOffset = line.end;
327
+ }
328
+ content = appendWorkspaceLines(content, insertionOffset, [`${indent}${key}:`]);
329
+ parent = findTopLevelWorkspaceBlock(content, parentKey);
330
+ return { content, block: findDirectWorkspaceBlock(parent, key, fieldPath) };
331
+ }
332
+ function applyWorkspaceAutoInstallPeers(content, allowGeneratedUpdates) {
333
+ const block = findTopLevelWorkspaceBlock(content, 'autoInstallPeers');
334
+ if (!block) {
335
+ const newline = workspaceNewline(content);
336
+ const separator = content.length > 0 && !/(?:\r\n|\n|\r)$/.test(content) ? newline : '';
337
+ return `${content}${separator}autoInstallPeers: false${newline}`;
338
+ }
339
+ const scalar = /^([ \t]*)(true|false)([ \t]*(?:#.*)?)$/i.exec(block.key.rest);
340
+ if (!scalar)
341
+ throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be the boolean false.');
342
+ for (let index = block.keyLineIndex + 1; index < block.sectionEndIndex; index += 1) {
343
+ if (workspaceLineIndent(block.lines[index].text) !== null) {
344
+ throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be a scalar boolean.');
345
+ }
346
+ }
347
+ if (scalar[2].toLowerCase() === 'false')
348
+ return content;
349
+ if (!allowGeneratedUpdates) {
350
+ throw new Error('Conflict in pnpm-workspace.yaml: autoInstallPeers must be false to avoid unsafe optional peer resolution.');
351
+ }
352
+ const start = block.keyLine.start + block.key.restOffset + scalar[1].length;
353
+ return `${content.slice(0, start)}false${content.slice(start + scalar[2].length)}`;
354
+ }
355
+ function applyWorkspacePolicy(content, policy, allowGeneratedUpdates) {
356
+ let merged = applyWorkspaceAutoInstallPeers(content, allowGeneratedUpdates);
357
+ merged = mergeTopLevelWorkspaceMap(merged, 'overrides', policy.overrides, allowGeneratedUpdates);
358
+ merged = mergeTopLevelWorkspaceList(merged, 'onlyBuiltDependencies', policy.onlyBuiltDependencies);
359
+ merged = mergeTopLevelWorkspaceList(merged, 'ignoredBuiltDependencies', policy.ignoredBuiltDependencies);
360
+ const ignoreMissing = ensureDirectWorkspaceBlock(merged, 'peerDependencyRules', 'ignoreMissing', 'pnpm-workspace.yaml peerDependencyRules');
361
+ merged = mergeWorkspaceListBlock(ignoreMissing.content, ignoreMissing.block, policy.peerDependencyRules.ignoreMissing, 'pnpm-workspace.yaml peerDependencyRules.ignoreMissing');
362
+ const allowedVersions = ensureDirectWorkspaceBlock(merged, 'peerDependencyRules', 'allowedVersions', 'pnpm-workspace.yaml peerDependencyRules');
363
+ return mergeWorkspaceMapBlock(allowedVersions.content, allowedVersions.block, policy.peerDependencyRules.allowedVersions, 'pnpm-workspace.yaml peerDependencyRules.allowedVersions', allowGeneratedUpdates);
364
+ }
365
+ export function mergeWorkspaceFile(root, desiredPackages, policy, allowGeneratedUpdates) {
366
+ const filePath = path.join(root, 'pnpm-workspace.yaml');
367
+ const current = pathEntryExists(filePath) ? readFileSync(filePath, 'utf8') : '';
368
+ let merged = mergeTopLevelWorkspaceList(current, 'packages', desiredPackages);
369
+ merged = applyWorkspacePolicy(merged, policy, allowGeneratedUpdates);
370
+ if (merged !== current || !pathEntryExists(filePath))
371
+ writeFile(filePath, merged);
372
+ }
@@ -0,0 +1,92 @@
1
+ # Anhedral generated-project conventions
2
+
3
+ Anhedral is a deterministic stack generator. These conventions describe the readable workspace it produces; they are not a new application framework or runtime API.
4
+
5
+ ## Core rule
6
+
7
+ Use the underlying tool's normal programming model. Anhedral decides where integrations meet and generates safe defaults, but developers write ordinary Next.js, Expo Router, Fastify, Drizzle, Electron, and WXT code.
8
+
9
+ ## Source ownership
10
+
11
+ ```text
12
+ apps/ deployable framework applications
13
+ packages/ client-safe or server-shared TypeScript packages
14
+ docs/ generated stack and development guidance
15
+ ```
16
+
17
+ - `apps/web`: Next.js App Router pages, layouts, components, hooks, and web utilities.
18
+ - `apps/mobile`: Expo Router screens, layouts, native components, and hooks.
19
+ - `apps/api`: Fastify routes, services, plugins, provider clients, and server environment validation.
20
+ - `apps/desktop`: Electron main/preload code and sandboxed React renderer code.
21
+ - `apps/desktop-updater-worker`: optional read-only Cloudflare Worker serving signed Electron updates from a bound private R2 bucket.
22
+ - `apps/extension`: WXT entrypoints, extension components, background logic, and content scripts.
23
+ - `packages/contracts`: Zod schemas for data crossing HTTP boundaries.
24
+ - `packages/api-client`: client-safe HTTP behavior shared across frontends.
25
+ - `packages/db`: Drizzle schema, database access, and reviewed SQL migrations.
26
+ - `packages/realtime`: client-safe Ably subscription behavior when selected.
27
+
28
+ ## End-to-end feature flow
29
+
30
+ ```text
31
+ Zod contract
32
+ -> Drizzle schema/query or server service
33
+ -> Fastify route
34
+ -> shared API client
35
+ -> Next.js / Expo / Electron / WXT UI
36
+ ```
37
+
38
+ Routes stay thin. They authenticate, validate, call a service, and return a declared contract. Business behavior and provider SDK calls live in server services. Frontends never import API services, database connections, or server environment modules.
39
+
40
+ When `electron-updater` is selected, desktop releases follow a separate delivery path: the packaged Electron main process reads generated update metadata from an HTTPS custom domain, the Worker streams known keys from its private R2 binding, and release tooling uploads signed artifacts before mutable channel metadata. Neither Cloudflare credentials nor a public bucket URL enter the desktop application.
41
+
42
+ ## Database
43
+
44
+ Database-enabled projects use managed Neon Postgres. Anhedral does not generate local Postgres, Docker Compose, or an embedded database substitute.
45
+
46
+ 1. Edit the user-owned `packages/db/src/app-schema.ts`; Anhedral keeps provider tables in `generated-schema.ts`.
47
+ 2. Run `pnpm db:generate`.
48
+ 3. Review and commit generated SQL and metadata.
49
+ 4. Run `pnpm verify:db`.
50
+ 5. Apply with `pnpm db:migrate` against the intended Neon branch or project.
51
+
52
+ Schema generation must not happen during an application build. Migrations are reviewed release artifacts.
53
+
54
+ ## Authentication and secrets
55
+
56
+ Clerk owns identity and sessions. The API derives identity from a verified server session and never trusts a client-supplied user ID. Only variables with a framework's explicit public prefix may enter client bundles. Database, Clerk secret, R2, RevenueCat, Ably, cron, and provider credentials remain server-only.
57
+
58
+ ## UI
59
+
60
+ DOM clients use source-owned shadcn/ui components. Expo uses React Native Reusables with the selected NativeWind or Uniwind configuration.
61
+
62
+ ```sh
63
+ anhedral ui add button dialog
64
+ anhedral ui add data-table --target web
65
+ ```
66
+
67
+ Generated UI files are application source and can be customized. Preserve accessibility, keyboard/focus behavior, and platform conventions.
68
+
69
+ ## Naming
70
+
71
+ - Directories and source files use lowercase kebab-case.
72
+ - React component filenames use kebab-case and export PascalCase components.
73
+ - Hooks start with `use-`.
74
+ - Framework-required entrypoint names stay unchanged.
75
+ - Shared packages use the `@shared/<name>` namespace.
76
+
77
+ ## Structural changes
78
+
79
+ Run `anhedral doctor` before changing generated structure. Preview new modules or UI with `--dry-run`. Anhedral refuses managed-file drift, symlinks, and unowned collisions instead of overwriting source.
80
+
81
+ Application product files are developer-owned. `README.md` and `PRODUCTION.md` become user-owned after initialization. Root configuration is mergeable. Integration substrate and generated guidance are recorded in `anhedral.json`.
82
+
83
+ ## Verification
84
+
85
+ Run the smallest relevant package check while iterating, then the generated root verification before handoff:
86
+
87
+ ```sh
88
+ pnpm typecheck
89
+ pnpm verify
90
+ pnpm build
91
+ anhedral doctor
92
+ ```
@@ -0,0 +1,192 @@
1
+ # Anhedral master stack map
2
+
3
+ This is the canonical whole-stack map for Anhedral. It shows the ordered flow
4
+ from skill-led planning through deterministic generation, source layout,
5
+ connected capabilities, cloud provisioning, command-driven operations, agent
6
+ experience, and production verification.
7
+
8
+ Read top to bottom. Arrows represent lifecycle order, not runtime network calls.
9
+ Runtime and provider connections are stated explicitly inside each stage.
10
+
11
+ ```text
12
+ +==========================================================================================================================+
13
+ | ANHEDRAL MASTER STACK MAP |
14
+ +--------------------------------------------------------------------------------------------------------------------------+
15
+ |OUTCOME One readable TypeScript codebase and one command surface connecting every application, cloud resource, release |
16
+ |path, runbook, and coding agent. |
17
+ |PRINCIPLE Ordinary framework code, source ownership, and generated integration boundaries. Anhedral assembles the stack; |
18
+ |it is not the application runtime. |
19
+ +==========================================================================================================================+
20
+ |
21
+ v
22
+ +==========================================================================================================================+
23
+ | 1 SELECT + PLAN |
24
+ +--------------------------------------------------------------------------------------------------------------------------+
25
+ |SKILL FIRST Install and invoke $anhedral-init. Ask for project name and custom-domain status before generating or |
26
+ |provisioning. |
27
+ |CREATE anhedral new <directory> | anhedral init EXTEND anhedral add <modules> | anhedral ui add <components> |
28
+ |INSPECT anhedral doctor AUTOMATE --dry-run | --json | --verbose | --skip-install |
29
+ |SURFACES web | mobile | api | desktop | extension |
30
+ |FEATURES db | auth | billing | storage | native-subscriptions | electron-updater |
31
+ |UI shadcn/ui for DOM clients | React Native Reusables for Expo | NativeWind or Uniwind |
32
+ |CLOSURE auth -> api + db | billing -> auth | storage -> auth | native-subscriptions -> mobile + billing | |
33
+ |electron-updater -> desktop |
34
+ +==========================================================================================================================+
35
+ |
36
+ v
37
+ +==========================================================================================================================+
38
+ | 2 GENERATE SAFELY |
39
+ +--------------------------------------------------------------------------------------------------------------------------+
40
+ |PIPELINE Resolve modules -> select immutable templates -> verify SHA-256 catalog -> stage files -> compose typed |
41
+ |contributions. |
42
+ |PIPELINE Run pinned UI providers only in staging -> record provenance and ownership -> atomic commit -> one root pnpm |
43
+ |install. |
44
+ |REFUSES traversal | symlinks | nested repositories/workspaces | caches | oversized files | unowned collisions | managed |
45
+ |drift. |
46
+ |OWNERSHIP Product source plus README/PRODUCTION are user-owned | root configuration is mergeable | integration substrate |
47
+ |is managed. |
48
+ |STATE anhedral.json records modules, versions, template/UI provenance, styling, file hashes, modes, and ownership. |
49
+ |WHY Deterministic, inspectable generation and safe incremental adds without replacing user work. |
50
+ +==========================================================================================================================+
51
+ |
52
+ v
53
+ +==========================================================================================================================+
54
+ | 3 ONE PNPM + TURBOREPO TYPESCRIPT WORKSPACE |
55
+ +--------------------------------------------------------------------------------------------------------------------------+
56
+ |apps/web Next.js App Router + React + shadcn/ui. WHY: SSR, routing, server-first web, and source-owned UI. |
57
+ |apps/mobile Expo Router + React Native Reusables. WHY: one native TypeScript product for iOS and Android. |
58
+ |apps/api Fastify + validation/security plugins. WHY: thin routes, services, structured logs, and explicit boundaries. |
59
+ |apps/desktop Electron + Vite + sandboxed React renderer. WHY: cross-platform desktop with a narrow privileged bridge. |
60
+ |apps/extension WXT + React + MV3 entrypoints. WHY: browser permissions, side panel/background, builds, and packaging. |
61
+ |apps/assets-private-proxy Cloudflare Worker + private R2 binding. WHY: controlled GET/HEAD media delivery without bucket |
62
+ |listing. |
63
+ |apps/desktop-updater-worker Cloudflare Worker + private R2 binding. WHY: custom-domain Electron metadata, ranges, and |
64
+ |downloads. |
65
+ |packages/contracts Zod schemas. WHY: one validated network contract shared across every boundary. |
66
+ |packages/api-client Typed client-safe HTTP. WHY: one request path for web, mobile, desktop, and extension. |
67
+ |packages/db Drizzle schema + reviewed SQL migrations. WHY: readable TypeScript schema and explicit release artifacts. |
68
+ |packages/realtime Ably client. WHY: scoped invalidation events while clients refetch authoritative state. |
69
+ +==========================================================================================================================+
70
+ |
71
+ v
72
+ +==========================================================================================================================+
73
+ | 4 CONNECTED PRODUCT CAPABILITIES |
74
+ +--------------------------------------------------------------------------------------------------------------------------+
75
+ |REQUESTS Zod contract -> service/Drizzle query -> Fastify route -> typed API client -> every selected client. |
76
+ |IDENTITY Clerk sessions in clients -> verified server identity; never trust a client-supplied user ID. |
77
+ |DATABASE Fastify -> Drizzle -> managed Neon Postgres with isolated dev/preview/production; no generated local Postgres. |
78
+ |STORAGE API presigns R2 PUT -> client uploads -> API confirms -> Worker streams confirmed assets or owner-authorized |
79
+ |reads. |
80
+ |BILLING Expo purchase -> RevenueCat -> signed webhook -> Neon entitlement + transactional outbox. |
81
+ |REALTIME Outbox -> scoped Ably event -> every client refetches the authoritative entitlement. |
82
+ |DESKTOP UPDATE Signed native build -> artifacts then latest metadata -> private R2 -> updates.<domain> Worker -> |
83
+ |electron-updater. |
84
+ |SECURITY Server-only secrets | exact CORS | rate limits/headers | Electron isolation | least privilege | private buckets. |
85
+ +==========================================================================================================================+
86
+ |
87
+ v
88
+ +==========================================================================================================================+
89
+ | 5 PROVISION + CONFIGURE THE CLOUD |
90
+ +--------------------------------------------------------------------------------------------------------------------------+
91
+ |DOMAIN Buy/confirm domain -> add Cloudflare zone -> preserve mail records -> delegate nameservers -> activate DNSSEC. |
92
+ |GITHUB + CI Organization repository, protected environments, pinned Actions, PR checks, branches, and release artifacts. |
93
+ |NEON Projects/branches per environment, pooled DATABASE_URL, backups, reviewed migrations, and controlled deploy gates. |
94
+ |CLERK Development/production instances, publishable/secret keys, domains, OAuth, redirects, webhooks, native and CRX IDs. |
95
+ |CLOUDFLARE Authoritative DNS, private R2 buckets, scoped tokens, CORS/lifecycle, Worker bindings, logs, and custom |
96
+ |domains. |
97
+ |VERCEL Services deployment for web/API, rewrites, environment scopes, preview/production, cron endpoints, domains, and |
98
+ |TLS. |
99
+ |REVENUECAT + ABLY Products, offerings, entitlements, store credentials, signed webhook, scoped realtime, and retry cron. |
100
+ |EAS + STORES Expo project, credentials, internal builds, TestFlight/Play testing, metadata, privacy, review, and rollout. |
101
+ |BROWSER STORE Stable extension ID, least host permissions, OAuth sync, privacy disclosure, trusted testing, and review. |
102
+ |DESKTOP Native-OS signing/notarization, electron-builder installers, private update channel, upgrade tests, and rollback. |
103
+ |DNS app.<domain> points from Cloudflare DNS to Vercel and stays DNS-only; assets/updates are Cloudflare Worker Custom |
104
+ |Domains. |
105
+ |AGENT STOPS User handles sign-in/MFA, purchases, secret generation/reveal, destructive confirmations, and final |
106
+ |submissions. |
107
+ +==========================================================================================================================+
108
+ |
109
+ v
110
+ +==========================================================================================================================+
111
+ | 6 GENERATED package.json COMMAND SURFACE |
112
+ +--------------------------------------------------------------------------------------------------------------------------+
113
+ |WORKSPACE dev | build | typecheck | verify WHY: run and validate exactly the selected monorepo. |
114
+ |APP LOOPS dev:web | dev:mobile | dev:api | dev:desktop | dev:extension |
115
+ |APP CHECKS verify:web | verify:mobile | verify:api | verify:desktop | verify:extension |
116
+ |VERCEL deploy:vercel:{link,preview,production,inspect,domain:inspect} WHY: connect, release, diagnose, and configure DNS. |
117
+ |MOBILE mobile:eas:{login,init} | mobile:build:{internal,production}:{ios,android} | mobile:submit:{ios,android} |
118
+ |DATABASE neon:{login,project:create} | db:{generate,migrate,check,studio} | verify:db |
119
+ |STORAGE r2:{login,bucket:create,cors:list,cors:set} | assets:proxy:{check,dev,deploy} | verify:assets-proxy |
120
+ |UPDATES desktop:updates:{cloudflare:login,bucket:create,provision,publish} |
121
+ |UPDATES desktop:updates:worker:{check,dev,deploy,types} | desktop:updates:build:{mac,win,linux} | verify:desktop-updates |
122
+ |DESKTOP desktop:build | verify:desktop EXTENSION extension:zip | verify:extension |
123
+ |STRUCTURE anhedral doctor | add/ui --dry-run WHY: detect drift and preview safe structural changes. |
124
+ +==========================================================================================================================+
125
+ |
126
+ v
127
+ +==========================================================================================================================+
128
+ | 7 DEVELOPER, AGENT + COMPUTER-USE EXPERIENCE |
129
+ +--------------------------------------------------------------------------------------------------------------------------+
130
+ |DOCS README.md first run/source map | docs/DEVELOPMENT.md recipes | docs/STACK.md tool map | PRODUCTION.md cloud runbook. |
131
+ |CONTROL SKILL.md agent workflow | ANHEDRAL.md ownership notes | anhedral.json source of truth | .env.example inventories. |
132
+ |CODING AGENT Ask name/domain -> resolve modules -> generate -> read docs/manifest -> doctor -> provision -> verify -> |
133
+ |hand off. |
134
+ |SUBAGENTS Lead owns mutations; delegate docs/env inventory, read-only DNS checks, and final verification without secrets. |
135
+ |COMPUTER USE Drive provider consoles after user authentication; configure accounts, domains, OAuth, DNS, stores, and |
136
+ |rollout. |
137
+ |SECRET HANDOFF Stop before Generate/Reveal/Create; name the variable/file; user pastes directly into uncommitted |
138
+ |env/provider UI. |
139
+ |UI DX Source-owned components, target routing, accessibility, ordinary framework APIs, and no hidden proprietary runtime. |
140
+ |OPERATIONS DX Exact root commands, selection-aware docs/scripts, machine-readable plans, provider ledger, and explicit |
141
+ |stop gates. |
142
+ +==========================================================================================================================+
143
+ |
144
+ v
145
+ +==========================================================================================================================+
146
+ | 8 VERIFY, RELEASE + OPERATE |
147
+ +--------------------------------------------------------------------------------------------------------------------------+
148
+ |LOCAL Smallest package check -> typecheck -> full verify -> build -> doctor; never weaken a failing gate. |
149
+ |DATABASE Generate -> review/commit SQL -> drift gate -> controlled migrate -> health/readiness checks. |
150
+ |RUNTIME API coverage, auth/CORS, billing replay/order, Ably reconnect, R2 upload/read/range/cache, Worker method/host |
151
+ |security. |
152
+ |CLIENTS Next build | Expo compatibility/device checks | packaged Electron smoke/upgrade | live WXT MV3 worker in Chrome. |
153
+ |INFRA Confirm provider team/resource names, protected env presence, DNS answers, TLS, Vercel health, Worker bindings, and |
154
+ |logs. |
155
+ |CI Pinned toolchain, strict pnpm policy, allowed build scripts, OSV audit, secret scan, deterministic outputs, and |
156
+ |release checks. |
157
+ |RELEASES Vercel preview/prod | TestFlight/Play tracks | browser-store trusted/review | signed desktop staged update |
158
+ |channel. |
159
+ |RECOVERY Transaction rollback, provider backups, database rollback plan, staged/phased rollout, telemetry, and documented |
160
+ |owners. |
161
+ +==========================================================================================================================+
162
+ ```
163
+
164
+ ## Runtime connection summary
165
+
166
+ ```text
167
+ web / mobile / desktop / extension
168
+ |
169
+ v
170
+ packages/api-client
171
+ |
172
+ v
173
+ Fastify API + Zod contracts
174
+ | | |
175
+ v v v
176
+ Clerk identity Neon private R2
177
+ |
178
+ v
179
+ RevenueCat entitlement
180
+ |
181
+ v
182
+ Ably event
183
+ |
184
+ v
185
+ clients refetch state
186
+
187
+ signed desktop release -> private update R2 -> updates.<domain> Worker -> electron-updater
188
+ ```
189
+
190
+ The runtime summary is intentionally smaller than the lifecycle map. It shows
191
+ only application data flow; DNS, provisioning, release gates, and agent actions
192
+ remain in the master map above.