@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 +11 -9
- package/dist/bin/install-pwa.d.ts +4 -2
- package/dist/bin/install-pwa.d.ts.map +1 -1
- package/dist/bin/install-pwa.js +353 -84
- package/dist/bin/install-pwa.js.map +1 -1
- package/dist/data.d.ts +105 -0
- package/dist/data.d.ts.map +1 -1
- package/dist/data.js +126 -0
- package/dist/data.js.map +1 -1
- package/dist/entries/stores.d.ts +2 -0
- package/dist/entries/stores.d.ts.map +1 -1
- package/dist/entries/stores.js +7 -0
- package/dist/entries/stores.js.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +15 -0
- package/dist/index.js.map +1 -1
- package/dist/stores/factories.d.ts +138 -0
- package/dist/stores/factories.d.ts.map +1 -0
- package/dist/stores/factories.js +154 -0
- package/dist/stores/factories.js.map +1 -0
- package/package.json +1 -1
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
|
|
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
|
-
###
|
|
155
|
+
### Interactive Prompts
|
|
154
156
|
|
|
155
|
-
|
|
|
156
|
-
|
|
157
|
-
|
|
|
158
|
-
|
|
|
159
|
-
|
|
|
160
|
-
|
|
|
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
|
|
19
|
+
* stellar-engine install pwa
|
|
18
20
|
* ```
|
|
19
21
|
*
|
|
20
22
|
* @see {@link main} for the entry point
|
|
21
|
-
* @see {@link
|
|
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
|
|
1
|
+
{"version":3,"file":"install-pwa.d.ts","sourceRoot":"","sources":["../../src/bin/install-pwa.ts"],"names":[],"mappings":";AAEA;;;;;;;;;;;;;;;;;;;;;;;GAuBG"}
|
package/dist/bin/install-pwa.js
CHANGED
|
@@ -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
|
|
19
|
+
* stellar-engine install pwa
|
|
18
20
|
* ```
|
|
19
21
|
*
|
|
20
22
|
* @see {@link main} for the entry point
|
|
21
|
-
* @see {@link
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
171
|
+
// INTERACTIVE SETUP
|
|
59
172
|
// =============================================================================
|
|
60
173
|
/**
|
|
61
|
-
*
|
|
174
|
+
* Promisified readline question helper.
|
|
62
175
|
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
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
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
195
|
+
* @throws {SystemExit} Exits with code 0 if the user declines to proceed.
|
|
72
196
|
*/
|
|
73
|
-
function
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3884
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3939
|
-
|
|
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
|
-
|
|
3943
|
-
execSync('npx husky init', { stdio: '
|
|
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
|
-
|
|
3949
|
-
// 5. Print summary
|
|
3950
|
-
console.log(
|
|
3951
|
-
|
|
3952
|
-
|
|
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
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
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
|
-
|
|
3965
|
-
console.error('Error:', err);
|
|
3966
|
-
process.exit(1);
|
|
3967
|
-
});
|
|
4236
|
+
routeCommand();
|
|
3968
4237
|
//# sourceMappingURL=install-pwa.js.map
|