@dunx/create-app 3.0.3 → 3.0.5

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/cli.js CHANGED
@@ -3,49 +3,726 @@
3
3
  import {
4
4
  FEATURES,
5
5
  ScaffoldError,
6
- TEMPLATES,
7
- featureNames,
6
+ blockingEntries,
8
7
  impliedBy,
8
+ isValidPackageName,
9
+ packageVersion,
10
+ resolveFeatures,
9
11
  scaffold
10
- } from "./chunk-x7s3axkt.js";
12
+ } from "./chunk-5tn3ybzv.js";
11
13
 
12
14
  // src/cli.ts
13
15
  import { parseArgs } from "util";
14
16
  import { relative } from "path";
15
17
 
16
- // src/stdin.ts
17
- var readLine = async () => {
18
- for await (const line of console) {
19
- return typeof line === "string" ? line.trim() : "";
18
+ // src/banner.ts
19
+ var LOGO = [
20
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2588\u2557 \u2588\u2588\u2557\u2588\u2588\u2557 \u2588\u2588\u2557",
21
+ "\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2588\u2588\u2557 \u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2554\u255D",
22
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2554\u2588\u2588\u2557 \u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2554\u255D ",
23
+ "\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551 \u2588\u2588\u2551\u2588\u2588\u2551\u255A\u2588\u2588\u2557\u2588\u2588\u2551 \u2588\u2588\u2554\u2588\u2588\u2557 ",
24
+ "\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D\u2588\u2588\u2551 \u255A\u2588\u2588\u2588\u2588\u2551\u2588\u2588\u2554\u255D \u2588\u2588\u2557",
25
+ "\u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u2550\u2550\u2550\u2550\u255D \u255A\u2550\u255D \u255A\u2550\u2550\u2550\u255D\u255A\u2550\u255D \u255A\u2550\u255D"
26
+ ];
27
+ var RAMP = [
28
+ "#00e6b8",
29
+ "#00d4aa",
30
+ "#12bfa4",
31
+ "#1fa89c",
32
+ "#2b8f92",
33
+ "#357788"
34
+ ];
35
+ var INDENT = " ";
36
+ var NEEDS = 2 + 35 + 2;
37
+
38
+ class Banner {
39
+ #style;
40
+ constructor(style) {
41
+ this.#style = style;
42
+ }
43
+ lines(width, version) {
44
+ const label = `create-app ${version}`;
45
+ if (width < NEEDS) {
46
+ return [
47
+ `${INDENT}${this.#style.bold(this.#style.accent("dunx"))} ${this.#style.muted(label)}`
48
+ ];
49
+ }
50
+ return [
51
+ ...LOGO.map((row, at) => `${INDENT}${this.#style.hex(RAMP[at] ?? "#00d4aa", row)}`),
52
+ `${INDENT}${this.#style.muted(label)}`
53
+ ];
54
+ }
55
+ }
56
+ Object.defineProperty(Banner, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "style: Style", typeOnly: "Style" }] });
57
+
58
+ // src/keys.ts
59
+ var KeyName = Object.freeze({
60
+ Up: "up",
61
+ Down: "down",
62
+ Left: "left",
63
+ Right: "right",
64
+ Home: "home",
65
+ End: "end",
66
+ Enter: "enter",
67
+ Space: "space",
68
+ Tab: "tab",
69
+ ShiftTab: "shiftTab",
70
+ Backspace: "backspace",
71
+ ClearLine: "clearLine",
72
+ Escape: "escape",
73
+ Interrupt: "interrupt",
74
+ Char: "char",
75
+ Unknown: "unknown"
76
+ });
77
+ var key = (name, char = "") => ({ name, char });
78
+ var CONTROLS = Object.freeze({
79
+ 3: KeyName.Interrupt,
80
+ 4: KeyName.Interrupt,
81
+ 8: KeyName.Backspace,
82
+ 9: KeyName.Tab,
83
+ 10: KeyName.Enter,
84
+ 13: KeyName.Enter,
85
+ 21: KeyName.ClearLine,
86
+ 32: KeyName.Space,
87
+ 127: KeyName.Backspace
88
+ });
89
+ var FINALS = Object.freeze({
90
+ A: KeyName.Up,
91
+ B: KeyName.Down,
92
+ C: KeyName.Right,
93
+ D: KeyName.Left,
94
+ F: KeyName.End,
95
+ H: KeyName.Home,
96
+ Z: KeyName.ShiftTab
97
+ });
98
+ var TILDES = Object.freeze({
99
+ "1": KeyName.Home,
100
+ "4": KeyName.End,
101
+ "7": KeyName.Home,
102
+ "8": KeyName.End
103
+ });
104
+ var ESCAPE = 27;
105
+ var isParameter = (code) => code >= 32 && code <= 63;
106
+
107
+ class KeyDecoder {
108
+ #pending = "";
109
+ #decoder = new TextDecoder("utf-8");
110
+ push(bytes) {
111
+ this.#pending += this.#decoder.decode(bytes, { stream: true });
112
+ const keys = [];
113
+ while (this.#pending.length > 0) {
114
+ const next = this.#take();
115
+ if (next === undefined)
116
+ break;
117
+ keys.push(next);
118
+ }
119
+ return keys;
120
+ }
121
+ #take() {
122
+ const buffer = this.#pending;
123
+ const code = buffer.charCodeAt(0);
124
+ if (code === ESCAPE)
125
+ return this.#takeEscape(buffer);
126
+ const named = CONTROLS[code];
127
+ if (named !== undefined) {
128
+ this.#pending = buffer.slice(1);
129
+ return key(named);
130
+ }
131
+ if (code < 32) {
132
+ this.#pending = buffer.slice(1);
133
+ return key(KeyName.Unknown);
134
+ }
135
+ const size = code >= 55296 && code <= 56319 ? 2 : 1;
136
+ this.#pending = buffer.slice(size);
137
+ return key(KeyName.Char, buffer.slice(0, size));
138
+ }
139
+ #takeEscape(buffer) {
140
+ if (buffer.length === 1) {
141
+ this.#pending = "";
142
+ return key(KeyName.Escape);
143
+ }
144
+ const introducer = buffer[1];
145
+ if (introducer !== "[" && introducer !== "O") {
146
+ this.#pending = buffer.slice(2);
147
+ return key(KeyName.Unknown);
148
+ }
149
+ let at = 2;
150
+ while (at < buffer.length && isParameter(buffer.charCodeAt(at)))
151
+ at += 1;
152
+ if (at === buffer.length)
153
+ return;
154
+ const parameters = buffer.slice(2, at);
155
+ const final = buffer[at] ?? "";
156
+ this.#pending = buffer.slice(at + 1);
157
+ if (final === "~")
158
+ return key(TILDES[parameters] ?? KeyName.Unknown);
159
+ return key(FINALS[final] ?? KeyName.Unknown);
160
+ }
161
+ }
162
+
163
+ // src/prompt.ts
164
+ class CancelledError extends Error {
165
+ name = "CancelledError";
166
+ }
167
+
168
+ class Prompt {
169
+ #done = false;
170
+ #cancelled = false;
171
+ #answer;
172
+ press(key2) {
173
+ if (this.#done)
174
+ return;
175
+ if (key2.name === KeyName.Interrupt || key2.name === KeyName.Escape) {
176
+ this.#cancelled = true;
177
+ this.#done = true;
178
+ return;
179
+ }
180
+ this.handle(key2);
181
+ }
182
+ finish(answer) {
183
+ this.#answer = answer;
184
+ this.#done = true;
185
+ }
186
+ get done() {
187
+ return this.#done;
188
+ }
189
+ get cancelled() {
190
+ return this.#cancelled;
191
+ }
192
+ get answer() {
193
+ if (!this.#done || this.#cancelled) {
194
+ throw new Error("The prompt was not answered.");
195
+ }
196
+ return this.#answer;
197
+ }
198
+ }
199
+ var CSI = "\x1B[";
200
+ var HIDE_CURSOR = `${CSI}?25l`;
201
+ var SHOW_CURSOR = `${CSI}?25h`;
202
+ var ERASE_DOWN = `${CSI}0J`;
203
+ var up = (lines) => lines > 0 ? `${CSI}${lines}A` : "";
204
+
205
+ class PromptRunner {
206
+ #tty;
207
+ constructor(tty) {
208
+ this.#tty = tty;
209
+ }
210
+ async ask(prompt) {
211
+ const width = Math.max(20, this.#tty.columns - 1);
212
+ let painted = 0;
213
+ const paint = () => {
214
+ const height = Math.max(3, this.#tty.rows - 1);
215
+ const lines = prompt.frame(width, height).slice(0, height);
216
+ this.#tty.write(`${up(painted)}${ERASE_DOWN}${lines.join(`
217
+ `)}
218
+ `);
219
+ painted = lines.length;
220
+ };
221
+ this.#tty.write(HIDE_CURSOR);
222
+ try {
223
+ await new Promise((resolve) => {
224
+ paint();
225
+ this.#tty.open((key2) => {
226
+ prompt.press(key2);
227
+ if (prompt.done)
228
+ resolve();
229
+ else
230
+ paint();
231
+ });
232
+ });
233
+ } finally {
234
+ this.#tty.close();
235
+ const closing = prompt.cancelled ? "" : `${prompt.summary(width)}
236
+ `;
237
+ this.#tty.write(`${up(painted)}${ERASE_DOWN}${closing}${SHOW_CURSOR}`);
238
+ }
239
+ if (prompt.cancelled)
240
+ throw new CancelledError("Cancelled.");
241
+ return prompt.answer;
242
+ }
243
+ }
244
+ Object.defineProperty(PromptRunner, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "tty: Tty", typeOnly: "Tty" }] });
245
+
246
+ // src/style.ts
247
+ var CSI2 = "\x1B[";
248
+ var RESET = `${CSI2}0m`;
249
+ var tone = (hex) => Bun.color(hex, "ansi-256") ?? "";
250
+ var ACCENT = tone("#00d4aa");
251
+ var MUTED = tone("#8a8f98");
252
+ var DANGER = tone("#e5534b");
253
+ var WARN = tone("#d29922");
254
+
255
+ class Style {
256
+ #enabled;
257
+ constructor(enabled = Bun.enableANSIColors) {
258
+ this.#enabled = enabled;
259
+ }
260
+ #wrap(code, text) {
261
+ return this.#enabled && code !== "" ? `${code}${text}${RESET}` : text;
262
+ }
263
+ accent(text) {
264
+ return this.#wrap(ACCENT, text);
265
+ }
266
+ muted(text) {
267
+ return this.#wrap(MUTED, text);
268
+ }
269
+ danger(text) {
270
+ return this.#wrap(DANGER, text);
271
+ }
272
+ warn(text) {
273
+ return this.#wrap(WARN, text);
274
+ }
275
+ bold(text) {
276
+ return this.#wrap(`${CSI2}1m`, text);
277
+ }
278
+ invert(text) {
279
+ return this.#wrap(`${CSI2}7m`, text);
280
+ }
281
+ hex(colour, text) {
282
+ return this.#wrap(tone(colour), text);
283
+ }
284
+ clip(text, width) {
285
+ if (width <= 0)
286
+ return "";
287
+ if (Bun.stringWidth(text) <= width)
288
+ return text;
289
+ return `${Bun.sliceAnsi(text, 0, Math.max(0, width - 1))}${this.muted("\u2026")}`;
290
+ }
291
+ }
292
+ Object.defineProperty(Style, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "enabled: boolean = Bun.enableANSIColors", optional: true }] });
293
+
294
+ // src/tty.ts
295
+ class Tty {
296
+ }
297
+
298
+ class ProcessTty extends Tty {
299
+ #decoder = new KeyDecoder;
300
+ #input;
301
+ #output;
302
+ #listener;
303
+ constructor(input = process.stdin, output = process.stdout) {
304
+ super();
305
+ this.#input = input;
306
+ this.#output = output;
307
+ }
308
+ static available() {
309
+ return process.stdin.isTTY === true && typeof process.stdin.setRawMode === "function" && process.stdout.isTTY === true;
310
+ }
311
+ get columns() {
312
+ return this.#output.columns ?? 80;
313
+ }
314
+ get rows() {
315
+ return this.#output.rows ?? 24;
316
+ }
317
+ write(text) {
318
+ this.#output.write(text);
319
+ }
320
+ open(onKey) {
321
+ const listener = (chunk) => {
322
+ for (const key2 of this.#decoder.push(chunk))
323
+ onKey(key2);
324
+ };
325
+ this.#listener = listener;
326
+ this.#input.setRawMode?.(true);
327
+ this.#input.resume();
328
+ this.#input.on("data", listener);
329
+ }
330
+ close() {
331
+ if (this.#listener === undefined)
332
+ return;
333
+ this.#input.off("data", this.#listener);
334
+ this.#listener = undefined;
335
+ this.#input.setRawMode?.(false);
336
+ this.#input.pause();
337
+ }
338
+ }
339
+ Object.defineProperty(ProcessTty, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "input: KeySource = process.stdin", optional: true }, { unresolved: "output: FrameSink = process.stdout", optional: true }] });
340
+
341
+ // src/wizard.ts
342
+ import { basename, resolve } from "path";
343
+
344
+ // src/prompts.ts
345
+ var ASKING = "?";
346
+ var ANSWERED = "\u2713";
347
+ var POINTER = "\u276F";
348
+ var CHOSEN = "\u25C9";
349
+ var REQUIRED = "\u25C8";
350
+ var UNCHOSEN = "\u25CB";
351
+
352
+ class TextPrompt extends Prompt {
353
+ style;
354
+ title;
355
+ #value;
356
+ #at;
357
+ #error = "";
358
+ constructor(style, title, initial) {
359
+ super();
360
+ this.style = style;
361
+ this.title = title;
362
+ this.#value = initial;
363
+ this.#at = initial.length;
364
+ }
365
+ validate(_value) {
366
+ return;
367
+ }
368
+ hint() {
369
+ return "Enter to accept. Ctrl+C to cancel.";
370
+ }
371
+ frame(width) {
372
+ const text = this.#value;
373
+ const at = this.#at;
374
+ const under = at < text.length ? text.slice(at, at + 1) : " ";
375
+ const drawn = text.slice(0, at) + this.style.invert(under) + text.slice(at + 1);
376
+ const footer = this.#error === "" ? this.style.muted(this.hint()) : this.style.danger(this.#error);
377
+ return [
378
+ `${this.style.accent(ASKING)} ${this.style.bold(this.title)}`,
379
+ this.style.clip(` ${drawn}`, width),
380
+ this.style.clip(` ${footer}`, width)
381
+ ];
382
+ }
383
+ summary(width) {
384
+ return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(this.#value)}`, width);
385
+ }
386
+ handle(key2) {
387
+ this.#error = "";
388
+ switch (key2.name) {
389
+ case KeyName.Char:
390
+ this.#value = this.#value.slice(0, this.#at) + key2.char + this.#value.slice(this.#at);
391
+ this.#at += key2.char.length;
392
+ return;
393
+ case KeyName.Space:
394
+ this.#error = "A space is not usable here.";
395
+ return;
396
+ case KeyName.Backspace:
397
+ if (this.#at === 0)
398
+ return;
399
+ this.#value = this.#value.slice(0, this.#at - 1) + this.#value.slice(this.#at);
400
+ this.#at -= 1;
401
+ return;
402
+ case KeyName.ClearLine:
403
+ this.#value = "";
404
+ this.#at = 0;
405
+ return;
406
+ case KeyName.Left:
407
+ this.#at = Math.max(0, this.#at - 1);
408
+ return;
409
+ case KeyName.Right:
410
+ this.#at = Math.min(this.#value.length, this.#at + 1);
411
+ return;
412
+ case KeyName.Home:
413
+ this.#at = 0;
414
+ return;
415
+ case KeyName.End:
416
+ this.#at = this.#value.length;
417
+ return;
418
+ case KeyName.Enter: {
419
+ const problem = this.validate(this.#value);
420
+ if (problem === undefined)
421
+ this.finish(this.#value);
422
+ else
423
+ this.#error = problem;
424
+ return;
425
+ }
426
+ default:
427
+ return;
428
+ }
429
+ }
430
+ }
431
+ Object.defineProperty(TextPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protected readonly style: Style", typeOnly: "Style" }, { unresolved: "readonly title: string" }, { unresolved: "initial: string" }] });
432
+
433
+ class ConfirmPrompt extends Prompt {
434
+ style;
435
+ title;
436
+ #value;
437
+ constructor(style, title, initial) {
438
+ super();
439
+ this.style = style;
440
+ this.title = title;
441
+ this.#value = initial;
442
+ }
443
+ #option(label, active) {
444
+ return active ? this.style.invert(` ${label} `) : this.style.muted(` ${label} `);
445
+ }
446
+ frame(width) {
447
+ return [
448
+ `${this.style.accent(ASKING)} ${this.style.bold(this.title)}`,
449
+ this.style.clip(` ${this.#option("yes", this.#value)} ${this.#option("no", !this.#value)}`, width),
450
+ this.style.clip(` ${this.style.muted("y or n. Enter takes the highlighted one.")}`, width)
451
+ ];
452
+ }
453
+ summary(width) {
454
+ const answer = this.#value ? "yes" : "no";
455
+ return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(answer)}`, width);
456
+ }
457
+ handle(key2) {
458
+ const typed = key2.char.toLowerCase();
459
+ if (typed === "y") {
460
+ this.finish(true);
461
+ return;
462
+ }
463
+ if (typed === "n") {
464
+ this.finish(false);
465
+ return;
466
+ }
467
+ if (key2.name === KeyName.Left || key2.name === KeyName.Right || key2.name === KeyName.Tab || key2.name === KeyName.Space) {
468
+ this.#value = !this.#value;
469
+ return;
470
+ }
471
+ if (key2.name === KeyName.Enter)
472
+ this.finish(this.#value);
473
+ }
474
+ }
475
+ Object.defineProperty(ConfirmPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "private readonly style: Style", typeOnly: "Style" }, { unresolved: "private readonly title: string" }, { unresolved: "initial: boolean" }] });
476
+ var CHROME = 4;
477
+
478
+ class SelectPrompt extends Prompt {
479
+ style;
480
+ title;
481
+ items;
482
+ #cursor = 0;
483
+ #offset = 0;
484
+ #chosen = new Set;
485
+ constructor(style, title, items, initial = []) {
486
+ super();
487
+ this.style = style;
488
+ this.title = title;
489
+ this.items = items;
490
+ for (const value of initial)
491
+ this.#chosen.add(value);
492
+ }
493
+ implied(_chosen) {
494
+ return new Set;
495
+ }
496
+ notes(_chosen) {
497
+ return [];
498
+ }
499
+ #marker(value, implied) {
500
+ if (this.#chosen.has(value))
501
+ return this.style.accent(CHOSEN);
502
+ if (implied.has(value))
503
+ return this.style.warn(REQUIRED);
504
+ return this.style.muted(UNCHOSEN);
505
+ }
506
+ #window(budget) {
507
+ const visible = Math.max(1, Math.min(this.items.length, budget));
508
+ if (this.#cursor < this.#offset)
509
+ this.#offset = this.#cursor;
510
+ if (this.#cursor >= this.#offset + visible) {
511
+ this.#offset = this.#cursor - visible + 1;
512
+ }
513
+ this.#offset = Math.min(this.#offset, Math.max(0, this.items.length - visible));
514
+ return this.items.slice(this.#offset, this.#offset + visible);
515
+ }
516
+ frame(width, height) {
517
+ const implied = this.implied(this.#chosen);
518
+ const notes = this.notes(this.#chosen);
519
+ const pad = Math.max(...this.items.map((item) => item.label.length));
520
+ const window = this.#window(height - CHROME - notes.length);
521
+ const lines = [
522
+ `${this.style.accent(ASKING)} ${this.style.bold(this.title)} ` + this.style.muted(this.#count(implied))
523
+ ];
524
+ if (this.#offset > 0) {
525
+ lines.push(this.style.muted(` \u2191 ${this.#offset} more`));
526
+ }
527
+ for (const [index, item] of window.entries()) {
528
+ const at = this.#offset + index;
529
+ const pointer = at === this.#cursor ? this.style.accent(POINTER) : " ";
530
+ const label = at === this.#cursor ? this.style.bold(item.label.padEnd(pad)) : item.label.padEnd(pad);
531
+ lines.push(this.style.clip(`${pointer} ${this.#marker(item.value, implied)} ${label} ${this.style.muted(item.hint)}`, width));
532
+ }
533
+ const below = this.items.length - this.#offset - window.length;
534
+ if (below > 0)
535
+ lines.push(this.style.muted(` \u2193 ${below} more`));
536
+ for (const note of notes) {
537
+ lines.push(this.style.clip(` ${this.style.warn(note)}`, width));
538
+ }
539
+ lines.push(this.style.clip(` ${this.style.muted("Space toggles. \u2191\u2193 moves. a all, n none. Enter continues.")}`, width));
540
+ return lines;
541
+ }
542
+ #count(implied) {
543
+ if (this.#chosen.size === 0)
544
+ return "nothing chosen yet";
545
+ const extra = implied.size === 0 ? "" : `, ${implied.size} pulled in`;
546
+ return `${this.#chosen.size} chosen${extra}`;
547
+ }
548
+ summary(width) {
549
+ const chosen = this.#ordered();
550
+ const answer = chosen.length === 0 ? "none, the minimal template" : chosen.join(", ");
551
+ return this.style.clip(`${this.style.accent(ANSWERED)} ${this.title} ${this.style.accent(answer)}`, width);
552
+ }
553
+ #ordered() {
554
+ return this.items.map((item) => item.value).filter((value) => this.#chosen.has(value));
555
+ }
556
+ #move(by) {
557
+ const count = this.items.length;
558
+ this.#cursor = (this.#cursor + by + count) % count;
559
+ }
560
+ handle(key2) {
561
+ switch (key2.name) {
562
+ case KeyName.Up:
563
+ case KeyName.ShiftTab:
564
+ this.#move(-1);
565
+ return;
566
+ case KeyName.Down:
567
+ case KeyName.Tab:
568
+ this.#move(1);
569
+ return;
570
+ case KeyName.Home:
571
+ this.#cursor = 0;
572
+ return;
573
+ case KeyName.End:
574
+ this.#cursor = this.items.length - 1;
575
+ return;
576
+ case KeyName.Space: {
577
+ const value = this.items[this.#cursor]?.value;
578
+ if (value === undefined)
579
+ return;
580
+ if (!this.#chosen.delete(value))
581
+ this.#chosen.add(value);
582
+ return;
583
+ }
584
+ case KeyName.Enter:
585
+ this.finish(this.#ordered());
586
+ return;
587
+ case KeyName.Char:
588
+ this.#type(key2.char);
589
+ return;
590
+ default:
591
+ return;
592
+ }
593
+ }
594
+ #type(char) {
595
+ if (char === "k")
596
+ this.#move(-1);
597
+ else if (char === "j")
598
+ this.#move(1);
599
+ else if (char === "a")
600
+ for (const item of this.items)
601
+ this.#chosen.add(item.value);
602
+ else if (char === "n")
603
+ this.#chosen.clear();
604
+ }
605
+ }
606
+ Object.defineProperty(SelectPrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "protected readonly style: Style", typeOnly: "Style" }, { unresolved: "readonly title: string" }, { unresolved: "protected readonly items: readonly SelectItem[]" }, { unresolved: "initial: readonly string[] = []", optional: true }] });
607
+
608
+ // src/wizard.ts
609
+ var DEFAULT_TARGET = "my-api";
610
+
611
+ class DirectoryPrompt extends TextPrompt {
612
+ validate(value) {
613
+ return value.trim() === "" ? "Name a directory to write into." : undefined;
614
+ }
615
+ hint() {
616
+ return 'Enter to accept. A single "." writes into the directory you are in.';
617
+ }
618
+ }
619
+
620
+ class PackageNamePrompt extends TextPrompt {
621
+ validate(value) {
622
+ return isValidPackageName(value) ? undefined : "npm forbids uppercase, and a leading dot or underscore.";
623
+ }
624
+ }
625
+
626
+ class FeaturePrompt extends SelectPrompt {
627
+ constructor(style, initial) {
628
+ super(style, "Features", FEATURES.map((feature) => ({
629
+ value: feature.name,
630
+ label: feature.name,
631
+ hint: feature.summary
632
+ })), initial);
633
+ }
634
+ #resolved(chosen) {
635
+ return resolveFeatures([...chosen]);
636
+ }
637
+ implied(chosen) {
638
+ return new Set(impliedBy([...chosen], this.#resolved(chosen)));
639
+ }
640
+ notes(chosen) {
641
+ const resolved = this.#resolved(chosen);
642
+ const lines = [];
643
+ const implied = impliedBy([...chosen], resolved);
644
+ if (implied.length > 0) {
645
+ const verb = implied.length === 1 ? "comes" : "come";
646
+ lines.push(`${implied.join(", ")} ${verb} along as a requirement.`);
647
+ }
648
+ const services = new Map;
649
+ for (const feature of resolved) {
650
+ if (feature.service === undefined)
651
+ continue;
652
+ const named = services.get(feature.service) ?? [];
653
+ named.push(feature.name);
654
+ services.set(feature.service, named);
655
+ }
656
+ for (const [service, named] of services) {
657
+ const verb = named.length === 1 ? "needs" : "need";
658
+ lines.push(`${named.join(", ")} ${verb} ${service}.`);
659
+ }
660
+ return lines;
661
+ }
662
+ }
663
+ Object.defineProperty(FeaturePrompt, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "style: Style", typeOnly: "Style" }, { unresolved: "initial: readonly string[]" }] });
664
+
665
+ class Wizard {
666
+ #runner;
667
+ #style;
668
+ constructor(runner, style) {
669
+ this.#runner = runner;
670
+ this.#style = style;
20
671
  }
21
- return "";
672
+ async run(defaults) {
673
+ const target = defaults.target ?? await this.#runner.ask(new DirectoryPrompt(this.#style, "Directory", DEFAULT_TARGET));
674
+ const name = await this.#name(defaults, target);
675
+ const features = await this.#runner.ask(new FeaturePrompt(this.#style, defaults.features));
676
+ const force = await this.#force(defaults, target);
677
+ return { target, name, features, force };
678
+ }
679
+ async#name(defaults, target) {
680
+ if (defaults.name !== undefined)
681
+ return defaults.name;
682
+ const derived = basename(resolve(defaults.cwd, target));
683
+ if (isValidPackageName(derived))
684
+ return derived;
685
+ return this.#runner.ask(new PackageNamePrompt(this.#style, "Package name", slug(derived)));
686
+ }
687
+ async#force(defaults, target) {
688
+ if (defaults.force)
689
+ return true;
690
+ const blocking = blockingEntries(resolve(defaults.cwd, target));
691
+ if (blocking.length === 0)
692
+ return false;
693
+ const shown = blocking.slice(0, 3).join(", ");
694
+ const rest = blocking.length > 3 ? `, +${blocking.length - 3} more` : "";
695
+ const proceed = await this.#runner.ask(new ConfirmPrompt(this.#style, `${target} already has files in it (${shown}${rest}). Write into it anyway?`, false));
696
+ if (!proceed)
697
+ throw new CancelledError("Nothing written.");
698
+ return true;
699
+ }
700
+ }
701
+ Object.defineProperty(Wizard, Symbol.for("dunx.deps"), { value: () => [{ unresolved: "runner: PromptRunner", typeOnly: "PromptRunner" }, { unresolved: "style: Style", typeOnly: "Style" }] });
702
+ var slug = (value) => {
703
+ const lowered = value.toLowerCase().replace(/[^a-z0-9\-._~]+/g, "-").replace(/^[-._]+/, "").replace(/-+$/, "");
704
+ return lowered === "" ? DEFAULT_TARGET : lowered;
22
705
  };
23
706
 
24
707
  // src/cli.ts
25
708
  var USAGE = `Scaffold a new dunx application.
26
709
 
27
- bunx @dunx/create-app <directory> [options]
710
+ bunx @dunx/create-app [directory]
711
+
712
+ It asks. Up and down move, space toggles a feature, Enter takes the selection,
713
+ and the list says which features your choices pull in and which need a service
714
+ running. Nothing about the app is chosen by a flag.
28
715
 
29
716
  Options:
30
- --name <name> package name for the app (default: the directory name)
31
- --with <a,b,c> features to compose the app from (see --list)
32
- --all every feature
33
- --template <name> ${TEMPLATES.join(" | ")} (default: minimal, when no --with)
34
- --list print the features and exit
35
- --force write into a directory that already has files in it
36
- --yes, -y take the default selection without prompting
37
- --help print this
38
-
39
- With no --with and no prompt, you get the minimal template: five files, one route.
40
- With features, the wiring is generated around them and each feature's directory is
41
- copied from dunx's own examples/full, which CI runs and tours on every push.
717
+ --name <name> package name for the app (default: the directory name)
718
+ --force write into a directory that already has files in it
719
+ --yes, -y skip the questions and take the minimal template
720
+ --help print this
721
+
722
+ With no terminal to ask in - piped, or CI - it takes the minimal template rather
723
+ than blocking on a question nothing can answer. To choose features from a script,
724
+ call \`scaffold({ features })\` from \`@dunx/create-app\` instead.
42
725
  `;
43
- var featureList = () => FEATURES.map((feature) => {
44
- const needs = feature.requires.length === 0 ? "" : ` (pulls in ${feature.requires.join(", ")})`;
45
- const service = feature.service === undefined ? "" : ` [needs ${feature.service}]`;
46
- return ` ${feature.name.padEnd(12)}${feature.summary}${needs}${service}`;
47
- }).join(`
48
- `);
49
726
  function fail(message) {
50
727
  console.error(message);
51
728
  process.exit(1);
@@ -55,10 +732,6 @@ var { values, positionals } = parseArgs({
55
732
  allowPositionals: true,
56
733
  options: {
57
734
  name: { type: "string" },
58
- template: { type: "string" },
59
- with: { type: "string" },
60
- all: { type: "boolean", default: false },
61
- list: { type: "boolean", default: false },
62
735
  force: { type: "boolean", default: false },
63
736
  yes: { type: "boolean", default: false, short: "y" },
64
737
  help: { type: "boolean", default: false, short: "h" }
@@ -68,59 +741,55 @@ if (values.help === true) {
68
741
  console.log(USAGE);
69
742
  process.exit(0);
70
743
  }
71
- if (values.list === true) {
72
- console.log(`Features:
73
- ${featureList()}`);
74
- process.exit(0);
75
- }
744
+ var style = new Style;
76
745
  var target = positionals[0];
77
- if (target === undefined) {
78
- fail(`Missing the target directory.
746
+ var withoutAsking = () => {
747
+ if (target === undefined) {
748
+ fail(`Missing the target directory.
79
749
 
80
750
  ${USAGE}`);
81
- }
82
- var split = (value) => value.split(",").map((part) => part.trim()).filter((part) => part !== "");
83
- var ask = async () => {
84
- if (values.all === true)
85
- return featureNames;
86
- if (values.with !== undefined)
87
- return split(values.with);
88
- if (values.yes === true || !process.stdin.isTTY)
89
- return [];
90
- console.log(`Features (empty for the minimal template):
91
- ${featureList()}
92
- `);
93
- console.log("Names or numbers, comma separated. `all` for everything.");
94
- process.stdout.write("> ");
95
- const answer = await readLine();
96
- if (answer === "")
97
- return [];
98
- if (answer === "all")
99
- return featureNames;
100
- return split(answer).map((part) => {
101
- const index = Number(part);
102
- return Number.isInteger(index) && index >= 1 && index <= FEATURES.length ? FEATURES[index - 1]?.name ?? part : part;
103
- });
104
- };
105
- try {
106
- const requested = await ask();
107
- const result = await scaffold({
751
+ }
752
+ return {
108
753
  target,
109
754
  ...values.name === undefined ? {} : { name: values.name },
110
- ...values.template === undefined ? {} : { template: values.template },
111
- features: requested,
755
+ features: [],
112
756
  force: values.force === true
757
+ };
758
+ };
759
+ var byAsking = async () => {
760
+ const tty = new ProcessTty;
761
+ const banner = new Banner(style).lines(tty.columns, await packageVersion());
762
+ console.log(["", ...banner, ""].join(`
763
+ `));
764
+ const wizard = new Wizard(new PromptRunner(tty), style);
765
+ const answers = await wizard.run({
766
+ target,
767
+ name: values.name,
768
+ features: [],
769
+ force: values.force === true,
770
+ cwd: process.cwd()
113
771
  });
772
+ return answers;
773
+ };
774
+ var interactive = values.yes !== true && ProcessTty.available();
775
+ try {
776
+ const options = interactive ? await byAsking() : withoutAsking();
777
+ const result = await scaffold(options);
114
778
  const where = relative(process.cwd(), result.directory);
115
- console.log(where === "" ? `Created ${result.name} here` : `Created ${result.name} in ${where}/`);
779
+ console.log(where === "" ? `
780
+ Created ${result.name} here` : `
781
+ Created ${result.name} in ${where}/`);
116
782
  if (result.features.length === 0) {
117
783
  console.log(` ${result.files.length} files from the ${result.template} template
118
784
  `);
119
785
  } else {
120
- const implied = impliedBy(requested, FEATURES.filter((feature) => result.features.includes(feature.name)));
121
- console.log(` ${result.files.length} files, ${result.features.length} features: ` + `${result.features.join(", ")}`);
786
+ const requested = options.features ?? [];
787
+ const implied = result.features.filter((name) => !requested.includes(name));
788
+ const noun = result.features.length === 1 ? "feature" : "features";
789
+ console.log(` ${result.files.length} files, ${result.features.length} ${noun}: ` + `${result.features.join(", ")}`);
122
790
  if (implied.length > 0) {
123
- console.log(` ${implied.join(", ")} came along as requirements`);
791
+ const as = implied.length === 1 ? "a requirement" : "requirements";
792
+ console.log(` ${implied.join(", ")} came along as ${as}`);
124
793
  }
125
794
  const services = FEATURES.filter((feature) => result.features.includes(feature.name) && feature.service !== undefined);
126
795
  for (const feature of services) {
@@ -132,8 +801,12 @@ try {
132
801
  if (where !== "")
133
802
  console.log(` cd ${where}`);
134
803
  console.log(" bun install");
135
- console.log(" bun run start");
804
+ console.log(` bun run dev ${style.muted("# or `start`, which does not watch")}`);
136
805
  } catch (error) {
806
+ if (error instanceof CancelledError) {
807
+ console.log(style.muted(error.message));
808
+ process.exit(130);
809
+ }
137
810
  if (error instanceof ScaffoldError)
138
811
  fail(error.message);
139
812
  throw error;