@prabhask5/stellar-engine 1.1.6 → 1.1.7

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 (145) hide show
  1. package/README.md +65 -25
  2. package/dist/actions/truncateTooltip.d.ts +42 -0
  3. package/dist/actions/truncateTooltip.d.ts.map +1 -0
  4. package/dist/actions/truncateTooltip.js +257 -0
  5. package/dist/actions/truncateTooltip.js.map +1 -0
  6. package/dist/auth/singleUser.d.ts.map +1 -1
  7. package/dist/auth/singleUser.js +6 -35
  8. package/dist/auth/singleUser.js.map +1 -1
  9. package/dist/bin/install-pwa.d.ts +9 -0
  10. package/dist/bin/install-pwa.d.ts.map +1 -0
  11. package/dist/bin/install-pwa.js +1421 -0
  12. package/dist/bin/install-pwa.js.map +1 -0
  13. package/dist/engine.d.ts.map +1 -1
  14. package/dist/engine.js +75 -1
  15. package/dist/engine.js.map +1 -1
  16. package/dist/entries/actions.d.ts +1 -0
  17. package/dist/entries/actions.d.ts.map +1 -1
  18. package/dist/entries/actions.js +1 -0
  19. package/dist/entries/actions.js.map +1 -1
  20. package/dist/entries/kit.d.ts +10 -9
  21. package/dist/entries/kit.d.ts.map +1 -1
  22. package/dist/entries/kit.js +7 -8
  23. package/dist/entries/kit.js.map +1 -1
  24. package/dist/entries/types.d.ts +1 -1
  25. package/dist/entries/types.d.ts.map +1 -1
  26. package/dist/entries/utils.d.ts +1 -1
  27. package/dist/entries/utils.d.ts.map +1 -1
  28. package/dist/entries/utils.js +1 -1
  29. package/dist/entries/utils.js.map +1 -1
  30. package/dist/entries/vite.d.ts +3 -0
  31. package/dist/entries/vite.d.ts.map +1 -0
  32. package/dist/entries/vite.js +3 -0
  33. package/dist/entries/vite.js.map +1 -0
  34. package/dist/index.d.ts +2 -2
  35. package/dist/index.d.ts.map +1 -1
  36. package/dist/index.js +1 -1
  37. package/dist/index.js.map +1 -1
  38. package/dist/kit/auth.d.ts +25 -0
  39. package/dist/kit/auth.d.ts.map +1 -0
  40. package/dist/kit/auth.js +31 -0
  41. package/dist/kit/auth.js.map +1 -0
  42. package/dist/kit/confirm.d.ts +30 -0
  43. package/dist/kit/confirm.d.ts.map +1 -0
  44. package/dist/kit/confirm.js +82 -0
  45. package/dist/kit/confirm.js.map +1 -0
  46. package/dist/kit/loads.d.ts +62 -0
  47. package/dist/kit/loads.d.ts.map +1 -0
  48. package/dist/kit/loads.js +89 -0
  49. package/dist/kit/loads.js.map +1 -0
  50. package/dist/kit/server.d.ts +43 -0
  51. package/dist/kit/server.d.ts.map +1 -0
  52. package/dist/kit/server.js +154 -0
  53. package/dist/kit/server.js.map +1 -0
  54. package/dist/kit/sw.d.ts +47 -0
  55. package/dist/kit/sw.d.ts.map +1 -0
  56. package/dist/kit/sw.js +164 -0
  57. package/dist/kit/sw.js.map +1 -0
  58. package/dist/runtime/runtimeConfig.d.ts +0 -8
  59. package/dist/runtime/runtimeConfig.d.ts.map +1 -1
  60. package/dist/runtime/runtimeConfig.js.map +1 -1
  61. package/dist/supabase/auth.d.ts +1 -1
  62. package/dist/supabase/auth.d.ts.map +1 -1
  63. package/dist/supabase/auth.js.map +1 -1
  64. package/dist/sw/build/vite-plugin.d.ts +29 -0
  65. package/dist/sw/build/vite-plugin.d.ts.map +1 -0
  66. package/dist/sw/build/vite-plugin.js +124 -0
  67. package/dist/sw/build/vite-plugin.js.map +1 -0
  68. package/dist/sw/sw.js +614 -0
  69. package/dist/types.d.ts +0 -19
  70. package/dist/types.d.ts.map +1 -1
  71. package/package.json +20 -22
  72. package/src/components/DeferredChangesBanner.svelte +477 -0
  73. package/src/components/SyncStatus.svelte +1732 -0
  74. package/dist/crdt/awareness.d.ts +0 -54
  75. package/dist/crdt/awareness.d.ts.map +0 -1
  76. package/dist/crdt/awareness.js +0 -219
  77. package/dist/crdt/awareness.js.map +0 -1
  78. package/dist/crdt/doc.d.ts +0 -56
  79. package/dist/crdt/doc.d.ts.map +0 -1
  80. package/dist/crdt/doc.js +0 -130
  81. package/dist/crdt/doc.js.map +0 -1
  82. package/dist/crdt/index.d.ts +0 -15
  83. package/dist/crdt/index.d.ts.map +0 -1
  84. package/dist/crdt/index.js +0 -20
  85. package/dist/crdt/index.js.map +0 -1
  86. package/dist/crdt/offline.d.ts +0 -91
  87. package/dist/crdt/offline.d.ts.map +0 -1
  88. package/dist/crdt/offline.js +0 -353
  89. package/dist/crdt/offline.js.map +0 -1
  90. package/dist/crdt/sync.d.ts +0 -58
  91. package/dist/crdt/sync.d.ts.map +0 -1
  92. package/dist/crdt/sync.js +0 -399
  93. package/dist/crdt/sync.js.map +0 -1
  94. package/dist/crdt/types.d.ts +0 -62
  95. package/dist/crdt/types.d.ts.map +0 -1
  96. package/dist/crdt/types.js +0 -7
  97. package/dist/crdt/types.js.map +0 -1
  98. package/dist/email/sendEmail.d.ts +0 -31
  99. package/dist/email/sendEmail.d.ts.map +0 -1
  100. package/dist/email/sendEmail.js +0 -39
  101. package/dist/email/sendEmail.js.map +0 -1
  102. package/dist/email/validateSmtp.d.ts +0 -18
  103. package/dist/email/validateSmtp.d.ts.map +0 -1
  104. package/dist/email/validateSmtp.js +0 -33
  105. package/dist/email/validateSmtp.js.map +0 -1
  106. package/dist/entries/crdt.d.ts +0 -3
  107. package/dist/entries/crdt.d.ts.map +0 -1
  108. package/dist/entries/crdt.js +0 -13
  109. package/dist/entries/crdt.js.map +0 -1
  110. package/dist/entries/email.d.ts +0 -4
  111. package/dist/entries/email.d.ts.map +0 -1
  112. package/dist/entries/email.js +0 -4
  113. package/dist/entries/email.js.map +0 -1
  114. package/dist/kit/authPresets.d.ts +0 -28
  115. package/dist/kit/authPresets.d.ts.map +0 -1
  116. package/dist/kit/authPresets.js +0 -23
  117. package/dist/kit/authPresets.js.map +0 -1
  118. package/dist/kit/configEndpoint.d.ts +0 -18
  119. package/dist/kit/configEndpoint.d.ts.map +0 -1
  120. package/dist/kit/configEndpoint.js +0 -27
  121. package/dist/kit/configEndpoint.js.map +0 -1
  122. package/dist/kit/deployEndpoint.d.ts +0 -22
  123. package/dist/kit/deployEndpoint.d.ts.map +0 -1
  124. package/dist/kit/deployEndpoint.js +0 -79
  125. package/dist/kit/deployEndpoint.js.map +0 -1
  126. package/dist/kit/layoutLoad.d.ts +0 -23
  127. package/dist/kit/layoutLoad.d.ts.map +0 -1
  128. package/dist/kit/layoutLoad.js +0 -41
  129. package/dist/kit/layoutLoad.js.map +0 -1
  130. package/dist/kit/protectedLoad.d.ts +0 -16
  131. package/dist/kit/protectedLoad.d.ts.map +0 -1
  132. package/dist/kit/protectedLoad.js +0 -28
  133. package/dist/kit/protectedLoad.js.map +0 -1
  134. package/dist/kit/setupLoad.d.ts +0 -11
  135. package/dist/kit/setupLoad.d.ts.map +0 -1
  136. package/dist/kit/setupLoad.js +0 -28
  137. package/dist/kit/setupLoad.js.map +0 -1
  138. package/dist/kit/validateEndpoint.d.ts +0 -9
  139. package/dist/kit/validateEndpoint.d.ts.map +0 -1
  140. package/dist/kit/validateEndpoint.js +0 -25
  141. package/dist/kit/validateEndpoint.js.map +0 -1
  142. package/dist/kit/vercelApi.d.ts +0 -6
  143. package/dist/kit/vercelApi.d.ts.map +0 -1
  144. package/dist/kit/vercelApi.js +0 -48
  145. package/dist/kit/vercelApi.js.map +0 -1
@@ -0,0 +1,1421 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * @fileoverview CLI script that scaffolds a PWA SvelteKit project using stellar-engine.
4
+ *
5
+ * Usage:
6
+ * stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]
7
+ */
8
+ import { writeFileSync, existsSync, mkdirSync } from 'fs';
9
+ import { join } from 'path';
10
+ import { execSync } from 'child_process';
11
+ // =============================================================================
12
+ // HELPERS
13
+ // =============================================================================
14
+ /**
15
+ * Writes a file only if it doesn't already exist. Returns whether the file was created.
16
+ */
17
+ function writeIfMissing(filePath, content, createdFiles, skippedFiles) {
18
+ const relPath = filePath.replace(process.cwd() + '/', '');
19
+ if (existsSync(filePath)) {
20
+ skippedFiles.push(relPath);
21
+ console.log(` [skip] ${relPath} already exists`);
22
+ }
23
+ else {
24
+ const dir = filePath.substring(0, filePath.lastIndexOf('/'));
25
+ if (!existsSync(dir)) {
26
+ mkdirSync(dir, { recursive: true });
27
+ }
28
+ writeFileSync(filePath, content, 'utf-8');
29
+ createdFiles.push(relPath);
30
+ console.log(` [write] ${relPath}`);
31
+ }
32
+ }
33
+ // =============================================================================
34
+ // ARG PARSING
35
+ // =============================================================================
36
+ function parseArgs(argv) {
37
+ const args = argv.slice(2);
38
+ if (args[0] !== 'install' || args[1] !== 'pwa') {
39
+ console.error('Usage: stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]');
40
+ process.exit(1);
41
+ }
42
+ let name = '';
43
+ let shortName = '';
44
+ let prefix = '';
45
+ let description = 'A self-hosted offline-first PWA';
46
+ for (let i = 2; i < args.length; i++) {
47
+ switch (args[i]) {
48
+ case '--name':
49
+ name = args[++i];
50
+ break;
51
+ case '--short_name':
52
+ shortName = args[++i];
53
+ break;
54
+ case '--prefix':
55
+ prefix = args[++i];
56
+ break;
57
+ case '--description':
58
+ description = args[++i];
59
+ break;
60
+ }
61
+ }
62
+ if (!name || !shortName || !prefix) {
63
+ console.error('Error: --name, --short_name, and --prefix are required.\n' +
64
+ 'Usage: stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]');
65
+ process.exit(1);
66
+ }
67
+ const kebabName = name.toLowerCase().replace(/\s+/g, '-');
68
+ return { name, shortName, prefix, description, kebabName };
69
+ }
70
+ // =============================================================================
71
+ // TEMPLATE GENERATORS
72
+ // =============================================================================
73
+ function generatePackageJson(opts) {
74
+ return (JSON.stringify({
75
+ name: opts.kebabName,
76
+ version: '1.0.0',
77
+ private: true,
78
+ scripts: {
79
+ dev: 'vite dev',
80
+ build: 'vite build',
81
+ preview: 'vite preview',
82
+ check: 'svelte-check --tsconfig ./tsconfig.json',
83
+ 'check:watch': 'svelte-check --tsconfig ./tsconfig.json --watch',
84
+ lint: 'eslint src',
85
+ 'lint:fix': 'eslint src --fix',
86
+ format: 'prettier --write "src/**/*.{js,ts,svelte,css,html}"',
87
+ 'format:check': 'prettier --check "src/**/*.{js,ts,svelte,css,html}"',
88
+ 'dead-code': 'knip',
89
+ 'dead-code:fix': 'knip --fix',
90
+ cleanup: 'npm run lint:fix && npm run format',
91
+ validate: 'npm run check && npm run lint && npm run dead-code',
92
+ prepare: 'husky'
93
+ },
94
+ devDependencies: {
95
+ '@eslint/js': '^9.39.2',
96
+ '@sveltejs/adapter-auto': '^4.0.0',
97
+ '@sveltejs/kit': '^2.21.0',
98
+ '@sveltejs/vite-plugin-svelte': '^5.0.0',
99
+ eslint: '^9.39.2',
100
+ 'eslint-plugin-svelte': '^3.14.0',
101
+ globals: '^17.2.0',
102
+ husky: '^9.1.7',
103
+ knip: '^5.82.1',
104
+ prettier: '^3.8.1',
105
+ 'prettier-plugin-svelte': '^3.4.1',
106
+ svelte: '^5.0.0',
107
+ 'svelte-check': '^4.3.5',
108
+ typescript: '^5.0.0',
109
+ 'typescript-eslint': '^8.54.0',
110
+ vite: '^6.0.0'
111
+ },
112
+ dependencies: {
113
+ '@prabhask5/stellar-engine': '^1.1.6'
114
+ },
115
+ type: 'module'
116
+ }, null, 2) + '\n');
117
+ }
118
+ function generateViteConfig(opts) {
119
+ return `/**
120
+ * @fileoverview Vite build configuration for the ${opts.shortName} PWA.
121
+ *
122
+ * This config handles three key concerns:
123
+ * 1. SvelteKit integration — via the official \`sveltekit()\` plugin
124
+ * 2. Service worker + asset manifest — via the \`stellarPWA()\` plugin from
125
+ * stellar-engine, which generates \`static/sw.js\` and \`asset-manifest.json\`
126
+ * at build time
127
+ * 3. Chunk-splitting — isolates heavy vendor libs (\`@supabase\`, \`dexie\`)
128
+ * into their own bundles for long-term caching
129
+ */
130
+
131
+ import { sveltekit } from '@sveltejs/kit/vite';
132
+ import { stellarPWA } from '@prabhask5/stellar-engine/vite';
133
+ import { defineConfig } from 'vite';
134
+
135
+ export default defineConfig({
136
+ plugins: [
137
+ sveltekit(),
138
+ stellarPWA({ prefix: '${opts.prefix}', name: '${opts.name}' })
139
+ ],
140
+ build: {
141
+ rollupOptions: {
142
+ output: {
143
+ manualChunks: (id) => {
144
+ if (id.includes('node_modules')) {
145
+ if (id.includes('@supabase')) return 'vendor-supabase';
146
+ if (id.includes('dexie')) return 'vendor-dexie';
147
+ }
148
+ }
149
+ }
150
+ },
151
+ chunkSizeWarningLimit: 500,
152
+ minify: 'esbuild',
153
+ target: 'es2020'
154
+ }
155
+ });
156
+ `;
157
+ }
158
+ function generateTsconfig() {
159
+ return (JSON.stringify({
160
+ extends: './.svelte-kit/tsconfig.json',
161
+ compilerOptions: {
162
+ allowJs: true,
163
+ checkJs: true,
164
+ esModuleInterop: true,
165
+ forceConsistentCasingInFileNames: true,
166
+ resolveJsonModule: true,
167
+ skipLibCheck: true,
168
+ sourceMap: true,
169
+ strict: true,
170
+ moduleResolution: 'bundler'
171
+ }
172
+ }, null, 2) + '\n');
173
+ }
174
+ function generateSvelteConfig(opts) {
175
+ return `/**
176
+ * @fileoverview SvelteKit project configuration for ${opts.shortName}.
177
+ *
178
+ * Keeps things minimal:
179
+ * - **Adapter** — \`adapter-auto\` automatically selects the right deployment
180
+ * adapter (Vercel, Netlify, Cloudflare, Node, etc.) based on the detected
181
+ * environment, so the config stays portable across hosting providers.
182
+ * - **Preprocessor** — \`vitePreprocess\` handles \`<style lang="...">\` and
183
+ * \`<script lang="ts">\` blocks using the Vite pipeline, keeping tooling
184
+ * consistent between Svelte components and the rest of the build.
185
+ */
186
+
187
+ // =============================================================================
188
+ // IMPORTS
189
+ // =============================================================================
190
+
191
+ import adapter from '@sveltejs/adapter-auto';
192
+ import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
193
+
194
+ // =============================================================================
195
+ // SVELTEKIT CONFIGURATION
196
+ // =============================================================================
197
+
198
+ /** @type {import('@sveltejs/kit').Config} */
199
+ const config = {
200
+ /** Use Vite's built-in transform pipeline for TypeScript, PostCSS, etc. */
201
+ preprocess: vitePreprocess(),
202
+
203
+ kit: {
204
+ /**
205
+ * \`adapter-auto\` inspects the deploy target at build time and picks
206
+ * the appropriate adapter automatically — no manual switching needed
207
+ * when moving between local dev, Vercel, or other platforms.
208
+ */
209
+ adapter: adapter()
210
+ }
211
+ };
212
+
213
+ export default config;
214
+ `;
215
+ }
216
+ function generateManifest(opts) {
217
+ return (JSON.stringify({
218
+ name: opts.name,
219
+ short_name: opts.shortName,
220
+ description: opts.description,
221
+ start_url: '/?pwa=true',
222
+ scope: '/',
223
+ id: '/',
224
+ display: 'standalone',
225
+ background_color: '#0f0f1a',
226
+ theme_color: '#1a1a2e',
227
+ orientation: 'portrait-primary',
228
+ icons: [
229
+ {
230
+ src: '/icon-192.png',
231
+ sizes: '192x192',
232
+ type: 'image/png',
233
+ purpose: 'any maskable'
234
+ },
235
+ {
236
+ src: '/icon-512.png',
237
+ sizes: '512x512',
238
+ type: 'image/png',
239
+ purpose: 'any maskable'
240
+ }
241
+ ],
242
+ categories: ['productivity', 'utilities'],
243
+ prefer_related_applications: false
244
+ }, null, 2) + '\n');
245
+ }
246
+ function generateAppDts(opts) {
247
+ return `/**
248
+ * @fileoverview Ambient type declarations for the ${opts.shortName} SvelteKit application.
249
+ *
250
+ * This file extends the global \`App\` namespace used by SvelteKit to provide
251
+ * type safety for framework-level hooks (\`locals\`, \`pageData\`, \`error\`, etc.).
252
+ *
253
+ * Currently, no custom interfaces are needed — the defaults provided by
254
+ * \`@sveltejs/kit\` are sufficient. Uncomment and populate the stubs below
255
+ * when server-side locals or shared page data types are introduced.
256
+ *
257
+ * @see https://kit.svelte.dev/docs/types#app — SvelteKit \`App\` namespace docs
258
+ */
259
+
260
+ // =============================================================================
261
+ // SVELTEKIT TYPE REFERENCES
262
+ // =============================================================================
263
+
264
+ /// <reference types="@sveltejs/kit" />
265
+
266
+ // =============================================================================
267
+ // GLOBAL APP TYPE DECLARATIONS
268
+ // =============================================================================
269
+
270
+ declare global {
271
+ namespace App {
272
+ /**
273
+ * Extend \`App.Locals\` to type data attached to \`event.locals\` inside
274
+ * SvelteKit hooks (e.g., authenticated user objects, request metadata).
275
+ */
276
+ // interface Locals {}
277
+ /**
278
+ * Extend \`App.PageData\` to type shared data returned from all
279
+ * \`+layout.server.ts\` / \`+page.server.ts\` load functions.
280
+ */
281
+ // interface PageData {}
282
+ }
283
+ }
284
+
285
+ /**
286
+ * Ensures this file is treated as an **ES module** (required for ambient
287
+ * \`declare global\` blocks to work correctly in TypeScript).
288
+ */
289
+ export {};
290
+ `;
291
+ }
292
+ function generateAppHtml(opts) {
293
+ return `<!doctype html>
294
+ <html lang="en">
295
+ <head>
296
+ <!-- ================================================================= -->
297
+ <!-- CORE DOCUMENT META -->
298
+ <!-- ================================================================= -->
299
+ <meta charset="utf-8" />
300
+ <title>${opts.name}</title>
301
+ <link rel="icon" href="%sveltekit.assets%/favicon.png" />
302
+
303
+ <!--
304
+ Viewport configuration:
305
+ - \`initial-scale=1\` → no zoom on load
306
+ - \`viewport-fit=cover\` → extend into safe-area insets (notch, home bar)
307
+ - \`width=device-width\` → responsive width
308
+ - \`interactive-widget=overlays-content\` → keyboard overlays rather than
309
+ resizing the viewport (prevents layout shift on mobile)
310
+ -->
311
+ <meta
312
+ name="viewport"
313
+ content="initial-scale=1, viewport-fit=cover, width=device-width, interactive-widget=overlays-content"
314
+ />
315
+
316
+ <!-- ================================================================= -->
317
+ <!-- SEO META TAGS -->
318
+ <!-- ================================================================= -->
319
+
320
+ <!-- Theme color matches \`--color-void\` for seamless safe-area blending -->
321
+ <meta name="theme-color" content="#050510" />
322
+ <meta
323
+ name="description"
324
+ content="${opts.name} - ${opts.description}"
325
+ />
326
+ <meta
327
+ name="keywords"
328
+ content="pwa, offline-first, productivity, utilities, svelte, sveltekit"
329
+ />
330
+ <meta name="author" content="${opts.shortName}" />
331
+ <meta name="robots" content="index, follow" />
332
+
333
+ <!-- ================================================================= -->
334
+ <!-- OPEN GRAPH / SOCIAL MEDIA -->
335
+ <!-- ================================================================= -->
336
+
337
+ <!-- Used by Facebook, LinkedIn, Discord, Slack, etc. for link previews -->
338
+ <meta property="og:type" content="website" />
339
+ <meta property="og:title" content="${opts.name}" />
340
+ <meta
341
+ property="og:description"
342
+ content="${opts.description}"
343
+ />
344
+ <meta property="og:image" content="%sveltekit.assets%/icon-512.png" />
345
+
346
+ <!-- ================================================================= -->
347
+ <!-- TWITTER CARD -->
348
+ <!-- ================================================================= -->
349
+
350
+ <!-- "summary" card type → square image + title + description -->
351
+ <meta name="twitter:card" content="summary" />
352
+ <meta name="twitter:title" content="${opts.name}" />
353
+ <meta
354
+ name="twitter:description"
355
+ content="${opts.description}"
356
+ />
357
+
358
+ <!-- ================================================================= -->
359
+ <!-- PWA / MANIFEST CONFIG -->
360
+ <!-- ================================================================= -->
361
+
362
+ <!-- Web App Manifest — defines name, icons, theme, display mode -->
363
+ <link rel="manifest" href="%sveltekit.assets%/manifest.json" />
364
+
365
+ <!-- ================================================================= -->
366
+ <!-- iOS PWA META TAGS -->
367
+ <!-- ================================================================= -->
368
+
369
+ <!--
370
+ iOS-specific PWA configuration:
371
+ - \`apple-mobile-web-app-capable\` → enables standalone PWA mode
372
+ - \`apple-mobile-web-app-status-bar-style\` → \`black-translucent\` lets
373
+ content extend behind the status bar for a true full-screen feel
374
+ - \`apple-mobile-web-app-title\` → name shown on home screen
375
+ - \`apple-touch-icon\` → icon used when "Add to Home Screen"
376
+ -->
377
+ <meta name="apple-mobile-web-app-capable" content="yes" />
378
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
379
+ <meta name="apple-mobile-web-app-title" content="${opts.shortName}" />
380
+ <link rel="apple-touch-icon" href="%sveltekit.assets%/icon-192.png" />
381
+
382
+ <!-- Prevent iOS from auto-detecting and styling phone numbers as links -->
383
+ <meta name="format-detection" content="telephone=no" />
384
+
385
+ <!-- SvelteKit injects component-level <head> content here -->
386
+ %sveltekit.head%
387
+ </head>
388
+ <body data-sveltekit-preload-data="hover">
389
+ <!-- ================================================================= -->
390
+ <!-- LANDSCAPE ORIENTATION BLOCKER -->
391
+ <!-- ================================================================= -->
392
+ <!-- TODO: Add landscape blocker UI. See stellar/src/app.html for a
393
+ full implementation with space-themed animations. The #landscape-blocker
394
+ div is shown via the @media query below when a phone is in landscape. -->
395
+ <div id="landscape-blocker">
396
+ <!-- TODO: Add your landscape blocker content here -->
397
+ </div>
398
+
399
+ <!-- ================================================================= -->
400
+ <!-- LANDSCAPE BLOCKER STYLES -->
401
+ <!-- ================================================================= -->
402
+ <style>
403
+ /* TODO: Add #landscape-blocker styling (hidden by default, shown by query below) */
404
+
405
+ /* ── Visibility Trigger ─────────────────────────────────────────── */
406
+ /*
407
+ * Show the blocker ONLY on phones in landscape:
408
+ * - max-height: 500px → landscape phone viewports
409
+ * - orientation → landscape
410
+ * - hover: none → excludes desktop/laptop (they have hover)
411
+ * - pointer: coarse → excludes stylus / mouse devices
412
+ */
413
+ @media (max-height: 500px) and (orientation: landscape) and (hover: none) and (pointer: coarse) {
414
+ #landscape-blocker {
415
+ display: flex;
416
+ }
417
+ }
418
+ </style>
419
+
420
+ <!-- ================================================================= -->
421
+ <!-- SVELTEKIT APP MOUNT -->
422
+ <!-- ================================================================= -->
423
+ <!-- \`display: contents\` makes the wrapper invisible to CSS layout -->
424
+ <div style="display: contents">%sveltekit.body%</div>
425
+
426
+ <!-- ================================================================= -->
427
+ <!-- iOS GESTURE / ZOOM PREVENTION SCRIPT -->
428
+ <!-- ================================================================= -->
429
+ <!--
430
+ Prevents unwanted zoom gestures in the iOS PWA:
431
+ 1. \`gesturestart/change/end\` → blocks pinch-to-zoom (Safari-specific)
432
+ 2. \`touchend\` debounce → blocks double-tap zoom (300 ms window)
433
+ 3. \`touchmove\` multi-touch → blocks two-finger zoom/scroll
434
+
435
+ All listeners use \`{ passive: false }\` so \`preventDefault()\` works.
436
+ Wrapped in an IIFE to avoid polluting the global scope.
437
+ -->
438
+ <script>
439
+ (function () {
440
+ // Prevent pinch-to-zoom
441
+ document.addEventListener(
442
+ 'gesturestart',
443
+ function (e) {
444
+ e.preventDefault();
445
+ },
446
+ { passive: false }
447
+ );
448
+
449
+ document.addEventListener(
450
+ 'gesturechange',
451
+ function (e) {
452
+ e.preventDefault();
453
+ },
454
+ { passive: false }
455
+ );
456
+
457
+ document.addEventListener(
458
+ 'gestureend',
459
+ function (e) {
460
+ e.preventDefault();
461
+ },
462
+ { passive: false }
463
+ );
464
+
465
+ // Prevent double-tap zoom — if two taps land within 300 ms, cancel the second
466
+ var lastTouchEnd = 0;
467
+ document.addEventListener(
468
+ 'touchend',
469
+ function (e) {
470
+ var now = Date.now();
471
+ if (now - lastTouchEnd <= 300) {
472
+ e.preventDefault();
473
+ }
474
+ lastTouchEnd = now;
475
+ },
476
+ { passive: false }
477
+ );
478
+
479
+ // Prevent multi-touch zoom — block touchmove when more than one finger is down
480
+ document.addEventListener(
481
+ 'touchmove',
482
+ function (e) {
483
+ if (e.touches.length > 1) {
484
+ e.preventDefault();
485
+ }
486
+ },
487
+ { passive: false }
488
+ );
489
+ })();
490
+ </script>
491
+
492
+ <!-- ================================================================= -->
493
+ <!-- SERVICE WORKER REGISTRATION -->
494
+ <!-- ================================================================= -->
495
+ <!--
496
+ Deferred registration strategy:
497
+ - Uses \`requestIdleCallback\` (with \`setTimeout\` fallback) so the SW
498
+ doesn't compete with first-paint work on the main thread.
499
+ - After registration, sets up three update-check triggers:
500
+ 1. \`visibilitychange\` → check when tab becomes visible again
501
+ 2. \`setInterval\` → every 5 minutes (iOS fallback — it doesn't
502
+ reliably fire visibilitychange in standalone PWA mode)
503
+ 3. \`online\` → check when connectivity is restored
504
+ -->
505
+ <script>
506
+ if ('serviceWorker' in navigator) {
507
+ // Use requestIdleCallback to defer SW registration until browser is idle
508
+ var registerSW = function () {
509
+ navigator.serviceWorker
510
+ .register('/sw.js')
511
+ .then(function (registration) {
512
+ // Primary: check for updates when tab becomes visible
513
+ document.addEventListener('visibilitychange', function () {
514
+ if (document.visibilityState === 'visible') {
515
+ registration.update();
516
+ }
517
+ });
518
+ // Fallback: periodic update check (iOS PWA doesn't fire visibilitychange reliably)
519
+ setInterval(
520
+ function () {
521
+ registration.update();
522
+ },
523
+ 5 * 60 * 1000
524
+ );
525
+ // Also check when device comes back online
526
+ window.addEventListener('online', function () {
527
+ registration.update();
528
+ });
529
+ })
530
+ .catch(function () {});
531
+ };
532
+ if ('requestIdleCallback' in window) {
533
+ requestIdleCallback(registerSW);
534
+ } else {
535
+ setTimeout(registerSW, 1);
536
+ }
537
+ }
538
+ </script>
539
+ </body>
540
+ </html>
541
+ `;
542
+ }
543
+ function generateReadme(opts) {
544
+ return `# ${opts.name}
545
+
546
+ > See [ARCHITECTURE.md](./ARCHITECTURE.md) for project structure.
547
+ > See [FRAMEWORKS.md](./FRAMEWORKS.md) for framework decisions.
548
+
549
+ ## Getting Started
550
+
551
+ \`\`\`bash
552
+ npm run dev
553
+ \`\`\`
554
+
555
+ ## Scripts
556
+
557
+ | Command | Description |
558
+ |---------|-------------|
559
+ | \`npm run dev\` | Start development server |
560
+ | \`npm run build\` | Production build |
561
+ | \`npm run check\` | Type-check with svelte-check |
562
+ | \`npm run lint\` | Lint with ESLint |
563
+ | \`npm run format\` | Format with Prettier |
564
+ | \`npm run dead-code\` | Dead code detection with Knip |
565
+ | \`npm run cleanup\` | Auto-fix lint + format |
566
+ | \`npm run validate\` | Full validation (check + lint + dead-code) |
567
+ `;
568
+ }
569
+ function generateArchitecture(opts) {
570
+ return `# Architecture
571
+
572
+ ## Overview
573
+
574
+ ${opts.name} is an offline-first PWA built with SvelteKit 2 and Svelte 5, powered by \`@prabhask5/stellar-engine\` for data sync and authentication.
575
+
576
+ ## Stack
577
+
578
+ - **Framework**: SvelteKit 2 + Svelte 5
579
+ - **Sync Engine**: \`@prabhask5/stellar-engine\` (IndexedDB + Supabase)
580
+ - **Backend**: Supabase (auth, Postgres, realtime)
581
+ - **PWA**: Custom service worker with smart caching
582
+
583
+ ## Project Structure
584
+
585
+ \`\`\`
586
+ src/
587
+ routes/ # SvelteKit routes
588
+ lib/ # Shared code
589
+ components/ # Svelte components
590
+ stores/ # Svelte stores
591
+ types/ # TypeScript types
592
+ static/
593
+ sw.js # Service worker (generated by stellarPWA plugin)
594
+ manifest.json # PWA manifest
595
+ \`\`\`
596
+ `;
597
+ }
598
+ function generateFrameworks() {
599
+ return `# Framework Decisions
600
+
601
+ ## SvelteKit 2 + Svelte 5
602
+
603
+ Svelte 5 introduces runes (\`$state\`, \`$derived\`, \`$effect\`, \`$props\`) for fine-grained reactivity. SvelteKit provides file-based routing, SSR, and the adapter system.
604
+
605
+ ## stellar-engine
606
+
607
+ \`@prabhask5/stellar-engine\` handles:
608
+ - **Offline-first data**: IndexedDB via Dexie with automatic sync to Supabase
609
+ - **Authentication**: Supabase Auth with offline mode support
610
+ - **Real-time sync**: Supabase Realtime subscriptions
611
+
612
+ ## Service Worker
613
+
614
+ Service worker (generated by \`stellarPWA\` Vite plugin) with:
615
+ - Immutable asset caching (content-hashed SvelteKit chunks)
616
+ - Versioned shell caching (HTML, manifest, icons)
617
+ - Network-first navigation with offline fallback
618
+ - Background precaching from asset manifest
619
+
620
+ ## Dev Tools
621
+
622
+ - **ESLint** — flat config with TypeScript + Svelte support
623
+ - **Prettier** — with svelte plugin
624
+ - **Knip** — dead code detection
625
+ - **Husky** — pre-commit hooks
626
+ `;
627
+ }
628
+ function generateGitignore() {
629
+ return `node_modules
630
+ .DS_Store
631
+ /build
632
+ /.svelte-kit
633
+ /package
634
+ .env
635
+ .env.*
636
+ !.env.example
637
+ vite.config.js.timestamp-*
638
+ vite.config.ts.timestamp-*
639
+ static/sw.js
640
+ static/asset-manifest.json
641
+ `;
642
+ }
643
+ function generateOfflineHtml(opts) {
644
+ return `<!-- TODO: Customize this offline fallback page with your app's branding.
645
+ This page is served by the service worker when the app is offline and
646
+ no cached HTML is available. -->
647
+ <!DOCTYPE html>
648
+ <html lang="en">
649
+ <head>
650
+ <meta charset="UTF-8">
651
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
652
+ <title>Offline - ${opts.name}</title>
653
+ <!-- TODO: Add your offline page styling here -->
654
+ </head>
655
+ <body>
656
+ <!-- TODO: Add your offline page content here -->
657
+ <h1>You're Offline</h1>
658
+ <p>Please check your internet connection and try again.</p>
659
+ <button onclick="location.reload()">Try Again</button>
660
+ </body>
661
+ </html>
662
+ `;
663
+ }
664
+ function generatePlaceholderSvg(color, label, fontSize = 64) {
665
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
666
+ <rect width="512" height="512" rx="64" fill="${color}"/>
667
+ <text x="256" y="280" text-anchor="middle" font-family="system-ui, sans-serif" font-size="${fontSize}" font-weight="700" fill="white">${label}</text>
668
+ </svg>
669
+ `;
670
+ }
671
+ function generateMonochromeSvg(label) {
672
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
673
+ <rect width="512" height="512" rx="64" fill="#ffffff"/>
674
+ <text x="256" y="280" text-anchor="middle" font-family="system-ui, sans-serif" font-size="64" font-weight="700" fill="black">${label}</text>
675
+ </svg>
676
+ `;
677
+ }
678
+ function generateSplashSvg(label) {
679
+ return `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
680
+ <rect width="512" height="512" rx="64" fill="#0f0f1a"/>
681
+ <text x="256" y="280" text-anchor="middle" font-family="system-ui, sans-serif" font-size="48" font-weight="700" fill="white">${label}</text>
682
+ </svg>
683
+ `;
684
+ }
685
+ function generateEmailPlaceholder(title) {
686
+ return `<!-- TODO: ${title} email template -->
687
+ <!-- See stellar-engine EMAIL_TEMPLATES.md for the full template format -->
688
+ <!DOCTYPE html>
689
+ <html><head><title>${title}</title></head>
690
+ <body><p>TODO: Implement ${title} email template</p></body>
691
+ </html>
692
+ `;
693
+ }
694
+ function generateSupabaseSchema(opts) {
695
+ return `-- ${opts.name} Database Schema for Supabase
696
+ -- Copy and paste this entire file into your Supabase SQL Editor
697
+
698
+ -- ============================================================
699
+ -- EXTENSIONS
700
+ -- ============================================================
701
+
702
+ create extension if not exists "uuid-ossp";
703
+
704
+ -- ============================================================
705
+ -- HELPER FUNCTIONS
706
+ -- ============================================================
707
+
708
+ -- Function to automatically set user_id on insert
709
+ create or replace function set_user_id()
710
+ returns trigger as $$
711
+ begin
712
+ new.user_id := auth.uid();
713
+ return new;
714
+ end;
715
+ $$ language plpgsql security definer set search_path = '';
716
+
717
+ -- Function to automatically update updated_at timestamp
718
+ create or replace function update_updated_at_column()
719
+ returns trigger as $$
720
+ begin
721
+ new.updated_at = timezone('utc'::text, now());
722
+ return new;
723
+ end;
724
+ $$ language plpgsql set search_path = '';
725
+
726
+ -- ============================================================
727
+ -- YOUR TABLES HERE
728
+ -- ============================================================
729
+ -- Example table showing the required column pattern:
730
+ --
731
+ -- create table items (
732
+ -- id uuid default uuid_generate_v4() primary key,
733
+ -- user_id uuid references auth.users(id) on delete cascade,
734
+ -- name text not null,
735
+ -- completed boolean default false not null,
736
+ -- "order" double precision default 0 not null,
737
+ -- created_at timestamp with time zone default timezone('utc'::text, now()) not null,
738
+ -- updated_at timestamp with time zone default timezone('utc'::text, now()) not null,
739
+ -- deleted boolean default false not null,
740
+ -- _version integer default 1 not null,
741
+ -- device_id text
742
+ -- );
743
+ --
744
+ -- alter table items enable row level security;
745
+ -- create policy "Users can manage own items" on items for all using (auth.uid() = user_id);
746
+ --
747
+ -- create trigger set_user_id_items before insert on items for each row execute function set_user_id();
748
+ -- create trigger update_items_updated_at before update on items for each row execute function update_updated_at_column();
749
+ --
750
+ -- create index idx_items_user_id on items(user_id);
751
+ -- create index idx_items_order on items("order");
752
+ -- create index idx_items_updated_at on items(updated_at);
753
+ -- create index idx_items_deleted on items(deleted) where deleted = false;
754
+
755
+ -- ============================================================
756
+ -- TRUSTED DEVICES (required for device verification)
757
+ -- ============================================================
758
+
759
+ create table trusted_devices (
760
+ id uuid default gen_random_uuid() primary key,
761
+ user_id uuid references auth.users(id) on delete cascade not null,
762
+ device_id text not null,
763
+ device_label text,
764
+ trusted_at timestamptz default now() not null,
765
+ last_used_at timestamptz default now() not null,
766
+ unique(user_id, device_id)
767
+ );
768
+
769
+ alter table trusted_devices enable row level security;
770
+ create policy "Users can manage own devices" on trusted_devices for all using (auth.uid() = user_id);
771
+
772
+ create trigger set_user_id_trusted_devices before insert on trusted_devices for each row execute function set_user_id();
773
+ create trigger update_trusted_devices_updated_at before update on trusted_devices for each row execute function update_updated_at_column();
774
+
775
+ create index idx_trusted_devices_user_id on trusted_devices(user_id);
776
+
777
+ -- ============================================================
778
+ -- REALTIME: Enable real-time subscriptions for all tables
779
+ -- ============================================================
780
+ -- Enable realtime for your tables:
781
+ -- alter publication supabase_realtime add table items;
782
+
783
+ alter publication supabase_realtime add table trusted_devices;
784
+ `;
785
+ }
786
+ function generateEslintConfig() {
787
+ return `import js from '@eslint/js';
788
+ import ts from 'typescript-eslint';
789
+ import svelte from 'eslint-plugin-svelte';
790
+ import globals from 'globals';
791
+
792
+ /** @type {import('eslint').Linter.Config[]} */
793
+ export default [
794
+ js.configs.recommended,
795
+ ...ts.configs.recommended,
796
+ ...svelte.configs['flat/recommended'],
797
+ {
798
+ languageOptions: {
799
+ globals: {
800
+ ...globals.browser,
801
+ ...globals.node
802
+ }
803
+ }
804
+ },
805
+ {
806
+ files: ['**/*.svelte'],
807
+ languageOptions: {
808
+ parserOptions: {
809
+ parser: ts.parser
810
+ }
811
+ },
812
+ rules: {
813
+ // Svelte 5 uses let for $props() destructuring by convention
814
+ 'prefer-const': 'off'
815
+ }
816
+ },
817
+ {
818
+ files: ['**/*.ts', '**/*.js'],
819
+ rules: {
820
+ 'prefer-const': 'error'
821
+ }
822
+ },
823
+ {
824
+ rules: {
825
+ // TypeScript
826
+ '@typescript-eslint/no-unused-vars': ['warn', {
827
+ argsIgnorePattern: '^_',
828
+ varsIgnorePattern: '^_'
829
+ }],
830
+ '@typescript-eslint/no-explicit-any': 'warn',
831
+
832
+ // General - allow console.log for debugging, only flag in production builds
833
+ 'no-console': 'off',
834
+ 'no-var': 'error',
835
+
836
+ // Svelte - relax some rules for flexibility
837
+ 'svelte/no-at-html-tags': 'warn',
838
+ 'svelte/valid-compile': ['error', { ignoreWarnings: true }],
839
+ 'svelte/require-each-key': 'warn',
840
+ 'svelte/no-navigation-without-resolve': 'off', // Too strict for app navigation patterns
841
+ 'svelte/prefer-svelte-reactivity': 'off', // SvelteDate/SvelteSet/SvelteMap not always needed
842
+ 'svelte/no-unused-svelte-ignore': 'warn' // Downgrade to warning
843
+ }
844
+ },
845
+ {
846
+ ignores: [
847
+ '.svelte-kit/**',
848
+ 'build/**',
849
+ 'dist/**',
850
+ 'node_modules/**',
851
+ 'static/**',
852
+ '*.config.js',
853
+ '*.config.ts'
854
+ ]
855
+ }
856
+ ];
857
+ `;
858
+ }
859
+ function generatePrettierrc() {
860
+ return (JSON.stringify({
861
+ useTabs: false,
862
+ tabWidth: 2,
863
+ singleQuote: true,
864
+ trailingComma: 'none',
865
+ printWidth: 100,
866
+ plugins: ['prettier-plugin-svelte'],
867
+ overrides: [
868
+ {
869
+ files: '*.svelte',
870
+ options: {
871
+ parser: 'svelte'
872
+ }
873
+ }
874
+ ]
875
+ }, null, 2) + '\n');
876
+ }
877
+ function generatePrettierignore() {
878
+ return `.svelte-kit
879
+ build
880
+ dist
881
+ node_modules
882
+ static
883
+ *.md
884
+ package-lock.json
885
+ `;
886
+ }
887
+ function generateKnipJson() {
888
+ return (JSON.stringify({
889
+ $schema: 'https://unpkg.com/knip@latest/schema.json',
890
+ entry: ['src/routes/**/*.{svelte,ts,js}', 'src/lib/**/*.{svelte,ts,js}'],
891
+ project: ['src/**/*.{svelte,ts,js}'],
892
+ ignore: ['src/app.d.ts', '**/*.test.ts', '**/*.spec.ts'],
893
+ sveltekit: {
894
+ config: 'svelte.config.js'
895
+ }
896
+ }, null, 2) + '\n');
897
+ }
898
+ function generateHuskyPreCommit() {
899
+ return `npm run cleanup && npm run validate && git add -u
900
+ `;
901
+ }
902
+ function generateRootLayoutTs(opts) {
903
+ return `import { browser } from '$app/environment';
904
+ import { redirect } from '@sveltejs/kit';
905
+ import { goto } from '$app/navigation';
906
+ import { initEngine, startSyncEngine, supabase } from '@prabhask5/stellar-engine';
907
+ import { initConfig } from '@prabhask5/stellar-engine/config';
908
+ import { resolveAuthState, lockSingleUser } from '@prabhask5/stellar-engine/auth';
909
+ import { resolveRootLayout } from '@prabhask5/stellar-engine/kit';
910
+ import type { AuthMode, OfflineCredentials, Session } from '@prabhask5/stellar-engine/types';
911
+ import type { LayoutLoad } from './$types';
912
+
913
+ export const ssr = true;
914
+ export const prerender = false;
915
+
916
+ // TODO: Configure initEngine() with your app-specific database schema.
917
+ // Call initEngine({...}) at module scope (guarded by \`if (browser)\`).
918
+ // See the stellar-engine documentation for the full config interface.
919
+ //
920
+ // Example:
921
+ // if (browser) {
922
+ // initEngine({
923
+ // tables: [
924
+ // { supabaseName: 'items', columns: 'id,user_id,name,...' }
925
+ // ],
926
+ // database: {
927
+ // name: '${opts.name.replace(/[^a-zA-Z0-9]/g, '')}DB',
928
+ // versions: [
929
+ // { version: 1, stores: { items: 'id, user_id, created_at, updated_at' } }
930
+ // ]
931
+ // },
932
+ // supabase,
933
+ // prefix: '${opts.prefix}',
934
+ // auth: { mode: 'single-user', singleUser: { gateType: 'code', codeLength: 6 } },
935
+ // onAuthStateChange: (event, session) => { /* handle auth events */ },
936
+ // onAuthKicked: async () => { await lockSingleUser(); goto('/login'); }
937
+ // });
938
+ // }
939
+
940
+ export interface LayoutData {
941
+ session: Session | null;
942
+ authMode: AuthMode;
943
+ offlineProfile: OfflineCredentials | null;
944
+ singleUserSetUp?: boolean;
945
+ }
946
+
947
+ export const load: LayoutLoad = async ({ url }): Promise<LayoutData> => {
948
+ if (browser) {
949
+ const config = await initConfig();
950
+ if (!config && url.pathname !== '/setup') {
951
+ redirect(307, '/setup');
952
+ }
953
+ if (!config) {
954
+ return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
955
+ }
956
+ const result = await resolveAuthState();
957
+ if (result.authMode !== 'none') {
958
+ await startSyncEngine();
959
+ }
960
+ return result;
961
+ }
962
+ return { session: null, authMode: 'none', offlineProfile: null, singleUserSetUp: false };
963
+ };
964
+ `;
965
+ }
966
+ function generateRootLayoutSvelte() {
967
+ return `<script lang="ts">
968
+ import { hydrateAuthState } from '@prabhask5/stellar-engine/kit';
969
+ import { authState } from '@prabhask5/stellar-engine/stores';
970
+ import type { LayoutData } from './+layout';
971
+
972
+ interface Props {
973
+ children?: import('svelte').Snippet;
974
+ data: LayoutData;
975
+ }
976
+
977
+ let { children, data }: Props = $props();
978
+
979
+ $effect(() => {
980
+ hydrateAuthState(data);
981
+ });
982
+
983
+ // TODO: Add app shell (navbar, tab bar, overlays, sign-out logic, etc.)
984
+ // TODO: Import and use UpdatePrompt from '$lib/components/UpdatePrompt.svelte'
985
+ // TODO: Import and use SyncStatus from '@prabhask5/stellar-engine/components/SyncStatus'
986
+ </script>
987
+
988
+ <!-- TODO: Add your app shell template (navbar, tab bar, page transitions, etc.) -->
989
+ {@render children?.()}
990
+ `;
991
+ }
992
+ function generateHomePage() {
993
+ return `<script lang="ts">
994
+ import { getUserProfile } from '@prabhask5/stellar-engine/auth';
995
+ import { onSyncComplete, authState } from '@prabhask5/stellar-engine/stores';
996
+
997
+ // TODO: Add home page state and logic
998
+ </script>
999
+
1000
+ <!-- TODO: Add home page template -->
1001
+ `;
1002
+ }
1003
+ function generateErrorPage() {
1004
+ return `<script lang="ts">
1005
+ import { page } from '$app/stores';
1006
+
1007
+ // TODO: Add error page logic (offline detection, retry handlers, etc.)
1008
+ </script>
1009
+
1010
+ <!-- TODO: Add error page template (status code display, retry button, go home button) -->
1011
+ `;
1012
+ }
1013
+ function generateSetupPageTs() {
1014
+ return `import { browser } from '$app/environment';
1015
+ import { redirect } from '@sveltejs/kit';
1016
+ import { getConfig } from '@prabhask5/stellar-engine/config';
1017
+ import { getValidSession, isAdmin } from '@prabhask5/stellar-engine/auth';
1018
+ import type { PageLoad } from './$types';
1019
+
1020
+ export const load: PageLoad = async () => {
1021
+ if (!browser) return {};
1022
+ if (!getConfig()) {
1023
+ return { isFirstSetup: true };
1024
+ }
1025
+ const session = await getValidSession();
1026
+ if (!session?.user) {
1027
+ redirect(307, '/login');
1028
+ }
1029
+ if (!isAdmin(session.user)) {
1030
+ redirect(307, '/');
1031
+ }
1032
+ return { isFirstSetup: false };
1033
+ };
1034
+ `;
1035
+ }
1036
+ function generateSetupPageSvelte() {
1037
+ return `<script lang="ts">
1038
+ import { setConfig } from '@prabhask5/stellar-engine/config';
1039
+ import { isOnline } from '@prabhask5/stellar-engine/stores';
1040
+ import { pollForNewServiceWorker } from '@prabhask5/stellar-engine/kit';
1041
+
1042
+ // TODO: Add setup wizard state (steps, form fields, validation, deployment)
1043
+ </script>
1044
+
1045
+ <!-- TODO: Add setup wizard template (Supabase credentials form, validation, Vercel deployment) -->
1046
+ `;
1047
+ }
1048
+ function generatePolicyPage() {
1049
+ return `<script lang="ts">
1050
+ // TODO: Add any needed imports
1051
+ </script>
1052
+
1053
+ <!-- TODO: Add privacy policy page content -->
1054
+ `;
1055
+ }
1056
+ function generateLoginPage() {
1057
+ return `<script lang="ts">
1058
+ import { onMount, onDestroy } from 'svelte';
1059
+ import { goto, invalidateAll } from '$app/navigation';
1060
+ import { page } from '$app/stores';
1061
+ import {
1062
+ setupSingleUser,
1063
+ unlockSingleUser,
1064
+ getSingleUserInfo,
1065
+ completeSingleUserSetup,
1066
+ completeDeviceVerification,
1067
+ pollDeviceVerification,
1068
+ fetchRemoteGateConfig,
1069
+ linkSingleUserDevice
1070
+ } from '@prabhask5/stellar-engine/auth';
1071
+ import { sendDeviceVerification } from '@prabhask5/stellar-engine';
1072
+
1073
+ // TODO: Add login page state (setup/unlock/link-device modes, PIN inputs, modals)
1074
+ // TODO: Add BroadcastChannel listener for auth-confirmed events from /confirm
1075
+ </script>
1076
+
1077
+ <!-- TODO: Add login page template (PIN inputs, setup wizard, device verification modal) -->
1078
+ `;
1079
+ }
1080
+ function generateConfirmPage() {
1081
+ return `<script lang="ts">
1082
+ import { onMount } from 'svelte';
1083
+ import { goto } from '$app/navigation';
1084
+ import { page } from '$app/stores';
1085
+ import { handleEmailConfirmation, broadcastAuthConfirmed } from '@prabhask5/stellar-engine/kit';
1086
+
1087
+ let status: 'verifying' | 'success' | 'error' | 'redirecting' | 'can_close' = 'verifying';
1088
+ let errorMessage = '';
1089
+
1090
+ const CHANNEL_NAME = 'auth-channel'; // TODO: Customize channel name
1091
+
1092
+ onMount(async () => {
1093
+ const tokenHash = $page.url.searchParams.get('token_hash');
1094
+ const type = $page.url.searchParams.get('type');
1095
+
1096
+ if (tokenHash && type) {
1097
+ const result = await handleEmailConfirmation(
1098
+ tokenHash,
1099
+ type as 'signup' | 'email' | 'email_change' | 'magiclink'
1100
+ );
1101
+
1102
+ if (!result.success) {
1103
+ status = 'error';
1104
+ errorMessage = result.error || 'Unknown error';
1105
+ return;
1106
+ }
1107
+
1108
+ status = 'success';
1109
+ await new Promise((resolve) => setTimeout(resolve, 800));
1110
+ }
1111
+
1112
+ const tabResult = await broadcastAuthConfirmed(CHANNEL_NAME, type || 'signup');
1113
+ if (tabResult === 'can_close') {
1114
+ status = 'can_close';
1115
+ } else if (tabResult === 'no_broadcast') {
1116
+ goto('/', { replaceState: true });
1117
+ }
1118
+ });
1119
+ </script>
1120
+
1121
+ <!-- TODO: Add confirmation page template (verifying/success/error/can_close states) -->
1122
+ `;
1123
+ }
1124
+ function generateConfigServer() {
1125
+ return `import { json } from '@sveltejs/kit';
1126
+ import { getServerConfig } from '@prabhask5/stellar-engine/kit';
1127
+ import type { RequestHandler } from './$types';
1128
+
1129
+ export const GET: RequestHandler = async () => {
1130
+ return json(getServerConfig());
1131
+ };
1132
+ `;
1133
+ }
1134
+ function generateDeployServer() {
1135
+ return `import { json } from '@sveltejs/kit';
1136
+ import { deployToVercel } from '@prabhask5/stellar-engine/kit';
1137
+ import type { RequestHandler } from './$types';
1138
+
1139
+ export const POST: RequestHandler = async ({ request }) => {
1140
+ const { supabaseUrl, supabaseAnonKey, vercelToken } = await request.json();
1141
+
1142
+ if (!supabaseUrl || !supabaseAnonKey || !vercelToken) {
1143
+ return json(
1144
+ { success: false, error: 'Supabase URL, Anon Key, and Vercel Token are required' },
1145
+ { status: 400 }
1146
+ );
1147
+ }
1148
+
1149
+ const projectId = process.env.VERCEL_PROJECT_ID;
1150
+ if (!projectId) {
1151
+ return json(
1152
+ { success: false, error: 'VERCEL_PROJECT_ID not found. This endpoint only works on Vercel.' },
1153
+ { status: 400 }
1154
+ );
1155
+ }
1156
+
1157
+ const result = await deployToVercel({ vercelToken, projectId, supabaseUrl, supabaseAnonKey });
1158
+ return json(result);
1159
+ };
1160
+ `;
1161
+ }
1162
+ function generateValidateServer() {
1163
+ return `import { createValidateHandler } from '@prabhask5/stellar-engine/kit';
1164
+ import type { RequestHandler } from './$types';
1165
+
1166
+ export const POST: RequestHandler = createValidateHandler();
1167
+ `;
1168
+ }
1169
+ function generateCatchallPage() {
1170
+ return `import { redirect } from '@sveltejs/kit';
1171
+
1172
+ export function load() {
1173
+ redirect(302, '/');
1174
+ }
1175
+ `;
1176
+ }
1177
+ function generateProtectedLayoutTs() {
1178
+ return `import { redirect } from '@sveltejs/kit';
1179
+ import { browser } from '$app/environment';
1180
+ import { resolveAuthState } from '@prabhask5/stellar-engine/auth';
1181
+ import type { AuthMode, OfflineCredentials, Session } from '@prabhask5/stellar-engine/types';
1182
+ import type { LayoutLoad } from './$types';
1183
+
1184
+ export interface ProtectedLayoutData {
1185
+ session: Session | null;
1186
+ authMode: AuthMode;
1187
+ offlineProfile: OfflineCredentials | null;
1188
+ }
1189
+
1190
+ export const load: LayoutLoad = async ({ url }): Promise<ProtectedLayoutData> => {
1191
+ if (browser) {
1192
+ const result = await resolveAuthState();
1193
+ if (result.authMode === 'none') {
1194
+ const returnUrl = url.pathname + url.search;
1195
+ const loginUrl =
1196
+ returnUrl && returnUrl !== '/'
1197
+ ? \`/login?redirect=\${encodeURIComponent(returnUrl)}\`
1198
+ : '/login';
1199
+ throw redirect(302, loginUrl);
1200
+ }
1201
+ return result;
1202
+ }
1203
+ return { session: null, authMode: 'none', offlineProfile: null };
1204
+ };
1205
+ `;
1206
+ }
1207
+ function generateProtectedLayoutSvelte() {
1208
+ return `<script lang="ts">
1209
+ interface Props {
1210
+ children?: import('svelte').Snippet;
1211
+ }
1212
+
1213
+ let { children }: Props = $props();
1214
+
1215
+ // TODO: Add conditional page backgrounds or other protected-area chrome
1216
+ </script>
1217
+
1218
+ {@render children?.()}
1219
+ `;
1220
+ }
1221
+ function generateProfilePage() {
1222
+ return `<script lang="ts">
1223
+ import { goto } from '$app/navigation';
1224
+ import {
1225
+ changeSingleUserGate,
1226
+ updateSingleUserProfile,
1227
+ getSingleUserInfo,
1228
+ changeSingleUserEmail,
1229
+ completeSingleUserEmailChange
1230
+ } from '@prabhask5/stellar-engine/auth';
1231
+ import { authState } from '@prabhask5/stellar-engine/stores';
1232
+ import { isDebugMode, setDebugMode } from '@prabhask5/stellar-engine/utils';
1233
+ import {
1234
+ resetDatabase,
1235
+ getTrustedDevices,
1236
+ removeTrustedDevice,
1237
+ getCurrentDeviceId
1238
+ } from '@prabhask5/stellar-engine';
1239
+
1240
+ // TODO: Add profile page state (form fields, device management, debug tools)
1241
+ </script>
1242
+
1243
+ <!-- TODO: Add profile page template (forms, cards, device list, debug tools) -->
1244
+ `;
1245
+ }
1246
+ function generateUpdatePromptComponent() {
1247
+ return `<script lang="ts">
1248
+ /**
1249
+ * @fileoverview UpdatePrompt — service-worker update notification.
1250
+ *
1251
+ * Uses monitorSwLifecycle() from stellar-engine to detect when a new
1252
+ * version is waiting to activate, and handleSwUpdate() to apply it.
1253
+ */
1254
+
1255
+ import { onMount, onDestroy } from 'svelte';
1256
+ import { monitorSwLifecycle, handleSwUpdate } from '@prabhask5/stellar-engine/kit';
1257
+
1258
+ /** Whether the update prompt is visible */
1259
+ let showPrompt = $state(false);
1260
+
1261
+ /** Guard flag to prevent double-reload */
1262
+ let reloading = false;
1263
+
1264
+ let cleanup: (() => void) | null = null;
1265
+
1266
+ onMount(() => {
1267
+ cleanup = monitorSwLifecycle({
1268
+ onUpdateAvailable: () => {
1269
+ showPrompt = true;
1270
+ }
1271
+ });
1272
+ });
1273
+
1274
+ onDestroy(() => {
1275
+ cleanup?.();
1276
+ });
1277
+
1278
+ /**
1279
+ * Apply the update: sends SKIP_WAITING to the waiting SW,
1280
+ * waits for controllerchange, then reloads the page.
1281
+ */
1282
+ async function handleRefresh() {
1283
+ if (reloading) return;
1284
+ reloading = true;
1285
+ showPrompt = false;
1286
+ await handleSwUpdate();
1287
+ }
1288
+
1289
+ /**
1290
+ * Dismiss the prompt. The update will apply on the next visit.
1291
+ */
1292
+ function handleDismiss() {
1293
+ showPrompt = false;
1294
+ }
1295
+ </script>
1296
+
1297
+ <!-- TODO: Add your update prompt UI here.
1298
+ Use showPrompt to conditionally render the toast/banner.
1299
+ Call handleRefresh() for the "Refresh" action.
1300
+ Call handleDismiss() for the "Later" / dismiss action.
1301
+
1302
+ Example structure:
1303
+ {#if showPrompt}
1304
+ <div class="update-toast">
1305
+ <span>A new version is available</span>
1306
+ <button onclick={handleDismiss}>Later</button>
1307
+ <button onclick={handleRefresh}>Refresh</button>
1308
+ </div>
1309
+ {/if}
1310
+ -->
1311
+ `;
1312
+ }
1313
+ function generateAppTypes() {
1314
+ return `// App types barrel — re-exports from stellar-engine plus app-specific types
1315
+ export type { SyncStatus, AuthMode, OfflineCredentials } from '@prabhask5/stellar-engine/types';
1316
+
1317
+ // TODO: Add app-specific type definitions below
1318
+ `;
1319
+ }
1320
+ // =============================================================================
1321
+ // MAIN FUNCTION
1322
+ // =============================================================================
1323
+ async function main() {
1324
+ const opts = parseArgs(process.argv);
1325
+ const cwd = process.cwd();
1326
+ console.log(`\n\u2728 stellar-engine install pwa\n Creating ${opts.name}...\n`);
1327
+ const createdFiles = [];
1328
+ const skippedFiles = [];
1329
+ // 1. Write package.json
1330
+ writeIfMissing(join(cwd, 'package.json'), generatePackageJson(opts), createdFiles, skippedFiles);
1331
+ // 2. Run npm install
1332
+ console.log('Installing dependencies...');
1333
+ execSync('npm install', { stdio: 'inherit', cwd });
1334
+ // 3. Write all template files
1335
+ const firstLetter = opts.shortName.charAt(0).toUpperCase();
1336
+ const files = [
1337
+ // Config files
1338
+ ['vite.config.ts', generateViteConfig(opts)],
1339
+ ['tsconfig.json', generateTsconfig()],
1340
+ ['svelte.config.js', generateSvelteConfig(opts)],
1341
+ ['eslint.config.js', generateEslintConfig()],
1342
+ ['.prettierrc', generatePrettierrc()],
1343
+ ['.prettierignore', generatePrettierignore()],
1344
+ ['knip.json', generateKnipJson()],
1345
+ ['.gitignore', generateGitignore()],
1346
+ // Documentation
1347
+ ['README.md', generateReadme(opts)],
1348
+ ['ARCHITECTURE.md', generateArchitecture(opts)],
1349
+ ['FRAMEWORKS.md', generateFrameworks()],
1350
+ // Static assets
1351
+ ['static/manifest.json', generateManifest(opts)],
1352
+ ['static/offline.html', generateOfflineHtml(opts)],
1353
+ // Placeholder icons
1354
+ ['static/icons/app.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
1355
+ ['static/icons/app-dark.svg', generatePlaceholderSvg('#1a1a2e', firstLetter)],
1356
+ ['static/icons/maskable.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
1357
+ ['static/icons/favicon.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
1358
+ ['static/icons/monochrome.svg', generateMonochromeSvg(firstLetter)],
1359
+ ['static/icons/splash.svg', generateSplashSvg(opts.shortName)],
1360
+ ['static/icons/apple-touch.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
1361
+ // Email placeholders
1362
+ ['static/change-email.html', generateEmailPlaceholder('Change Email')],
1363
+ ['static/device-verification-email.html', generateEmailPlaceholder('Device Verification')],
1364
+ ['static/signup-email.html', generateEmailPlaceholder('Signup Email')],
1365
+ // Supabase schema
1366
+ ['supabase-schema.sql', generateSupabaseSchema(opts)],
1367
+ // Source files
1368
+ ['src/app.html', generateAppHtml(opts)],
1369
+ ['src/app.d.ts', generateAppDts(opts)],
1370
+ // Route files
1371
+ ['src/routes/+layout.ts', generateRootLayoutTs(opts)],
1372
+ ['src/routes/+layout.svelte', generateRootLayoutSvelte()],
1373
+ ['src/routes/+page.svelte', generateHomePage()],
1374
+ ['src/routes/+error.svelte', generateErrorPage()],
1375
+ ['src/routes/setup/+page.ts', generateSetupPageTs()],
1376
+ ['src/routes/setup/+page.svelte', generateSetupPageSvelte()],
1377
+ ['src/routes/policy/+page.svelte', generatePolicyPage()],
1378
+ ['src/routes/login/+page.svelte', generateLoginPage()],
1379
+ ['src/routes/confirm/+page.svelte', generateConfirmPage()],
1380
+ ['src/routes/api/config/+server.ts', generateConfigServer()],
1381
+ ['src/routes/api/setup/deploy/+server.ts', generateDeployServer()],
1382
+ ['src/routes/api/setup/validate/+server.ts', generateValidateServer()],
1383
+ ['src/routes/[...catchall]/+page.ts', generateCatchallPage()],
1384
+ ['src/routes/(protected)/+layout.ts', generateProtectedLayoutTs()],
1385
+ ['src/routes/(protected)/+layout.svelte', generateProtectedLayoutSvelte()],
1386
+ ['src/routes/(protected)/profile/+page.svelte', generateProfilePage()],
1387
+ ['src/lib/types.ts', generateAppTypes()],
1388
+ // Component files
1389
+ ['src/lib/components/UpdatePrompt.svelte', generateUpdatePromptComponent()]
1390
+ ];
1391
+ for (const [relativePath, content] of files) {
1392
+ writeIfMissing(join(cwd, relativePath), content, createdFiles, skippedFiles);
1393
+ }
1394
+ // 4. Set up husky
1395
+ console.log('Setting up husky...');
1396
+ execSync('npx husky init', { stdio: 'inherit', cwd });
1397
+ // Overwrite the default pre-commit (husky init creates one with "npm test")
1398
+ const preCommitPath = join(cwd, '.husky/pre-commit');
1399
+ writeFileSync(preCommitPath, generateHuskyPreCommit(), 'utf-8');
1400
+ createdFiles.push('.husky/pre-commit');
1401
+ console.log(' [write] .husky/pre-commit');
1402
+ // 5. Print summary
1403
+ console.log(`\n\u2705 Done! Created ${createdFiles.length} files.`);
1404
+ if (skippedFiles.length > 0) {
1405
+ console.log(` Skipped ${skippedFiles.length} existing files.`);
1406
+ }
1407
+ console.log(`
1408
+ Next steps:
1409
+ 1. Set up Supabase and add .env with PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY and PUBLIC_SUPABASE_URL
1410
+ 2. Run supabase-schema.sql in the Supabase SQL Editor
1411
+ 3. Add your app icons: static/favicon.png, static/icon-192.png, static/icon-512.png
1412
+ 4. Start building: npm run dev`);
1413
+ }
1414
+ // =============================================================================
1415
+ // RUN
1416
+ // =============================================================================
1417
+ main().catch((err) => {
1418
+ console.error('Error:', err);
1419
+ process.exit(1);
1420
+ });
1421
+ //# sourceMappingURL=install-pwa.js.map