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
@@ -0,0 +1,401 @@
1
+ import path from 'node:path';
2
+ import { writeFile } from '../util.js';
3
+ import { DESKTOP_DEPENDENCIES } from '../dependencies.js';
4
+ import { childPackageName, htmlText, identifierSegment, jsString } from '../render.js';
5
+ export async function scaffoldDesktop(root, options) {
6
+ const { projectName, displayName } = options;
7
+ const dir = path.join(root, 'apps/desktop');
8
+ writePackageJson(dir, projectName);
9
+ writeTsConfig(dir);
10
+ writeViteConfig(dir);
11
+ writePostcssConfig(dir);
12
+ writeDevScript(dir);
13
+ writeShadcnConfig(dir);
14
+ writeEnvExample(dir);
15
+ writeSourceFiles(dir, displayName);
16
+ }
17
+ function writePackageJson(dir, projectName) {
18
+ const updatePublish = {};
19
+ writeFile(path.join(dir, 'package.json'), JSON.stringify({
20
+ name: childPackageName(projectName, 'desktop'),
21
+ version: '0.1.0',
22
+ private: true,
23
+ type: 'module',
24
+ main: 'dist/main/main.js',
25
+ scripts: {
26
+ dev: 'tsc -p tsconfig.main.json && node scripts/dev.mjs',
27
+ build: 'tsc --noEmit && tsc -p tsconfig.main.json && vite build',
28
+ typecheck: 'tsc --noEmit',
29
+ 'build:mac': `pnpm build && electron-builder --mac${''}`,
30
+ 'build:win': `pnpm build && electron-builder --win${''}`,
31
+ 'build:linux': `pnpm build && electron-builder --linux${''}`,
32
+ package: `pnpm build && electron-builder${''}`,
33
+ ...({}),
34
+ },
35
+ build: {
36
+ appId: `dev.anhedral.${identifierSegment(projectName)}`,
37
+ productName: projectName,
38
+ directories: {
39
+ output: 'release',
40
+ },
41
+ files: [
42
+ 'dist/**/*',
43
+ 'package.json',
44
+ ],
45
+ mac: {
46
+ target: ['dmg', 'zip'],
47
+ },
48
+ win: {
49
+ target: ['nsis', 'zip'],
50
+ },
51
+ linux: {
52
+ executableName: identifierSegment(projectName),
53
+ target: ['AppImage', 'deb'],
54
+ },
55
+ ...updatePublish,
56
+ },
57
+ dependencies: DESKTOP_DEPENDENCIES.dependencies,
58
+ devDependencies: DESKTOP_DEPENDENCIES.devDependencies,
59
+ }, null, 2) + '\n');
60
+ }
61
+ function writeTsConfig(dir) {
62
+ writeFile(path.join(dir, 'tsconfig.main.json'), JSON.stringify({
63
+ compilerOptions: {
64
+ target: 'ES2022',
65
+ module: 'NodeNext',
66
+ moduleResolution: 'NodeNext',
67
+ strict: true,
68
+ skipLibCheck: true,
69
+ esModuleInterop: true,
70
+ outDir: 'dist/main',
71
+ rootDir: 'src/main',
72
+ types: ['node'],
73
+ },
74
+ include: ['src/main/**/*'],
75
+ }, null, 2) + '\n');
76
+ }
77
+ function writeViteConfig(dir) {
78
+ writeFile(path.join(dir, 'vite.config.ts'), `import { defineConfig } from 'vite';
79
+ import react from '@vitejs/plugin-react';
80
+ import path from 'node:path';
81
+
82
+ export default defineConfig({
83
+ base: './',
84
+ plugins: [react()],
85
+ resolve: {
86
+ alias: {
87
+ '@': path.resolve(__dirname, 'src/renderer'),
88
+ },
89
+ },
90
+ build: {
91
+ outDir: 'dist/renderer',
92
+ emptyOutDir: true,
93
+ // Clerk is already loaded behind a dynamic import. Its optional account-management
94
+ // surface is intentionally a large async chunk and does not inflate initial rendering.
95
+ chunkSizeWarningLimit: 1600,
96
+ },
97
+ });
98
+ `);
99
+ }
100
+ function writePostcssConfig(dir) {
101
+ writeFile(path.join(dir, 'postcss.config.mjs'), `const config = {
102
+ plugins: {
103
+ '@tailwindcss/postcss': {},
104
+ },
105
+ };
106
+
107
+ export default config;
108
+ `);
109
+ }
110
+ function writeDevScript(dir) {
111
+ writeFile(path.join(dir, 'scripts/dev.mjs'), `import { spawn } from 'node:child_process';
112
+ import { once } from 'node:events';
113
+
114
+ const host = '127.0.0.1';
115
+ const port = '5173';
116
+ const devServerUrl = 'http://' + host + ':' + port;
117
+ const viteCommand = process.platform === 'win32' ? 'vite.cmd' : 'vite';
118
+ const electronCommand = process.platform === 'win32' ? 'electron.cmd' : 'electron';
119
+
120
+ function start(command, args, options = {}) {
121
+ const invocation = process.platform === 'win32'
122
+ ? {
123
+ command: process.env.ComSpec || 'cmd.exe',
124
+ args: ['/d', '/s', '/c', 'call', command, ...args],
125
+ }
126
+ : { command, args };
127
+ return spawn(invocation.command, invocation.args, {
128
+ cwd: process.cwd(),
129
+ stdio: 'inherit',
130
+ ...options,
131
+ });
132
+ }
133
+
134
+ function stop(child) {
135
+ if (child && child.exitCode === null && !child.killed) child.kill('SIGTERM');
136
+ }
137
+
138
+ async function waitForServer(server, url, timeoutMs = 30_000) {
139
+ const deadline = Date.now() + timeoutMs;
140
+ while (Date.now() < deadline) {
141
+ if (server.exitCode !== null) throw new Error('Vite exited before the dev server was ready.');
142
+ try {
143
+ const response = await fetch(url);
144
+ if (response.ok) return;
145
+ } catch {
146
+ // The server is still starting.
147
+ }
148
+ await new Promise((resolve) => setTimeout(resolve, 150));
149
+ }
150
+ throw new Error('Timed out waiting for Vite at ' + url + '.');
151
+ }
152
+
153
+ const vite = start(viteCommand, ['--host', host, '--port', port, '--strictPort']);
154
+ let electron;
155
+ let shuttingDown = false;
156
+
157
+ for (const signal of ['SIGINT', 'SIGTERM']) {
158
+ process.once(signal, () => {
159
+ shuttingDown = true;
160
+ stop(electron);
161
+ stop(vite);
162
+ });
163
+ }
164
+
165
+ try {
166
+ await waitForServer(vite, devServerUrl);
167
+ electron = start(electronCommand, ['.'], {
168
+ env: { ...process.env, VITE_DEV_SERVER_URL: devServerUrl },
169
+ });
170
+ vite.once('exit', (code) => {
171
+ if (!shuttingDown) {
172
+ console.error('Vite exited while Electron was running.');
173
+ process.exitCode = code ?? 1;
174
+ stop(electron);
175
+ }
176
+ });
177
+ const [code] = await once(electron, 'exit');
178
+ if (process.exitCode === undefined) process.exitCode = typeof code === 'number' ? code : 1;
179
+ } catch (error) {
180
+ console.error(error instanceof Error ? error.message : error);
181
+ process.exitCode = 1;
182
+ } finally {
183
+ shuttingDown = true;
184
+ stop(electron);
185
+ stop(vite);
186
+ }
187
+ `);
188
+ }
189
+ function writeShadcnConfig(dir) {
190
+ writeFile(path.join(dir, 'components.json'), JSON.stringify({
191
+ $schema: 'https://ui.shadcn.com/schema.json',
192
+ style: 'new-york',
193
+ rsc: false,
194
+ tsx: true,
195
+ tailwind: {
196
+ config: '',
197
+ css: 'src/renderer/styles.css',
198
+ baseColor: 'neutral',
199
+ cssVariables: true,
200
+ prefix: '',
201
+ },
202
+ aliases: {
203
+ components: '@/components',
204
+ utils: '@/lib/utils',
205
+ ui: '@/components/ui',
206
+ lib: '@/lib',
207
+ hooks: '@/hooks',
208
+ },
209
+ iconLibrary: 'lucide',
210
+ }, null, 2) + '\n');
211
+ }
212
+ function writeEnvExample(dir) {
213
+ writeFile(path.join(dir, '.env.example'), '');
214
+ }
215
+ function writeSourceFiles(dir, displayName) {
216
+ const displayNameLiteral = jsString(displayName);
217
+ const displayNameHtml = htmlText(displayName);
218
+ writeFile(path.join(dir, 'src/main/app-window.ts'), `import { BrowserWindow, shell } from 'electron';
219
+ import path from 'node:path';
220
+ import { fileURLToPath, pathToFileURL } from 'node:url';
221
+
222
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
223
+ const rendererIndex = path.join(__dirname, '../renderer/index.html');
224
+
225
+ function isSafeExternalUrl(value: string): boolean {
226
+ try {
227
+ const url = new URL(value);
228
+ return url.protocol === 'https:' && !url.username && !url.password;
229
+ } catch {
230
+ return false;
231
+ }
232
+ }
233
+
234
+ function isAllowedAppNavigation(value: string): boolean {
235
+ try {
236
+ const target = new URL(value);
237
+ if (process.env.VITE_DEV_SERVER_URL) {
238
+ return target.origin === new URL(process.env.VITE_DEV_SERVER_URL).origin;
239
+ }
240
+ return target.href === pathToFileURL(rendererIndex).href;
241
+ } catch {
242
+ return false;
243
+ }
244
+ }
245
+
246
+ // Product-owned window options and navigation policy belong in this file.
247
+ // Anhedral's managed main.ts imports this seam and wires lifecycle integrations around it.
248
+ export function createAppWindow(): BrowserWindow {
249
+ const window = new BrowserWindow({
250
+ width: 1180,
251
+ height: 760,
252
+ minWidth: 860,
253
+ minHeight: 560,
254
+ title: ${displayNameLiteral},
255
+ webPreferences: {
256
+ preload: path.join(__dirname, 'preload.cjs'),
257
+ contextIsolation: true,
258
+ nodeIntegration: false,
259
+ sandbox: true,
260
+ webSecurity: true,
261
+ },
262
+ });
263
+
264
+ window.webContents.on('will-navigate', (event, url) => {
265
+ if (!isAllowedAppNavigation(url)) event.preventDefault();
266
+ });
267
+ window.webContents.setWindowOpenHandler(({ url }) => {
268
+ if (isSafeExternalUrl(url)) {
269
+ void shell.openExternal(url).catch((error) => {
270
+ console.error('Unable to open external URL:', error);
271
+ });
272
+ }
273
+ return { action: 'deny' };
274
+ });
275
+
276
+ if (process.env.VITE_DEV_SERVER_URL) {
277
+ void window.loadURL(process.env.VITE_DEV_SERVER_URL).catch((error) => {
278
+ console.error('Unable to load the development renderer:', error);
279
+ });
280
+ } else {
281
+ void window.loadFile(rendererIndex).catch((error) => {
282
+ console.error('Unable to load the packaged renderer:', error);
283
+ });
284
+ }
285
+
286
+ return window;
287
+ }
288
+ `);
289
+ writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session } from 'electron';
290
+ import { createAppWindow } from './app-window.js';
291
+
292
+
293
+ app.whenReady().then(() => {
294
+ session.defaultSession.setPermissionCheckHandler(() => false);
295
+ session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
296
+ createAppWindow();
297
+ });
298
+ app.on('window-all-closed', () => {
299
+ if (process.platform !== 'darwin') app.quit();
300
+ });
301
+ app.on('activate', () => {
302
+ if (BrowserWindow.getAllWindows().length === 0) createAppWindow();
303
+ });
304
+ `);
305
+ writeFile(path.join(dir, 'src/main/preload.cts'), `import { contextBridge } from 'electron';
306
+
307
+ contextBridge.exposeInMainWorld('anhedral', {
308
+ platform: process.platform,
309
+ });
310
+ `);
311
+ writeFile(path.join(dir, 'index.html'), `<!doctype html>
312
+ <html lang="en">
313
+ <head>
314
+ <meta charset="UTF-8" />
315
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
316
+ <meta http-equiv="Content-Security-Policy" content="default-src 'self'; base-uri 'none'; object-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; connect-src 'self' https: ws://127.0.0.1:* wss:; img-src 'self' data: https:; font-src 'self' data: https:; frame-src https:; form-action 'self' https:" />
317
+ <title>${displayNameHtml}</title>
318
+ </head>
319
+ <body>
320
+ <div id="root"></div>
321
+ <script type="module" src="/src/renderer/main.tsx"></script>
322
+ </body>
323
+ </html>
324
+ `);
325
+ writeFile(path.join(dir, 'src/renderer/lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
326
+ import { twMerge } from 'tailwind-merge';
327
+
328
+ export function cn(...inputs: ClassValue[]) {
329
+ return twMerge(clsx(inputs));
330
+ }
331
+ `);
332
+ writeFile(path.join(dir, 'src/renderer/components/ui/button.tsx'), `import * as React from 'react';
333
+ import { cn } from '@/lib/utils';
334
+
335
+ export function Button({ className, type = 'button', ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
336
+ return (
337
+ <button
338
+ type={type}
339
+ className={cn('inline-flex h-9 items-center justify-center rounded-md bg-primary px-4 text-sm font-medium text-primary-foreground transition-colors hover:bg-primary/90 disabled:opacity-50', className)}
340
+ {...props}
341
+ />
342
+ );
343
+ }
344
+ `);
345
+ writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
346
+ import ReactDOM from 'react-dom/client';
347
+ import './styles.css';
348
+
349
+ function App() {
350
+ return (
351
+ <main className="flex min-h-screen flex-col gap-6 bg-background p-8 text-foreground">
352
+ <section className="flex flex-col gap-3">
353
+ <h1 className="text-3xl font-semibold">{${displayNameLiteral}}</h1>
354
+ <p className="max-w-2xl text-muted-foreground">
355
+ ${"Electron + shadcn/ui desktop client ready for your application."}
356
+ </p>
357
+ </section>
358
+
359
+
360
+
361
+
362
+ </main>
363
+ );
364
+ }
365
+
366
+ ReactDOM.createRoot(document.getElementById('root')!).render(
367
+ <React.StrictMode>
368
+ <App />
369
+ </React.StrictMode>,
370
+ );
371
+ `);
372
+ writeFile(path.join(dir, 'src/renderer/styles.css'), `@import "tailwindcss";
373
+
374
+ :root {
375
+ --background: oklch(1 0 0);
376
+ --foreground: oklch(0.145 0 0);
377
+ --primary: oklch(0.205 0 0);
378
+ --primary-foreground: oklch(0.985 0 0);
379
+ --muted-foreground: oklch(0.556 0 0);
380
+ }
381
+
382
+ @theme inline {
383
+ --color-background: var(--background);
384
+ --color-foreground: var(--foreground);
385
+ --color-primary: var(--primary);
386
+ --color-primary-foreground: var(--primary-foreground);
387
+ --color-muted-foreground: var(--muted-foreground);
388
+ }
389
+
390
+ body {
391
+ margin: 0;
392
+ background: var(--background);
393
+ color: var(--foreground);
394
+ font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
395
+ }
396
+
397
+ button {
398
+ border: 0;
399
+ }
400
+ `);
401
+ }