@dunx/create-app 3.0.2 → 3.0.4

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,39 +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-nn9ekg83.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";
17
+
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;
671
+ }
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;
705
+ };
706
+
707
+ // src/cli.ts
15
708
  var USAGE = `Scaffold a new dunx application.
16
709
 
17
- 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.
18
715
 
19
716
  Options:
20
- --name <name> package name for the app (default: the directory name)
21
- --with <a,b,c> features to compose the app from (see --list)
22
- --all every feature
23
- --template <name> ${TEMPLATES.join(" | ")} (default: minimal, when no --with)
24
- --list print the features and exit
25
- --force write into a directory that already has files in it
26
- --yes, -y take the default selection without prompting
27
- --help print this
28
-
29
- With no --with and no prompt, you get the minimal template: five files, one route.
30
- With features, the wiring is generated around them and each feature's directory is
31
- 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.
32
725
  `;
33
- var featureList = () => FEATURES.map((feature) => {
34
- const needs = feature.requires.length === 0 ? "" : ` (pulls in ${feature.requires.join(", ")})`;
35
- const service = feature.service === undefined ? "" : ` [needs ${feature.service}]`;
36
- return ` ${feature.name.padEnd(12)}${feature.summary}${needs}${service}`;
37
- }).join(`
38
- `);
39
726
  function fail(message) {
40
727
  console.error(message);
41
728
  process.exit(1);
@@ -45,10 +732,6 @@ var { values, positionals } = parseArgs({
45
732
  allowPositionals: true,
46
733
  options: {
47
734
  name: { type: "string" },
48
- template: { type: "string" },
49
- with: { type: "string" },
50
- all: { type: "boolean", default: false },
51
- list: { type: "boolean", default: false },
52
735
  force: { type: "boolean", default: false },
53
736
  yes: { type: "boolean", default: false, short: "y" },
54
737
  help: { type: "boolean", default: false, short: "h" }
@@ -58,60 +741,55 @@ if (values.help === true) {
58
741
  console.log(USAGE);
59
742
  process.exit(0);
60
743
  }
61
- if (values.list === true) {
62
- console.log(`Features:
63
- ${featureList()}`);
64
- process.exit(0);
65
- }
744
+ var style = new Style;
66
745
  var target = positionals[0];
67
- if (target === undefined) {
68
- fail(`Missing the target directory.
746
+ var withoutAsking = () => {
747
+ if (target === undefined) {
748
+ fail(`Missing the target directory.
69
749
 
70
750
  ${USAGE}`);
71
- }
72
- var split = (value) => value.split(",").map((part) => part.trim()).filter((part) => part !== "");
73
- var ask = async () => {
74
- if (values.all === true)
75
- return featureNames;
76
- if (values.with !== undefined)
77
- return split(values.with);
78
- if (values.yes === true || !process.stdin.isTTY)
79
- return [];
80
- console.log(`Features (empty for the minimal template):
81
- ${featureList()}
82
- `);
83
- console.log("Names or numbers, comma separated. `all` for everything.");
84
- process.stdout.write("> ");
85
- const line = (await console[Symbol.asyncIterator]().next()).value;
86
- const answer = typeof line === "string" ? line.trim() : "";
87
- if (answer === "")
88
- return [];
89
- if (answer === "all")
90
- return featureNames;
91
- return split(answer).map((part) => {
92
- const index = Number(part);
93
- return Number.isInteger(index) && index >= 1 && index <= FEATURES.length ? FEATURES[index - 1]?.name ?? part : part;
94
- });
95
- };
96
- try {
97
- const requested = await ask();
98
- const result = await scaffold({
751
+ }
752
+ return {
99
753
  target,
100
754
  ...values.name === undefined ? {} : { name: values.name },
101
- ...values.template === undefined ? {} : { template: values.template },
102
- features: requested,
755
+ features: [],
103
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()
104
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);
105
778
  const where = relative(process.cwd(), result.directory);
106
- 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}/`);
107
782
  if (result.features.length === 0) {
108
783
  console.log(` ${result.files.length} files from the ${result.template} template
109
784
  `);
110
785
  } else {
111
- const implied = impliedBy(requested, FEATURES.filter((feature) => result.features.includes(feature.name)));
112
- 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(", ")}`);
113
790
  if (implied.length > 0) {
114
- 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}`);
115
793
  }
116
794
  const services = FEATURES.filter((feature) => result.features.includes(feature.name) && feature.service !== undefined);
117
795
  for (const feature of services) {
@@ -123,8 +801,12 @@ try {
123
801
  if (where !== "")
124
802
  console.log(` cd ${where}`);
125
803
  console.log(" bun install");
126
- console.log(" bun run start");
804
+ console.log(` bun run dev ${style.muted("# or `start`, which does not watch")}`);
127
805
  } catch (error) {
806
+ if (error instanceof CancelledError) {
807
+ console.log(style.muted(error.message));
808
+ process.exit(130);
809
+ }
128
810
  if (error instanceof ScaffoldError)
129
811
  fail(error.message);
130
812
  throw error;