@loom-js/create 0.0.0-stage → 0.1.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.
Files changed (42) hide show
  1. package/README.md +51 -2
  2. package/bin/create.mjs +18 -0
  3. package/dist/index.mjs +1202 -0
  4. package/dist/templates/biome/js/base/public/styles/base.css +17 -0
  5. package/dist/templates/biome/js/base/src/app.js +23 -0
  6. package/dist/templates/biome/js/base/src/pages/about.js +12 -0
  7. package/dist/templates/biome/js/base/src/pages/home.js +22 -0
  8. package/dist/templates/biome/js/base/src/routes.js +3 -0
  9. package/dist/templates/biome/js/server/src/prerender.entry.js +22 -0
  10. package/dist/templates/biome/js/tailwind/scripts/dev.mjs +66 -0
  11. package/dist/templates/biome/js/tailwind/src/app.js +23 -0
  12. package/dist/templates/biome/js/ui/pink/src/pages/home.js +21 -0
  13. package/dist/templates/biome/ts/base/public/styles/base.css +17 -0
  14. package/dist/templates/biome/ts/base/src/app.ts +23 -0
  15. package/dist/templates/biome/ts/base/src/pages/about.ts +12 -0
  16. package/dist/templates/biome/ts/base/src/pages/home.ts +22 -0
  17. package/dist/templates/biome/ts/base/src/routes.ts +3 -0
  18. package/dist/templates/biome/ts/base/src/types.d.ts +4 -0
  19. package/dist/templates/biome/ts/server/src/prerender.entry.ts +26 -0
  20. package/dist/templates/biome/ts/tailwind/scripts/dev.mjs +66 -0
  21. package/dist/templates/biome/ts/tailwind/src/app.ts +23 -0
  22. package/dist/templates/biome/ts/ui/pink/src/pages/home.ts +21 -0
  23. package/dist/templates/prettier/js/base/public/styles/base.css +17 -0
  24. package/dist/templates/prettier/js/base/src/app.js +23 -0
  25. package/dist/templates/prettier/js/base/src/pages/about.js +12 -0
  26. package/dist/templates/prettier/js/base/src/pages/home.js +22 -0
  27. package/dist/templates/prettier/js/base/src/routes.js +3 -0
  28. package/dist/templates/prettier/js/server/src/prerender.entry.js +22 -0
  29. package/dist/templates/prettier/js/tailwind/scripts/dev.mjs +66 -0
  30. package/dist/templates/prettier/js/tailwind/src/app.js +23 -0
  31. package/dist/templates/prettier/js/ui/pink/src/pages/home.js +21 -0
  32. package/dist/templates/prettier/ts/base/public/styles/base.css +17 -0
  33. package/dist/templates/prettier/ts/base/src/app.ts +23 -0
  34. package/dist/templates/prettier/ts/base/src/pages/about.ts +12 -0
  35. package/dist/templates/prettier/ts/base/src/pages/home.ts +22 -0
  36. package/dist/templates/prettier/ts/base/src/routes.ts +3 -0
  37. package/dist/templates/prettier/ts/base/src/types.d.ts +4 -0
  38. package/dist/templates/prettier/ts/server/src/prerender.entry.ts +26 -0
  39. package/dist/templates/prettier/ts/tailwind/scripts/dev.mjs +66 -0
  40. package/dist/templates/prettier/ts/tailwind/src/app.ts +23 -0
  41. package/dist/templates/prettier/ts/ui/pink/src/pages/home.ts +21 -0
  42. package/package.json +51 -3
package/dist/index.mjs ADDED
@@ -0,0 +1,1202 @@
1
+ // <define:__STAMPED_RANGES__>
2
+ var define_STAMPED_RANGES_default = { "@loom-js/build": "^0.4.0", "@loom-js/core": "^0.22.0", "@loom-js/devtools": "^0.2.0", "@loom-js/pink": "^0.10.2", "@biomejs/biome": "^2.6.5", "@tailwindcss/cli": "^4.3.3", prettier: "^3.9.6", tailwindcss: "^4.3.3", turbo: "^2.10.13", typescript: "^7.0.2" };
3
+
4
+ // src/choices.ts
5
+ var NONE = "none";
6
+ var DEFAULT_TARGET = "loom-app";
7
+ var DEFAULTS = {
8
+ formatter: "biome",
9
+ git: { kind: "new" },
10
+ install: true,
11
+ js: false,
12
+ layout: "single",
13
+ spa: false,
14
+ tailwind: false,
15
+ ui: NONE
16
+ };
17
+
18
+ // src/json.ts
19
+ var INDENT = " ";
20
+ var LINE_WIDTH = 80;
21
+ var isPrimitive = (value) => value === null || typeof value !== "object";
22
+ var print = (value, depth, prefixLength) => {
23
+ if (isPrimitive(value)) {
24
+ return JSON.stringify(value);
25
+ }
26
+ const inner = INDENT.repeat(depth + 1);
27
+ const outer = INDENT.repeat(depth);
28
+ if (Array.isArray(value)) {
29
+ if (value.length === 0) {
30
+ return "[]";
31
+ }
32
+ const inline = `[${value.map((item) => print(item, 0, 0)).join(", ")}]`;
33
+ if (value.every(isPrimitive) && prefixLength + inline.length + 1 <= LINE_WIDTH) {
34
+ return inline;
35
+ }
36
+ const items = value.map(
37
+ (item) => `${inner}${print(item, depth + 1, inner.length)}`
38
+ );
39
+ return `[
40
+ ${items.join(",\n")}
41
+ ${outer}]`;
42
+ }
43
+ const entries = Object.entries(value);
44
+ if (entries.length === 0) {
45
+ return "{}";
46
+ }
47
+ const lines = entries.map(([key, item]) => {
48
+ const keyText = `${inner}${JSON.stringify(key)}: `;
49
+ return `${keyText}${print(item, depth + 1, keyText.length)}`;
50
+ });
51
+ return `{
52
+ ${lines.join(",\n")}
53
+ ${outer}}`;
54
+ };
55
+ var toJson = (value) => `${print(value, 0, 0)}
56
+ `;
57
+ var toPackageJson = (value) => `${JSON.stringify(value, null, 4)}
58
+ `;
59
+
60
+ // src/registry/formatters.ts
61
+ var IGNORED = [
62
+ "build",
63
+ ".generated",
64
+ "package-lock.json",
65
+ "pnpm-lock.yaml",
66
+ "yarn.lock",
67
+ "bun.lock"
68
+ ];
69
+ var biome = {
70
+ checkCommand: ["biome", "check"],
71
+ devDependencies: ["@biomejs/biome"],
72
+ files: ({ ignored }) => ({
73
+ "biome.json": toJson({
74
+ // The installed version's schema, so it never drifts from the tool.
75
+ $schema: "./node_modules/@biomejs/biome/configuration_schema.json",
76
+ assist: {
77
+ actions: {
78
+ source: {
79
+ organizeImports: {
80
+ level: "on",
81
+ options: {
82
+ groups: [
83
+ [
84
+ ":NODE:",
85
+ ":PACKAGE:",
86
+ ":PACKAGE_WITH_PROTOCOL:"
87
+ ],
88
+ ":BLANK_LINE:",
89
+ [":ALIAS:", ":PATH:"]
90
+ ]
91
+ }
92
+ }
93
+ }
94
+ }
95
+ },
96
+ css: {
97
+ formatter: { quoteStyle: "single" },
98
+ parser: { tailwindDirectives: true }
99
+ },
100
+ files: {
101
+ includes: [
102
+ "**",
103
+ ...[...IGNORED, ...ignored].map((path6) => `!**/${path6}`)
104
+ ]
105
+ },
106
+ formatter: { indentStyle: "space", indentWidth: 4, lineWidth: 80 },
107
+ javascript: {
108
+ formatter: {
109
+ quoteStyle: "single",
110
+ semicolons: "always",
111
+ trailingCommas: "none"
112
+ }
113
+ },
114
+ linter: { enabled: false },
115
+ yaml: { formatter: { quoteStyle: "single" } }
116
+ })
117
+ }),
118
+ fixCommand: ["biome", "check", "--write"],
119
+ id: "biome",
120
+ label: "Biome"
121
+ };
122
+ var prettier = {
123
+ checkCommand: ["prettier", "--check", "."],
124
+ devDependencies: ["prettier"],
125
+ files: ({ ignored }) => ({
126
+ ".prettierignore": `${[...IGNORED, ...ignored].join("\n")}
127
+ `,
128
+ ".prettierrc": toJson({
129
+ semi: true,
130
+ singleQuote: true,
131
+ tabWidth: 4,
132
+ trailingComma: "none"
133
+ })
134
+ }),
135
+ fixCommand: ["prettier", "--write", "."],
136
+ id: "prettier",
137
+ label: "Prettier"
138
+ };
139
+ var FORMATTERS = [biome, prettier];
140
+
141
+ // src/registry/task-runners.ts
142
+ var TURBO_TASKS = {
143
+ build: { dependsOn: ["^build"], outputs: ["build/**"] },
144
+ dev: { cache: false, persistent: true }
145
+ };
146
+ var turbo = {
147
+ devDependencies: ["turbo"],
148
+ gitignore: [".turbo"],
149
+ id: "turbo",
150
+ label: "Turborepo",
151
+ rootFiles: ({ scripts }) => ({
152
+ "turbo.json": toJson({
153
+ $schema: "https://turborepo.com/schema.json",
154
+ tasks: Object.fromEntries(
155
+ scripts.map((script) => [script, TURBO_TASKS[script] ?? {}])
156
+ )
157
+ })
158
+ }),
159
+ runScript: (script) => `turbo run ${script}`
160
+ };
161
+ var TASK_RUNNERS = [turbo];
162
+
163
+ // src/registry/ui-kits.ts
164
+ var pink = {
165
+ bodyClass: "theme-light",
166
+ id: "pink",
167
+ label: "Pink (@loom-js/pink)",
168
+ packages: ["@loom-js/pink"],
169
+ stylesheets: ["@loom-js/pink/pink.css", "@loom-js/pink/icons.css"],
170
+ tailwind: {
171
+ // Pink block classes Tailwind would also generate as utilities.
172
+ sourceExclusions: ["container", "list-item", "table", "table-row"],
173
+ stylesheet: "@loom-js/pink/pink.css"
174
+ }
175
+ };
176
+ var UI_KITS = [pink];
177
+
178
+ // src/registry/index.ts
179
+ var REGISTRIES = {
180
+ formatters: FORMATTERS,
181
+ taskRunners: TASK_RUNNERS,
182
+ uiKits: UI_KITS
183
+ };
184
+ var findEntry = (entries, id) => entries.find((entry) => entry.id === id);
185
+ var entryOf = (entries, id) => {
186
+ const entry = findEntry(entries, id);
187
+ if (!entry) {
188
+ throw new Error(`[create] unknown registry entry "${id}".`);
189
+ }
190
+ return entry;
191
+ };
192
+
193
+ // src/args.ts
194
+ import { parseArgs } from "node:util";
195
+ var UsageError = class extends Error {
196
+ };
197
+ var LAYOUTS = ["single", "monorepo"];
198
+ var OPTIONS = {
199
+ formatter: { type: "string" },
200
+ git: { type: "string" },
201
+ help: { short: "h", type: "boolean" },
202
+ install: { type: "boolean" },
203
+ js: { type: "boolean" },
204
+ layout: { type: "string" },
205
+ "no-install": { type: "boolean" },
206
+ spa: { type: "boolean" },
207
+ tailwind: { type: "boolean" },
208
+ "task-runner": { type: "string" },
209
+ ui: { type: "string" },
210
+ yes: { short: "y", type: "boolean" }
211
+ };
212
+ var registryValue = (flag, value, entries, allowNone) => {
213
+ if (allowNone && value === NONE || findEntry(entries, value)) {
214
+ return value;
215
+ }
216
+ const ids = [...allowNone ? [NONE] : [], ...entries.map(({ id }) => id)];
217
+ throw new UsageError(
218
+ `--${flag} "${value}" is not one of: ${ids.join(", ")}.`
219
+ );
220
+ };
221
+ var looksLikeRepository = (value) => /[:/\\]/.test(value);
222
+ var gitValue = (value) => {
223
+ if (value === "new" || value === NONE) {
224
+ return { kind: value };
225
+ }
226
+ if (looksLikeRepository(value)) {
227
+ return { kind: "clone", url: value };
228
+ }
229
+ throw new UsageError(
230
+ `--git takes new, none or a repository URL, not "${value}".`
231
+ );
232
+ };
233
+ var parse = (argv) => {
234
+ try {
235
+ return parseArgs({
236
+ allowPositionals: true,
237
+ args: [...argv],
238
+ options: OPTIONS,
239
+ strict: true
240
+ });
241
+ } catch (error) {
242
+ throw new UsageError(
243
+ error instanceof Error ? error.message : String(error)
244
+ );
245
+ }
246
+ };
247
+ var parseCliArgs = (argv, registries) => {
248
+ const { positionals, values } = parse(argv);
249
+ const answers = {};
250
+ if (positionals.length > 1) {
251
+ throw new UsageError(
252
+ `expected one directory, got ${positionals.length}: ${positionals.join(" ")}.`
253
+ );
254
+ }
255
+ if (positionals[0] !== void 0) {
256
+ answers.target = positionals[0];
257
+ }
258
+ if (values.layout !== void 0) {
259
+ if (!LAYOUTS.includes(values.layout)) {
260
+ throw new UsageError(
261
+ `--layout "${values.layout}" is not one of: ${LAYOUTS.join(", ")}.`
262
+ );
263
+ }
264
+ answers.layout = values.layout;
265
+ }
266
+ if (values["task-runner"] !== void 0) {
267
+ if (answers.layout === "single") {
268
+ throw new UsageError(
269
+ "--task-runner applies only to --layout monorepo."
270
+ );
271
+ }
272
+ answers.layout = "monorepo";
273
+ answers.taskRunner = registryValue(
274
+ "task-runner",
275
+ values["task-runner"],
276
+ registries.taskRunners,
277
+ false
278
+ );
279
+ }
280
+ if (values.ui !== void 0) {
281
+ answers.ui = registryValue("ui", values.ui, registries.uiKits, true);
282
+ }
283
+ if (values.formatter !== void 0) {
284
+ answers.formatter = registryValue(
285
+ "formatter",
286
+ values.formatter,
287
+ registries.formatters,
288
+ true
289
+ );
290
+ }
291
+ if (values.git !== void 0) {
292
+ answers.git = gitValue(values.git);
293
+ }
294
+ if (values.install && values["no-install"]) {
295
+ throw new UsageError("--install and --no-install contradict.");
296
+ }
297
+ if (values.install || values["no-install"]) {
298
+ answers.install = Boolean(values.install);
299
+ }
300
+ if (values.spa) {
301
+ answers.spa = true;
302
+ }
303
+ if (values.tailwind) {
304
+ answers.tailwind = true;
305
+ }
306
+ if (values.js) {
307
+ answers.js = true;
308
+ }
309
+ return {
310
+ answers,
311
+ help: Boolean(values.help),
312
+ yes: Boolean(values.yes)
313
+ };
314
+ };
315
+
316
+ // src/package-manager.ts
317
+ var NAMES = ["npm", "pnpm", "yarn", "bun"];
318
+ var detectPackageManager = (env) => {
319
+ const [name, version] = env.npm_config_user_agent?.split(" ")[0]?.split("/") ?? [];
320
+ const known = NAMES.find((candidate) => candidate === name);
321
+ return known ? { name: known, version } : { name: "npm" };
322
+ };
323
+ var resolveVersion = async (packageManager, spawn2, cwd) => {
324
+ if (packageManager.version) {
325
+ return packageManager.version;
326
+ }
327
+ const { code, stdout } = await spawn2(packageManager.name, ["--version"], {
328
+ cwd
329
+ });
330
+ return code === 0 ? stdout.trim() : void 0;
331
+ };
332
+ var installCommand = ({ name }) => [name, "install"];
333
+ var runCommand = ({ name }, script) => name === "npm" || name === "bun" ? `${name} run ${script}` : `${name} ${script}`;
334
+
335
+ // src/prompts.ts
336
+ var askText = async (io, question, fallback) => (await io.ask(`${question} (${fallback}) `)).trim() || fallback;
337
+ var askConfirm = async (io, question, fallback) => {
338
+ for (; ; ) {
339
+ const answer = (await io.ask(`${question} (${fallback ? "Y/n" : "y/N"}) `)).trim().toLowerCase();
340
+ if (!answer) {
341
+ return fallback;
342
+ }
343
+ if (answer === "y" || answer === "yes") {
344
+ return true;
345
+ }
346
+ if (answer === "n" || answer === "no") {
347
+ return false;
348
+ }
349
+ }
350
+ };
351
+ var askChoice = async (io, question, options, fallback) => {
352
+ const fallbackIndex = Math.max(
353
+ 0,
354
+ options.findIndex(({ value }) => value === fallback)
355
+ );
356
+ const list = options.map(({ label }, index) => ` ${index + 1}) ${label}`).join("\n");
357
+ for (; ; ) {
358
+ const answer = (await io.ask(`${question}
359
+ ${list}
360
+ (${fallbackIndex + 1}) `)).trim();
361
+ const index = answer ? Number(answer) - 1 : fallbackIndex;
362
+ const option = options[index];
363
+ if (option) {
364
+ return option.value;
365
+ }
366
+ }
367
+ };
368
+ var entryOptions = (entries, withNone) => [
369
+ ...withNone ? [{ label: "None", value: NONE }] : [],
370
+ ...entries.map(({ id, label }) => ({ label, value: id }))
371
+ ];
372
+ var askGit = async (io) => {
373
+ const kind = await askChoice(
374
+ io,
375
+ "Git repository?",
376
+ [
377
+ { label: "New repository with an initial commit", value: "new" },
378
+ { label: "Clone an existing repository", value: "clone" },
379
+ { label: "None", value: NONE }
380
+ ],
381
+ "new"
382
+ );
383
+ if (kind !== "clone") {
384
+ return { kind };
385
+ }
386
+ for (; ; ) {
387
+ const url = (await io.ask("Repository URL to clone: ")).trim();
388
+ if (url) {
389
+ return { kind: "clone", url };
390
+ }
391
+ }
392
+ };
393
+ var resolveChoices = async ({ answers, yes }, io, registries) => {
394
+ const ask = io.interactive && !yes;
395
+ const defaultTaskRunner = registries.taskRunners[0]?.id;
396
+ const target = answers.target ?? (ask ? await askText(io, "Project directory?", DEFAULT_TARGET) : DEFAULT_TARGET);
397
+ const layout = answers.layout ?? (ask ? await askChoice(
398
+ io,
399
+ "Layout?",
400
+ [
401
+ { label: "Single app", value: "single" },
402
+ { label: "Monorepo (app at apps/web)", value: "monorepo" }
403
+ ],
404
+ DEFAULTS.layout
405
+ ) : DEFAULTS.layout);
406
+ const taskRunner = layout !== "monorepo" ? void 0 : answers.taskRunner ?? // A one-entry registry has nothing to ask.
407
+ (ask && defaultTaskRunner && registries.taskRunners.length > 1 ? await askChoice(
408
+ io,
409
+ "Task runner?",
410
+ entryOptions(registries.taskRunners, false),
411
+ defaultTaskRunner
412
+ ) : defaultTaskRunner);
413
+ const spa = answers.spa ?? (ask ? await askChoice(
414
+ io,
415
+ "Rendering?",
416
+ [
417
+ {
418
+ label: "Server-first (prerender + hydrate)",
419
+ value: false
420
+ },
421
+ { label: "Client-only (SPA)", value: true }
422
+ ],
423
+ DEFAULTS.spa
424
+ ) : DEFAULTS.spa);
425
+ const ui = answers.ui ?? (ask ? await askChoice(
426
+ io,
427
+ "UI kit?",
428
+ entryOptions(registries.uiKits, true),
429
+ DEFAULTS.ui
430
+ ) : DEFAULTS.ui);
431
+ const tailwind = answers.tailwind ?? (ask ? await askConfirm(io, "Add Tailwind CSS?", DEFAULTS.tailwind) : DEFAULTS.tailwind);
432
+ const formatter = answers.formatter ?? (ask ? await askChoice(
433
+ io,
434
+ "Formatter?",
435
+ entryOptions(registries.formatters, true),
436
+ DEFAULTS.formatter
437
+ ) : DEFAULTS.formatter);
438
+ const js = answers.js ?? (ask ? await askChoice(
439
+ io,
440
+ "Language?",
441
+ [
442
+ { label: "TypeScript", value: false },
443
+ { label: "JavaScript", value: true }
444
+ ],
445
+ DEFAULTS.js
446
+ ) : DEFAULTS.js);
447
+ const git = answers.git ?? (ask ? await askGit(io) : DEFAULTS.git);
448
+ const install = answers.install ?? (ask ? await askConfirm(io, "Install dependencies?", DEFAULTS.install) : DEFAULTS.install);
449
+ return {
450
+ formatter,
451
+ git,
452
+ install,
453
+ js,
454
+ layout,
455
+ spa,
456
+ tailwind,
457
+ target,
458
+ taskRunner,
459
+ ui
460
+ };
461
+ };
462
+
463
+ // src/target.ts
464
+ import { readdir } from "node:fs/promises";
465
+ import path from "node:path";
466
+ var NPM_NAME = /^[a-z0-9~-][a-z0-9._~-]*$/;
467
+ var NPM_NAME_MAX = 214;
468
+ var packageNameOf = (target, cwd) => {
469
+ const name = path.basename(path.resolve(cwd, target)).toLowerCase().replace(/\s+/g, "-");
470
+ if (!NPM_NAME.test(name) || name.length > NPM_NAME_MAX) {
471
+ throw new UsageError(
472
+ `"${name}" is not a valid npm package name; pick another directory name.`
473
+ );
474
+ }
475
+ return name;
476
+ };
477
+ var checkTarget = async (directory) => {
478
+ let entries;
479
+ try {
480
+ entries = await readdir(directory);
481
+ } catch {
482
+ return { exists: false };
483
+ }
484
+ const occupied = entries.filter((entry) => entry !== ".git");
485
+ if (occupied.length > 0) {
486
+ throw new UsageError(
487
+ `${directory} is not empty (${occupied.slice(0, 3).join(", ")}${occupied.length > 3 ? ", \u2026" : ""}); pick an empty or new directory.`
488
+ );
489
+ }
490
+ return { exists: true };
491
+ };
492
+
493
+ // src/usage.ts
494
+ var COLUMN = 38;
495
+ var idList = (entries, withNone) => [...withNone ? [NONE] : [], ...entries.map(({ id }) => id)].join("|");
496
+ var row = ([flag, description]) => ` ${flag.padEnd(COLUMN - 3)} ${description}`;
497
+ var usage = ({ formatters, taskRunners, uiKits }) => {
498
+ const rows = [
499
+ ["--layout <single|monorepo>", "App layout (default: single)"],
500
+ [
501
+ `--task-runner <${idList(taskRunners, false)}>`,
502
+ `Monorepo task runner (default: ${taskRunners[0]?.id})`
503
+ ],
504
+ ["--spa", "Client-only rendering (default: server-first)"],
505
+ [`--ui <${idList(uiKits, true)}>`, `UI kit (default: ${NONE})`],
506
+ ["--tailwind", "Tailwind CSS v4"],
507
+ [
508
+ `--formatter <${idList(formatters, true)}>`,
509
+ `Formatter (default: ${formatters[0]?.id})`
510
+ ],
511
+ ["--js", "JavaScript instead of TypeScript"],
512
+ [
513
+ "--git <new|none|url>",
514
+ "New repo, none, or clone a URL (default: new)"
515
+ ],
516
+ ["--install, --no-install", "Install dependencies (default: install)"],
517
+ ["-y, --yes", "Accept the defaults for every unanswered choice"],
518
+ ["-h, --help", "Show this help"]
519
+ ];
520
+ return `Usage: create @loom-js [directory] [options]
521
+
522
+ Run it through your package manager: npm create @loom-js, pnpm create
523
+ @loom-js, yarn create @loom-js or bun create @loom-js (or npx, pnpm dlx,
524
+ yarn dlx or bunx @loom-js/create). In a terminal, unanswered choices are
525
+ prompted for; otherwise they take their defaults.
526
+
527
+ Options:
528
+ ${rows.map(row).join("\n")}`;
529
+ };
530
+
531
+ // src/versions.ts
532
+ var STAMPED_RANGES = define_STAMPED_RANGES_default;
533
+ var rangeOf = (name) => {
534
+ const range = STAMPED_RANGES[name];
535
+ if (!range) {
536
+ throw new Error(`[create] no stamped range for "${name}".`);
537
+ }
538
+ return range;
539
+ };
540
+
541
+ // src/git.ts
542
+ import { access, readFile } from "node:fs/promises";
543
+ import path2 from "node:path";
544
+ var KEPT_FROM_CLONE = ["README.md", "LICENSE", ".gitignore"];
545
+ var NEW_COMMIT_MESSAGE = "Initial commit from @loom-js/create";
546
+ var CLONE_COMMIT_MESSAGE = "Add a loom app from @loom-js/create";
547
+ var exists = (filePath) => access(filePath).then(
548
+ () => true,
549
+ () => false
550
+ );
551
+ var hasGit = async (spawn2, cwd) => (await spawn2("git", ["--version"], { cwd })).code === 0;
552
+ var cloneConflicts = async (directory, planned) => {
553
+ const conflicts = [];
554
+ for (const filePath of planned) {
555
+ if (!KEPT_FROM_CLONE.includes(filePath) && await exists(path2.join(directory, filePath))) {
556
+ conflicts.push(filePath);
557
+ }
558
+ }
559
+ return conflicts;
560
+ };
561
+ var mergeGitignore = (existing, ours) => {
562
+ const present = new Set(existing.split("\n").map((line) => line.trim()));
563
+ const missing = ours.split("\n").filter((line) => line.trim() && !present.has(line.trim()));
564
+ if (missing.length === 0) {
565
+ return existing;
566
+ }
567
+ const base = existing.endsWith("\n") ? existing : `${existing}
568
+ `;
569
+ return `${base}${missing.join("\n")}
570
+ `;
571
+ };
572
+ var readCloned = (directory, filePath) => readFile(path2.join(directory, filePath), "utf8").catch(() => void 0);
573
+ var commitAll = async (directory, message, spawn2, io) => {
574
+ const identity = await spawn2("git", ["config", "user.email"], {
575
+ cwd: directory
576
+ });
577
+ if (identity.code !== 0 || !identity.stdout.trim()) {
578
+ io.print(
579
+ [
580
+ "git has no identity, so nothing was committed. To finish:",
581
+ ' git config --global user.name "Your Name"',
582
+ " git config --global user.email you@example.com",
583
+ ` git add -A && git commit -m "${message}"`
584
+ ].join("\n")
585
+ );
586
+ return false;
587
+ }
588
+ const add = await spawn2("git", ["add", "-A"], { cwd: directory });
589
+ const commit = add.code === 0 ? await spawn2("git", ["commit", "-q", "-m", message], {
590
+ cwd: directory
591
+ }) : add;
592
+ if (commit.code !== 0) {
593
+ io.error(`git commit failed:
594
+ ${commit.stderr.trim()}`);
595
+ return false;
596
+ }
597
+ return true;
598
+ };
599
+
600
+ // src/layers.ts
601
+ import { readdir as readdir2, readFile as readFile2 } from "node:fs/promises";
602
+ import path3 from "node:path";
603
+ var layerNamesFor = ({ spa, tailwind, ui }) => [
604
+ "base",
605
+ ...spa ? [] : ["server"],
606
+ ...ui === NONE ? [] : [`ui/${ui}`],
607
+ ...tailwind ? ["tailwind"] : []
608
+ ];
609
+ var mergeLayers = (layers) => {
610
+ const files = {};
611
+ const owners = /* @__PURE__ */ new Map();
612
+ for (const { files: layerFiles, name } of layers) {
613
+ for (const [filePath, content] of Object.entries(layerFiles)) {
614
+ const owner = owners.get(filePath);
615
+ if (owner && owner !== "base") {
616
+ throw new Error(
617
+ `[create] layers "${owner}" and "${name}" both write ${filePath}.`
618
+ );
619
+ }
620
+ owners.set(filePath, name);
621
+ files[filePath] = content;
622
+ }
623
+ }
624
+ return files;
625
+ };
626
+ var diskLayerReader = (root) => async (name) => {
627
+ const directory = path3.join(root, name);
628
+ const entries = await readdir2(directory, {
629
+ recursive: true,
630
+ withFileTypes: true
631
+ });
632
+ const files = {};
633
+ for (const entry of entries.filter((item) => item.isFile())) {
634
+ const absolute = path3.join(entry.parentPath, entry.name);
635
+ const relative = path3.relative(directory, absolute).split(path3.sep).join("/");
636
+ files[relative] = await readFile2(absolute, "utf8");
637
+ }
638
+ return { files, name };
639
+ };
640
+
641
+ // src/render/bootstrap.ts
642
+ var renderBootstrap = ({ choices, uiKit }) => {
643
+ const stylesheets = (uiKit?.stylesheets ?? []).filter(
644
+ // With Tailwind, the kit's main sheet is imported by `styles.css`.
645
+ (sheet) => !(choices.tailwind && sheet === uiKit?.tailwind.stylesheet)
646
+ );
647
+ const coreImport = choices.spa ? "import { APP_ROOT_ID, init } from '@loom-js/core';" : [
648
+ "import {",
649
+ " APP_ROOT_ID,",
650
+ " hydrate,",
651
+ " primeResources,",
652
+ " STATE_SCRIPT_ID",
653
+ "} from '@loom-js/core';"
654
+ ].join("\n");
655
+ const boot = choices.spa ? ["init({ app: App(), root: document.getElementById(APP_ROOT_ID) });"] : [
656
+ "// A prerendered page embeds its state; prime it so nothing refetches.",
657
+ "const payload = document.getElementById(STATE_SCRIPT_ID)?.textContent;",
658
+ "",
659
+ "if (payload) {",
660
+ " primeResources(JSON.parse(payload));",
661
+ "}",
662
+ "",
663
+ "hydrate({ app: App(), root: document.getElementById(APP_ROOT_ID) });"
664
+ ];
665
+ return [
666
+ coreImport,
667
+ ...stylesheets.map((sheet) => `import '${sheet}';`),
668
+ "",
669
+ "import { App } from './app';",
670
+ "",
671
+ "// Reloads the page when `loom dev` rebuilds; production builds drop it.",
672
+ "if (__DEV__) {",
673
+ " new EventSource('/esbuild').addEventListener('change', () =>",
674
+ " location.reload()",
675
+ " );",
676
+ "}",
677
+ "",
678
+ ...boot,
679
+ ""
680
+ ].join("\n");
681
+ };
682
+
683
+ // src/render/compiler-config.ts
684
+ var PATHS = { "@app/*": ["./src/*"] };
685
+ var COMPILER_OPTIONS = {
686
+ esModuleInterop: true,
687
+ isolatedModules: true,
688
+ lib: ["ES2022", "DOM", "DOM.Iterable"],
689
+ module: "ESNext",
690
+ moduleDetection: "force",
691
+ moduleResolution: "Bundler",
692
+ noEmit: true,
693
+ noUncheckedIndexedAccess: true,
694
+ resolveJsonModule: true,
695
+ skipLibCheck: true,
696
+ strict: true,
697
+ target: "ES2022"
698
+ };
699
+ var renderCompilerConfig = ({
700
+ choices
701
+ }) => {
702
+ if (choices.js) {
703
+ return {
704
+ "jsconfig.json": toJson({
705
+ compilerOptions: {
706
+ module: "ESNext",
707
+ moduleResolution: "Bundler",
708
+ paths: PATHS,
709
+ target: "ES2022"
710
+ },
711
+ include: ["src", "loom.config.mjs"]
712
+ })
713
+ };
714
+ }
715
+ const include = ["src", "loom.config.ts"];
716
+ return {
717
+ "tsconfig.json": toJson(
718
+ choices.layout === "monorepo" ? {
719
+ extends: "../../tsconfig.base.json",
720
+ compilerOptions: { paths: PATHS },
721
+ include
722
+ } : {
723
+ compilerOptions: { ...COMPILER_OPTIONS, paths: PATHS },
724
+ include
725
+ }
726
+ )
727
+ };
728
+ };
729
+
730
+ // src/render/context.ts
731
+ var TAILWIND_INPUT = "./src/styles.css";
732
+ var TAILWIND_OUTPUT = "./.generated/styles.css";
733
+ var BASE_STYLESHEET = "./public/styles/base.css";
734
+ var appScripts = ({ choices }) => [
735
+ "dev",
736
+ "build",
737
+ ...choices.js ? [] : ["type-check"]
738
+ ];
739
+ var formatterInApp = ({ choices, formatter }) => Boolean(formatter) && choices.layout === "single";
740
+
741
+ // src/render/loom-config.ts
742
+ var LINE_WIDTH2 = 80;
743
+ var stringArray = (key, values, indent) => {
744
+ const quoted = values.map((value) => `'${value}'`);
745
+ const inline = `${indent}${key}: [${quoted.join(", ")}],`;
746
+ return inline.length <= LINE_WIDTH2 ? inline : `${indent}${key}: [
747
+ ${quoted.map((value) => `${indent} ${value}`).join(",\n")}
748
+ ${indent}],`;
749
+ };
750
+ var renderLoomConfig = ({ choices, name, uiKit }) => {
751
+ const styles = choices.tailwind ? [BASE_STYLESHEET, TAILWIND_OUTPUT] : [BASE_STYLESHEET];
752
+ const html = [
753
+ ...uiKit?.bodyClass ? [` bodyClass: '${uiKit.bodyClass}',`] : [],
754
+ ` title: () => '${name}'`
755
+ ];
756
+ const prerender = choices.spa ? [] : [
757
+ " prerender: {",
758
+ " entry: './src/prerender.entry',",
759
+ " routes: () => routes",
760
+ " },"
761
+ ];
762
+ return [
763
+ "import { defineConfig } from '@loom-js/build';",
764
+ "",
765
+ "import { routes } from './src/routes';",
766
+ "",
767
+ "export default defineConfig({",
768
+ " entry: './src/bootstrap',",
769
+ " html: {",
770
+ ...html,
771
+ " },",
772
+ ...prerender,
773
+ " routes,",
774
+ stringArray("styles", styles, " ").replace(/,$/, ""),
775
+ "});",
776
+ ""
777
+ ].join("\n");
778
+ };
779
+
780
+ // src/render/package-json.ts
781
+ var sorted = (record) => Object.fromEntries(
782
+ Object.entries(record).sort(
783
+ ([left], [right]) => left.localeCompare(right)
784
+ )
785
+ );
786
+ var rangesOf = (names) => sorted(Object.fromEntries(names.map((name) => [name, rangeOf(name)])));
787
+ var formatterScripts = ({
788
+ formatter
789
+ }) => formatter ? {
790
+ format: formatter.fixCommand.join(" "),
791
+ "format:check": formatter.checkCommand.join(" ")
792
+ } : {};
793
+ var renderAppPackageJson = (context) => {
794
+ const { choices, formatter, name, uiKit } = context;
795
+ const tailwindBuild = `tailwindcss -i ${TAILWIND_INPUT} -o ${TAILWIND_OUTPUT}`;
796
+ const ownsFormatter = formatterInApp(context);
797
+ return toPackageJson({
798
+ name: choices.layout === "monorepo" ? "web" : name,
799
+ version: "0.0.0",
800
+ private: true,
801
+ type: "module",
802
+ scripts: sorted({
803
+ build: choices.tailwind ? `${tailwindBuild} && loom build` : "loom build",
804
+ dev: choices.tailwind ? "node scripts/dev.mjs" : "loom dev",
805
+ ...choices.js ? {} : { "type-check": "tsc --noEmit" },
806
+ ...ownsFormatter ? formatterScripts(context) : {}
807
+ }),
808
+ dependencies: rangesOf(["@loom-js/core", ...uiKit?.packages ?? []]),
809
+ devDependencies: rangesOf([
810
+ "@loom-js/build",
811
+ "@loom-js/devtools",
812
+ ...choices.js ? [] : ["typescript"],
813
+ ...choices.tailwind ? ["@tailwindcss/cli", "tailwindcss"] : [],
814
+ ...ownsFormatter && formatter ? formatter.devDependencies : []
815
+ ])
816
+ });
817
+ };
818
+ var renderRootPackageJson = (context, scripts, packageManagerField) => {
819
+ const { formatter, name, packageManager, taskRunner } = context;
820
+ if (!taskRunner) {
821
+ throw new Error("[create] a monorepo needs a task runner.");
822
+ }
823
+ return toPackageJson({
824
+ name,
825
+ private: true,
826
+ ...packageManagerField ? { packageManager: packageManagerField } : {},
827
+ ...packageManager.name === "pnpm" ? {} : { workspaces: ["apps/*", "packages/*"] },
828
+ scripts: sorted({
829
+ ...Object.fromEntries(
830
+ scripts.map((script) => [script, taskRunner.runScript(script)])
831
+ ),
832
+ ...formatterScripts(context)
833
+ }),
834
+ devDependencies: rangesOf([
835
+ ...taskRunner.devDependencies,
836
+ ...formatter?.devDependencies ?? []
837
+ ])
838
+ });
839
+ };
840
+
841
+ // src/render/repo-files.ts
842
+ var SCRIPT_PURPOSE = {
843
+ build: ({ choices }) => choices.spa ? "build the app into `build/`." : "build the app into `build/`, prerendering every route.",
844
+ dev: () => "start the dev server, reloading on every change.",
845
+ format: ({ formatter }) => `format every file with ${formatter?.label}.`,
846
+ "format:check": () => "fail on any unformatted file.",
847
+ "type-check": () => "type-check with `tsc`."
848
+ };
849
+ var renderGitignore = ({ choices, taskRunner }) => `${[
850
+ "node_modules",
851
+ "build",
852
+ ".DS_Store",
853
+ "*.log",
854
+ ...choices.tailwind ? [".generated"] : [],
855
+ ...choices.layout === "monorepo" ? taskRunner?.gitignore ?? [] : []
856
+ ].join("\n")}
857
+ `;
858
+ var renderReadme = (context) => {
859
+ const { choices, formatter, name, packageManager } = context;
860
+ const scripts = [
861
+ ...appScripts(context),
862
+ ...formatter ? ["format", "format:check"] : []
863
+ ];
864
+ const where = choices.layout === "monorepo" ? "A monorepo with a [loom](https://loom-js-docs.vercel.app/docs) app in `apps/web`" : "A [loom](https://loom-js-docs.vercel.app/docs) app";
865
+ return [
866
+ `# ${name}`,
867
+ "",
868
+ `${where}, created with \`@loom-js/create\`.`,
869
+ "",
870
+ "## Scripts",
871
+ "",
872
+ ...scripts.map(
873
+ (script) => `- \`${runCommand(packageManager, script)}\`: ${SCRIPT_PURPOSE[script]?.(context)}`
874
+ ),
875
+ ""
876
+ ].join("\n");
877
+ };
878
+ var buildScriptPackages = ({ choices }) => [
879
+ ...choices.tailwind ? ["@parcel/watcher"] : [],
880
+ "esbuild"
881
+ ];
882
+ var yamlKey = (name) => /^[\w-]+$/.test(name) ? name : `'${name}'`;
883
+ var renderPnpmWorkspace = (context) => {
884
+ const packages = buildScriptPackages(context);
885
+ return [
886
+ ...context.choices.layout === "monorepo" ? ["packages:", " - apps/*", " - packages/*"] : [],
887
+ "allowBuilds:",
888
+ ...packages.map((name) => ` ${yamlKey(name)}: true`),
889
+ "onlyBuiltDependencies:",
890
+ ...packages.map((name) => ` - ${yamlKey(name)}`),
891
+ ""
892
+ ].join("\n");
893
+ };
894
+ var renderYarnrc = (version) => version && Number(version.split(".")[0]) >= 2 ? { ".yarnrc.yml": "nodeLinker: node-modules\n" } : {};
895
+
896
+ // src/render/styles.ts
897
+ var renderTailwindStyles = ({ uiKit }) => {
898
+ if (!uiKit) {
899
+ return "@import 'tailwindcss';\n";
900
+ }
901
+ return [
902
+ `/* ${uiKit.label} first: its layer order puts Tailwind's preflight`,
903
+ " before the kit and Tailwind's utilities after it. */",
904
+ `@import '${uiKit.tailwind.stylesheet}';`,
905
+ "@import 'tailwindcss';",
906
+ ...uiKit.tailwind.sourceExclusions.length > 0 ? [
907
+ "",
908
+ "/* Kit classes Tailwind would also generate as utilities. */",
909
+ `@source not inline('${uiKit.tailwind.sourceExclusions.join(" ")}');`
910
+ ] : [],
911
+ ""
912
+ ].join("\n");
913
+ };
914
+
915
+ // src/scaffold.ts
916
+ import { mkdir, writeFile } from "node:fs/promises";
917
+ import path4 from "node:path";
918
+ var MONOREPO_APP_DIR = "apps/web";
919
+ var when = (condition, files) => condition ? files() : {};
920
+ var prefixed = (prefix, files) => Object.fromEntries(
921
+ Object.entries(files).map(([filePath, content]) => [
922
+ prefix ? `${prefix}/${filePath}` : filePath,
923
+ content
924
+ ])
925
+ );
926
+ var appFiles = async (context, readLayer) => {
927
+ const { choices } = context;
928
+ const layers = await Promise.all(
929
+ layerNamesFor(choices).map((name) => readLayer(name))
930
+ );
931
+ const extension = choices.js ? "js" : "ts";
932
+ return {
933
+ ...mergeLayers(layers),
934
+ [choices.js ? "loom.config.mjs" : "loom.config.ts"]: renderLoomConfig(context),
935
+ "package.json": renderAppPackageJson(context),
936
+ [`src/bootstrap.${extension}`]: renderBootstrap(context),
937
+ ...when(choices.tailwind, () => ({
938
+ "src/styles.css": renderTailwindStyles(context)
939
+ })),
940
+ ...renderCompilerConfig(context)
941
+ };
942
+ };
943
+ var repoFiles = (context, options) => ({
944
+ ".gitignore": renderGitignore(context),
945
+ "README.md": renderReadme(context),
946
+ ...context.formatter?.files({
947
+ ignored: context.choices.layout === "monorepo" ? context.taskRunner?.gitignore ?? [] : []
948
+ }) ?? {},
949
+ ...when(context.packageManager.name === "pnpm", () => ({
950
+ "pnpm-workspace.yaml": renderPnpmWorkspace(context)
951
+ })),
952
+ ...when(
953
+ context.packageManager.name === "yarn",
954
+ () => renderYarnrc(options.packageManagerVersion)
955
+ )
956
+ });
957
+ var workspaceFiles = (context, options) => {
958
+ const { choices, taskRunner } = context;
959
+ const delegated = appScripts(context);
960
+ return {
961
+ "package.json": renderRootPackageJson(
962
+ context,
963
+ delegated,
964
+ options.packageManagerField
965
+ ),
966
+ ...when(!choices.js, () => ({
967
+ "tsconfig.base.json": toJson({ compilerOptions: COMPILER_OPTIONS })
968
+ })),
969
+ ...taskRunner?.rootFiles({ scripts: delegated }) ?? {},
970
+ "packages/.gitkeep": ""
971
+ };
972
+ };
973
+ var planScaffold = async (context, readLayer, options = {}) => {
974
+ const app = await appFiles(context, readLayer);
975
+ if (context.choices.layout === "single") {
976
+ return { ...app, ...repoFiles(context, options) };
977
+ }
978
+ return {
979
+ ...prefixed(MONOREPO_APP_DIR, app),
980
+ ...repoFiles(context, options),
981
+ ...workspaceFiles(context, options)
982
+ };
983
+ };
984
+ var writeScaffold = async (directory, files) => {
985
+ for (const [filePath, content] of Object.entries(files)) {
986
+ const absolute = path4.join(directory, ...filePath.split("/"));
987
+ await mkdir(path4.dirname(absolute), { recursive: true });
988
+ await writeFile(absolute, content);
989
+ }
990
+ };
991
+
992
+ // src/cli.ts
993
+ import { mkdir as mkdir2, readdir as readdir3, rm } from "node:fs/promises";
994
+ import path5 from "node:path";
995
+ import { fileURLToPath } from "node:url";
996
+ var defaultTemplates = () => fileURLToPath(new URL("./templates/", import.meta.url));
997
+ var contextOf = (choices, name, packageManager, registries) => ({
998
+ choices,
999
+ formatter: choices.formatter === NONE ? void 0 : entryOf(registries.formatters, choices.formatter),
1000
+ name,
1001
+ packageManager,
1002
+ taskRunner: choices.taskRunner ? entryOf(registries.taskRunners, choices.taskRunner) : void 0,
1003
+ uiKit: choices.ui === NONE ? void 0 : entryOf(registries.uiKits, choices.ui)
1004
+ });
1005
+ var treeOf = (choices, registries, root) => path5.join(
1006
+ root,
1007
+ choices.formatter === NONE ? registries.formatters[0]?.id ?? "" : choices.formatter,
1008
+ choices.js ? "js" : "ts"
1009
+ );
1010
+ var removeClone = async (directory, created) => {
1011
+ if (created) {
1012
+ await rm(directory, { force: true, recursive: true });
1013
+ return;
1014
+ }
1015
+ for (const entry of await readdir3(directory)) {
1016
+ await rm(path5.join(directory, entry), { force: true, recursive: true });
1017
+ }
1018
+ };
1019
+ var nextSteps = (choices, packageManager, installed) => [
1020
+ "Next:",
1021
+ ...choices.target === "." ? [] : [` cd ${choices.target}`],
1022
+ ...installed ? [] : [` ${installCommand(packageManager).join(" ")}`],
1023
+ ` ${runCommand(packageManager, "dev")}`,
1024
+ ...choices.git.kind === "clone" ? [" git push"] : []
1025
+ ];
1026
+ var run = async ({
1027
+ argv,
1028
+ cwd,
1029
+ env,
1030
+ io,
1031
+ registries = REGISTRIES,
1032
+ spawn: spawn2,
1033
+ templatesDirectory = defaultTemplates()
1034
+ }) => {
1035
+ try {
1036
+ const parsed = parseCliArgs(argv, registries);
1037
+ if (parsed.help) {
1038
+ io.print(usage(registries));
1039
+ return 0;
1040
+ }
1041
+ const choices = await resolveChoices(parsed, io, registries);
1042
+ const name = packageNameOf(choices.target, cwd);
1043
+ const directory = path5.resolve(cwd, choices.target);
1044
+ const { exists: exists2 } = await checkTarget(directory);
1045
+ const detected = detectPackageManager(env);
1046
+ const needsVersion = choices.layout === "monorepo" || detected.name === "yarn";
1047
+ const version = needsVersion ? await resolveVersion(detected, spawn2, cwd) : detected.version;
1048
+ const packageManager = { ...detected, version };
1049
+ const context = contextOf(choices, name, packageManager, registries);
1050
+ const files = await planScaffold(
1051
+ context,
1052
+ diskLayerReader(treeOf(choices, registries, templatesDirectory)),
1053
+ {
1054
+ packageManagerField: version ? `${packageManager.name}@${version}` : void 0,
1055
+ packageManagerVersion: version
1056
+ }
1057
+ );
1058
+ if (choices.git.kind === "clone") {
1059
+ const clone = await spawn2(
1060
+ "git",
1061
+ ["clone", choices.git.url, directory],
1062
+ { cwd }
1063
+ );
1064
+ if (clone.code !== 0) {
1065
+ io.error(clone.stderr.trim() || "git clone failed.");
1066
+ return 1;
1067
+ }
1068
+ const conflicts = await cloneConflicts(
1069
+ directory,
1070
+ Object.keys(files)
1071
+ );
1072
+ if (conflicts.length > 0) {
1073
+ await removeClone(directory, !exists2);
1074
+ io.error(
1075
+ `The repository already has ${conflicts.join(", ")}; nothing was written.`
1076
+ );
1077
+ return 1;
1078
+ }
1079
+ for (const kept of KEPT_FROM_CLONE) {
1080
+ const cloned = await readCloned(directory, kept);
1081
+ if (cloned === void 0) {
1082
+ continue;
1083
+ }
1084
+ if (kept === ".gitignore" && files[kept] !== void 0) {
1085
+ files[kept] = mergeGitignore(cloned, files[kept]);
1086
+ } else {
1087
+ delete files[kept];
1088
+ }
1089
+ }
1090
+ }
1091
+ await mkdir2(directory, { recursive: true });
1092
+ await writeScaffold(directory, files);
1093
+ io.print(`Created ${name} in ${directory}.`);
1094
+ if (choices.install) {
1095
+ const [command = "npm", ...args] = installCommand(packageManager);
1096
+ const install = await spawn2(command, args, {
1097
+ cwd: directory,
1098
+ inherit: true
1099
+ });
1100
+ if (install.code !== 0) {
1101
+ io.error(
1102
+ `Install failed. The files are in place; run it yourself:
1103
+ cd ${choices.target}
1104
+ ${installCommand(packageManager).join(" ")}`
1105
+ );
1106
+ return 1;
1107
+ }
1108
+ }
1109
+ if (choices.git.kind !== "none") {
1110
+ if (!await hasGit(spawn2, directory)) {
1111
+ io.print("git is not installed, so no repository was set up.");
1112
+ } else {
1113
+ if (choices.git.kind === "new") {
1114
+ await spawn2("git", ["init", "-q"], { cwd: directory });
1115
+ }
1116
+ await commitAll(
1117
+ directory,
1118
+ choices.git.kind === "new" ? NEW_COMMIT_MESSAGE : CLONE_COMMIT_MESSAGE,
1119
+ spawn2,
1120
+ io
1121
+ );
1122
+ }
1123
+ }
1124
+ io.print(
1125
+ nextSteps(choices, packageManager, choices.install).join("\n")
1126
+ );
1127
+ return 0;
1128
+ } catch (error) {
1129
+ if (error instanceof UsageError) {
1130
+ io.error(`${usage(registries)}
1131
+
1132
+ ${error.message}`);
1133
+ return 1;
1134
+ }
1135
+ throw error;
1136
+ } finally {
1137
+ io.close();
1138
+ }
1139
+ };
1140
+
1141
+ // src/io.ts
1142
+ import { spawn } from "node:child_process";
1143
+ import { createInterface } from "node:readline/promises";
1144
+ var nodeIo = () => {
1145
+ const interactive = Boolean(process.stdin.isTTY && process.stdout.isTTY);
1146
+ let readline;
1147
+ return {
1148
+ ask: (question) => {
1149
+ readline ??= createInterface({
1150
+ input: process.stdin,
1151
+ output: process.stdout
1152
+ });
1153
+ return readline.question(question);
1154
+ },
1155
+ close: () => readline?.close(),
1156
+ error: (text) => console.error(text),
1157
+ interactive,
1158
+ print: (text) => console.info(text)
1159
+ };
1160
+ };
1161
+ var nodeSpawn = (command, args, { cwd, inherit }) => new Promise((resolve) => {
1162
+ let stdout = "";
1163
+ let stderr = "";
1164
+ const child = spawn(command, args, {
1165
+ cwd,
1166
+ // Package managers are `.cmd` shims on Windows.
1167
+ shell: process.platform === "win32",
1168
+ stdio: inherit ? "inherit" : "pipe"
1169
+ });
1170
+ child.stdout?.on("data", (chunk) => stdout += chunk);
1171
+ child.stderr?.on("data", (chunk) => stderr += chunk);
1172
+ child.on("error", () => resolve({ code: null, stderr, stdout }));
1173
+ child.on("close", (code) => resolve({ code, stderr, stdout }));
1174
+ });
1175
+ export {
1176
+ DEFAULTS,
1177
+ NONE,
1178
+ REGISTRIES,
1179
+ STAMPED_RANGES,
1180
+ UsageError,
1181
+ checkTarget,
1182
+ detectPackageManager,
1183
+ entryOf,
1184
+ findEntry,
1185
+ installCommand,
1186
+ layerNamesFor,
1187
+ mergeGitignore,
1188
+ mergeLayers,
1189
+ nodeIo,
1190
+ nodeSpawn,
1191
+ packageNameOf,
1192
+ parseCliArgs,
1193
+ planScaffold,
1194
+ rangeOf,
1195
+ resolveChoices,
1196
+ resolveVersion,
1197
+ run,
1198
+ runCommand,
1199
+ toJson,
1200
+ toPackageJson,
1201
+ usage
1202
+ };