@prabhask5/stellar-engine 1.1.9 → 1.1.10

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 CHANGED
@@ -106,12 +106,14 @@ if (!auth.singleUserSetUp) {
106
106
 
107
107
  ## Install PWA Command
108
108
 
109
- Scaffold a complete offline-first PWA project:
109
+ Scaffold a complete offline-first PWA project with an interactive walkthrough:
110
110
 
111
111
  ```bash
112
- npx @prabhask5/stellar-engine install pwa --name "My App" --short_name "App" --prefix "myapp" [--description "..."]
112
+ npx @prabhask5/stellar-engine install pwa
113
113
  ```
114
114
 
115
+ The wizard guides you through each option (app name, short name, prefix, description), validates input inline, shows a confirmation summary, then scaffolds with animated progress.
116
+
115
117
  ### What it generates
116
118
 
117
119
  The command creates a full SvelteKit 2 + Svelte 5 project with:
@@ -150,14 +152,14 @@ The command creates a full SvelteKit 2 + Svelte 5 project with:
150
152
 
151
153
  **Git hooks (1):** `.husky/pre-commit` with lint + format + validate
152
154
 
153
- ### Parameters
155
+ ### Interactive Prompts
154
156
 
155
- | Flag | Required | Description |
156
- |------|----------|-------------|
157
- | `--name` | Yes | Full app name (e.g., "My Stellar App") |
158
- | `--short_name` | Yes | Short name for PWA home screen |
159
- | `--prefix` | Yes | App prefix for localStorage keys, SW, debug utils |
160
- | `--description` | No | App description (default: "A self-hosted offline-first PWA") |
157
+ | Prompt | Required | Description |
158
+ |--------|----------|-------------|
159
+ | App Name | Yes | Full app name (e.g., "Stellar Planner") |
160
+ | Short Name | Yes | Short name for PWA home screen (under 12 chars) |
161
+ | Prefix | Yes | Lowercase key for localStorage, caches, SW (auto-suggested from name) |
162
+ | Description | No | App description (default: "A self-hosted offline-first PWA") |
161
163
 
162
164
  ## Subpath exports
163
165
 
@@ -12,13 +12,15 @@
12
12
  *
13
13
  * Files are written non-destructively: existing files are skipped, not overwritten.
14
14
  *
15
+ * Launches an interactive walkthrough when invoked as `stellar-engine install pwa`.
16
+ *
15
17
  * @example
16
18
  * ```bash
17
- * stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]
19
+ * stellar-engine install pwa
18
20
  * ```
19
21
  *
20
22
  * @see {@link main} for the entry point
21
- * @see {@link parseArgs} for CLI argument parsing
23
+ * @see {@link runInteractiveSetup} for the interactive walkthrough
22
24
  * @see {@link writeIfMissing} for the non-destructive file write strategy
23
25
  */
24
26
  export {};
@@ -1 +1 @@
1
- {"version":3,"file":"install-pwa.d.ts","sourceRoot":"","sources":["../../src/bin/install-pwa.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;GAqBG"}
1
+ {"version":3,"file":"install-pwa.d.ts","sourceRoot":"","sources":["../../src/bin/install-pwa.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
@@ -12,18 +12,21 @@
12
12
  *
13
13
  * Files are written non-destructively: existing files are skipped, not overwritten.
14
14
  *
15
+ * Launches an interactive walkthrough when invoked as `stellar-engine install pwa`.
16
+ *
15
17
  * @example
16
18
  * ```bash
17
- * stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]
19
+ * stellar-engine install pwa
18
20
  * ```
19
21
  *
20
22
  * @see {@link main} for the entry point
21
- * @see {@link parseArgs} for CLI argument parsing
23
+ * @see {@link runInteractiveSetup} for the interactive walkthrough
22
24
  * @see {@link writeIfMissing} for the non-destructive file write strategy
23
25
  */
24
26
  import { writeFileSync, existsSync, mkdirSync } from 'fs';
25
27
  import { join } from 'path';
26
28
  import { execSync } from 'child_process';
29
+ import { createInterface } from 'readline';
27
30
  // =============================================================================
28
31
  // HELPERS
29
32
  // =============================================================================
@@ -37,12 +40,14 @@ import { execSync } from 'child_process';
37
40
  * @param content - The file content to write.
38
41
  * @param createdFiles - Accumulator for newly-created file paths (relative).
39
42
  * @param skippedFiles - Accumulator for skipped file paths (relative).
43
+ * @param quiet - When `true`, suppresses per-file console output (used during animated progress).
40
44
  */
41
- function writeIfMissing(filePath, content, createdFiles, skippedFiles) {
45
+ function writeIfMissing(filePath, content, createdFiles, skippedFiles, quiet = false) {
42
46
  const relPath = filePath.replace(process.cwd() + '/', '');
43
47
  if (existsSync(filePath)) {
44
48
  skippedFiles.push(relPath);
45
- console.log(` [skip] ${relPath} already exists`);
49
+ if (!quiet)
50
+ console.log(` [skip] ${relPath} already exists`);
46
51
  }
47
52
  else {
48
53
  const dir = filePath.substring(0, filePath.lastIndexOf('/'));
@@ -51,59 +56,239 @@ function writeIfMissing(filePath, content, createdFiles, skippedFiles) {
51
56
  }
52
57
  writeFileSync(filePath, content, 'utf-8');
53
58
  createdFiles.push(relPath);
54
- console.log(` [write] ${relPath}`);
59
+ if (!quiet)
60
+ console.log(` [write] ${relPath}`);
61
+ }
62
+ }
63
+ // =============================================================================
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}`;
55
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
+ };
56
169
  }
57
170
  // =============================================================================
58
- // ARG PARSING
171
+ // INTERACTIVE SETUP
59
172
  // =============================================================================
60
173
  /**
61
- * Parse command-line arguments into an {@link InstallOptions} object.
174
+ * Promisified readline question helper.
62
175
  *
63
- * Expects the format:
64
- * `stellar-engine install pwa --name "..." --short_name "..." --prefix "..." [--description "..."]`
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
+ /**
186
+ * Run the interactive setup walkthrough, collecting all required options
187
+ * from the user via sequential prompts.
65
188
  *
66
- * Exits the process with a usage message if required arguments are missing.
189
+ * Displays a welcome banner, then prompts for App Name, Short Name, Prefix,
190
+ * and Description with inline validation. Shows a confirmation summary and
191
+ * asks the user to proceed before returning.
67
192
  *
68
- * @param argv - The raw `process.argv` array.
69
- * @returns The parsed {@link InstallOptions}.
193
+ * @returns A promise that resolves with the validated {@link InstallOptions}.
70
194
  *
71
- * @throws {SystemExit} Exits with code 1 if `--name`, `--short_name`, or `--prefix` are missing.
195
+ * @throws {SystemExit} Exits with code 0 if the user declines to proceed.
72
196
  */
73
- function parseArgs(argv) {
74
- const args = argv.slice(2);
75
- if (args[0] !== 'install' || args[1] !== 'pwa') {
76
- console.error('Usage: stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]');
77
- process.exit(1);
78
- }
197
+ 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 ── */
79
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;
220
+ }
221
+ }
222
+ console.log();
223
+ /* ── Short Name ── */
80
224
  let shortName = '';
81
- let prefix = '';
82
- let description = 'A self-hosted offline-first PWA';
83
- for (let i = 2; i < args.length; i++) {
84
- switch (args[i]) {
85
- case '--name':
86
- name = args[++i];
87
- break;
88
- case '--short_name':
89
- shortName = args[++i];
90
- break;
91
- case '--prefix':
92
- prefix = args[++i];
93
- break;
94
- case '--description':
95
- description = args[++i];
96
- break;
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;
97
240
  }
98
241
  }
99
- if (!name || !shortName || !prefix) {
100
- console.error('Error: --name, --short_name, and --prefix are required.\n' +
101
- 'Usage: stellar-engine install pwa --name "App Name" --short_name "Short" --prefix "myprefix" [--description "..."]');
102
- process.exit(1);
242
+ console.log();
243
+ /* ── Prefix ── */
244
+ 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;
260
+ }
103
261
  }
262
+ console.log();
263
+ /* ── Description ── */
264
+ 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();
104
273
  /* Derive kebab-case name for package.json from the full name */
105
274
  const kebabName = name.toLowerCase().replace(/\s+/g, '-');
106
- return { name, shortName, prefix, description, kebabName };
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();
287
+ process.exit(0);
288
+ }
289
+ rl.close();
290
+ console.log();
291
+ return opts;
107
292
  }
108
293
  // =============================================================================
109
294
  // TEMPLATE GENERATORS
@@ -3850,38 +4035,102 @@ export type { SyncStatus, AuthMode, OfflineCredentials } from '@prabhask5/stella
3850
4035
  `;
3851
4036
  }
3852
4037
  // =============================================================================
4038
+ // COMMAND ROUTING
4039
+ // =============================================================================
4040
+ /**
4041
+ * Available CLI commands. Add new entries here to register additional commands.
4042
+ */
4043
+ const COMMANDS = [
4044
+ {
4045
+ name: 'install pwa',
4046
+ usage: 'stellar-engine install pwa',
4047
+ description: 'Scaffold a complete offline-first SvelteKit PWA project'
4048
+ }
4049
+ ];
4050
+ /**
4051
+ * Print the help screen listing all available commands.
4052
+ */
4053
+ function printHelp() {
4054
+ console.log();
4055
+ console.log(doubleBoxWithHeader([` ${bold('\u2726 stellar-engine CLI \u2726')} `], ['Available commands: ']));
4056
+ console.log();
4057
+ for (const cmd of COMMANDS) {
4058
+ console.log(` ${cyan(cmd.usage)}`);
4059
+ console.log(` ${dim(cmd.description)}`);
4060
+ console.log();
4061
+ }
4062
+ console.log(` Run a command to get started.\n`);
4063
+ }
4064
+ /**
4065
+ * Route CLI arguments to the appropriate command handler.
4066
+ * Prints help and exits if the command is not recognised.
4067
+ */
4068
+ function routeCommand() {
4069
+ const args = process.argv.slice(2);
4070
+ const command = args.slice(0, 2).join(' ');
4071
+ if (command === 'install pwa') {
4072
+ main().catch((err) => {
4073
+ console.error('Error:', err);
4074
+ process.exit(1);
4075
+ });
4076
+ return;
4077
+ }
4078
+ /* Unrecognised command or no args — show help */
4079
+ printHelp();
4080
+ process.exit(args.length === 0 ? 0 : 1);
4081
+ }
4082
+ // =============================================================================
3853
4083
  // MAIN FUNCTION
3854
4084
  // =============================================================================
4085
+ /**
4086
+ * Write a group of files quietly and return the count written.
4087
+ *
4088
+ * @param entries - Array of `[relativePath, content]` pairs.
4089
+ * @param cwd - The current working directory.
4090
+ * @param createdFiles - Accumulator for newly-created file paths.
4091
+ * @param skippedFiles - Accumulator for skipped file paths.
4092
+ * @returns The number of files in the group.
4093
+ */
4094
+ function writeGroup(entries, cwd, createdFiles, skippedFiles) {
4095
+ for (const [rel, content] of entries) {
4096
+ writeIfMissing(join(cwd, rel), content, createdFiles, skippedFiles, true);
4097
+ }
4098
+ return entries.length;
4099
+ }
3855
4100
  /**
3856
4101
  * Main entry point for the CLI scaffolding tool.
3857
4102
  *
3858
4103
  * **Execution flow:**
3859
- * 1. Parse CLI arguments into {@link InstallOptions}.
4104
+ * 1. Run interactive walkthrough to collect {@link InstallOptions}.
3860
4105
  * 2. Write `package.json` (if missing).
3861
4106
  * 3. Run `npm install` to fetch dependencies.
3862
- * 4. Write all template files (config, routes, components, assets, docs).
4107
+ * 4. Write all template files by category with animated progress.
3863
4108
  * 5. Initialise Husky and write the pre-commit hook.
3864
- * 6. Print a summary of created/skipped files and next steps.
4109
+ * 6. Print a styled summary of created/skipped files and next steps.
3865
4110
  *
3866
4111
  * @returns A promise that resolves when scaffolding is complete.
3867
4112
  *
3868
4113
  * @throws {Error} If `npm install` or `npx husky init` fails.
3869
4114
  */
3870
4115
  async function main() {
3871
- const opts = parseArgs(process.argv);
4116
+ const opts = await runInteractiveSetup();
3872
4117
  const cwd = process.cwd();
3873
- console.log(`\n\u2728 stellar-engine install pwa\n Creating ${opts.name}...\n`);
3874
4118
  const createdFiles = [];
3875
4119
  const skippedFiles = [];
3876
4120
  // 1. Write package.json
3877
- writeIfMissing(join(cwd, 'package.json'), generatePackageJson(opts), createdFiles, skippedFiles);
4121
+ let sp = createSpinner('Writing package.json');
4122
+ writeIfMissing(join(cwd, 'package.json'), generatePackageJson(opts), createdFiles, skippedFiles, true);
4123
+ sp.succeed('Writing package.json');
3878
4124
  // 2. Run npm install
3879
- console.log('Installing dependencies...');
4125
+ sp = createSpinner('Installing dependencies...');
4126
+ sp.stop();
4127
+ console.log(` ${cyan(SPINNER_FRAMES[0])} Installing dependencies...\n`);
3880
4128
  execSync('npm install', { stdio: 'inherit', cwd });
3881
- // 3. Write all template files
4129
+ console.log(`\n ${green('\u2713')} Installing dependencies`);
4130
+ // 3. Write all template files by category
3882
4131
  const firstLetter = opts.shortName.charAt(0).toUpperCase();
3883
- const files = [
3884
- // Config files
4132
+ /* ── Config files ── */
4133
+ const configFiles = [
3885
4134
  ['vite.config.ts', generateViteConfig(opts)],
3886
4135
  ['tsconfig.json', generateTsconfig()],
3887
4136
  ['svelte.config.js', generateSvelteConfig(opts)],
@@ -3889,15 +4138,24 @@ async function main() {
3889
4138
  ['.prettierrc', generatePrettierrc()],
3890
4139
  ['.prettierignore', generatePrettierignore()],
3891
4140
  ['knip.json', generateKnipJson()],
3892
- ['.gitignore', generateGitignore()],
3893
- // Documentation
4141
+ ['.gitignore', generateGitignore()]
4142
+ ];
4143
+ sp = createSpinner('Config files');
4144
+ const configCount = writeGroup(configFiles, cwd, createdFiles, skippedFiles);
4145
+ sp.succeed(`Config files ${dim(`${configCount} files`)}`);
4146
+ /* ── Documentation ── */
4147
+ const docFiles = [
3894
4148
  ['README.md', generateReadme(opts)],
3895
4149
  ['ARCHITECTURE.md', generateArchitecture(opts)],
3896
- ['FRAMEWORKS.md', generateFrameworks()],
3897
- // Static assets
4150
+ ['FRAMEWORKS.md', generateFrameworks()]
4151
+ ];
4152
+ sp = createSpinner('Documentation');
4153
+ const docCount = writeGroup(docFiles, cwd, createdFiles, skippedFiles);
4154
+ sp.succeed(`Documentation ${dim(`${docCount} files`)}`);
4155
+ /* ── Static assets ── */
4156
+ const staticFiles = [
3898
4157
  ['static/manifest.json', generateManifest(opts)],
3899
4158
  ['static/offline.html', generateOfflineHtml(opts)],
3900
- // Placeholder icons
3901
4159
  ['static/icons/app.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
3902
4160
  ['static/icons/app-dark.svg', generatePlaceholderSvg('#1a1a2e', firstLetter)],
3903
4161
  ['static/icons/maskable.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
@@ -3905,16 +4163,24 @@ async function main() {
3905
4163
  ['static/icons/monochrome.svg', generateMonochromeSvg(firstLetter)],
3906
4164
  ['static/icons/splash.svg', generateSplashSvg(opts.shortName)],
3907
4165
  ['static/icons/apple-touch.svg', generatePlaceholderSvg('#6c5ce7', firstLetter)],
3908
- // Email placeholders
3909
4166
  ['static/change-email.html', generateEmailPlaceholder('Change Email')],
3910
4167
  ['static/device-verification-email.html', generateEmailPlaceholder('Device Verification')],
3911
4168
  ['static/signup-email.html', generateEmailPlaceholder('Signup Email')],
3912
- // Supabase schema
3913
- ['supabase-schema.sql', generateSupabaseSchema(opts)],
3914
- // Source files
4169
+ ['supabase-schema.sql', generateSupabaseSchema(opts)]
4170
+ ];
4171
+ sp = createSpinner('Static assets');
4172
+ const staticCount = writeGroup(staticFiles, cwd, createdFiles, skippedFiles);
4173
+ sp.succeed(`Static assets ${dim(`${staticCount} files`)}`);
4174
+ /* ── Source files ── */
4175
+ const sourceFiles = [
3915
4176
  ['src/app.html', generateAppHtml(opts)],
3916
- ['src/app.d.ts', generateAppDts(opts)],
3917
- // Route files
4177
+ ['src/app.d.ts', generateAppDts(opts)]
4178
+ ];
4179
+ sp = createSpinner('Source files');
4180
+ const sourceCount = writeGroup(sourceFiles, cwd, createdFiles, skippedFiles);
4181
+ sp.succeed(`Source files ${dim(`${sourceCount} files`)}`);
4182
+ /* ── Route files ── */
4183
+ const routeFiles = [
3918
4184
  ['src/routes/+layout.ts', generateRootLayoutTs(opts)],
3919
4185
  ['src/routes/+layout.svelte', generateRootLayoutSvelte(opts)],
3920
4186
  ['src/routes/+page.svelte', generateHomePage(opts)],
@@ -3930,39 +4196,42 @@ async function main() {
3930
4196
  ['src/routes/[...catchall]/+page.ts', generateCatchallPage()],
3931
4197
  ['src/routes/(protected)/+layout.ts', generateProtectedLayoutTs()],
3932
4198
  ['src/routes/(protected)/+layout.svelte', generateProtectedLayoutSvelte()],
3933
- ['src/routes/(protected)/profile/+page.svelte', generateProfilePage(opts)],
4199
+ ['src/routes/(protected)/profile/+page.svelte', generateProfilePage(opts)]
4200
+ ];
4201
+ sp = createSpinner('Route files');
4202
+ const routeCount = writeGroup(routeFiles, cwd, createdFiles, skippedFiles);
4203
+ sp.succeed(`Route files ${dim(`${routeCount} files`)}`);
4204
+ /* ── Library & components ── */
4205
+ const libFiles = [
3934
4206
  ['src/lib/types.ts', generateAppTypes()],
3935
- // Component files
3936
4207
  ['src/lib/components/UpdatePrompt.svelte', generateUpdatePromptComponent()]
3937
4208
  ];
3938
- for (const [relativePath, content] of files) {
3939
- writeIfMissing(join(cwd, relativePath), content, createdFiles, skippedFiles);
3940
- }
4209
+ sp = createSpinner('Library & components');
4210
+ const libCount = writeGroup(libFiles, cwd, createdFiles, skippedFiles);
4211
+ sp.succeed(`Library & components ${dim(`${libCount} files`)}`);
3941
4212
  // 4. Set up husky
3942
- console.log('Setting up husky...');
3943
- execSync('npx husky init', { stdio: 'inherit', cwd });
3944
- /* Overwrite the default pre-commit (husky init creates one with "npm test") */
4213
+ sp = createSpinner('Git hooks');
4214
+ execSync('npx husky init', { stdio: 'pipe', cwd });
3945
4215
  const preCommitPath = join(cwd, '.husky/pre-commit');
3946
4216
  writeFileSync(preCommitPath, generateHuskyPreCommit(), 'utf-8');
3947
4217
  createdFiles.push('.husky/pre-commit');
3948
- console.log(' [write] .husky/pre-commit');
3949
- // 5. Print summary
3950
- console.log(`\n\u2705 Done! Created ${createdFiles.length} files.`);
3951
- if (skippedFiles.length > 0) {
3952
- console.log(` Skipped ${skippedFiles.length} existing files.`);
3953
- }
4218
+ sp.succeed(`Git hooks ${dim('1 file')}`);
4219
+ // 5. Print final summary
4220
+ console.log();
4221
+ console.log(doubleBoxWithHeader([` ${green(bold('\u2713 Setup complete!'))} `], [
4222
+ `Created: ${bold(String(createdFiles.length))} files${' '.repeat(34 - String(createdFiles.length).length)}`,
4223
+ `Skipped: ${bold(String(skippedFiles.length))} files${' '.repeat(34 - String(skippedFiles.length).length)}`
4224
+ ]));
3954
4225
  console.log(`
3955
- Next steps:
3956
- 1. Set up Supabase and add .env with PUBLIC_SUPABASE_PUBLISHABLE_DEFAULT_KEY and PUBLIC_SUPABASE_URL
3957
- 2. Run supabase-schema.sql in the Supabase SQL Editor
3958
- 3. Add your app icons: static/favicon.png, static/icon-192.png, static/icon-512.png
3959
- 4. Start building: npm run dev`);
4226
+ ${bold('Next steps:')}
4227
+ 1. Set up Supabase and add .env with your keys
4228
+ 2. Run supabase-schema.sql in Supabase SQL Editor
4229
+ 3. Add app icons in static/icons/
4230
+ 4. Start building: ${cyan('npm run dev')}
4231
+ `);
3960
4232
  }
3961
4233
  // =============================================================================
3962
4234
  // RUN
3963
4235
  // =============================================================================
3964
- main().catch((err) => {
3965
- console.error('Error:', err);
3966
- process.exit(1);
3967
- });
4236
+ routeCommand();
3968
4237
  //# sourceMappingURL=install-pwa.js.map