@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.
- package/README.md +55 -1
- package/dist/bin/install-pwa.js +234 -317
- package/dist/bin/install-pwa.js.map +1 -1
- package/dist/config.d.ts +11 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +8 -2
- package/dist/config.js.map +1 -1
- package/dist/crdt/awareness.d.ts +128 -0
- package/dist/crdt/awareness.d.ts.map +1 -0
- package/dist/crdt/awareness.js +284 -0
- package/dist/crdt/awareness.js.map +1 -0
- package/dist/crdt/channel.d.ts +165 -0
- package/dist/crdt/channel.d.ts.map +1 -0
- package/dist/crdt/channel.js +522 -0
- package/dist/crdt/channel.js.map +1 -0
- package/dist/crdt/config.d.ts +58 -0
- package/dist/crdt/config.d.ts.map +1 -0
- package/dist/crdt/config.js +123 -0
- package/dist/crdt/config.js.map +1 -0
- package/dist/crdt/helpers.d.ts +104 -0
- package/dist/crdt/helpers.d.ts.map +1 -0
- package/dist/crdt/helpers.js +116 -0
- package/dist/crdt/helpers.js.map +1 -0
- package/dist/crdt/offline.d.ts +58 -0
- package/dist/crdt/offline.d.ts.map +1 -0
- package/dist/crdt/offline.js +130 -0
- package/dist/crdt/offline.js.map +1 -0
- package/dist/crdt/persistence.d.ts +65 -0
- package/dist/crdt/persistence.d.ts.map +1 -0
- package/dist/crdt/persistence.js +171 -0
- package/dist/crdt/persistence.js.map +1 -0
- package/dist/crdt/provider.d.ts +109 -0
- package/dist/crdt/provider.d.ts.map +1 -0
- package/dist/crdt/provider.js +543 -0
- package/dist/crdt/provider.js.map +1 -0
- package/dist/crdt/store.d.ts +111 -0
- package/dist/crdt/store.d.ts.map +1 -0
- package/dist/crdt/store.js +158 -0
- package/dist/crdt/store.js.map +1 -0
- package/dist/crdt/types.d.ts +281 -0
- package/dist/crdt/types.d.ts.map +1 -0
- package/dist/crdt/types.js +26 -0
- package/dist/crdt/types.js.map +1 -0
- package/dist/database.d.ts +1 -1
- package/dist/database.d.ts.map +1 -1
- package/dist/database.js +28 -7
- package/dist/database.js.map +1 -1
- package/dist/diagnostics.d.ts +75 -0
- package/dist/diagnostics.d.ts.map +1 -1
- package/dist/diagnostics.js +114 -2
- package/dist/diagnostics.js.map +1 -1
- package/dist/engine.d.ts.map +1 -1
- package/dist/engine.js +21 -1
- package/dist/engine.js.map +1 -1
- package/dist/entries/crdt.d.ts +32 -0
- package/dist/entries/crdt.d.ts.map +1 -0
- package/dist/entries/crdt.js +52 -0
- package/dist/entries/crdt.js.map +1 -0
- package/dist/entries/types.d.ts +1 -0
- package/dist/entries/types.d.ts.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/package.json +9 -2
- package/dist/operations.d.ts +0 -73
- package/dist/operations.d.ts.map +0 -1
- package/dist/operations.js +0 -227
- package/dist/operations.js.map +0 -1
- package/dist/reconnectHandler.d.ts +0 -16
- package/dist/reconnectHandler.d.ts.map +0 -1
- package/dist/reconnectHandler.js +0 -21
- package/dist/reconnectHandler.js.map +0 -1
package/dist/bin/install-pwa.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
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 = {
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
4595
|
+
s.start('Writing package.json...');
|
|
4682
4596
|
writeIfMissing(join(cwd, 'package.json'), generatePackageJson(opts), createdFiles, skippedFiles, true);
|
|
4683
|
-
|
|
4597
|
+
s.stop('package.json ready');
|
|
4684
4598
|
// 2. Run npm install
|
|
4685
|
-
|
|
4686
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4693
|
-
const
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4715
|
-
|
|
4716
|
-
|
|
4717
|
-
|
|
4718
|
-
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
|
|
4728
|
-
|
|
4729
|
-
|
|
4730
|
-
|
|
4731
|
-
|
|
4732
|
-
|
|
4733
|
-
|
|
4734
|
-
|
|
4735
|
-
|
|
4736
|
-
|
|
4737
|
-
|
|
4738
|
-
|
|
4739
|
-
|
|
4740
|
-
|
|
4741
|
-
|
|
4742
|
-
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
4747
|
-
|
|
4748
|
-
|
|
4749
|
-
|
|
4750
|
-
|
|
4751
|
-
|
|
4752
|
-
|
|
4753
|
-
|
|
4754
|
-
|
|
4755
|
-
|
|
4756
|
-
|
|
4757
|
-
|
|
4758
|
-
|
|
4759
|
-
|
|
4760
|
-
|
|
4761
|
-
|
|
4762
|
-
|
|
4763
|
-
|
|
4764
|
-
|
|
4765
|
-
|
|
4766
|
-
|
|
4767
|
-
|
|
4768
|
-
|
|
4769
|
-
|
|
4770
|
-
|
|
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
|
-
|
|
4773
|
-
|
|
4774
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4784
|
-
|
|
4785
|
-
|
|
4786
|
-
|
|
4787
|
-
|
|
4788
|
-
|
|
4789
|
-
|
|
4790
|
-
|
|
4791
|
-
|
|
4792
|
-
|
|
4793
|
-
|
|
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
|