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.
- package/README.md +29 -269
- package/assets/anhedral-cli-init-technical.svg +322 -0
- package/assets/anhedral-cli-init.svg +257 -173
- package/dist/bin.js +28 -191
- package/dist/dependencies.d.ts +7 -57
- package/dist/dependencies.js +96 -308
- package/dist/index.d.ts +2 -7
- package/dist/index.js +1 -4
- package/dist/platforms/desktop.js +401 -0
- package/dist/platforms/extension.js +401 -0
- package/dist/platforms/mobile.js +232 -0
- package/dist/project.d.ts +0 -26
- package/dist/standard-products.d.ts +2 -0
- package/dist/standard-products.js +6 -0
- package/dist/standard.d.ts +18 -0
- package/dist/standard.js +578 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/application-stack-standard.md +700 -0
- package/package.json +34 -51
- package/CHANGELOG.md +0 -178
- package/CONTRIBUTING.md +0 -77
- package/SECURITY.md +0 -37
- package/SKILL.md +0 -339
- package/assets/images/svg/ably-symbol-color.svg +0 -8
- package/bin/anhedral.js +0 -2
- package/dist/architecture/contributions.d.ts +0 -28
- package/dist/architecture/contributions.js +0 -125
- package/dist/architecture/index.d.ts +0 -12
- package/dist/architecture/index.js +0 -6
- package/dist/architecture/manifest.d.ts +0 -53
- package/dist/architecture/manifest.js +0 -283
- package/dist/architecture/modules.d.ts +0 -41
- package/dist/architecture/modules.js +0 -187
- package/dist/architecture/plan.d.ts +0 -58
- package/dist/architecture/plan.js +0 -145
- package/dist/architecture/templates.d.ts +0 -10
- package/dist/architecture/templates.js +0 -23
- package/dist/cli.d.ts +0 -32
- package/dist/cli.js +0 -281
- package/dist/composers/root.d.ts +0 -6
- package/dist/composers/root.js +0 -35
- package/dist/print.d.ts +0 -8
- package/dist/print.js +0 -48
- package/dist/prompts.d.ts +0 -6
- package/dist/prompts.js +0 -41
- package/dist/scaffold.d.ts +0 -64
- package/dist/scaffold.js +0 -2102
- package/dist/template-source.d.ts +0 -20
- package/dist/template-source.js +0 -180
- package/dist/templates/api.d.ts +0 -2
- package/dist/templates/api.js +0 -3187
- package/dist/templates/assets-private-proxy.d.ts +0 -3
- package/dist/templates/assets-private-proxy.js +0 -277
- package/dist/templates/desktop.js +0 -792
- package/dist/templates/electron-updater.d.ts +0 -3
- package/dist/templates/electron-updater.js +0 -358
- package/dist/templates/extension.js +0 -856
- package/dist/templates/first-run.d.ts +0 -2
- package/dist/templates/first-run.js +0 -167
- package/dist/templates/mobile.js +0 -861
- package/dist/templates/shared.d.ts +0 -2
- package/dist/templates/shared.js +0 -577
- package/dist/templates/web.d.ts +0 -2
- package/dist/templates/web.js +0 -569
- package/dist/toolchain.d.ts +0 -3
- package/dist/toolchain.js +0 -8
- package/dist/ui.d.ts +0 -33
- package/dist/ui.js +0 -199
- package/dist/workspace-config.d.ts +0 -11
- package/dist/workspace-config.js +0 -372
- package/docs/NORTHSTAR.md +0 -114
- package/docs/conventions.md +0 -94
- package/docs/engineering-principles.md +0 -88
- package/docs/master-stack-map.md +0 -199
- package/docs/output-tree-contract.md +0 -493
- package/docs/references/manual-scaffolding.md +0 -421
- package/docs/references/provisioning.md +0 -577
- package/scripts/render-master-stack-map.mjs +0 -510
- package/templates/api-fastify/apps/api/tsconfig.json +0 -20
- package/templates/catalog.json +0 -23
- package/templates/db-drizzle/packages/db/tsconfig.json +0 -14
- package/templates/desktop-electron/apps/desktop/tsconfig.json +0 -37
- package/templates/extension-wxt/apps/extension/tsconfig.json +0 -41
- package/templates/mobile-expo/apps/mobile/expo-env.d.ts +0 -1
- package/templates/web-next/apps/web/next-env.d.ts +0 -6
- /package/dist/{templates → platforms}/desktop.d.ts +0 -0
- /package/dist/{templates → platforms}/extension.d.ts +0 -0
- /package/dist/{templates → platforms}/mobile.d.ts +0 -0
|
@@ -1,792 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import { writeFile } from '../util.js';
|
|
3
|
-
import { anhedralPrint } from '../print.js';
|
|
4
|
-
import { DESKTOP_DEPENDENCIES, ELECTRON_UPDATER_DEPENDENCIES } from '../dependencies.js';
|
|
5
|
-
import { childPackageName, htmlText, identifierSegment, jsString } from '../render.js';
|
|
6
|
-
function selectedDependencies(options) {
|
|
7
|
-
const dependencies = { ...(DESKTOP_DEPENDENCIES.dependencies ?? {}) };
|
|
8
|
-
if (!options.apps.api)
|
|
9
|
-
delete dependencies['@shared/api-client'];
|
|
10
|
-
if (!options.features.billing)
|
|
11
|
-
delete dependencies['@shared/realtime'];
|
|
12
|
-
if (!options.features.auth) {
|
|
13
|
-
delete dependencies['@clerk/clerk-js'];
|
|
14
|
-
delete dependencies['@clerk/ui'];
|
|
15
|
-
delete dependencies['@solana/web3.js'];
|
|
16
|
-
delete dependencies.bs58;
|
|
17
|
-
}
|
|
18
|
-
if (options.features.electronUpdater) {
|
|
19
|
-
Object.assign(dependencies, ELECTRON_UPDATER_DEPENDENCIES.dependencies);
|
|
20
|
-
}
|
|
21
|
-
return dependencies;
|
|
22
|
-
}
|
|
23
|
-
export async function scaffoldDesktop(root, options) {
|
|
24
|
-
const { projectName, displayName } = options;
|
|
25
|
-
const dir = path.join(root, 'apps/desktop');
|
|
26
|
-
anhedralPrint.section('Desktop (Electron + shadcn/ui)');
|
|
27
|
-
anhedralPrint.step('Writing Electron desktop app');
|
|
28
|
-
writePackageJson(dir, projectName, options);
|
|
29
|
-
writeTsConfig(dir);
|
|
30
|
-
writeViteConfig(dir);
|
|
31
|
-
writePostcssConfig(dir);
|
|
32
|
-
writeDevScript(dir);
|
|
33
|
-
writeShadcnConfig(dir);
|
|
34
|
-
writeEnvExample(dir, options);
|
|
35
|
-
writeSourceFiles(dir, displayName, options);
|
|
36
|
-
anhedralPrint.done('Electron desktop app written');
|
|
37
|
-
}
|
|
38
|
-
function writePackageJson(dir, projectName, options) {
|
|
39
|
-
const updatePublish = options.features.electronUpdater ? {
|
|
40
|
-
artifactName: '${productName}-${version}-${os}-${arch}.${ext}',
|
|
41
|
-
publish: [{
|
|
42
|
-
provider: 'generic',
|
|
43
|
-
url: '${env.DESKTOP_UPDATE_BASE_URL}/releases/${os}/${arch}',
|
|
44
|
-
useMultipleRangeRequest: false,
|
|
45
|
-
}],
|
|
46
|
-
} : {};
|
|
47
|
-
writeFile(path.join(dir, 'package.json'), JSON.stringify({
|
|
48
|
-
name: childPackageName(projectName, 'desktop'),
|
|
49
|
-
version: '0.1.0',
|
|
50
|
-
private: true,
|
|
51
|
-
type: 'module',
|
|
52
|
-
main: 'dist/main/main.js',
|
|
53
|
-
scripts: {
|
|
54
|
-
dev: 'tsc -p tsconfig.main.json && node scripts/dev.mjs',
|
|
55
|
-
build: 'tsc --noEmit && tsc -p tsconfig.main.json && vite build',
|
|
56
|
-
typecheck: 'tsc --noEmit',
|
|
57
|
-
'build:mac': `pnpm build && electron-builder --mac${options.features.electronUpdater ? ' --publish never' : ''}`,
|
|
58
|
-
'build:win': `pnpm build && electron-builder --win${options.features.electronUpdater ? ' --publish never' : ''}`,
|
|
59
|
-
'build:linux': `pnpm build && electron-builder --linux${options.features.electronUpdater ? ' --publish never' : ''}`,
|
|
60
|
-
package: `pnpm build && electron-builder${options.features.electronUpdater ? ' --publish never' : ''}`,
|
|
61
|
-
...(options.features.electronUpdater ? {
|
|
62
|
-
'updates:build:mac': 'pnpm build && electron-builder --mac --publish never',
|
|
63
|
-
'updates:build:win': 'pnpm build && electron-builder --win --publish never',
|
|
64
|
-
'updates:build:linux': 'pnpm build && electron-builder --linux --publish never',
|
|
65
|
-
} : {}),
|
|
66
|
-
},
|
|
67
|
-
build: {
|
|
68
|
-
appId: `dev.anhedral.${identifierSegment(projectName)}`,
|
|
69
|
-
productName: projectName,
|
|
70
|
-
directories: {
|
|
71
|
-
output: 'release',
|
|
72
|
-
},
|
|
73
|
-
files: [
|
|
74
|
-
'dist/**/*',
|
|
75
|
-
'package.json',
|
|
76
|
-
],
|
|
77
|
-
mac: {
|
|
78
|
-
target: ['dmg', 'zip'],
|
|
79
|
-
},
|
|
80
|
-
win: {
|
|
81
|
-
target: ['nsis', 'zip'],
|
|
82
|
-
},
|
|
83
|
-
linux: {
|
|
84
|
-
executableName: identifierSegment(projectName),
|
|
85
|
-
target: ['AppImage', 'deb'],
|
|
86
|
-
},
|
|
87
|
-
...updatePublish,
|
|
88
|
-
},
|
|
89
|
-
dependencies: selectedDependencies(options),
|
|
90
|
-
devDependencies: DESKTOP_DEPENDENCIES.devDependencies,
|
|
91
|
-
}, null, 2) + '\n');
|
|
92
|
-
}
|
|
93
|
-
function writeTsConfig(dir) {
|
|
94
|
-
writeFile(path.join(dir, 'tsconfig.main.json'), JSON.stringify({
|
|
95
|
-
compilerOptions: {
|
|
96
|
-
target: 'ES2022',
|
|
97
|
-
module: 'NodeNext',
|
|
98
|
-
moduleResolution: 'NodeNext',
|
|
99
|
-
strict: true,
|
|
100
|
-
skipLibCheck: true,
|
|
101
|
-
esModuleInterop: true,
|
|
102
|
-
outDir: 'dist/main',
|
|
103
|
-
rootDir: 'src/main',
|
|
104
|
-
types: ['node'],
|
|
105
|
-
},
|
|
106
|
-
include: ['src/main/**/*'],
|
|
107
|
-
}, null, 2) + '\n');
|
|
108
|
-
}
|
|
109
|
-
function writeViteConfig(dir) {
|
|
110
|
-
writeFile(path.join(dir, 'vite.config.ts'), `import { defineConfig } from 'vite';
|
|
111
|
-
import react from '@vitejs/plugin-react';
|
|
112
|
-
import path from 'node:path';
|
|
113
|
-
|
|
114
|
-
export default defineConfig({
|
|
115
|
-
base: './',
|
|
116
|
-
plugins: [react()],
|
|
117
|
-
resolve: {
|
|
118
|
-
alias: {
|
|
119
|
-
'@': path.resolve(__dirname, 'src/renderer'),
|
|
120
|
-
},
|
|
121
|
-
},
|
|
122
|
-
build: {
|
|
123
|
-
outDir: 'dist/renderer',
|
|
124
|
-
emptyOutDir: true,
|
|
125
|
-
},
|
|
126
|
-
});
|
|
127
|
-
`);
|
|
128
|
-
}
|
|
129
|
-
function writePostcssConfig(dir) {
|
|
130
|
-
writeFile(path.join(dir, 'postcss.config.mjs'), `const config = {
|
|
131
|
-
plugins: {
|
|
132
|
-
'@tailwindcss/postcss': {},
|
|
133
|
-
},
|
|
134
|
-
};
|
|
135
|
-
|
|
136
|
-
export default config;
|
|
137
|
-
`);
|
|
138
|
-
}
|
|
139
|
-
function writeDevScript(dir) {
|
|
140
|
-
writeFile(path.join(dir, 'scripts/dev.mjs'), `import { spawn } from 'node:child_process';
|
|
141
|
-
import { once } from 'node:events';
|
|
142
|
-
|
|
143
|
-
const host = '127.0.0.1';
|
|
144
|
-
const port = '5173';
|
|
145
|
-
const devServerUrl = 'http://' + host + ':' + port;
|
|
146
|
-
const viteCommand = process.platform === 'win32' ? 'vite.cmd' : 'vite';
|
|
147
|
-
const electronCommand = process.platform === 'win32' ? 'electron.cmd' : 'electron';
|
|
148
|
-
|
|
149
|
-
function start(command, args, options = {}) {
|
|
150
|
-
const invocation = process.platform === 'win32'
|
|
151
|
-
? {
|
|
152
|
-
command: process.env.ComSpec || 'cmd.exe',
|
|
153
|
-
args: ['/d', '/s', '/c', 'call', command, ...args],
|
|
154
|
-
}
|
|
155
|
-
: { command, args };
|
|
156
|
-
return spawn(invocation.command, invocation.args, {
|
|
157
|
-
cwd: process.cwd(),
|
|
158
|
-
stdio: 'inherit',
|
|
159
|
-
...options,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
function stop(child) {
|
|
164
|
-
if (child && child.exitCode === null && !child.killed) child.kill('SIGTERM');
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
async function waitForServer(server, url, timeoutMs = 30_000) {
|
|
168
|
-
const deadline = Date.now() + timeoutMs;
|
|
169
|
-
while (Date.now() < deadline) {
|
|
170
|
-
if (server.exitCode !== null) throw new Error('Vite exited before the dev server was ready.');
|
|
171
|
-
try {
|
|
172
|
-
const response = await fetch(url);
|
|
173
|
-
if (response.ok) return;
|
|
174
|
-
} catch {
|
|
175
|
-
// The server is still starting.
|
|
176
|
-
}
|
|
177
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
178
|
-
}
|
|
179
|
-
throw new Error('Timed out waiting for Vite at ' + url + '.');
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const vite = start(viteCommand, ['--host', host, '--port', port, '--strictPort']);
|
|
183
|
-
let electron;
|
|
184
|
-
let shuttingDown = false;
|
|
185
|
-
|
|
186
|
-
for (const signal of ['SIGINT', 'SIGTERM']) {
|
|
187
|
-
process.once(signal, () => {
|
|
188
|
-
shuttingDown = true;
|
|
189
|
-
stop(electron);
|
|
190
|
-
stop(vite);
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
try {
|
|
195
|
-
await waitForServer(vite, devServerUrl);
|
|
196
|
-
electron = start(electronCommand, ['.'], {
|
|
197
|
-
env: { ...process.env, VITE_DEV_SERVER_URL: devServerUrl },
|
|
198
|
-
});
|
|
199
|
-
vite.once('exit', (code) => {
|
|
200
|
-
if (!shuttingDown) {
|
|
201
|
-
console.error('Vite exited while Electron was running.');
|
|
202
|
-
process.exitCode = code ?? 1;
|
|
203
|
-
stop(electron);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
const [code] = await once(electron, 'exit');
|
|
207
|
-
if (process.exitCode === undefined) process.exitCode = typeof code === 'number' ? code : 1;
|
|
208
|
-
} catch (error) {
|
|
209
|
-
console.error(error instanceof Error ? error.message : error);
|
|
210
|
-
process.exitCode = 1;
|
|
211
|
-
} finally {
|
|
212
|
-
shuttingDown = true;
|
|
213
|
-
stop(electron);
|
|
214
|
-
stop(vite);
|
|
215
|
-
}
|
|
216
|
-
`);
|
|
217
|
-
}
|
|
218
|
-
function writeShadcnConfig(dir) {
|
|
219
|
-
writeFile(path.join(dir, 'components.json'), JSON.stringify({
|
|
220
|
-
$schema: 'https://ui.shadcn.com/schema.json',
|
|
221
|
-
style: 'new-york',
|
|
222
|
-
rsc: false,
|
|
223
|
-
tsx: true,
|
|
224
|
-
tailwind: {
|
|
225
|
-
config: '',
|
|
226
|
-
css: 'src/renderer/styles.css',
|
|
227
|
-
baseColor: 'neutral',
|
|
228
|
-
cssVariables: true,
|
|
229
|
-
prefix: '',
|
|
230
|
-
},
|
|
231
|
-
aliases: {
|
|
232
|
-
components: '@/components',
|
|
233
|
-
utils: '@/lib/utils',
|
|
234
|
-
ui: '@/components/ui',
|
|
235
|
-
lib: '@/lib',
|
|
236
|
-
hooks: '@/hooks',
|
|
237
|
-
},
|
|
238
|
-
iconLibrary: 'lucide',
|
|
239
|
-
}, null, 2) + '\n');
|
|
240
|
-
}
|
|
241
|
-
function writeEnvExample(dir, options) {
|
|
242
|
-
const lines = [
|
|
243
|
-
options.apps.api ? 'VITE_API_URL=http://localhost:8787/api' : null,
|
|
244
|
-
options.features.auth ? 'VITE_CLERK_PUBLISHABLE_KEY=pk_test_***' : null,
|
|
245
|
-
].filter((value) => value !== null);
|
|
246
|
-
writeFile(path.join(dir, '.env.example'), lines.join('\n') + (lines.length > 0 ? '\n' : ''));
|
|
247
|
-
if (options.features.electronUpdater) {
|
|
248
|
-
writeFile(path.join(dir, 'electron-builder.env.example'), 'DESKTOP_UPDATE_BASE_URL=https://updates.example.com\n');
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
function writeSourceFiles(dir, displayName, options) {
|
|
252
|
-
const displayNameLiteral = jsString(displayName);
|
|
253
|
-
const displayNameHtml = htmlText(displayName);
|
|
254
|
-
const description = options.apps.api
|
|
255
|
-
? 'Electron + shadcn/ui desktop client using the same shared API client as web, mobile, and extension.'
|
|
256
|
-
: 'Electron + shadcn/ui desktop client ready for your application.';
|
|
257
|
-
const updaterImport = options.features.electronUpdater
|
|
258
|
-
? "import electronUpdater, { type AppUpdater } from 'electron-updater';\n"
|
|
259
|
-
: '';
|
|
260
|
-
const updaterRuntime = options.features.electronUpdater ? `
|
|
261
|
-
const UPDATE_CHECK_INTERVAL_MS = 4 * 60 * 60 * 1000;
|
|
262
|
-
let updateTimer: NodeJS.Timeout | undefined;
|
|
263
|
-
|
|
264
|
-
function getAutoUpdater(): AppUpdater {
|
|
265
|
-
const { autoUpdater } = electronUpdater;
|
|
266
|
-
return autoUpdater;
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
async function checkForDesktopUpdate(updater: AppUpdater): Promise<void> {
|
|
270
|
-
try {
|
|
271
|
-
await updater.checkForUpdatesAndNotify();
|
|
272
|
-
} catch (error) {
|
|
273
|
-
console.error('Desktop update check failed:', error);
|
|
274
|
-
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
function startDesktopUpdates(): void {
|
|
278
|
-
if (!app.isPackaged) return;
|
|
279
|
-
const updater = getAutoUpdater();
|
|
280
|
-
updater.autoDownload = true;
|
|
281
|
-
updater.autoInstallOnAppQuit = true;
|
|
282
|
-
updater.on('error', (error) => {
|
|
283
|
-
console.error('Desktop updater error:', error);
|
|
284
|
-
});
|
|
285
|
-
void checkForDesktopUpdate(updater);
|
|
286
|
-
updateTimer = setInterval(() => {
|
|
287
|
-
void checkForDesktopUpdate(updater);
|
|
288
|
-
}, UPDATE_CHECK_INTERVAL_MS);
|
|
289
|
-
updateTimer.unref();
|
|
290
|
-
}
|
|
291
|
-
` : '';
|
|
292
|
-
const updaterStart = options.features.electronUpdater ? ' startDesktopUpdates();\n' : '';
|
|
293
|
-
const updaterStop = options.features.electronUpdater
|
|
294
|
-
? "app.on('before-quit', () => { if (updateTimer) clearInterval(updateTimer); });\n"
|
|
295
|
-
: '';
|
|
296
|
-
writeFile(path.join(dir, 'src/main/app-window.ts'), `import { BrowserWindow, shell } from 'electron';
|
|
297
|
-
import path from 'node:path';
|
|
298
|
-
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
299
|
-
|
|
300
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
301
|
-
const rendererIndex = path.join(__dirname, '../renderer/index.html');
|
|
302
|
-
|
|
303
|
-
function isSafeExternalUrl(value: string): boolean {
|
|
304
|
-
try {
|
|
305
|
-
const url = new URL(value);
|
|
306
|
-
return url.protocol === 'https:' && !url.username && !url.password;
|
|
307
|
-
} catch {
|
|
308
|
-
return false;
|
|
309
|
-
}
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
function isAllowedAppNavigation(value: string): boolean {
|
|
313
|
-
try {
|
|
314
|
-
const target = new URL(value);
|
|
315
|
-
if (process.env.VITE_DEV_SERVER_URL) {
|
|
316
|
-
return target.origin === new URL(process.env.VITE_DEV_SERVER_URL).origin;
|
|
317
|
-
}
|
|
318
|
-
return target.href === pathToFileURL(rendererIndex).href;
|
|
319
|
-
} catch {
|
|
320
|
-
return false;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
// Product-owned window options and navigation policy belong in this file.
|
|
325
|
-
// Anhedral's managed main.ts imports this seam and wires lifecycle integrations around it.
|
|
326
|
-
export function createAppWindow(): BrowserWindow {
|
|
327
|
-
const window = new BrowserWindow({
|
|
328
|
-
width: 1180,
|
|
329
|
-
height: 760,
|
|
330
|
-
minWidth: 860,
|
|
331
|
-
minHeight: 560,
|
|
332
|
-
title: ${displayNameLiteral},
|
|
333
|
-
webPreferences: {
|
|
334
|
-
preload: path.join(__dirname, 'preload.cjs'),
|
|
335
|
-
contextIsolation: true,
|
|
336
|
-
nodeIntegration: false,
|
|
337
|
-
sandbox: true,
|
|
338
|
-
webSecurity: true,
|
|
339
|
-
},
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
window.webContents.on('will-navigate', (event, url) => {
|
|
343
|
-
if (!isAllowedAppNavigation(url)) event.preventDefault();
|
|
344
|
-
});
|
|
345
|
-
window.webContents.setWindowOpenHandler(({ url }) => {
|
|
346
|
-
if (isSafeExternalUrl(url)) {
|
|
347
|
-
void shell.openExternal(url).catch((error) => {
|
|
348
|
-
console.error('Unable to open external URL:', error);
|
|
349
|
-
});
|
|
350
|
-
}
|
|
351
|
-
return { action: 'deny' };
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
if (process.env.VITE_DEV_SERVER_URL) {
|
|
355
|
-
void window.loadURL(process.env.VITE_DEV_SERVER_URL).catch((error) => {
|
|
356
|
-
console.error('Unable to load the development renderer:', error);
|
|
357
|
-
});
|
|
358
|
-
} else {
|
|
359
|
-
void window.loadFile(rendererIndex).catch((error) => {
|
|
360
|
-
console.error('Unable to load the packaged renderer:', error);
|
|
361
|
-
});
|
|
362
|
-
}
|
|
363
|
-
|
|
364
|
-
return window;
|
|
365
|
-
}
|
|
366
|
-
`);
|
|
367
|
-
writeFile(path.join(dir, 'src/main/main.ts'), `import { app, BrowserWindow, session } from 'electron';
|
|
368
|
-
import { createAppWindow } from './app-window.js';
|
|
369
|
-
${updaterImport}${updaterRuntime}
|
|
370
|
-
|
|
371
|
-
app.whenReady().then(() => {
|
|
372
|
-
session.defaultSession.setPermissionCheckHandler(() => false);
|
|
373
|
-
session.defaultSession.setPermissionRequestHandler((_webContents, _permission, callback) => callback(false));
|
|
374
|
-
createAppWindow();
|
|
375
|
-
${updaterStart}});
|
|
376
|
-
app.on('window-all-closed', () => {
|
|
377
|
-
if (process.platform !== 'darwin') app.quit();
|
|
378
|
-
});
|
|
379
|
-
app.on('activate', () => {
|
|
380
|
-
if (BrowserWindow.getAllWindows().length === 0) createAppWindow();
|
|
381
|
-
});
|
|
382
|
-
${updaterStop}`);
|
|
383
|
-
writeFile(path.join(dir, 'src/main/preload.cts'), `import { contextBridge } from 'electron';
|
|
384
|
-
|
|
385
|
-
contextBridge.exposeInMainWorld('anhedral', {
|
|
386
|
-
platform: process.platform,
|
|
387
|
-
});
|
|
388
|
-
`);
|
|
389
|
-
writeFile(path.join(dir, 'index.html'), `<!doctype html>
|
|
390
|
-
<html lang="en">
|
|
391
|
-
<head>
|
|
392
|
-
<meta charset="UTF-8" />
|
|
393
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
394
|
-
<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:" />
|
|
395
|
-
<title>${displayNameHtml}</title>
|
|
396
|
-
</head>
|
|
397
|
-
<body>
|
|
398
|
-
<div id="root"></div>
|
|
399
|
-
<script type="module" src="/src/renderer/main.tsx"></script>
|
|
400
|
-
</body>
|
|
401
|
-
</html>
|
|
402
|
-
`);
|
|
403
|
-
writeFile(path.join(dir, 'src/renderer/lib/utils.ts'), `import { clsx, type ClassValue } from 'clsx';
|
|
404
|
-
import { twMerge } from 'tailwind-merge';
|
|
405
|
-
|
|
406
|
-
export function cn(...inputs: ClassValue[]) {
|
|
407
|
-
return twMerge(clsx(inputs));
|
|
408
|
-
}
|
|
409
|
-
`);
|
|
410
|
-
if (options.apps.api) {
|
|
411
|
-
const authImport = options.features.auth ? "import { getAuthToken } from './auth';\n" : '';
|
|
412
|
-
const getToken = options.features.auth ? 'getAuthToken' : 'async () => null';
|
|
413
|
-
writeFile(path.join(dir, 'src/renderer/lib/api.ts'), `import { ApiClient, normalizeApiBaseUrl } from '@shared/api-client';
|
|
414
|
-
${authImport}
|
|
415
|
-
|
|
416
|
-
function apiBaseUrl(): string {
|
|
417
|
-
const configured = import.meta.env.VITE_API_URL?.trim();
|
|
418
|
-
const candidate = configured || (import.meta.env.DEV ? 'http://localhost:8787/api' : '');
|
|
419
|
-
if (!candidate) throw new Error('VITE_API_URL is required in production builds');
|
|
420
|
-
const normalized = normalizeApiBaseUrl(candidate, 'VITE_API_URL');
|
|
421
|
-
const url = new URL(normalized);
|
|
422
|
-
if (import.meta.env.PROD && url.protocol !== 'https:') {
|
|
423
|
-
throw new Error('VITE_API_URL must use https: in production');
|
|
424
|
-
}
|
|
425
|
-
return normalized;
|
|
426
|
-
}
|
|
427
|
-
|
|
428
|
-
export const api = new ApiClient({
|
|
429
|
-
baseUrl: apiBaseUrl(),
|
|
430
|
-
getToken: ${getToken},
|
|
431
|
-
});
|
|
432
|
-
`);
|
|
433
|
-
}
|
|
434
|
-
if (options.features.auth) {
|
|
435
|
-
writeFile(path.join(dir, 'src/renderer/lib/auth.ts'), `import type { Clerk as ClerkInstance } from '@clerk/clerk-js';
|
|
436
|
-
|
|
437
|
-
const publishableKey = import.meta.env.VITE_CLERK_PUBLISHABLE_KEY || '';
|
|
438
|
-
let initialization: Promise<ClerkInstance | null> | null = null;
|
|
439
|
-
|
|
440
|
-
export function isClerkConfigured(): boolean {
|
|
441
|
-
return publishableKey.length > 0 && !publishableKey.includes('***');
|
|
442
|
-
}
|
|
443
|
-
|
|
444
|
-
export function initializeClerk(): Promise<ClerkInstance | null> {
|
|
445
|
-
if (!isClerkConfigured()) return Promise.resolve(null);
|
|
446
|
-
if (!initialization) {
|
|
447
|
-
initialization = Promise.all([
|
|
448
|
-
import('@clerk/clerk-js'),
|
|
449
|
-
import('@clerk/ui'),
|
|
450
|
-
]).then(async ([{ Clerk }, { ui }]) => {
|
|
451
|
-
const clerk = new Clerk(publishableKey);
|
|
452
|
-
await clerk.load({ ui });
|
|
453
|
-
return clerk;
|
|
454
|
-
}).catch((error) => {
|
|
455
|
-
initialization = null;
|
|
456
|
-
throw error;
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
return initialization;
|
|
460
|
-
}
|
|
461
|
-
|
|
462
|
-
export async function getAuthToken(): Promise<string | null> {
|
|
463
|
-
const clerk = await initializeClerk();
|
|
464
|
-
return clerk?.session?.getToken() ?? null;
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
export async function getAuthUserId(): Promise<string | null> {
|
|
468
|
-
const clerk = await initializeClerk();
|
|
469
|
-
return clerk?.user?.id ?? null;
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
export async function subscribeToAuthState(listener: (userId: string | null) => void): Promise<() => void> {
|
|
473
|
-
const clerk = await initializeClerk();
|
|
474
|
-
if (!clerk) {
|
|
475
|
-
listener(null);
|
|
476
|
-
return () => undefined;
|
|
477
|
-
}
|
|
478
|
-
const synchronize = () => listener(clerk.user?.id ?? null);
|
|
479
|
-
synchronize();
|
|
480
|
-
return clerk.addListener(synchronize);
|
|
481
|
-
}
|
|
482
|
-
|
|
483
|
-
export async function openAccount(): Promise<boolean> {
|
|
484
|
-
const clerk = await initializeClerk();
|
|
485
|
-
if (!clerk) return false;
|
|
486
|
-
|
|
487
|
-
if (clerk.user) clerk.openUserProfile();
|
|
488
|
-
else clerk.openSignIn();
|
|
489
|
-
return true;
|
|
490
|
-
}
|
|
491
|
-
`);
|
|
492
|
-
}
|
|
493
|
-
if (options.features.billing) {
|
|
494
|
-
writeFile(path.join(dir, 'src/renderer/hooks/use-entitlement.ts'), `import { subscribeToSubscriptionChanges } from '@shared/realtime';
|
|
495
|
-
import * as React from 'react';
|
|
496
|
-
import { api } from '../lib/api';
|
|
497
|
-
export function useEntitlement(identity: string | null) {
|
|
498
|
-
const [entitlement, setEntitlement] = React.useState<Awaited<ReturnType<typeof api.getEntitlement>> | null>(null);
|
|
499
|
-
const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
|
|
500
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
501
|
-
const revision = React.useRef(0);
|
|
502
|
-
const identityRef = React.useRef(identity);
|
|
503
|
-
const refresh = React.useCallback(async () => {
|
|
504
|
-
if (!identity) return;
|
|
505
|
-
try {
|
|
506
|
-
const next = await api.getEntitlement();
|
|
507
|
-
if (identityRef.current !== identity) return;
|
|
508
|
-
revision.current = Math.max(revision.current, next.revision);
|
|
509
|
-
setEntitlement(next);
|
|
510
|
-
setLoadedIdentity(identity);
|
|
511
|
-
setError(null);
|
|
512
|
-
} catch (cause) {
|
|
513
|
-
if (identityRef.current !== identity) return;
|
|
514
|
-
setLoadedIdentity(identity);
|
|
515
|
-
setError(cause instanceof Error ? cause.message : 'Unable to load subscription');
|
|
516
|
-
}
|
|
517
|
-
}, [identity]);
|
|
518
|
-
React.useEffect(() => {
|
|
519
|
-
identityRef.current = identity;
|
|
520
|
-
revision.current = 0;
|
|
521
|
-
setEntitlement(null);
|
|
522
|
-
setLoadedIdentity(null);
|
|
523
|
-
setError(null);
|
|
524
|
-
void refresh();
|
|
525
|
-
}, [identity, refresh]);
|
|
526
|
-
React.useEffect(() => {
|
|
527
|
-
if (!identity) return;
|
|
528
|
-
const onVisibility = () => { if (document.visibilityState === 'visible') void refresh(); };
|
|
529
|
-
document.addEventListener('visibilitychange', onVisibility);
|
|
530
|
-
return () => document.removeEventListener('visibilitychange', onVisibility);
|
|
531
|
-
}, [identity, refresh]);
|
|
532
|
-
React.useEffect(() => {
|
|
533
|
-
if (!identity) return;
|
|
534
|
-
return subscribeToSubscriptionChanges({
|
|
535
|
-
userId: identity,
|
|
536
|
-
getTokenRequest: () => api.getRealtimeToken(),
|
|
537
|
-
onChange: (nextRevision) => { if (nextRevision > revision.current) void refresh(); },
|
|
538
|
-
onError: (cause) => {
|
|
539
|
-
if (identityRef.current === identity) {
|
|
540
|
-
setLoadedIdentity(identity);
|
|
541
|
-
setError(cause.message);
|
|
542
|
-
}
|
|
543
|
-
},
|
|
544
|
-
});
|
|
545
|
-
}, [identity, refresh]);
|
|
546
|
-
return {
|
|
547
|
-
entitlement: loadedIdentity === identity ? entitlement : null,
|
|
548
|
-
error: loadedIdentity === identity ? error : null,
|
|
549
|
-
};
|
|
550
|
-
}
|
|
551
|
-
`);
|
|
552
|
-
}
|
|
553
|
-
writeFile(path.join(dir, 'src/renderer/components/ui/button.tsx'), `import * as React from 'react';
|
|
554
|
-
import { cn } from '@/lib/utils';
|
|
555
|
-
|
|
556
|
-
export function Button({ className, type = 'button', ...props }: React.ButtonHTMLAttributes<HTMLButtonElement>) {
|
|
557
|
-
return (
|
|
558
|
-
<button
|
|
559
|
-
type={type}
|
|
560
|
-
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)}
|
|
561
|
-
{...props}
|
|
562
|
-
/>
|
|
563
|
-
);
|
|
564
|
-
}
|
|
565
|
-
`);
|
|
566
|
-
if (options.apps.api && options.features.database) {
|
|
567
|
-
writeFile(path.join(dir, 'src/renderer/components/item-list.tsx'), `import { createItem, listItems, type Item } from '@shared/api-client';
|
|
568
|
-
import * as React from 'react';
|
|
569
|
-
import { api } from '@/lib/api';
|
|
570
|
-
import { Button } from '@/components/ui/button';
|
|
571
|
-
|
|
572
|
-
export function ItemList({ identity = 'public' }: { identity?: string | null }) {
|
|
573
|
-
const [items, setItems] = React.useState<Item[]>([]);
|
|
574
|
-
const [loadedIdentity, setLoadedIdentity] = React.useState<string | null>(null);
|
|
575
|
-
const [name, setName] = React.useState('');
|
|
576
|
-
const [status, setStatus] = React.useState<'idle' | 'loading' | 'ready' | 'saving'>('idle');
|
|
577
|
-
const [error, setError] = React.useState<string | null>(null);
|
|
578
|
-
const identityRef = React.useRef(identity);
|
|
579
|
-
const visibleItems = loadedIdentity === identity ? items : [];
|
|
580
|
-
const visibleError = loadedIdentity === identity ? error : null;
|
|
581
|
-
const isLoading = status === 'loading' || loadedIdentity !== identity;
|
|
582
|
-
|
|
583
|
-
React.useEffect(() => {
|
|
584
|
-
identityRef.current = identity;
|
|
585
|
-
if (!identity) return;
|
|
586
|
-
let active = true;
|
|
587
|
-
setLoadedIdentity(identity);
|
|
588
|
-
setItems([]);
|
|
589
|
-
setName('');
|
|
590
|
-
setError(null);
|
|
591
|
-
setStatus('loading');
|
|
592
|
-
void listItems(api).then((nextItems) => {
|
|
593
|
-
if (active) setItems(nextItems);
|
|
594
|
-
}).catch((cause: unknown) => {
|
|
595
|
-
if (active) setError(cause instanceof Error ? cause.message : 'Unable to load items');
|
|
596
|
-
}).finally(() => {
|
|
597
|
-
if (active) setStatus('ready');
|
|
598
|
-
});
|
|
599
|
-
return () => { active = false; };
|
|
600
|
-
}, [identity]);
|
|
601
|
-
|
|
602
|
-
async function submit(event: React.FormEvent<HTMLFormElement>) {
|
|
603
|
-
event.preventDefault();
|
|
604
|
-
const nextName = name.trim();
|
|
605
|
-
if (!identity || !nextName || status !== 'ready' || loadedIdentity !== identity) return;
|
|
606
|
-
const submittedIdentity = identity;
|
|
607
|
-
setStatus('saving');
|
|
608
|
-
try {
|
|
609
|
-
const created = await createItem(api, { name: nextName });
|
|
610
|
-
if (identityRef.current !== submittedIdentity) return;
|
|
611
|
-
setItems((current) => [created, ...current]);
|
|
612
|
-
setName('');
|
|
613
|
-
setError(null);
|
|
614
|
-
} catch (cause) {
|
|
615
|
-
if (identityRef.current === submittedIdentity) {
|
|
616
|
-
setError(cause instanceof Error ? cause.message : 'Unable to create item');
|
|
617
|
-
}
|
|
618
|
-
} finally {
|
|
619
|
-
if (identityRef.current === submittedIdentity) setStatus('ready');
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
|
|
623
|
-
if (!identity) {
|
|
624
|
-
return <p className="text-muted-foreground">Open your account and sign in to use the working starter feature.</p>;
|
|
625
|
-
}
|
|
626
|
-
|
|
627
|
-
return (
|
|
628
|
-
<section aria-labelledby="starter-feature-title" className="flex max-w-2xl flex-col gap-3 rounded-lg border border-slate-200 bg-white p-5">
|
|
629
|
-
<h2 className="text-xl font-semibold" id="starter-feature-title">Working starter feature</h2>
|
|
630
|
-
<form className="flex gap-2" onSubmit={(event) => void submit(event)}>
|
|
631
|
-
<label className="sr-only" htmlFor="item-name">Item name</label>
|
|
632
|
-
<input
|
|
633
|
-
className="h-9 flex-1 rounded-md border border-slate-300 bg-background px-3 text-sm"
|
|
634
|
-
id="item-name"
|
|
635
|
-
maxLength={120}
|
|
636
|
-
onChange={(event) => setName(event.target.value)}
|
|
637
|
-
placeholder="Your first item"
|
|
638
|
-
value={name}
|
|
639
|
-
/>
|
|
640
|
-
<Button disabled={status !== 'ready' || loadedIdentity !== identity || !name.trim()} type="submit">
|
|
641
|
-
{status === 'saving' ? 'Adding…' : 'Add item'}
|
|
642
|
-
</Button>
|
|
643
|
-
</form>
|
|
644
|
-
{visibleError ? (
|
|
645
|
-
<p className="text-sm text-red-700" role="alert">
|
|
646
|
-
{visibleError}. Check DATABASE_URL, run pnpm db:migrate, and make sure the API is running.
|
|
647
|
-
</p>
|
|
648
|
-
) : null}
|
|
649
|
-
{isLoading ? <p className="text-sm text-muted-foreground">Loading items…</p> : null}
|
|
650
|
-
{!isLoading && visibleItems.length === 0 && !visibleError
|
|
651
|
-
? <p className="text-sm text-muted-foreground">Your database is connected. Add the first item.</p>
|
|
652
|
-
: null}
|
|
653
|
-
{visibleItems.length > 0 ? (
|
|
654
|
-
<ul className="divide-y rounded-md border border-slate-200">
|
|
655
|
-
{visibleItems.map((item) => <li className="px-3 py-2 text-sm" key={item.id}>{item.name}</li>)}
|
|
656
|
-
</ul>
|
|
657
|
-
) : null}
|
|
658
|
-
</section>
|
|
659
|
-
);
|
|
660
|
-
}
|
|
661
|
-
`);
|
|
662
|
-
}
|
|
663
|
-
const authImport = options.features.auth
|
|
664
|
-
? "import { getAuthUserId, isClerkConfigured, openAccount, subscribeToAuthState } from '@/lib/auth';\n"
|
|
665
|
-
: '';
|
|
666
|
-
const entitlementImport = options.features.billing ? "import { useEntitlement } from '@/hooks/use-entitlement';\n" : '';
|
|
667
|
-
const itemListImport = options.apps.api && options.features.database ? "import { ItemList } from '@/components/item-list';\n" : '';
|
|
668
|
-
const entitlementState = options.features.billing ? " const { entitlement, error: entitlementError } = useEntitlement(clerkState === 'ready' ? clerkUserId : null);\n" : '';
|
|
669
|
-
const entitlementStatus = options.features.billing ? ` {entitlement ? <p className="text-muted-foreground">Plan: {entitlement.entitlement} ({entitlement.status})</p> : null}
|
|
670
|
-
{entitlementError ? <p role="alert" className="text-red-700">{entitlementError}</p> : null}
|
|
671
|
-
` : '';
|
|
672
|
-
const authState = options.features.auth ? ` const [clerkState, setClerkState] = React.useState<'unconfigured' | 'signed-out' | 'loading' | 'ready' | 'error'>(
|
|
673
|
-
() => isClerkConfigured() ? 'loading' : 'unconfigured',
|
|
674
|
-
);
|
|
675
|
-
const [clerkUserId, setClerkUserId] = React.useState<string | null>(null);
|
|
676
|
-
|
|
677
|
-
React.useEffect(() => {
|
|
678
|
-
if (!isClerkConfigured()) return;
|
|
679
|
-
let disposed = false;
|
|
680
|
-
let unsubscribe: (() => void) | undefined;
|
|
681
|
-
void subscribeToAuthState((userId) => {
|
|
682
|
-
if (!disposed) {
|
|
683
|
-
setClerkUserId(userId);
|
|
684
|
-
setClerkState(userId ? 'ready' : 'signed-out');
|
|
685
|
-
}
|
|
686
|
-
}).then((stop) => {
|
|
687
|
-
if (disposed) stop();
|
|
688
|
-
else unsubscribe = stop;
|
|
689
|
-
}).catch(() => {
|
|
690
|
-
if (!disposed) setClerkState('error');
|
|
691
|
-
});
|
|
692
|
-
return () => {
|
|
693
|
-
disposed = true;
|
|
694
|
-
unsubscribe?.();
|
|
695
|
-
};
|
|
696
|
-
}, []);
|
|
697
|
-
|
|
698
|
-
const handleAccount = async () => {
|
|
699
|
-
setClerkState('loading');
|
|
700
|
-
try {
|
|
701
|
-
const opened = await openAccount();
|
|
702
|
-
const userId = opened ? await getAuthUserId() : null;
|
|
703
|
-
setClerkUserId(userId);
|
|
704
|
-
setClerkState(opened ? (userId ? 'ready' : 'signed-out') : 'unconfigured');
|
|
705
|
-
} catch {
|
|
706
|
-
setClerkUserId(null);
|
|
707
|
-
setClerkState('error');
|
|
708
|
-
}
|
|
709
|
-
};
|
|
710
|
-
|
|
711
|
-
` : '';
|
|
712
|
-
const authStatus = options.features.auth ? ` <p
|
|
713
|
-
role={clerkState === 'error' || clerkState === 'unconfigured' ? 'alert' : 'status'}
|
|
714
|
-
aria-live={clerkState === 'error' || clerkState === 'unconfigured' ? 'assertive' : 'polite'}
|
|
715
|
-
className={clerkState === 'error' || clerkState === 'unconfigured' ? 'text-red-700' : 'text-muted-foreground'}
|
|
716
|
-
>
|
|
717
|
-
{clerkState === 'unconfigured' ? 'Set VITE_CLERK_PUBLISHABLE_KEY to enable accounts.' : null}
|
|
718
|
-
{clerkState === 'signed-out' ? 'Sign in to use account-backed features.' : null}
|
|
719
|
-
{clerkState === 'loading' ? 'Loading account services…' : null}
|
|
720
|
-
{clerkState === 'ready' ? 'Signed in. Account-backed features are ready.' : null}
|
|
721
|
-
{clerkState === 'error' ? 'Clerk could not initialize. Check the publishable key and network connection.' : null}
|
|
722
|
-
</p>
|
|
723
|
-
` : '';
|
|
724
|
-
const buttonAction = options.features.auth
|
|
725
|
-
? " disabled={clerkState === 'loading' || clerkState === 'unconfigured'} onClick={() => void handleAccount()}"
|
|
726
|
-
: '';
|
|
727
|
-
const accountButton = options.features.auth
|
|
728
|
-
? ` <Button${buttonAction}>Open account</Button>\n`
|
|
729
|
-
: '';
|
|
730
|
-
const buttonImport = options.features.auth ? "import { Button } from '@/components/ui/button';\n" : '';
|
|
731
|
-
const itemList = options.apps.api && options.features.database
|
|
732
|
-
? options.features.auth
|
|
733
|
-
? " <ItemList identity={clerkState === 'ready' ? clerkUserId : null} />\n"
|
|
734
|
-
: " <ItemList />\n"
|
|
735
|
-
: '';
|
|
736
|
-
writeFile(path.join(dir, 'src/renderer/main.tsx'), `import React from 'react';
|
|
737
|
-
import ReactDOM from 'react-dom/client';
|
|
738
|
-
${buttonImport}${authImport}${entitlementImport}${itemListImport}import './styles.css';
|
|
739
|
-
|
|
740
|
-
function App() {
|
|
741
|
-
${authState}${entitlementState} return (
|
|
742
|
-
<main className="flex min-h-screen flex-col gap-6 bg-background p-8 text-foreground">
|
|
743
|
-
<section className="flex flex-col gap-3">
|
|
744
|
-
<h1 className="text-3xl font-semibold">{${displayNameLiteral}}</h1>
|
|
745
|
-
<p className="max-w-2xl text-muted-foreground">
|
|
746
|
-
${description}
|
|
747
|
-
</p>
|
|
748
|
-
</section>
|
|
749
|
-
${authStatus}
|
|
750
|
-
${entitlementStatus}
|
|
751
|
-
${accountButton}
|
|
752
|
-
${itemList}
|
|
753
|
-
</main>
|
|
754
|
-
);
|
|
755
|
-
}
|
|
756
|
-
|
|
757
|
-
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
758
|
-
<React.StrictMode>
|
|
759
|
-
<App />
|
|
760
|
-
</React.StrictMode>,
|
|
761
|
-
);
|
|
762
|
-
`);
|
|
763
|
-
writeFile(path.join(dir, 'src/renderer/styles.css'), `@import "tailwindcss";
|
|
764
|
-
|
|
765
|
-
:root {
|
|
766
|
-
--background: oklch(1 0 0);
|
|
767
|
-
--foreground: oklch(0.145 0 0);
|
|
768
|
-
--primary: oklch(0.205 0 0);
|
|
769
|
-
--primary-foreground: oklch(0.985 0 0);
|
|
770
|
-
--muted-foreground: oklch(0.556 0 0);
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
@theme inline {
|
|
774
|
-
--color-background: var(--background);
|
|
775
|
-
--color-foreground: var(--foreground);
|
|
776
|
-
--color-primary: var(--primary);
|
|
777
|
-
--color-primary-foreground: var(--primary-foreground);
|
|
778
|
-
--color-muted-foreground: var(--muted-foreground);
|
|
779
|
-
}
|
|
780
|
-
|
|
781
|
-
body {
|
|
782
|
-
margin: 0;
|
|
783
|
-
background: var(--background);
|
|
784
|
-
color: var(--foreground);
|
|
785
|
-
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
786
|
-
}
|
|
787
|
-
|
|
788
|
-
button {
|
|
789
|
-
border: 0;
|
|
790
|
-
}
|
|
791
|
-
`);
|
|
792
|
-
}
|