create-paratix 0.0.1 → 0.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/dist/index.js ADDED
@@ -0,0 +1,1183 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/index.ts
4
+ import { existsSync, mkdirSync, writeFileSync } from "fs";
5
+ import { basename as basename2, join as join2, resolve } from "path";
6
+
7
+ // src/interactivePrompts.ts
8
+ import { createInterface } from "readline/promises";
9
+
10
+ // src/hostFingerprintBootstrap.ts
11
+ import { createHash } from "crypto";
12
+ import { Client } from "ssh2";
13
+ var DEFAULT_HOST_FINGERPRINT_PORT = 22;
14
+ var DEFAULT_READY_TIMEOUT_MS = 1e4;
15
+ function computeFingerprint(key) {
16
+ const hash = createHash("sha256").update(key).digest("base64");
17
+ return `SHA256:${hash.replaceAll("=", "")}`;
18
+ }
19
+ function toError(error, host, port) {
20
+ const message = error instanceof Error ? error.message : String(error);
21
+ return new Error(`Failed to read the host key from ${host}:${port}: ${message}`);
22
+ }
23
+ function createConnectionConfig(parameters) {
24
+ const { captureFingerprint, host, port, readyTimeoutMs } = parameters;
25
+ return {
26
+ host,
27
+ hostVerifier: (key) => {
28
+ captureFingerprint(computeFingerprint(Buffer.from(key)));
29
+ return false;
30
+ },
31
+ port,
32
+ readyTimeout: readyTimeoutMs,
33
+ username: "paratix-hostkey-scan"
34
+ };
35
+ }
36
+ function resolveBootstrapOptions(options) {
37
+ const clientFactory = options.clientFactory ?? (() => new Client());
38
+ return {
39
+ client: clientFactory(),
40
+ port: options.port ?? DEFAULT_HOST_FINGERPRINT_PORT,
41
+ readyTimeoutMs: options.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS
42
+ };
43
+ }
44
+ function cleanupClient(client) {
45
+ client.removeAllListeners();
46
+ try {
47
+ client.end();
48
+ } catch {
49
+ }
50
+ }
51
+ function createSettlementHandlers(parameters) {
52
+ const { cleanup, host, port, reject, resolve: resolve2 } = parameters;
53
+ let settled = false;
54
+ return {
55
+ rejectOnce: (error) => {
56
+ if (settled) return;
57
+ settled = true;
58
+ cleanup();
59
+ reject(toError(error, host, port));
60
+ },
61
+ resolveOnce: (fingerprint) => {
62
+ if (settled) return;
63
+ settled = true;
64
+ cleanup();
65
+ resolve2(fingerprint);
66
+ }
67
+ };
68
+ }
69
+ function registerFingerprintListeners(parameters) {
70
+ const { client, onFingerprint, rejectOnce, resolveOnce } = parameters;
71
+ client.on("close", () => {
72
+ const capturedFingerprint = onFingerprint();
73
+ if (capturedFingerprint != null) {
74
+ resolveOnce(capturedFingerprint);
75
+ }
76
+ });
77
+ client.on("error", (error) => {
78
+ const capturedFingerprint = onFingerprint();
79
+ if (capturedFingerprint != null) {
80
+ resolveOnce(capturedFingerprint);
81
+ return;
82
+ }
83
+ rejectOnce(error);
84
+ });
85
+ }
86
+ async function readFingerprintFromClient(client, parameters) {
87
+ const { host, port, readyTimeoutMs } = parameters;
88
+ let capturedFingerprint = null;
89
+ return new Promise((resolve2, reject) => {
90
+ const cleanup = () => {
91
+ cleanupClient(client);
92
+ };
93
+ const { rejectOnce, resolveOnce } = createSettlementHandlers({
94
+ cleanup,
95
+ host,
96
+ port,
97
+ reject,
98
+ resolve: resolve2
99
+ });
100
+ registerFingerprintListeners({
101
+ client,
102
+ onFingerprint: () => capturedFingerprint,
103
+ rejectOnce,
104
+ resolveOnce
105
+ });
106
+ try {
107
+ client.connect(
108
+ createConnectionConfig({
109
+ captureFingerprint: (fingerprint) => {
110
+ capturedFingerprint = fingerprint;
111
+ },
112
+ host,
113
+ port,
114
+ readyTimeoutMs
115
+ })
116
+ );
117
+ } catch (error) {
118
+ rejectOnce(error);
119
+ }
120
+ });
121
+ }
122
+ async function readHostFingerprintViaSsh2(host, options = {}) {
123
+ const { client, port, readyTimeoutMs } = resolveBootstrapOptions(options);
124
+ return readFingerprintFromClient(client, { host, port, readyTimeoutMs });
125
+ }
126
+
127
+ // src/promptUi.ts
128
+ import { emitKeypressEvents } from "readline";
129
+ function createSelectLines(prompt, options, selectedIndex) {
130
+ return [
131
+ prompt,
132
+ "",
133
+ "Use the arrow keys to choose an option:",
134
+ ...options.flatMap((option, index) => {
135
+ const prefix = index === selectedIndex ? ">" : " ";
136
+ return [`${prefix} ${option.label}`, ` ${option.description}`];
137
+ }),
138
+ "",
139
+ "Press Enter to confirm."
140
+ ];
141
+ }
142
+ function redrawSelect(lines, renderedLines) {
143
+ if (renderedLines > 0) {
144
+ for (let index = 0; index < renderedLines; index++) {
145
+ process.stdout.write("\x1B[1A\x1B[2K");
146
+ }
147
+ }
148
+ process.stdout.write(`${lines.join("\n")}
149
+ `);
150
+ return lines.length;
151
+ }
152
+ function ensureInteractiveTerminal() {
153
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
154
+ throw new Error(
155
+ "Interactive selection requires a TTY. Use --initial-user <root|name> in non-interactive environments."
156
+ );
157
+ }
158
+ }
159
+ function prepareSelectInput() {
160
+ emitKeypressEvents(process.stdin);
161
+ const previousRawMode = process.stdin.isRaw;
162
+ process.stdin.setRawMode(true);
163
+ process.stdin.resume();
164
+ return { previousRawMode };
165
+ }
166
+ function cleanupSelectInput(previousRawMode) {
167
+ process.stdin.setRawMode(previousRawMode ?? false);
168
+ process.stdin.pause();
169
+ process.stdout.write("\x1B[?25h");
170
+ }
171
+ function createSelectRenderer(prompt, options) {
172
+ let renderedLines = 0;
173
+ let selectedIndex = 0;
174
+ return {
175
+ moveDown: () => {
176
+ selectedIndex = (selectedIndex + 1) % options.length;
177
+ },
178
+ moveUp: () => {
179
+ selectedIndex = (selectedIndex - 1 + options.length) % options.length;
180
+ },
181
+ render: () => {
182
+ renderedLines = redrawSelect(createSelectLines(prompt, options, selectedIndex), renderedLines);
183
+ },
184
+ selectedValue: () => options[selectedIndex].value
185
+ };
186
+ }
187
+ function finishSelection(cleanup, resolver, value) {
188
+ cleanup();
189
+ resolver(value);
190
+ }
191
+ function handleNavigationKey(keyName, renderer) {
192
+ if (keyName === "down") {
193
+ renderer.moveDown();
194
+ renderer.render();
195
+ return true;
196
+ }
197
+ if (keyName === "up") {
198
+ renderer.moveUp();
199
+ renderer.render();
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+ function handleConfirmationKey(keyName, renderer, confirmSelection) {
205
+ if (keyName !== "enter" && keyName !== "return") {
206
+ return false;
207
+ }
208
+ process.stdout.write("\n");
209
+ confirmSelection(renderer.selectedValue());
210
+ return true;
211
+ }
212
+ function handleCancelKey(key, cleanup, reject) {
213
+ if (key.ctrl && key.name === "c") {
214
+ process.stdout.write("\n");
215
+ cleanup();
216
+ reject(new Error("Prompt cancelled."));
217
+ }
218
+ }
219
+ async function runTerminalSelect(prompt, options) {
220
+ ensureInteractiveTerminal();
221
+ const { previousRawMode } = prepareSelectInput();
222
+ const renderer = createSelectRenderer(prompt, options);
223
+ return new Promise((resolve2, reject) => {
224
+ const cleanup = () => {
225
+ process.stdin.removeListener("keypress", onKeypress);
226
+ cleanupSelectInput(previousRawMode);
227
+ };
228
+ const onKeypress = (_character, key) => {
229
+ if (handleNavigationKey(key.name, renderer)) {
230
+ return;
231
+ }
232
+ if (handleConfirmationKey(key.name, renderer, (value) => {
233
+ finishSelection(cleanup, resolve2, value);
234
+ })) {
235
+ return;
236
+ }
237
+ handleCancelKey(key, cleanup, reject);
238
+ };
239
+ process.stdout.write("\x1B[?25l");
240
+ renderer.render();
241
+ process.stdin.on("keypress", onKeypress);
242
+ });
243
+ }
244
+ function createTerminalSelect() {
245
+ return {
246
+ close: () => void 0,
247
+ select: async (prompt, options) => runTerminalSelect(prompt, options)
248
+ };
249
+ }
250
+
251
+ // src/publicKeySelection.ts
252
+ import { readdirSync, readFileSync } from "fs";
253
+ import { homedir } from "os";
254
+ import { basename, join } from "path";
255
+ var PUBLIC_KEY_PROMPT_OPTIONS = [
256
+ {
257
+ description: "Read a public key from ~/.ssh and embed it directly into server.ts for the bootstrap admin user.",
258
+ label: "Use local public key",
259
+ value: "local"
260
+ },
261
+ {
262
+ description: "Keep the placeholder in server.ts and paste your public key manually before the first apply.",
263
+ label: "Keep placeholder",
264
+ value: "placeholder"
265
+ }
266
+ ];
267
+ var supportedOpenSshAlgorithms = /* @__PURE__ */ new Set([
268
+ "ecdsa-sha2-nistp256",
269
+ "ecdsa-sha2-nistp384",
270
+ "ecdsa-sha2-nistp521",
271
+ "sk-ecdsa-sha2-nistp256@openssh.com",
272
+ "sk-ssh-ed25519@openssh.com",
273
+ "ssh-ed25519",
274
+ "ssh-rsa"
275
+ ]);
276
+ function parseOpenSshPublicKey(value) {
277
+ if (value.length === 0 || value.includes("\n")) {
278
+ return null;
279
+ }
280
+ const parts = value.split(new RegExp("\\s+", "v"));
281
+ if (parts.length < 2) {
282
+ return null;
283
+ }
284
+ const algorithm = parts[0];
285
+ const encodedKey = parts[1];
286
+ if (!supportedOpenSshAlgorithms.has(algorithm)) {
287
+ return null;
288
+ }
289
+ return {
290
+ algorithm,
291
+ encodedKey
292
+ };
293
+ }
294
+ function trimBase64Padding(value) {
295
+ let endIndex = value.length;
296
+ while (endIndex > 0 && value[endIndex - 1] === "=") {
297
+ endIndex--;
298
+ }
299
+ return value.slice(0, endIndex);
300
+ }
301
+ function isBase64AlphaNumeric(character) {
302
+ return character >= "A" && character <= "Z" || character >= "a" && character <= "z" || character >= "0" && character <= "9";
303
+ }
304
+ function isBase64DataCharacter(character) {
305
+ return isBase64AlphaNumeric(character) || character === "+" || character === "/";
306
+ }
307
+ function updatePaddingState(character, state) {
308
+ if (character !== "=") {
309
+ return null;
310
+ }
311
+ const nextState = {
312
+ paddingCount: state.paddingCount + 1,
313
+ sawPadding: true
314
+ };
315
+ return nextState.paddingCount <= 2 ? nextState : null;
316
+ }
317
+ function hasValidBase64Alphabet(value) {
318
+ if (value.length === 0) {
319
+ return false;
320
+ }
321
+ const state = { paddingCount: 0, sawPadding: false };
322
+ for (const character of value) {
323
+ if (isBase64DataCharacter(character)) {
324
+ if (state.sawPadding) {
325
+ return false;
326
+ }
327
+ continue;
328
+ }
329
+ const nextState = updatePaddingState(character, state);
330
+ if (nextState != null) {
331
+ state.paddingCount = nextState.paddingCount;
332
+ state.sawPadding = nextState.sawPadding;
333
+ continue;
334
+ }
335
+ return false;
336
+ }
337
+ return true;
338
+ }
339
+ function isCanonicalBase64(value) {
340
+ if (!hasValidBase64Alphabet(value)) {
341
+ return false;
342
+ }
343
+ try {
344
+ const decoded = Buffer.from(value, "base64");
345
+ if (decoded.length === 0) {
346
+ return false;
347
+ }
348
+ const normalizedValue = trimBase64Padding(value);
349
+ const encodedAgain = trimBase64Padding(decoded.toString("base64"));
350
+ return encodedAgain === normalizedValue;
351
+ } catch {
352
+ return false;
353
+ }
354
+ }
355
+ function isValidAdminPublicKey(value) {
356
+ const parsedKey = parseOpenSshPublicKey(value.trim());
357
+ return parsedKey != null && isCanonicalBase64(parsedKey.encodedKey);
358
+ }
359
+ function validateAdminPublicKey(exitWithMessage2, value, optionName = "--admin-public-key") {
360
+ const normalizedValue = value.trim();
361
+ if (!isValidAdminPublicKey(normalizedValue)) {
362
+ exitWithMessage2(
363
+ `Error: Invalid value for "${optionName}" \u2014 provide a valid single-line OpenSSH public key.`
364
+ );
365
+ }
366
+ return normalizedValue;
367
+ }
368
+ function readAdminPublicKeyFile(exitWithMessage2, path) {
369
+ let value;
370
+ try {
371
+ value = readFileSync(path, "utf8");
372
+ } catch (error) {
373
+ const message = error instanceof Error ? error.message : String(error);
374
+ exitWithMessage2(`Error: Failed to read "--admin-public-key-file" from "${path}": ${message}`);
375
+ }
376
+ return validateAdminPublicKey(exitWithMessage2, value, "--admin-public-key-file");
377
+ }
378
+ function discoverLocalPublicKeys(sshDirectory = join(homedir(), ".ssh")) {
379
+ try {
380
+ return readdirSync(sshDirectory).filter((entry) => entry.endsWith(".pub")).sort((left, right) => left.localeCompare(right)).flatMap((entry) => {
381
+ const path = join(sshDirectory, entry);
382
+ try {
383
+ const key = readFileSync(path, "utf8").trim();
384
+ if (!isValidAdminPublicKey(key)) {
385
+ return [];
386
+ }
387
+ return [{ key, label: basename(entry), path }];
388
+ } catch {
389
+ return [];
390
+ }
391
+ });
392
+ } catch {
393
+ return [];
394
+ }
395
+ }
396
+ function createPublicKeyOptions(publicKeys) {
397
+ return publicKeys.map((publicKey) => ({
398
+ description: publicKey.path,
399
+ label: publicKey.label,
400
+ value: publicKey.path
401
+ }));
402
+ }
403
+ async function promptForAdminPublicKey(select, publicKeys = discoverLocalPublicKeys()) {
404
+ const publicKeyMode = await select(
405
+ "How should create-paratix configure the admin SSH public key?",
406
+ PUBLIC_KEY_PROMPT_OPTIONS
407
+ );
408
+ if (publicKeyMode !== "local") {
409
+ return void 0;
410
+ }
411
+ if (publicKeys.length === 0) {
412
+ console.error(
413
+ "No readable public keys were found in ~/.ssh. Keeping the placeholder in server.ts."
414
+ );
415
+ return void 0;
416
+ }
417
+ if (publicKeys.length === 1) {
418
+ return publicKeys[0]?.key;
419
+ }
420
+ const selectedPath = await select(
421
+ "Select the public key to embed into server.ts:",
422
+ createPublicKeyOptions(publicKeys)
423
+ );
424
+ return publicKeys.find((publicKey) => publicKey.path === selectedPath)?.key;
425
+ }
426
+
427
+ // src/scaffoldConfig.ts
428
+ var CLI_USAGE = "Usage: create-paratix <project-name> [--host <domain-or-ip>] [--initial-user <root|name>] [--admin-public-key <ssh-public-key>] [--admin-public-key-file <path>]";
429
+ function normalizeInitialUserName(name) {
430
+ return name.trim();
431
+ }
432
+ function isValidInitialUserName(name) {
433
+ return new RegExp("^(?:root|[a-z_][a-z0-9_\\x2d]*\\$?)$", "v").test(name);
434
+ }
435
+ function parseInitialUserConfig(exitWithMessage2, value) {
436
+ const normalizedValue = normalizeInitialUserName(value);
437
+ if (!isValidInitialUserName(normalizedValue)) {
438
+ exitWithMessage2(
439
+ `Error: Invalid initial user "${value}" \u2014 use "root" or a valid lowercase Linux username.`
440
+ );
441
+ }
442
+ return normalizedValue === "root" ? { kind: "root" } : { kind: "admin", user: normalizedValue };
443
+ }
444
+ function normalizeHost(value) {
445
+ return value.trim();
446
+ }
447
+ function isValidHost(value) {
448
+ const normalizedValue = normalizeHost(value);
449
+ return normalizedValue.length > 0 && !new RegExp("\\s", "v").test(normalizedValue);
450
+ }
451
+ function validateHost(exitWithMessage2, value) {
452
+ const normalizedValue = normalizeHost(value);
453
+ if (!isValidHost(normalizedValue)) {
454
+ exitWithMessage2(
455
+ `Error: Invalid host "${value}" \u2014 use a domain name, IPv4, or IPv6 address without spaces.`
456
+ );
457
+ }
458
+ return normalizedValue;
459
+ }
460
+ async function promptForHost(prompt) {
461
+ for (; ; ) {
462
+ const host = normalizeHost(await prompt("Server host (domain or IP): "));
463
+ if (isValidHost(host)) {
464
+ return host;
465
+ }
466
+ console.error("Error: Please enter a domain name, IPv4, or IPv6 address without spaces.");
467
+ }
468
+ }
469
+ function parseArgumentValue(argv, index, parameters) {
470
+ const value = argv.at(index + 1);
471
+ if (value == null || value.startsWith("--")) {
472
+ parameters.exitWithMessage(`Error: Missing value for "${parameters.optionName}".`);
473
+ }
474
+ return value;
475
+ }
476
+ function handleUnknownOption(argument, exitWithMessage2) {
477
+ if (argument === "--bootstrap-root") {
478
+ exitWithMessage2('Error: "--bootstrap-root" was removed. Use "--initial-user root" instead.');
479
+ }
480
+ exitWithMessage2(`Error: Unknown option "${argument}".`);
481
+ }
482
+ function parseOptionAssignment(parameters) {
483
+ if (parameters.argument === "--host") {
484
+ return {
485
+ host: parseArgumentValue(parameters.argv, parameters.index, {
486
+ exitWithMessage: parameters.exitWithMessage,
487
+ optionName: "--host"
488
+ })
489
+ };
490
+ }
491
+ if (parameters.argument === "--initial-user") {
492
+ return {
493
+ initialUser: parseArgumentValue(parameters.argv, parameters.index, {
494
+ exitWithMessage: parameters.exitWithMessage,
495
+ optionName: "--initial-user"
496
+ })
497
+ };
498
+ }
499
+ if (parameters.argument === "--admin-public-key") {
500
+ return {
501
+ adminPublicKey: parseArgumentValue(parameters.argv, parameters.index, {
502
+ exitWithMessage: parameters.exitWithMessage,
503
+ optionName: "--admin-public-key"
504
+ })
505
+ };
506
+ }
507
+ if (parameters.argument === "--admin-public-key-file") {
508
+ return {
509
+ adminPublicKeyFile: parseArgumentValue(parameters.argv, parameters.index, {
510
+ exitWithMessage: parameters.exitWithMessage,
511
+ optionName: "--admin-public-key-file"
512
+ })
513
+ };
514
+ }
515
+ return null;
516
+ }
517
+ function parseCliArguments(argv, exitWithMessage2) {
518
+ const parsed = {
519
+ adminPublicKey: void 0,
520
+ adminPublicKeyFile: void 0,
521
+ host: void 0,
522
+ initialUser: void 0,
523
+ projectName: void 0
524
+ };
525
+ for (let index = 0; index < argv.length; index++) {
526
+ const argument = argv[index];
527
+ const optionAssignment = parseOptionAssignment({ argument, argv, exitWithMessage: exitWithMessage2, index });
528
+ if (optionAssignment != null) {
529
+ Object.assign(parsed, optionAssignment);
530
+ index++;
531
+ continue;
532
+ }
533
+ parsed.projectName = handlePositionalOrUnknownArgument(
534
+ parsed.projectName,
535
+ argument,
536
+ exitWithMessage2
537
+ );
538
+ }
539
+ validatePublicKeyOptions(parsed, exitWithMessage2);
540
+ return parsed;
541
+ }
542
+ function handlePositionalOrUnknownArgument(projectName, argument, exitWithMessage2) {
543
+ if (argument.startsWith("--")) {
544
+ handleUnknownOption(argument, exitWithMessage2);
545
+ }
546
+ if (projectName == null) {
547
+ return argument;
548
+ }
549
+ exitWithMessage2(CLI_USAGE);
550
+ }
551
+ function validatePublicKeyOptions(parsed, exitWithMessage2) {
552
+ if (parsed.adminPublicKey == null || parsed.adminPublicKeyFile == null) {
553
+ return;
554
+ }
555
+ exitWithMessage2('Error: Use either "--admin-public-key" or "--admin-public-key-file", not both.');
556
+ }
557
+
558
+ // src/interactivePrompts.ts
559
+ var NOOP = () => void 0;
560
+ var INTERACTIVE_SELECTION_UNAVAILABLE = "Interactive selection is unavailable.";
561
+ var UNAVAILABLE_SELECT = (() => {
562
+ throw new Error(INTERACTIVE_SELECTION_UNAVAILABLE);
563
+ });
564
+ var INITIAL_USER_OPTIONS = [
565
+ {
566
+ description: "Fresh server with SSH access only as root. Paratix bootstraps a dedicated admin user first.",
567
+ label: "Root user",
568
+ value: "root"
569
+ },
570
+ {
571
+ description: "A named admin user already exists. Paratix connects directly as that user and skips root bootstrap.",
572
+ label: "Admin user",
573
+ value: "admin"
574
+ }
575
+ ];
576
+ var HOST_FINGERPRINT_OPTIONS = [
577
+ {
578
+ description: "Read the currently presented host key from SSH port 22 via ssh2 and pin its fingerprint in server.ts.",
579
+ label: "Scan host key",
580
+ value: "scan"
581
+ },
582
+ {
583
+ description: "Keep the expectedHostFingerprint placeholder in server.ts and verify the host key manually later.",
584
+ label: "Keep placeholder",
585
+ value: "placeholder"
586
+ }
587
+ ];
588
+ function createTerminalPrompt() {
589
+ const readline = createInterface({ input: process.stdin, output: process.stdout });
590
+ return {
591
+ close: () => {
592
+ readline.close();
593
+ },
594
+ prompt: async (question) => readline.question(question)
595
+ };
596
+ }
597
+ function createPromptSession(prompt) {
598
+ const terminalPrompt = prompt == null ? createTerminalPrompt() : null;
599
+ const terminalSelect = prompt == null ? createTerminalSelect() : null;
600
+ if (terminalPrompt != null && terminalSelect != null) {
601
+ return {
602
+ ask: terminalPrompt.prompt,
603
+ chooseInitialUser: terminalSelect.select,
604
+ closePrompt: terminalPrompt.close,
605
+ closeSelect: terminalSelect.close
606
+ };
607
+ }
608
+ if (prompt == null) {
609
+ throw new Error("Interactive prompt is unavailable.");
610
+ }
611
+ return {
612
+ ask: prompt,
613
+ chooseInitialUser: UNAVAILABLE_SELECT,
614
+ closePrompt: NOOP,
615
+ closeSelect: NOOP
616
+ };
617
+ }
618
+ async function promptForAdminUser(ask, closePrompt) {
619
+ for (; ; ) {
620
+ const adminUser = normalizeInitialUserName(await ask("Admin username: "));
621
+ if (isValidInitialUserName(adminUser) && adminUser !== "root") {
622
+ closePrompt();
623
+ return { kind: "admin", user: adminUser };
624
+ }
625
+ console.error(
626
+ 'Error: Invalid admin username. Use a valid lowercase Linux username other than "root".'
627
+ );
628
+ }
629
+ }
630
+ async function promptForHost2(prompt, closePrompt = NOOP) {
631
+ if (prompt != null) {
632
+ try {
633
+ return await promptForHost(prompt);
634
+ } finally {
635
+ closePrompt();
636
+ }
637
+ }
638
+ const terminalPrompt = createTerminalPrompt();
639
+ try {
640
+ return await promptForHost(terminalPrompt.prompt);
641
+ } finally {
642
+ terminalPrompt.close();
643
+ }
644
+ }
645
+ async function promptForInitialUserConfig(prompt, select) {
646
+ const promptSession = createPromptSession(prompt);
647
+ const chooseInitialUser = select ?? promptSession.chooseInitialUser;
648
+ try {
649
+ const initialUserType = await chooseInitialUser(
650
+ "Which SSH user already works for the first connection to this server?",
651
+ INITIAL_USER_OPTIONS
652
+ );
653
+ if (initialUserType === "root") {
654
+ promptSession.closeSelect();
655
+ promptSession.closePrompt();
656
+ return { kind: "root" };
657
+ }
658
+ return await promptForAdminUser(promptSession.ask, promptSession.closePrompt);
659
+ } finally {
660
+ promptSession.closeSelect();
661
+ }
662
+ }
663
+ async function promptForAdminPublicKey2(select, publicKeys) {
664
+ const terminalSelect = select == null ? createTerminalSelect() : null;
665
+ const choose = select ?? terminalSelect?.select;
666
+ if (choose == null) {
667
+ throw new Error(INTERACTIVE_SELECTION_UNAVAILABLE);
668
+ }
669
+ try {
670
+ return await promptForAdminPublicKey(choose, publicKeys);
671
+ } finally {
672
+ terminalSelect?.close();
673
+ }
674
+ }
675
+ async function promptForHostFingerprint(host, select, scanner = readHostFingerprintViaSsh2) {
676
+ const terminalSelect = select == null ? createTerminalSelect() : null;
677
+ const choose = select ?? terminalSelect?.select;
678
+ if (choose == null) {
679
+ throw new Error(INTERACTIVE_SELECTION_UNAVAILABLE);
680
+ }
681
+ try {
682
+ const hostKeyMode = await choose(
683
+ `How should create-paratix bootstrap the SSH host key for ${host}?`,
684
+ HOST_FINGERPRINT_OPTIONS
685
+ );
686
+ if (hostKeyMode !== "scan") {
687
+ return void 0;
688
+ }
689
+ try {
690
+ return await scanner(host);
691
+ } catch (error) {
692
+ console.error(
693
+ `${error instanceof Error ? error.message : String(error)} Keeping the expectedHostFingerprint placeholder in server.ts.`
694
+ );
695
+ return void 0;
696
+ }
697
+ } finally {
698
+ terminalSelect?.close();
699
+ }
700
+ }
701
+
702
+ // src/scaffoldRuntime.ts
703
+ import { execSync } from "child_process";
704
+ var MS_PER_MINUTE = 6e4;
705
+ var INSTALL_TIMEOUT_MS = 12e4;
706
+ function detectPackageManager() {
707
+ const agent = process.env.npm_config_user_agent ?? "";
708
+ if (agent.startsWith("pnpm")) {
709
+ return { command: "pnpm install", name: "pnpm" };
710
+ }
711
+ if (agent.startsWith("yarn")) {
712
+ return { command: "yarn install", name: "yarn" };
713
+ }
714
+ if (agent.startsWith("bun")) {
715
+ return { command: "bun install", name: "bun" };
716
+ }
717
+ return { command: "npm install", name: "npm" };
718
+ }
719
+ function installDependencies(projectDirectory, pm) {
720
+ console.log(`Installing dependencies with ${pm.name}...`);
721
+ try {
722
+ execSync(pm.command, { cwd: projectDirectory, stdio: "inherit", timeout: INSTALL_TIMEOUT_MS });
723
+ return true;
724
+ } catch (error) {
725
+ if (error instanceof Error && "signal" in error && error.signal === "SIGTERM") {
726
+ console.error(
727
+ `Installation timed out after ${Math.round(INSTALL_TIMEOUT_MS / MS_PER_MINUTE)} minutes.`
728
+ );
729
+ } else {
730
+ const message = error instanceof Error ? error.message : String(error);
731
+ console.error(`Failed to install dependencies: ${message}`);
732
+ }
733
+ console.error("Run install manually.");
734
+ return false;
735
+ }
736
+ }
737
+ function getCommandPrefix(pm) {
738
+ return pm.name === "npm" ? "npm run" : pm.name;
739
+ }
740
+ function printSuccessMessage(projectName, pm) {
741
+ const prefix = getCommandPrefix(pm);
742
+ console.log(`
743
+ Project created successfully!
744
+
745
+ cd ${projectName}
746
+
747
+ Edit server.ts with your server details, then:
748
+
749
+ ${prefix} apply:dry
750
+ ${prefix} apply
751
+ `);
752
+ }
753
+ function printPartialSuccessMessage(projectName, pm) {
754
+ const prefix = getCommandPrefix(pm);
755
+ console.log(`
756
+ Project files created, but dependency installation failed.
757
+
758
+ cd ${projectName}
759
+
760
+ Install dependencies manually, then run:
761
+
762
+ ${prefix} apply:dry
763
+ ${prefix} apply
764
+ `);
765
+ }
766
+
767
+ // src/templates.ts
768
+ var TSCONFIG_TEMPLATE = `{
769
+ "compilerOptions": {
770
+ "target": "ES2024",
771
+ "module": "ESNext",
772
+ "moduleResolution": "Bundler",
773
+ "types": ["node"],
774
+ "strict": true,
775
+ "esModuleInterop": true,
776
+ "skipLibCheck": true
777
+ },
778
+ "include": ["**/*.ts"]
779
+ }
780
+ `;
781
+ var GITIGNORE_TEMPLATE = `node_modules/
782
+ dist/
783
+ .env
784
+ *.log
785
+ `;
786
+ var PRETTIER_RC_TEMPLATE = `{
787
+ "semi": false,
788
+ "singleQuote": false,
789
+ "bracketSpacing": true,
790
+ "arrowParens": "always",
791
+ "tabWidth": 2,
792
+ "trailingComma": "es5",
793
+ "printWidth": 100
794
+ }
795
+ `;
796
+ var PRETTIER_IGNORE_TEMPLATE = `pnpm-lock.yaml
797
+ package-lock.json
798
+ yarn.lock
799
+ bun.lockb
800
+ `;
801
+ var ESLINT_CONFIG_TEMPLATE = `import { getEslintConfig } from "eslint-config-setup"
802
+
803
+ export default await getEslintConfig({ node: true })
804
+ `;
805
+ var ENV_EXAMPLE_TEMPLATE = `# Server configuration
806
+ # SUDO_PASSWORD=your-sudo-password
807
+ # SSH_KEY_PATH=~/.ssh/id_ed25519
808
+ `;
809
+ var AUTO_UPGRADES_20_TEMPLATE = `APT::Periodic::Update-Package-Lists "1";
810
+ APT::Periodic::Unattended-Upgrade "1";
811
+ `;
812
+ var UNATTENDED_UPGRADES_50_TEMPLATE = `Unattended-Upgrade::Origins-Pattern {
813
+ "origin=\${distro_id},archive=\${distro_codename}-security";
814
+ };
815
+
816
+ Unattended-Upgrade::Automatic-Reboot "true";
817
+ Unattended-Upgrade::Automatic-Reboot-Time "03:30";
818
+ `;
819
+ function createAdminNopasswdSudoersContent(adminUser) {
820
+ return `# Bootstrap default: dedicated admin user with passwordless sudo.
821
+ # This keeps the post-bootstrap Paratix workflow non-interactive after the
822
+ # initial root run. If you prefer password-protected sudo later, replace this
823
+ # with a stricter policy after the bootstrap is complete.
824
+ ${adminUser} ALL=(ALL:ALL) NOPASSWD:ALL
825
+ `;
826
+ }
827
+ function createBaseServerHeader({
828
+ adminPublicKey,
829
+ adminUserDeclaration,
830
+ expectedHostFingerprint,
831
+ host,
832
+ sshUser
833
+ }) {
834
+ const strictHostKeyCheckingDeclaration = expectedHostFingerprint == null ? 'const strictHostKeyChecking = FIRST_RUN ? "accept-new" : "yes";' : 'const strictHostKeyChecking = "yes";';
835
+ const expectedHostFingerprintLine = expectedHostFingerprint == null ? ' // expectedHostFingerprint: "SHA256:REPLACE_ME_WITH_YOUR_HOST_FINGERPRINT",' : ` expectedHostFingerprint: ${JSON.stringify(expectedHostFingerprint)}, // captured from port 22 during scaffolding`;
836
+ return `import { firstRun, recipe, server } from "paratix";
837
+ import { file, hostname, net, package as packages, ssh, sshd, sysctl, ufw, user } from "paratix/modules";
838
+
839
+ ${adminUserDeclaration}
840
+ const adminPublicKey = ${JSON.stringify(adminPublicKey ?? "ssh-ed25519 REPLACE_ME_WITH_YOUR_PUBLIC_KEY")};
841
+ const serverName = "my-server";
842
+ const FIRST_RUN = process.env["PARATIX_FIRST_RUN"] === "true";
843
+ const sshPorts = FIRST_RUN ? [22] : [2222];
844
+ const firewallTcpPorts = FIRST_RUN ? [22, 2222, 80, 443] : [2222, 80, 443];
845
+ ${strictHostKeyCheckingDeclaration}
846
+
847
+ export default server({
848
+ name: serverName,
849
+ host: ${JSON.stringify(host)},
850
+ ssh: {
851
+ ports: sshPorts,
852
+ privateKey: "~/.ssh/id_ed25519", // "~" is expanded by Paratix
853
+ // FIRST_RUN keeps the bootstrap path explicit:
854
+ // - pass "paratix apply ... --first-run" for the bootstrap run
855
+ // - later runs omit that flag and go through port 2222 with strict host-key checking again
856
+ strictHostKeyChecking,
857
+ user: ${sshUser},
858
+ ${expectedHostFingerprintLine}
859
+ // expectedHostPublicKey: "ssh-ed25519 REPLACE_ME_WITH_YOUR_HOST_PUBLIC_KEY",
860
+ },
861
+ env: {
862
+ FIRST_RUN,
863
+ SERVER_NAME: serverName,
864
+ SSH_PORT: 2222,
865
+ },
866
+ run: [
867
+ net.hosts("127.0.1.1", [serverName]),
868
+ hostname.set(serverName),
869
+ packages.upgrade("2026-03-01"),
870
+ packages.installed("curl", "htop", "ufw"),
871
+ `;
872
+ }
873
+ function createFirewallRecipe() {
874
+ return `
875
+ recipe("firewall", [
876
+ ufw.rule("allow", firewallTcpPorts),
877
+ ufw.enabled(),
878
+ ]),
879
+ `;
880
+ }
881
+ function createKernelHardeningRecipe() {
882
+ return `
883
+ recipe("kernel-hardening", [
884
+ sysctl.set("fs.protected_hardlinks", "1"),
885
+ sysctl.set("fs.protected_symlinks", "1"),
886
+ sysctl.set("kernel.dmesg_restrict", "1"),
887
+ sysctl.set("kernel.kptr_restrict", "2"),
888
+ sysctl.set("net.ipv4.conf.all.rp_filter", "1"),
889
+ sysctl.set("net.ipv4.conf.default.rp_filter", "1"),
890
+ sysctl.set("net.ipv4.tcp_syncookies", "1"),
891
+ ]),
892
+ `;
893
+ }
894
+ function createAutomaticSecurityUpgradesRecipe() {
895
+ return `
896
+ recipe("automatic-security-upgrades", [
897
+ packages.installed("unattended-upgrades"),
898
+ file.copy("/etc/apt/apt.conf.d/20auto-upgrades", "./files/20auto-upgrades", {
899
+ mode: "0644",
900
+ owner: "root:root",
901
+ }),
902
+ file.copy("/etc/apt/apt.conf.d/50unattended-upgrades", "./files/50unattended-upgrades", {
903
+ mode: "0644",
904
+ owner: "root:root",
905
+ }),
906
+ ]),
907
+ `;
908
+ }
909
+ function createFirstRunStopModule() {
910
+ return `
911
+ firstRun.stop("Bootstrap foundation complete; rerun without --first-run to continue."),
912
+
913
+ // Add application and user-facing services below this line.
914
+ `;
915
+ }
916
+ function createAdminRecipe(recipeName) {
917
+ return `
918
+ recipe("${recipeName}", [
919
+ user.present(adminUser, {
920
+ groups: ["sudo"],
921
+ shell: "/bin/bash",
922
+ }),
923
+ ssh.authorizedKeys(adminUser, adminPublicKey),
924
+ ]),
925
+ `;
926
+ }
927
+ function createHardenedAdminServerTemplate(parameters) {
928
+ const { adminPublicKey, expectedHostFingerprint, host, initialAdminUser } = parameters;
929
+ const adminUserDeclaration = `const adminUser = "${initialAdminUser}";`;
930
+ return `${createBaseServerHeader({
931
+ adminPublicKey,
932
+ adminUserDeclaration,
933
+ expectedHostFingerprint,
934
+ host,
935
+ sshUser: "adminUser"
936
+ })}
937
+ ${createAdminRecipe("admin-access")}
938
+ ${createFirewallRecipe()}
939
+ recipe("ssh-hardening", [
940
+ sshd.port(2222),
941
+ sshd.config({
942
+ PasswordAuthentication: "no",
943
+ PermitRootLogin: "no",
944
+ }),
945
+ ]),
946
+ ${createKernelHardeningRecipe()}
947
+ ${createAutomaticSecurityUpgradesRecipe()}
948
+ ${createFirstRunStopModule()}
949
+ ],
950
+ });
951
+ `;
952
+ }
953
+ function createBootstrapRootServerTemplate(host, adminPublicKey, expectedHostFingerprint) {
954
+ const adminUserDeclaration = 'const adminUser = "paratix";';
955
+ return `${createBaseServerHeader({
956
+ adminPublicKey,
957
+ adminUserDeclaration,
958
+ expectedHostFingerprint,
959
+ host,
960
+ sshUser: 'FIRST_RUN ? "root" : adminUser'
961
+ })}
962
+ ${createAdminRecipe("bootstrap-admin-user")}
963
+ recipe("bootstrap-admin-sudo", [
964
+ file.copy(
965
+ "/etc/sudoers.d/90-paratix-admin-nopasswd",
966
+ "./files/admin-nopasswd-sudoers",
967
+ {
968
+ mode: "0440",
969
+ owner: "root:root",
970
+ }
971
+ ),
972
+ ]),
973
+ ${createFirewallRecipe()}
974
+ // Transitional bootstrap mode:
975
+ // 1. Run this once as root with "--first-run" to create the dedicated admin user.
976
+ // 2. The generated sudoers drop-in keeps the new admin path non-interactive via NOPASSWD sudo.
977
+ // 3. Later runs omit "--first-run" and connect as the dedicated admin user on port 2222.
978
+ // 4. The next regular run disables root login completely.
979
+ recipe("ssh-hardening-transition", [
980
+ sshd.port(2222),
981
+ sshd.config({
982
+ PasswordAuthentication: "no",
983
+ PermitRootLogin: FIRST_RUN ? "prohibit-password" : "no",
984
+ }),
985
+ ]),
986
+ ${createKernelHardeningRecipe()}
987
+ ${createAutomaticSecurityUpgradesRecipe()}
988
+ ${createFirstRunStopModule()}
989
+ ],
990
+ });
991
+ `;
992
+ }
993
+ function createServerTemplate(options) {
994
+ return options.initialUser.kind === "root" ? createBootstrapRootServerTemplate(
995
+ options.host,
996
+ options.adminPublicKey,
997
+ options.expectedHostFingerprint
998
+ ) : createHardenedAdminServerTemplate({
999
+ adminPublicKey: options.adminPublicKey,
1000
+ expectedHostFingerprint: options.expectedHostFingerprint,
1001
+ host: options.host,
1002
+ initialAdminUser: options.initialUser.user
1003
+ });
1004
+ }
1005
+
1006
+ // src/index.ts
1007
+ function writeSharedScaffoldFiles(projectDirectory) {
1008
+ writeFileSync(join2(projectDirectory, "tsconfig.json"), TSCONFIG_TEMPLATE);
1009
+ writeFileSync(join2(projectDirectory, ".gitignore"), GITIGNORE_TEMPLATE);
1010
+ writeFileSync(join2(projectDirectory, ".prettierrc"), PRETTIER_RC_TEMPLATE);
1011
+ writeFileSync(join2(projectDirectory, ".prettierignore"), PRETTIER_IGNORE_TEMPLATE);
1012
+ writeFileSync(join2(projectDirectory, "eslint.config.ts"), ESLINT_CONFIG_TEMPLATE);
1013
+ writeFileSync(join2(projectDirectory, ".env.example"), ENV_EXAMPLE_TEMPLATE);
1014
+ }
1015
+ function writeScaffoldSupportFiles(projectDirectory, initialUser) {
1016
+ writeFileSync(join2(projectDirectory, "files", ".gitkeep"), "");
1017
+ writeFileSync(join2(projectDirectory, "files", "20auto-upgrades"), AUTO_UPGRADES_20_TEMPLATE);
1018
+ writeFileSync(
1019
+ join2(projectDirectory, "files", "50unattended-upgrades"),
1020
+ UNATTENDED_UPGRADES_50_TEMPLATE
1021
+ );
1022
+ if (initialUser.kind !== "root") return;
1023
+ writeFileSync(
1024
+ join2(projectDirectory, "files", "admin-nopasswd-sudoers"),
1025
+ createAdminNopasswdSudoersContent("paratix")
1026
+ );
1027
+ }
1028
+ function writeProjectFiles(projectDirectory, options) {
1029
+ mkdirSync(projectDirectory, { recursive: true });
1030
+ mkdirSync(join2(projectDirectory, "files"), { recursive: true });
1031
+ const host = options?.host ?? "1.2.3.4";
1032
+ const initialUser = options?.initialUser ?? { kind: "admin", user: "paratix" };
1033
+ const adminPublicKey = options?.adminPublicKey;
1034
+ const expectedHostFingerprint = options?.expectedHostFingerprint;
1035
+ const packageJson = {
1036
+ dependencies: {
1037
+ paratix: "^0.1.0"
1038
+ },
1039
+ devDependencies: {
1040
+ "@types/node": "^24.5.2",
1041
+ eslint: "^10.0.3",
1042
+ "eslint-config-setup": "^0.3.3",
1043
+ prettier: "^3.6.2",
1044
+ tsx: "^4.20.6"
1045
+ },
1046
+ engines: {
1047
+ node: ">=24.0.0"
1048
+ },
1049
+ name: derivePackageName(projectDirectory),
1050
+ private: true,
1051
+ scripts: {
1052
+ apply: "paratix apply server.ts",
1053
+ "apply:dry": "paratix apply server.ts --dry-run",
1054
+ "format:check": "prettier --check .",
1055
+ "format:fix": "prettier --write .",
1056
+ lint: "eslint ."
1057
+ },
1058
+ type: "module"
1059
+ };
1060
+ writeFileSync(join2(projectDirectory, "package.json"), `${JSON.stringify(packageJson, null, 2)}
1061
+ `);
1062
+ writeFileSync(
1063
+ join2(projectDirectory, "server.ts"),
1064
+ createServerTemplate({ adminPublicKey, expectedHostFingerprint, host, initialUser })
1065
+ );
1066
+ writeSharedScaffoldFiles(projectDirectory);
1067
+ writeScaffoldSupportFiles(projectDirectory, initialUser);
1068
+ }
1069
+ function isValidProjectName(name) {
1070
+ const trimmed = name.trim();
1071
+ return new RegExp("^[a-z0-9][a-z0-9\\x2d]*$", "v").test(trimmed);
1072
+ }
1073
+ function normalizeProjectName(name) {
1074
+ return name.trim();
1075
+ }
1076
+ function derivePackageName(projectDirectory) {
1077
+ return basename2(projectDirectory.replaceAll("\\", "/"));
1078
+ }
1079
+ function exitWithMessage(message) {
1080
+ console.error(message);
1081
+ process.exit(1);
1082
+ }
1083
+ function parseCliArguments2(argv) {
1084
+ return parseCliArguments(argv, exitWithMessage);
1085
+ }
1086
+ function parseInitialUserConfig2(value) {
1087
+ return parseInitialUserConfig(exitWithMessage, value);
1088
+ }
1089
+ function validateHost2(value) {
1090
+ return validateHost(exitWithMessage, value);
1091
+ }
1092
+ function validateProjectName(name) {
1093
+ if (name == null || name === "") {
1094
+ exitWithMessage("Usage: create-paratix <project-name>");
1095
+ }
1096
+ const normalizedName = normalizeProjectName(name);
1097
+ if (!isValidProjectName(normalizedName)) {
1098
+ exitWithMessage(
1099
+ `Error: Invalid project name "${name}" \u2014 use only lowercase letters, numbers, and hyphens.`
1100
+ );
1101
+ }
1102
+ return normalizedName;
1103
+ }
1104
+ function scaffoldProject(projectName, pm, options) {
1105
+ const normalizedProjectName = normalizeProjectName(projectName);
1106
+ const projectDirectory = resolve(normalizedProjectName);
1107
+ if (existsSync(projectDirectory)) {
1108
+ exitWithMessage(`Error: Directory "${normalizedProjectName}" already exists.`);
1109
+ }
1110
+ console.log(`Creating Paratix project in ${projectDirectory}...`);
1111
+ writeProjectFiles(projectDirectory, options);
1112
+ const installer = options?.installer ?? installDependencies;
1113
+ const installed = installer(projectDirectory, pm);
1114
+ if (!installed) {
1115
+ process.exitCode = 1;
1116
+ printPartialSuccessMessage(normalizedProjectName, pm);
1117
+ return false;
1118
+ }
1119
+ printSuccessMessage(normalizedProjectName, pm);
1120
+ return true;
1121
+ }
1122
+ async function resolveCliOrPromptAdminPublicKey(parameters) {
1123
+ const { adminPublicKey, adminPublicKeyFile } = parameters;
1124
+ if (adminPublicKey !== void 0) {
1125
+ return validateAdminPublicKey(exitWithMessage, adminPublicKey);
1126
+ }
1127
+ if (adminPublicKeyFile !== void 0) {
1128
+ return readAdminPublicKeyFile(exitWithMessage, adminPublicKeyFile);
1129
+ }
1130
+ if (process.stdin.isTTY && process.stdout.isTTY) {
1131
+ return promptForAdminPublicKey2();
1132
+ }
1133
+ return void 0;
1134
+ }
1135
+ function main() {
1136
+ const { adminPublicKey, adminPublicKeyFile, host, initialUser, projectName } = parseCliArguments2(
1137
+ process.argv.slice(2)
1138
+ );
1139
+ const normalizedProjectName = validateProjectName(projectName);
1140
+ const pm = detectPackageManager();
1141
+ void (async () => {
1142
+ const validatedHost = host == null ? await promptForHost2() : validateHost2(host);
1143
+ const resolvedExpectedHostFingerprint = process.stdin.isTTY && process.stdout.isTTY ? await promptForHostFingerprint(validatedHost) : void 0;
1144
+ const initialUserConfig = initialUser == null ? await promptForInitialUserConfig() : parseInitialUserConfig2(initialUser);
1145
+ const resolvedAdminPublicKey = await resolveCliOrPromptAdminPublicKey({
1146
+ adminPublicKey,
1147
+ adminPublicKeyFile
1148
+ });
1149
+ scaffoldProject(normalizedProjectName, pm, {
1150
+ adminPublicKey: resolvedAdminPublicKey,
1151
+ expectedHostFingerprint: resolvedExpectedHostFingerprint,
1152
+ host: validatedHost,
1153
+ initialUser: initialUserConfig
1154
+ });
1155
+ })().catch((error) => {
1156
+ console.error(error instanceof Error ? error.message : String(error));
1157
+ process.exitCode = 1;
1158
+ });
1159
+ }
1160
+ function isDirectExecution(moduleUrl, argv1) {
1161
+ return argv1 != null && moduleUrl.endsWith(argv1.replaceAll("\\", "/"));
1162
+ }
1163
+ if (isDirectExecution(import.meta.url, process.argv[1])) {
1164
+ main();
1165
+ }
1166
+ export {
1167
+ isDirectExecution,
1168
+ isValidHost,
1169
+ isValidInitialUserName,
1170
+ isValidProjectName,
1171
+ normalizeHost,
1172
+ normalizeInitialUserName,
1173
+ normalizeProjectName,
1174
+ parseCliArguments2 as parseCliArguments,
1175
+ parseInitialUserConfig2 as parseInitialUserConfig,
1176
+ promptForAdminPublicKey2 as promptForAdminPublicKey,
1177
+ promptForHost2 as promptForHost,
1178
+ promptForHostFingerprint,
1179
+ promptForInitialUserConfig,
1180
+ scaffoldProject,
1181
+ validateHost2 as validateHost,
1182
+ writeProjectFiles
1183
+ };