@prabhask5/stellar-engine 1.1.17 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/README.md +55 -1
  2. package/dist/bin/install-pwa.js +234 -317
  3. package/dist/bin/install-pwa.js.map +1 -1
  4. package/dist/config.d.ts +11 -0
  5. package/dist/config.d.ts.map +1 -1
  6. package/dist/config.js +8 -2
  7. package/dist/config.js.map +1 -1
  8. package/dist/crdt/awareness.d.ts +128 -0
  9. package/dist/crdt/awareness.d.ts.map +1 -0
  10. package/dist/crdt/awareness.js +284 -0
  11. package/dist/crdt/awareness.js.map +1 -0
  12. package/dist/crdt/channel.d.ts +165 -0
  13. package/dist/crdt/channel.d.ts.map +1 -0
  14. package/dist/crdt/channel.js +522 -0
  15. package/dist/crdt/channel.js.map +1 -0
  16. package/dist/crdt/config.d.ts +58 -0
  17. package/dist/crdt/config.d.ts.map +1 -0
  18. package/dist/crdt/config.js +123 -0
  19. package/dist/crdt/config.js.map +1 -0
  20. package/dist/crdt/helpers.d.ts +104 -0
  21. package/dist/crdt/helpers.d.ts.map +1 -0
  22. package/dist/crdt/helpers.js +116 -0
  23. package/dist/crdt/helpers.js.map +1 -0
  24. package/dist/crdt/offline.d.ts +58 -0
  25. package/dist/crdt/offline.d.ts.map +1 -0
  26. package/dist/crdt/offline.js +130 -0
  27. package/dist/crdt/offline.js.map +1 -0
  28. package/dist/crdt/persistence.d.ts +65 -0
  29. package/dist/crdt/persistence.d.ts.map +1 -0
  30. package/dist/crdt/persistence.js +171 -0
  31. package/dist/crdt/persistence.js.map +1 -0
  32. package/dist/crdt/provider.d.ts +109 -0
  33. package/dist/crdt/provider.d.ts.map +1 -0
  34. package/dist/crdt/provider.js +543 -0
  35. package/dist/crdt/provider.js.map +1 -0
  36. package/dist/crdt/store.d.ts +111 -0
  37. package/dist/crdt/store.d.ts.map +1 -0
  38. package/dist/crdt/store.js +158 -0
  39. package/dist/crdt/store.js.map +1 -0
  40. package/dist/crdt/types.d.ts +281 -0
  41. package/dist/crdt/types.d.ts.map +1 -0
  42. package/dist/crdt/types.js +26 -0
  43. package/dist/crdt/types.js.map +1 -0
  44. package/dist/database.d.ts +1 -1
  45. package/dist/database.d.ts.map +1 -1
  46. package/dist/database.js +28 -7
  47. package/dist/database.js.map +1 -1
  48. package/dist/diagnostics.d.ts +75 -0
  49. package/dist/diagnostics.d.ts.map +1 -1
  50. package/dist/diagnostics.js +114 -2
  51. package/dist/diagnostics.js.map +1 -1
  52. package/dist/engine.d.ts.map +1 -1
  53. package/dist/engine.js +21 -1
  54. package/dist/engine.js.map +1 -1
  55. package/dist/entries/crdt.d.ts +32 -0
  56. package/dist/entries/crdt.d.ts.map +1 -0
  57. package/dist/entries/crdt.js +52 -0
  58. package/dist/entries/crdt.js.map +1 -0
  59. package/dist/entries/types.d.ts +1 -0
  60. package/dist/entries/types.d.ts.map +1 -1
  61. package/dist/index.d.ts +3 -0
  62. package/dist/index.d.ts.map +1 -1
  63. package/dist/index.js +7 -0
  64. package/dist/index.js.map +1 -1
  65. package/package.json +9 -2
  66. package/dist/operations.d.ts +0 -73
  67. package/dist/operations.d.ts.map +0 -1
  68. package/dist/operations.js +0 -227
  69. package/dist/operations.js.map +0 -1
  70. package/dist/reconnectHandler.d.ts +0 -16
  71. package/dist/reconnectHandler.d.ts.map +0 -1
  72. package/dist/reconnectHandler.js +0 -21
  73. package/dist/reconnectHandler.js.map +0 -1
@@ -26,7 +26,8 @@
26
26
  import { writeFileSync, existsSync, mkdirSync } from 'fs';
27
27
  import { join } from 'path';
28
28
  import { execSync } from 'child_process';
29
- import { createInterface } from 'readline';
29
+ import * as p from '@clack/prompts';
30
+ import color from 'picocolors';
30
31
  // =============================================================================
31
32
  // HELPERS
32
33
  // =============================================================================
@@ -61,127 +62,8 @@ function writeIfMissing(filePath, content, createdFiles, skippedFiles, quiet = f
61
62
  }
62
63
  }
63
64
  // =============================================================================
64
- // ANSI STYLE HELPERS
65
- // =============================================================================
66
- /** Wrap text in ANSI bold. */
67
- const bold = (s) => `\x1b[1m${s}\x1b[22m`;
68
- /** Wrap text in ANSI dim. */
69
- const dim = (s) => `\x1b[2m${s}\x1b[22m`;
70
- /** Wrap text in ANSI cyan. */
71
- const cyan = (s) => `\x1b[36m${s}\x1b[39m`;
72
- /** Wrap text in ANSI green. */
73
- const green = (s) => `\x1b[32m${s}\x1b[39m`;
74
- /** Wrap text in ANSI yellow. */
75
- const yellow = (s) => `\x1b[33m${s}\x1b[39m`;
76
- /** Wrap text in ANSI red. */
77
- const red = (s) => `\x1b[31m${s}\x1b[39m`;
78
- /**
79
- * Draw a box around lines of text using Unicode box-drawing characters.
80
- *
81
- * @param lines - The lines of text to display inside the box.
82
- * @param style - `"double"` for `╔═╗║╚═╝`, `"single"` for `┌─┐│└─┘`.
83
- * @param title - Optional title to display in the top border.
84
- * @returns The formatted box string with leading two-space indent.
85
- */
86
- function box(lines, style, title) {
87
- const [tl, h, tr, v, bl, br] = style === 'double'
88
- ? ['\u2554', '\u2550', '\u2557', '\u2551', '\u255a', '\u255d']
89
- : ['\u250c', '\u2500', '\u2510', '\u2502', '\u2514', '\u2518'];
90
- const width = Math.max(...lines.map((l) => l.length), (title ?? '').length + 4, 50);
91
- let top;
92
- if (title) {
93
- const titleStr = `${h} ${title} `;
94
- top = ` ${tl}${titleStr}${h.repeat(width - titleStr.length)}${tr}`;
95
- }
96
- else {
97
- top = ` ${tl}${h.repeat(width)}${tr}`;
98
- }
99
- const mid = lines.map((l) => ` ${v} ${l.padEnd(width - 2)}${v}`).join('\n');
100
- const bot = ` ${bl}${h.repeat(width)}${br}`;
101
- return `${top}\n${mid}\n${bot}`;
102
- }
103
- /**
104
- * Draw a double-bordered box with a header and body separated by a mid-rule.
105
- *
106
- * @param header - The header line(s) to display above the divider.
107
- * @param body - The body lines to display below the divider.
108
- * @returns The formatted box string with leading two-space indent.
109
- */
110
- function doubleBoxWithHeader(header, body) {
111
- const width = Math.max(...header.map((l) => l.length), ...body.map((l) => l.length), 50);
112
- const top = ` \u2554${'═'.repeat(width)}\u2557`;
113
- const headLines = header.map((l) => ` \u2551 ${l.padEnd(width - 2)}\u2551`).join('\n');
114
- const mid = ` \u2560${'═'.repeat(width)}\u2563`;
115
- const bodyLines = body.map((l) => ` \u2551 ${l.padEnd(width - 2)}\u2551`).join('\n');
116
- const bot = ` \u255a${'═'.repeat(width)}\u255d`;
117
- return `${top}\n${headLines}\n${mid}\n${bodyLines}\n${bot}`;
118
- }
119
- // =============================================================================
120
- // SPINNER
121
- // =============================================================================
122
- /** Braille spinner frames for animated progress. */
123
- const SPINNER_FRAMES = [
124
- '\u280b',
125
- '\u2819',
126
- '\u2839',
127
- '\u2838',
128
- '\u283c',
129
- '\u2834',
130
- '\u2826',
131
- '\u2827',
132
- '\u2807',
133
- '\u280f'
134
- ];
135
- /**
136
- * Create a terminal spinner that updates a single line in-place.
137
- *
138
- * @param text - Initial text to display beside the spinner.
139
- * @returns An object with `update`, `succeed`, and `stop` methods.
140
- */
141
- function createSpinner(text) {
142
- let frame = 0;
143
- let current = text;
144
- let timer = null;
145
- const render = () => {
146
- const spinner = cyan(SPINNER_FRAMES[frame % SPINNER_FRAMES.length]);
147
- process.stdout.write(`\r ${spinner} ${current}`);
148
- frame++;
149
- };
150
- timer = setInterval(render, 80);
151
- render();
152
- return {
153
- update(newText) {
154
- current = newText;
155
- },
156
- succeed(finalText) {
157
- if (timer)
158
- clearInterval(timer);
159
- timer = null;
160
- process.stdout.write(`\r ${green('\u2713')} ${finalText}\x1b[K\n`);
161
- },
162
- stop() {
163
- if (timer)
164
- clearInterval(timer);
165
- timer = null;
166
- process.stdout.write('\x1b[K');
167
- }
168
- };
169
- }
170
- // =============================================================================
171
65
  // INTERACTIVE SETUP
172
66
  // =============================================================================
173
- /**
174
- * Promisified readline question helper.
175
- *
176
- * @param rl - The readline interface.
177
- * @param prompt - The prompt string to display.
178
- * @returns The user's input string.
179
- */
180
- function ask(rl, prompt) {
181
- return new Promise((resolve) => {
182
- rl.question(prompt, (answer) => resolve(answer));
183
- });
184
- }
185
67
  /**
186
68
  * Run the interactive setup walkthrough, collecting all required options
187
69
  * from the user via sequential prompts.
@@ -192,102 +74,80 @@ function ask(rl, prompt) {
192
74
  *
193
75
  * @returns A promise that resolves with the validated {@link InstallOptions}.
194
76
  *
195
- * @throws {SystemExit} Exits with code 0 if the user declines to proceed.
77
+ * @throws {SystemExit} Exits with code 0 if the user cancels or declines to proceed.
196
78
  */
197
79
  async function runInteractiveSetup() {
198
- const rl = createInterface({ input: process.stdin, output: process.stdout });
199
- /* ── Welcome banner ── */
200
- console.log();
201
- console.log(doubleBoxWithHeader([` ${bold('\u2726 stellar-engine \u00b7 PWA scaffolder \u2726')}`], [
202
- 'Creates a complete offline-first SvelteKit PWA ',
203
- 'with auth, sync, and service worker support. '
204
- ]));
205
- console.log();
206
- /* ── App Name ── */
207
- let name = '';
208
- while (!name) {
209
- console.log(box([
210
- 'The full name of your application. ',
211
- 'Used in the page title, README, and manifest. ',
212
- `Example: ${dim('"Stellar Planner"')} `
213
- ], 'single', 'App Name'));
214
- const input = (await ask(rl, ` ${yellow('\u2192')} App name: `)).trim();
215
- if (!input) {
216
- console.log(red(' App name is required.\n'));
217
- }
218
- else {
219
- name = input;
80
+ p.intro(color.bold('\u2726 stellar-engine \u00b7 PWA scaffolder'));
81
+ const name = await p.text({
82
+ message: 'App name',
83
+ placeholder: 'e.g. Stellar Planner',
84
+ validate(value) {
85
+ if (!value || !value.trim())
86
+ return 'App name is required.';
220
87
  }
88
+ });
89
+ if (p.isCancel(name)) {
90
+ p.cancel('Setup cancelled.');
91
+ process.exit(0);
221
92
  }
222
- console.log();
223
- /* ── Short Name ── */
224
- let shortName = '';
225
- while (!shortName) {
226
- console.log(box([
227
- 'A short label for the home screen and app bar. ',
228
- 'Must be under 12 characters. ',
229
- `Example: ${dim('"Stellar"')} `
230
- ], 'single', 'Short Name'));
231
- const input = (await ask(rl, ` ${yellow('\u2192')} Short name: `)).trim();
232
- if (!input) {
233
- console.log(red(' Short name is required.\n'));
234
- }
235
- else if (input.length >= 12) {
236
- console.log(red(' Short name must be under 12 characters.\n'));
237
- }
238
- else {
239
- shortName = input;
93
+ const shortName = await p.text({
94
+ message: 'Short name',
95
+ placeholder: 'e.g. Stellar (under 12 chars)',
96
+ validate(value) {
97
+ if (!value || !value.trim())
98
+ return 'Short name is required.';
99
+ if (value.trim().length >= 12)
100
+ return 'Short name must be under 12 characters.';
240
101
  }
102
+ });
103
+ if (p.isCancel(shortName)) {
104
+ p.cancel('Setup cancelled.');
105
+ process.exit(0);
241
106
  }
242
- console.log();
243
- /* ── Prefix ── */
244
107
  const suggestedPrefix = name.toLowerCase().replace(/[^a-z0-9]/g, '');
245
- let prefix = '';
246
- while (!prefix) {
247
- console.log(box([
248
- 'Lowercase key used for localStorage, caches, ',
249
- 'and the service worker scope. ',
250
- 'No spaces. Letters and numbers only. ',
251
- `Suggested: ${dim(`"${suggestedPrefix}"`)}${' '.repeat(Math.max(0, 36 - suggestedPrefix.length - 3))}`
252
- ], 'single', 'Prefix'));
253
- const input = (await ask(rl, ` ${yellow('\u2192')} Prefix ${dim(`(${suggestedPrefix})`)}: `)).trim();
254
- const value = input || suggestedPrefix;
255
- if (!/^[a-z][a-z0-9]*$/.test(value)) {
256
- console.log(red(' Prefix must be lowercase, start with a letter, no spaces.\n'));
257
- }
258
- else {
259
- prefix = value;
108
+ const prefix = await p.text({
109
+ message: 'Prefix',
110
+ placeholder: suggestedPrefix,
111
+ defaultValue: suggestedPrefix,
112
+ validate(value) {
113
+ const v = (value ?? '').trim() || suggestedPrefix;
114
+ if (!/^[a-z][a-z0-9]*$/.test(v))
115
+ return 'Prefix must be lowercase, start with a letter, no spaces.';
260
116
  }
117
+ });
118
+ if (p.isCancel(prefix)) {
119
+ p.cancel('Setup cancelled.');
120
+ process.exit(0);
261
121
  }
262
- console.log();
263
- /* ── Description ── */
264
122
  const defaultDesc = 'A self-hosted offline-first PWA';
265
- console.log(box([
266
- 'A brief description for meta tags and manifest. ',
267
- `Press Enter to use the default. `,
268
- `Default: ${dim(`"${defaultDesc}"`)}`
269
- ], 'single', 'Description'));
270
- const descInput = (await ask(rl, ` ${yellow('\u2192')} Description ${dim('(optional)')}: `)).trim();
271
- const description = descInput || defaultDesc;
272
- console.log();
273
- /* Derive kebab-case name for package.json from the full name */
123
+ const description = await p.text({
124
+ message: 'Description',
125
+ placeholder: defaultDesc,
126
+ defaultValue: defaultDesc
127
+ });
128
+ if (p.isCancel(description)) {
129
+ p.cancel('Setup cancelled.');
130
+ process.exit(0);
131
+ }
274
132
  const kebabName = name.toLowerCase().replace(/\s+/g, '-');
275
- const opts = { name, shortName, prefix, description, kebabName };
276
- /* ── Confirmation summary ── */
277
- console.log(box([
278
- `${bold('Name:')} ${opts.name}${' '.repeat(Math.max(0, 38 - opts.name.length))}`,
279
- `${bold('Short name:')} ${opts.shortName}${' '.repeat(Math.max(0, 38 - opts.shortName.length))}`,
280
- `${bold('Prefix:')} ${opts.prefix}${' '.repeat(Math.max(0, 38 - opts.prefix.length))}`,
281
- `${bold('Description:')} ${opts.description}${' '.repeat(Math.max(0, 38 - opts.description.length))}`
282
- ], 'single', 'Configuration'));
283
- const proceed = (await ask(rl, ` Proceed? ${dim('(Y/n)')}: `)).trim().toLowerCase();
284
- if (proceed === 'n' || proceed === 'no') {
285
- console.log(dim('\n Setup cancelled.\n'));
286
- rl.close();
133
+ const opts = {
134
+ name: name.trim(),
135
+ shortName: shortName.trim(),
136
+ prefix: prefix.trim() || suggestedPrefix,
137
+ description: description.trim() || defaultDesc,
138
+ kebabName
139
+ };
140
+ p.note([
141
+ `${color.bold('Name:')} ${opts.name}`,
142
+ `${color.bold('Short name:')} ${opts.shortName}`,
143
+ `${color.bold('Prefix:')} ${opts.prefix}`,
144
+ `${color.bold('Description:')} ${opts.description}`
145
+ ].join('\n'), 'Configuration');
146
+ const confirmed = await p.confirm({ message: 'Proceed with this configuration?' });
147
+ if (p.isCancel(confirmed) || !confirmed) {
148
+ p.cancel('Setup cancelled.');
287
149
  process.exit(0);
288
150
  }
289
- rl.close();
290
- console.log();
291
151
  return opts;
292
152
  }
293
153
  // =============================================================================
@@ -1254,6 +1114,56 @@ create index idx_trusted_devices_user_id on trusted_devices(user_id);
1254
1114
  -- alter publication supabase_realtime add table items;
1255
1115
 
1256
1116
  alter publication supabase_realtime add table trusted_devices;
1117
+
1118
+ -- ============================================================
1119
+ -- CRDT DOCUMENT STORAGE (optional — only needed for collaborative editing)
1120
+ -- ============================================================
1121
+ -- Stores Yjs CRDT document state for collaborative real-time editing.
1122
+ -- Each row represents the latest merged state of a single collaborative document.
1123
+ -- The engine persists full Yjs binary state periodically (every ~30s), not per keystroke.
1124
+ -- Real-time updates between clients are distributed via Supabase Broadcast (WebSocket),
1125
+ -- so this table is only for durable persistence and offline-to-online reconciliation.
1126
+ --
1127
+ -- Key columns:
1128
+ -- state — Full Yjs document state (Y.encodeStateAsUpdate), base64 encoded
1129
+ -- state_vector — Yjs state vector (Y.encodeStateVector) for efficient delta computation
1130
+ -- state_size — Byte size of state column, used for monitoring and compaction decisions
1131
+ -- device_id — Identifies which device last persisted, used for echo suppression
1132
+ --
1133
+ -- To enable: add \`crdt: {}\` to your initEngine() config.
1134
+ -- To skip: delete or comment out this section if you don't need collaborative editing.
1135
+
1136
+ create table crdt_documents (
1137
+ id uuid primary key default gen_random_uuid(),
1138
+ page_id uuid not null,
1139
+ state text not null,
1140
+ state_vector text not null,
1141
+ state_size integer not null default 0,
1142
+ user_id uuid not null references auth.users(id),
1143
+ device_id text not null,
1144
+ updated_at timestamptz not null default now(),
1145
+ created_at timestamptz not null default now()
1146
+ );
1147
+
1148
+ alter table crdt_documents enable row level security;
1149
+
1150
+ create policy "Users can manage own CRDT documents"
1151
+ on crdt_documents for all
1152
+ using (auth.uid() = user_id);
1153
+
1154
+ create trigger set_crdt_documents_user_id
1155
+ before insert on crdt_documents
1156
+ for each row execute function set_user_id();
1157
+
1158
+ create trigger update_crdt_documents_updated_at
1159
+ before update on crdt_documents
1160
+ for each row execute function update_updated_at_column();
1161
+
1162
+ create index idx_crdt_documents_page_id on crdt_documents(page_id);
1163
+ create index idx_crdt_documents_user_id on crdt_documents(user_id);
1164
+
1165
+ -- Unique constraint per page per user (upsert target for persistence)
1166
+ create unique index idx_crdt_documents_page_user on crdt_documents(page_id, user_id);
1257
1167
  `;
1258
1168
  }
1259
1169
  // ---------------------------------------------------------------------------
@@ -4611,15 +4521,10 @@ const COMMANDS = [
4611
4521
  * Print the help screen listing all available commands.
4612
4522
  */
4613
4523
  function printHelp() {
4614
- console.log();
4615
- console.log(doubleBoxWithHeader([` ${bold('\u2726 stellar-engine CLI \u2726')} `], ['Available commands: ']));
4616
- console.log();
4617
- for (const cmd of COMMANDS) {
4618
- console.log(` ${cyan(cmd.usage)}`);
4619
- console.log(` ${dim(cmd.description)}`);
4620
- console.log();
4621
- }
4622
- console.log(` Run a command to get started.\n`);
4524
+ p.intro(color.bold('\u2726 stellar-engine CLI'));
4525
+ const commandList = COMMANDS.map((cmd) => `${color.cyan(cmd.usage)}\n${color.dim(cmd.description)}`).join('\n\n');
4526
+ p.note(commandList, 'Available commands');
4527
+ p.outro('Run a command to get started.');
4623
4528
  }
4624
4529
  /**
4625
4530
  * Route CLI arguments to the appropriate command handler.
@@ -4643,17 +4548,25 @@ function routeCommand() {
4643
4548
  // MAIN FUNCTION
4644
4549
  // =============================================================================
4645
4550
  /**
4646
- * Write a group of files quietly and return the count written.
4551
+ * Write a group of files quietly, updating the spinner with per-file progress.
4647
4552
  *
4648
4553
  * @param entries - Array of `[relativePath, content]` pairs.
4649
4554
  * @param cwd - The current working directory.
4650
4555
  * @param createdFiles - Accumulator for newly-created file paths.
4651
4556
  * @param skippedFiles - Accumulator for skipped file paths.
4557
+ * @param label - The category label shown in the spinner (e.g. "Config files").
4558
+ * @param spinner - The clack spinner instance to update per-file.
4559
+ * @param runningTotal - The total files written so far across all groups.
4652
4560
  * @returns The number of files in the group.
4653
4561
  */
4654
- function writeGroup(entries, cwd, createdFiles, skippedFiles) {
4655
- for (const [rel, content] of entries) {
4562
+ function writeGroup(entries, cwd, createdFiles, skippedFiles, label, spinner, runningTotal) {
4563
+ for (let i = 0; i < entries.length; i++) {
4564
+ const [rel, content] = entries[i];
4565
+ const existed = existsSync(join(cwd, rel));
4656
4566
  writeIfMissing(join(cwd, rel), content, createdFiles, skippedFiles, true);
4567
+ const status = existed ? color.dim('skip') : color.green('write');
4568
+ const current = runningTotal + i + 1;
4569
+ spinner.message(`${label} [${i + 1}/${entries.length}] ${status} ${color.dim(rel)} ${color.dim(`(${current} total)`)}`);
4657
4570
  }
4658
4571
  return entries.length;
4659
4572
  }
@@ -4677,121 +4590,125 @@ async function main() {
4677
4590
  const cwd = process.cwd();
4678
4591
  const createdFiles = [];
4679
4592
  const skippedFiles = [];
4593
+ const s = p.spinner();
4680
4594
  // 1. Write package.json
4681
- let sp = createSpinner('Writing package.json');
4595
+ s.start('Writing package.json...');
4682
4596
  writeIfMissing(join(cwd, 'package.json'), generatePackageJson(opts), createdFiles, skippedFiles, true);
4683
- sp.succeed('Writing package.json');
4597
+ s.stop('package.json ready');
4684
4598
  // 2. Run npm install
4685
- sp = createSpinner('Installing dependencies...');
4686
- sp.stop();
4687
- console.log(` ${cyan(SPINNER_FRAMES[0])} Installing dependencies...\n`);
4599
+ s.start('Installing dependencies...');
4600
+ s.stop('Installing dependencies (npm output below)');
4688
4601
  execSync('npm install', { stdio: 'inherit', cwd });
4689
- console.log(`\n ${green('\u2713')} Installing dependencies`);
4602
+ p.log.success('Dependencies installed');
4690
4603
  // 3. Write all template files by category
4691
4604
  const firstLetter = opts.shortName.charAt(0).toUpperCase();
4692
- /* ── Config files ── */
4693
- const configFiles = [
4694
- ['vite.config.ts', generateViteConfig(opts)],
4695
- ['tsconfig.json', generateTsconfig()],
4696
- ['svelte.config.js', generateSvelteConfig(opts)],
4697
- ['eslint.config.js', generateEslintConfig()],
4698
- ['.prettierrc', generatePrettierrc()],
4699
- ['.prettierignore', generatePrettierignore()],
4700
- ['knip.json', generateKnipJson()],
4701
- ['.gitignore', generateGitignore()]
4702
- ];
4703
- sp = createSpinner('Config files');
4704
- const configCount = writeGroup(configFiles, cwd, createdFiles, skippedFiles);
4705
- sp.succeed(`Config files ${dim(`${configCount} files`)}`);
4706
- /* ── Documentation ── */
4707
- const docFiles = [
4708
- ['README.md', generateReadme(opts)],
4709
- ['ARCHITECTURE.md', generateArchitecture(opts)],
4710
- ['FRAMEWORKS.md', generateFrameworks()]
4711
- ];
4712
- sp = createSpinner('Documentation');
4713
- const docCount = writeGroup(docFiles, cwd, createdFiles, skippedFiles);
4714
- sp.succeed(`Documentation ${dim(`${docCount} files`)}`);
4715
- /* ── Static assets ── */
4716
- const staticFiles = [
4717
- ['static/manifest.json', generateManifest(opts)],
4718
- ['static/offline.html', generateOfflineHtml(opts)],
4719
- ['static/icons/app.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4720
- ['static/icons/app-dark.svg', generatePlaceholderSvg('#1a1a2e', firstLetter)],
4721
- ['static/icons/maskable.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4722
- ['static/icons/favicon.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4723
- ['static/icons/monochrome.svg', generateMonochromeSvg(firstLetter)],
4724
- ['static/icons/splash.svg', generateSplashSvg(opts.shortName)],
4725
- ['static/icons/apple-touch.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4726
- ['static/change-email.html', generateEmailPlaceholder('Change Email')],
4727
- ['static/device-verification-email.html', generateEmailPlaceholder('Device Verification')],
4728
- ['static/signup-email.html', generateEmailPlaceholder('Signup Email')],
4729
- ['supabase-schema.sql', generateSupabaseSchema(opts)]
4730
- ];
4731
- sp = createSpinner('Static assets');
4732
- const staticCount = writeGroup(staticFiles, cwd, createdFiles, skippedFiles);
4733
- sp.succeed(`Static assets ${dim(`${staticCount} files`)}`);
4734
- /* ── Source files ── */
4735
- const sourceFiles = [
4736
- ['src/app.html', generateAppHtml(opts)],
4737
- ['src/app.d.ts', generateAppDts(opts)]
4738
- ];
4739
- sp = createSpinner('Source files');
4740
- const sourceCount = writeGroup(sourceFiles, cwd, createdFiles, skippedFiles);
4741
- sp.succeed(`Source files ${dim(`${sourceCount} files`)}`);
4742
- /* ── Route files ── */
4743
- const routeFiles = [
4744
- ['src/routes/+layout.ts', generateRootLayoutTs(opts)],
4745
- ['src/routes/+layout.svelte', generateRootLayoutSvelte(opts)],
4746
- ['src/routes/+page.svelte', generateHomePage(opts)],
4747
- ['src/routes/+error.svelte', generateErrorPage(opts)],
4748
- ['src/routes/setup/+page.ts', generateSetupPageTs()],
4749
- ['src/routes/setup/+page.svelte', generateSetupPageSvelte(opts)],
4750
- ['src/routes/policy/+page.svelte', generatePolicyPage(opts)],
4751
- ['src/routes/login/+page.svelte', generateLoginPage(opts)],
4752
- ['src/routes/confirm/+page.svelte', generateConfirmPage(opts)],
4753
- ['src/routes/api/config/+server.ts', generateConfigServer()],
4754
- ['src/routes/api/setup/deploy/+server.ts', generateDeployServer()],
4755
- ['src/routes/api/setup/validate/+server.ts', generateValidateServer()],
4756
- ['src/routes/[...catchall]/+page.ts', generateCatchallPage()],
4757
- ['src/routes/(protected)/+layout.ts', generateProtectedLayoutTs()],
4758
- ['src/routes/(protected)/+layout.svelte', generateProtectedLayoutSvelte()],
4759
- ['src/routes/(protected)/profile/+page.svelte', generateProfilePage(opts)],
4760
- ['src/routes/demo/+page.svelte', generateDemoPage(opts)]
4761
- ];
4762
- sp = createSpinner('Route files');
4763
- const routeCount = writeGroup(routeFiles, cwd, createdFiles, skippedFiles);
4764
- sp.succeed(`Route files ${dim(`${routeCount} files`)}`);
4765
- /* ── Library & components ── */
4766
- const libFiles = [
4767
- ['src/lib/types.ts', generateAppTypes()],
4768
- ['src/lib/components/UpdatePrompt.svelte', generateUpdatePromptComponent()],
4769
- ['src/lib/demo/mockData.ts', generateDemoMockData()],
4770
- ['src/lib/demo/config.ts', generateDemoConfig()]
4605
+ let filesWritten = 0;
4606
+ const groups = [
4607
+ {
4608
+ label: 'Config files',
4609
+ entries: [
4610
+ ['vite.config.ts', generateViteConfig(opts)],
4611
+ ['tsconfig.json', generateTsconfig()],
4612
+ ['svelte.config.js', generateSvelteConfig(opts)],
4613
+ ['eslint.config.js', generateEslintConfig()],
4614
+ ['.prettierrc', generatePrettierrc()],
4615
+ ['.prettierignore', generatePrettierignore()],
4616
+ ['knip.json', generateKnipJson()],
4617
+ ['.gitignore', generateGitignore()]
4618
+ ]
4619
+ },
4620
+ {
4621
+ label: 'Documentation',
4622
+ entries: [
4623
+ ['README.md', generateReadme(opts)],
4624
+ ['ARCHITECTURE.md', generateArchitecture(opts)],
4625
+ ['FRAMEWORKS.md', generateFrameworks()]
4626
+ ]
4627
+ },
4628
+ {
4629
+ label: 'Static assets',
4630
+ entries: [
4631
+ ['static/manifest.json', generateManifest(opts)],
4632
+ ['static/offline.html', generateOfflineHtml(opts)],
4633
+ ['static/icons/app.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4634
+ ['static/icons/app-dark.svg', generatePlaceholderSvg('#1a1a2e', firstLetter)],
4635
+ ['static/icons/maskable.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4636
+ ['static/icons/favicon.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4637
+ ['static/icons/monochrome.svg', generateMonochromeSvg(firstLetter)],
4638
+ ['static/icons/splash.svg', generateSplashSvg(opts.shortName)],
4639
+ ['static/icons/apple-touch.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
4640
+ ['static/change-email.html', generateEmailPlaceholder('Change Email')],
4641
+ ['static/device-verification-email.html', generateEmailPlaceholder('Device Verification')],
4642
+ ['static/signup-email.html', generateEmailPlaceholder('Signup Email')],
4643
+ ['supabase-schema.sql', generateSupabaseSchema(opts)]
4644
+ ]
4645
+ },
4646
+ {
4647
+ label: 'Source files',
4648
+ entries: [
4649
+ ['src/app.html', generateAppHtml(opts)],
4650
+ ['src/app.d.ts', generateAppDts(opts)]
4651
+ ]
4652
+ },
4653
+ {
4654
+ label: 'Route files',
4655
+ entries: [
4656
+ ['src/routes/+layout.ts', generateRootLayoutTs(opts)],
4657
+ ['src/routes/+layout.svelte', generateRootLayoutSvelte(opts)],
4658
+ ['src/routes/+page.svelte', generateHomePage(opts)],
4659
+ ['src/routes/+error.svelte', generateErrorPage(opts)],
4660
+ ['src/routes/setup/+page.ts', generateSetupPageTs()],
4661
+ ['src/routes/setup/+page.svelte', generateSetupPageSvelte(opts)],
4662
+ ['src/routes/policy/+page.svelte', generatePolicyPage(opts)],
4663
+ ['src/routes/login/+page.svelte', generateLoginPage(opts)],
4664
+ ['src/routes/confirm/+page.svelte', generateConfirmPage(opts)],
4665
+ ['src/routes/api/config/+server.ts', generateConfigServer()],
4666
+ ['src/routes/api/setup/deploy/+server.ts', generateDeployServer()],
4667
+ ['src/routes/api/setup/validate/+server.ts', generateValidateServer()],
4668
+ ['src/routes/[...catchall]/+page.ts', generateCatchallPage()],
4669
+ ['src/routes/(protected)/+layout.ts', generateProtectedLayoutTs()],
4670
+ ['src/routes/(protected)/+layout.svelte', generateProtectedLayoutSvelte()],
4671
+ ['src/routes/(protected)/profile/+page.svelte', generateProfilePage(opts)],
4672
+ ['src/routes/demo/+page.svelte', generateDemoPage(opts)]
4673
+ ]
4674
+ },
4675
+ {
4676
+ label: 'Library & components',
4677
+ entries: [
4678
+ ['src/lib/types.ts', generateAppTypes()],
4679
+ ['src/lib/components/UpdatePrompt.svelte', generateUpdatePromptComponent()],
4680
+ ['src/lib/demo/mockData.ts', generateDemoMockData()],
4681
+ ['src/lib/demo/config.ts', generateDemoConfig()]
4682
+ ]
4683
+ }
4771
4684
  ];
4772
- sp = createSpinner('Library & components');
4773
- const libCount = writeGroup(libFiles, cwd, createdFiles, skippedFiles);
4774
- sp.succeed(`Library & components ${dim(`${libCount} files`)}`);
4685
+ for (const group of groups) {
4686
+ s.start(`${group.label} [0/${group.entries.length}]...`);
4687
+ filesWritten += writeGroup(group.entries, cwd, createdFiles, skippedFiles, group.label, s, filesWritten);
4688
+ s.stop(`${group.label} ${color.dim(`\u2014 ${group.entries.length} files`)}`);
4689
+ }
4775
4690
  // 4. Set up husky
4776
- sp = createSpinner('Git hooks');
4691
+ s.start('Setting up git hooks...');
4777
4692
  execSync('npx husky init', { stdio: 'pipe', cwd });
4778
4693
  const preCommitPath = join(cwd, '.husky/pre-commit');
4779
4694
  writeFileSync(preCommitPath, generateHuskyPreCommit(), 'utf-8');
4780
4695
  createdFiles.push('.husky/pre-commit');
4781
- sp.succeed(`Git hooks ${dim('1 file')}`);
4696
+ filesWritten++;
4697
+ s.stop(`Git hooks ${color.dim('\u2014 1 file')}`);
4698
+ p.log.success(`All project files generated ${color.dim(`(${filesWritten} total)`)}`);
4782
4699
  // 5. Print final summary
4783
- console.log();
4784
- console.log(doubleBoxWithHeader([` ${green(bold('\u2713 Setup complete!'))} `], [
4785
- `Created: ${bold(String(createdFiles.length))} files${' '.repeat(34 - String(createdFiles.length).length)}`,
4786
- `Skipped: ${bold(String(skippedFiles.length))} files${' '.repeat(34 - String(skippedFiles.length).length)}`
4787
- ]));
4788
- console.log(`
4789
- ${bold('Next steps:')}
4790
- 1. Set up Supabase and add .env with your keys
4791
- 2. Run supabase-schema.sql in Supabase SQL Editor
4792
- 3. Add app icons in static/icons/
4793
- 4. Start building: ${cyan('npm run dev')}
4794
- `);
4700
+ p.note([
4701
+ `${color.green('Created:')} ${color.bold(String(createdFiles.length))} files`,
4702
+ `${color.dim('Skipped:')} ${color.bold(String(skippedFiles.length))} files`
4703
+ ].join('\n'), 'Setup complete!');
4704
+ p.log.step([
4705
+ color.bold('Next steps:'),
4706
+ ' 1. Set up Supabase and add .env with your keys',
4707
+ ' 2. Run supabase-schema.sql in Supabase SQL Editor',
4708
+ ' 3. Add app icons in static/icons/',
4709
+ ` 4. Start building: ${color.cyan('npm run dev')}`
4710
+ ].join('\n'));
4711
+ p.outro('Happy building!');
4795
4712
  }
4796
4713
  // =============================================================================
4797
4714
  // RUN