@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.
- package/README.md +65 -25
- package/dist/actions/truncateTooltip.d.ts +42 -0
- package/dist/actions/truncateTooltip.d.ts.map +1 -0
- package/dist/actions/truncateTooltip.js +257 -0
- package/dist/actions/truncateTooltip.js.map +1 -0
- package/dist/auth/singleUser.d.ts.map +1 -1
- package/dist/auth/singleUser.js +6 -35
- package/dist/auth/singleUser.js.map +1 -1
- package/dist/bin/install-pwa.d.ts +9 -0
- package/dist/bin/install-pwa.d.ts.map +1 -0
- package/dist/bin/install-pwa.js +1421 -0
- package/dist/bin/install-pwa.js.map +1 -0
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +75 -1
- package/dist/engine.js.map +1 -1
- package/dist/entries/actions.d.ts +1 -0
- package/dist/entries/actions.d.ts.map +1 -1
- package/dist/entries/actions.js +1 -0
- package/dist/entries/actions.js.map +1 -1
- package/dist/entries/kit.d.ts +10 -9
- package/dist/entries/kit.d.ts.map +1 -1
- package/dist/entries/kit.js +7 -8
- package/dist/entries/kit.js.map +1 -1
- package/dist/entries/types.d.ts +1 -1
- package/dist/entries/types.d.ts.map +1 -1
- package/dist/entries/utils.d.ts +1 -1
- package/dist/entries/utils.d.ts.map +1 -1
- package/dist/entries/utils.js +1 -1
- package/dist/entries/utils.js.map +1 -1
- package/dist/entries/vite.d.ts +3 -0
- package/dist/entries/vite.d.ts.map +1 -0
- package/dist/entries/vite.js +3 -0
- package/dist/entries/vite.js.map +1 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/kit/auth.d.ts +25 -0
- package/dist/kit/auth.d.ts.map +1 -0
- package/dist/kit/auth.js +31 -0
- package/dist/kit/auth.js.map +1 -0
- package/dist/kit/confirm.d.ts +30 -0
- package/dist/kit/confirm.d.ts.map +1 -0
- package/dist/kit/confirm.js +82 -0
- package/dist/kit/confirm.js.map +1 -0
- package/dist/kit/loads.d.ts +62 -0
- package/dist/kit/loads.d.ts.map +1 -0
- package/dist/kit/loads.js +89 -0
- package/dist/kit/loads.js.map +1 -0
- package/dist/kit/server.d.ts +43 -0
- package/dist/kit/server.d.ts.map +1 -0
- package/dist/kit/server.js +154 -0
- package/dist/kit/server.js.map +1 -0
- package/dist/kit/sw.d.ts +47 -0
- package/dist/kit/sw.d.ts.map +1 -0
- package/dist/kit/sw.js +164 -0
- package/dist/kit/sw.js.map +1 -0
- package/dist/runtime/runtimeConfig.d.ts +0 -8
- package/dist/runtime/runtimeConfig.d.ts.map +1 -1
- package/dist/runtime/runtimeConfig.js.map +1 -1
- package/dist/supabase/auth.d.ts +1 -1
- package/dist/supabase/auth.d.ts.map +1 -1
- package/dist/supabase/auth.js.map +1 -1
- package/dist/sw/build/vite-plugin.d.ts +29 -0
- package/dist/sw/build/vite-plugin.d.ts.map +1 -0
- package/dist/sw/build/vite-plugin.js +124 -0
- package/dist/sw/build/vite-plugin.js.map +1 -0
- package/dist/sw/sw.js +614 -0
- package/dist/types.d.ts +0 -19
- package/dist/types.d.ts.map +1 -1
- package/package.json +20 -22
- package/src/components/DeferredChangesBanner.svelte +477 -0
- package/src/components/SyncStatus.svelte +1732 -0
- package/dist/crdt/awareness.d.ts +0 -54
- package/dist/crdt/awareness.d.ts.map +0 -1
- package/dist/crdt/awareness.js +0 -219
- package/dist/crdt/awareness.js.map +0 -1
- package/dist/crdt/doc.d.ts +0 -56
- package/dist/crdt/doc.d.ts.map +0 -1
- package/dist/crdt/doc.js +0 -130
- package/dist/crdt/doc.js.map +0 -1
- package/dist/crdt/index.d.ts +0 -15
- package/dist/crdt/index.d.ts.map +0 -1
- package/dist/crdt/index.js +0 -20
- package/dist/crdt/index.js.map +0 -1
- package/dist/crdt/offline.d.ts +0 -91
- package/dist/crdt/offline.d.ts.map +0 -1
- package/dist/crdt/offline.js +0 -353
- package/dist/crdt/offline.js.map +0 -1
- package/dist/crdt/sync.d.ts +0 -58
- package/dist/crdt/sync.d.ts.map +0 -1
- package/dist/crdt/sync.js +0 -399
- package/dist/crdt/sync.js.map +0 -1
- package/dist/crdt/types.d.ts +0 -62
- package/dist/crdt/types.d.ts.map +0 -1
- package/dist/crdt/types.js +0 -7
- package/dist/crdt/types.js.map +0 -1
- package/dist/email/sendEmail.d.ts +0 -31
- package/dist/email/sendEmail.d.ts.map +0 -1
- package/dist/email/sendEmail.js +0 -39
- package/dist/email/sendEmail.js.map +0 -1
- package/dist/email/validateSmtp.d.ts +0 -18
- package/dist/email/validateSmtp.d.ts.map +0 -1
- package/dist/email/validateSmtp.js +0 -33
- package/dist/email/validateSmtp.js.map +0 -1
- package/dist/entries/crdt.d.ts +0 -3
- package/dist/entries/crdt.d.ts.map +0 -1
- package/dist/entries/crdt.js +0 -13
- package/dist/entries/crdt.js.map +0 -1
- package/dist/entries/email.d.ts +0 -4
- package/dist/entries/email.d.ts.map +0 -1
- package/dist/entries/email.js +0 -4
- package/dist/entries/email.js.map +0 -1
- package/dist/kit/authPresets.d.ts +0 -28
- package/dist/kit/authPresets.d.ts.map +0 -1
- package/dist/kit/authPresets.js +0 -23
- package/dist/kit/authPresets.js.map +0 -1
- package/dist/kit/configEndpoint.d.ts +0 -18
- package/dist/kit/configEndpoint.d.ts.map +0 -1
- package/dist/kit/configEndpoint.js +0 -27
- package/dist/kit/configEndpoint.js.map +0 -1
- package/dist/kit/deployEndpoint.d.ts +0 -22
- package/dist/kit/deployEndpoint.d.ts.map +0 -1
- package/dist/kit/deployEndpoint.js +0 -79
- package/dist/kit/deployEndpoint.js.map +0 -1
- package/dist/kit/layoutLoad.d.ts +0 -23
- package/dist/kit/layoutLoad.d.ts.map +0 -1
- package/dist/kit/layoutLoad.js +0 -41
- package/dist/kit/layoutLoad.js.map +0 -1
- package/dist/kit/protectedLoad.d.ts +0 -16
- package/dist/kit/protectedLoad.d.ts.map +0 -1
- package/dist/kit/protectedLoad.js +0 -28
- package/dist/kit/protectedLoad.js.map +0 -1
- package/dist/kit/setupLoad.d.ts +0 -11
- package/dist/kit/setupLoad.d.ts.map +0 -1
- package/dist/kit/setupLoad.js +0 -28
- package/dist/kit/setupLoad.js.map +0 -1
- package/dist/kit/validateEndpoint.d.ts +0 -9
- package/dist/kit/validateEndpoint.d.ts.map +0 -1
- package/dist/kit/validateEndpoint.js +0 -25
- package/dist/kit/validateEndpoint.js.map +0 -1
- package/dist/kit/vercelApi.d.ts +0 -6
- package/dist/kit/vercelApi.d.ts.map +0 -1
- package/dist/kit/vercelApi.js +0 -48
- 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
|