@alxia/create 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.
package/dist/index.js ADDED
@@ -0,0 +1,599 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // src/main.ts
5
+ import { stat as stat2 } from "node:fs/promises";
6
+ import { relative, resolve } from "node:path";
7
+
8
+ // src/args.ts
9
+ var NAME = "create-alxia";
10
+ var TEMPLATES = ["api", "react-router"];
11
+ function isTemplate(value) {
12
+ return TEMPLATES.includes(value);
13
+ }
14
+ function parseArgs(args) {
15
+ let dir;
16
+ let template;
17
+ let install = true;
18
+ let help = false;
19
+ for (let i = 0;i < args.length; i++) {
20
+ const arg = args[i];
21
+ if (arg === "--help" || arg === "-h")
22
+ help = true;
23
+ else if (arg === "--no-install")
24
+ install = false;
25
+ else if (arg === "--template" || arg === "-t" || arg.startsWith("--template=")) {
26
+ const value = arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[++i];
27
+ if (value === undefined || value === "") {
28
+ return { error: `${arg} needs a template: ${TEMPLATES.join(" or ")}.` };
29
+ }
30
+ if (!isTemplate(value)) {
31
+ return {
32
+ error: `unknown template ${value}: use ${TEMPLATES.join(" or ")}.`
33
+ };
34
+ }
35
+ template = value;
36
+ } else if (arg.startsWith("-"))
37
+ return { error: `unknown option ${arg}.` };
38
+ else if (dir === undefined)
39
+ dir = arg;
40
+ else
41
+ return { error: `one directory only, given ${dir} and ${arg}.` };
42
+ }
43
+ return {
44
+ ...dir === undefined ? {} : { dir },
45
+ ...template === undefined ? {} : { template },
46
+ install,
47
+ help
48
+ };
49
+ }
50
+
51
+ // src/target.ts
52
+ import { readdir, rm, stat } from "node:fs/promises";
53
+ import { basename, join } from "node:path";
54
+ function packageName(dir) {
55
+ const name = basename(dir).toLowerCase().replace(/[^a-z0-9._~-]+/g, "-").replace(/^[._-]+|-+$/g, "");
56
+ return name || "alxia-app";
57
+ }
58
+ async function refusal(target, shown = target) {
59
+ const info = await stat(target).catch(() => {
60
+ return;
61
+ });
62
+ if (!info)
63
+ return;
64
+ if (!info.isDirectory())
65
+ return `${shown} exists and is not a directory.`;
66
+ const entries = (await readdir(target)).sort();
67
+ if (entries.length === 0)
68
+ return;
69
+ return `${shown} is not empty (${entries.slice(0, 3).join(", ")}${entries.length > 3 ? ", ..." : ""}), ` + "and create-alxia writes only into an empty directory. Choose another directory, or empty this one.";
70
+ }
71
+ async function clean(target, existed) {
72
+ if (!existed) {
73
+ await rm(target, { recursive: true, force: true });
74
+ return;
75
+ }
76
+ for (const entry of await readdir(target).catch(() => [])) {
77
+ await rm(join(target, entry), { recursive: true, force: true });
78
+ }
79
+ }
80
+
81
+ // src/templates/react-router.ts
82
+ import { join as join2 } from "node:path";
83
+
84
+ // src/versions.ts
85
+ import { dirname } from "node:path";
86
+ var ALXIA_PACKAGES = [
87
+ "@alxia/client",
88
+ "@alxia/core",
89
+ "@alxia/react-router"
90
+ ];
91
+ var PEER_RANGES = {
92
+ typescript: "^6.0.3 || ^7.0.0",
93
+ zod: "^4.2.0",
94
+ "react-router": "^8.0.0",
95
+ vite: "^7.0.0 || ^8.0.0"
96
+ };
97
+ function highestMajor(range) {
98
+ const majors = [...range.matchAll(/(?:^|[\s|^~>=])(\d+)/g)].map((match) => Number(match[1]));
99
+ if (majors.length === 0)
100
+ throw new Error(`no major in range ${range}`);
101
+ return Math.max(...majors);
102
+ }
103
+ async function alxiaRanges(manifestUrl = new URL("../package.json", import.meta.url)) {
104
+ const manifest = await Bun.file(manifestUrl).json();
105
+ const declared = manifest.devDependencies ?? {};
106
+ const ranges = {};
107
+ for (const name of ALXIA_PACKAGES) {
108
+ const range = declared[name];
109
+ if (range === undefined) {
110
+ throw new Error(`@alxia/create: its package.json names no ${name}`);
111
+ }
112
+ ranges[name] = range.startsWith("workspace:") ? await workspaceRange(name, range, dirname(manifestUrl.pathname)) : range;
113
+ }
114
+ return ranges;
115
+ }
116
+ async function workspaceRange(name, range, from) {
117
+ const file = Bun.resolveSync(`${name}/package.json`, from);
118
+ const { version } = await Bun.file(file).json();
119
+ const prefix = range.slice("workspace:".length);
120
+ return prefix === "*" ? version : `${prefix}${version}`;
121
+ }
122
+
123
+ // src/templates/react-router.ts
124
+ function scaffoldCommand(dir) {
125
+ return [
126
+ `create-react-router@${highestMajor(PEER_RANGES["react-router"])}`,
127
+ dir,
128
+ "--yes",
129
+ "--no-install",
130
+ "--no-git-init",
131
+ "--no-agent-skills",
132
+ "--no-motion"
133
+ ];
134
+ }
135
+
136
+ class ScaffoldChanged extends Error {
137
+ constructor(file, expected) {
138
+ super(`create-react-router's ${file} is not what this @alxia/create expects: ${expected}. ` + "Nothing was kept. Try the newest @alxia/create, or add @alxia/react-router to a React Router app by hand, as its README shows.");
139
+ this.name = "ScaffoldChanged";
140
+ }
141
+ }
142
+ var BUNFIG = `[run]
143
+ # The React Router CLI is a node script: this starts it on Bun, which
144
+ # alxia's server needs, even where a node is installed.
145
+ bun = true
146
+ `;
147
+ var IMPORT = `import { alxia } from "@alxia/react-router/vite";
148
+ `;
149
+ function addPlugin(config) {
150
+ const calls = config.match(/\breactRouter\(\)/g) ?? [];
151
+ if (!config.includes('from "@react-router/dev/vite"') || !/plugins:\s*\[[^\]]*\breactRouter\(\)[^\]]*\]/.test(config) || calls.length !== 1) {
152
+ throw new ScaffoldChanged("vite.config.ts", 'reactRouter() from "@react-router/dev/vite", called once in plugins: [...]');
153
+ }
154
+ if (config.includes("@alxia/react-router/vite")) {
155
+ throw new ScaffoldChanged("vite.config.ts", "it already imports @alxia/react-router/vite");
156
+ }
157
+ return IMPORT + config.replace(/\breactRouter\(\)/, "reactRouter(), alxia()");
158
+ }
159
+ function addAlxia(manifest, alxia) {
160
+ const scripts = manifest["scripts"];
161
+ if (scripts?.["dev"] !== "react-router dev" || scripts["build"] !== "react-router build" || scripts["start"] === undefined || manifest.dependencies?.["react-router"] === undefined) {
162
+ throw new ScaffoldChanged("package.json", "react-router in its dependencies, and the scripts `dev: react-router dev`, `build: react-router build` and a `start`");
163
+ }
164
+ const dependencies = sorted({
165
+ ...manifest.dependencies,
166
+ "@alxia/core": alxia["@alxia/core"],
167
+ "@alxia/react-router": alxia["@alxia/react-router"]
168
+ });
169
+ return {
170
+ ...manifest,
171
+ scripts: { ...scripts, start: "bun build/server/index.js" },
172
+ dependencies
173
+ };
174
+ }
175
+ function sorted(deps) {
176
+ return Object.fromEntries(Object.entries(deps).sort(([a], [b]) => a < b ? -1 : 1));
177
+ }
178
+ async function alxiaLayer(dir, alxia) {
179
+ const read = async (file) => {
180
+ const handle = Bun.file(join2(dir, file));
181
+ if (!await handle.exists()) {
182
+ throw new ScaffoldChanged(file, "it is missing");
183
+ }
184
+ return handle.text();
185
+ };
186
+ const manifest = addAlxia(JSON.parse(await read("package.json")), alxia);
187
+ const config = addPlugin(await read("vite.config.ts"));
188
+ if (await Bun.file(join2(dir, "bunfig.toml")).exists()) {
189
+ throw new ScaffoldChanged("bunfig.toml", "none, and there is one");
190
+ }
191
+ return {
192
+ manifest,
193
+ files: { "vite.config.ts": config, "bunfig.toml": BUNFIG }
194
+ };
195
+ }
196
+
197
+ // src/write.ts
198
+ import { mkdir } from "node:fs/promises";
199
+ import { basename as basename2, dirname as dirname2, join as join3 } from "node:path";
200
+
201
+ // src/registry.ts
202
+ function registryUrl(env) {
203
+ const url = env["BUN_CONFIG_REGISTRY"] || env["npm_config_registry"] || "https://registry.npmjs.org";
204
+ return url.replace(/\/+$/, "");
205
+ }
206
+ function allowedRange(name) {
207
+ if (name === "react-router" || name.startsWith("@react-router/")) {
208
+ return PEER_RANGES["react-router"];
209
+ }
210
+ return name in PEER_RANGES ? PEER_RANGES[name] : undefined;
211
+ }
212
+ function newestWithin(versions, range) {
213
+ return versions.filter((v) => !v.includes("-") && Bun.semver.satisfies(v, range)).sort(Bun.semver.order).at(-1);
214
+ }
215
+ async function packument(name, registry) {
216
+ const call = registry.fetch ?? ((request) => fetch(request));
217
+ const response = await call(new Request(`${registry.url}/${name.replace("/", "%2f")}`, {
218
+ headers: { accept: "application/vnd.npm.install-v1+json" },
219
+ signal: AbortSignal.timeout(registry.timeout ?? 5000)
220
+ }));
221
+ if (!response.ok)
222
+ throw new Error(`${response.status}`);
223
+ return await response.json();
224
+ }
225
+ function choose(versions, range, latest) {
226
+ if (range !== undefined)
227
+ return newestWithin(versions, range);
228
+ return latest && versions.includes(latest) ? latest : newestWithin(versions, "*");
229
+ }
230
+ async function bumpDependencies(manifest, registry, keep = new Set) {
231
+ const fields = ["dependencies", "devDependencies"];
232
+ const names = fields.flatMap((field) => Object.keys(manifest[field] ?? {}).filter((name) => !keep.has(name)));
233
+ const fetched = new Map(await Promise.all(names.map(async (name) => [
234
+ name,
235
+ await packument(name, registry).catch(() => {
236
+ return;
237
+ })
238
+ ])));
239
+ const moved = [];
240
+ const held = [];
241
+ const failed = [];
242
+ const unmatched = [];
243
+ const chosen = new Map;
244
+ const ordered = [...names].sort((a, b) => Number(b === "react-router") - Number(a === "react-router"));
245
+ for (const name of ordered) {
246
+ const field = fields.find((f) => manifest[f]?.[name] !== undefined);
247
+ const deps = field && manifest[field];
248
+ const current = deps?.[name];
249
+ if (!deps || current === undefined)
250
+ continue;
251
+ const meta = fetched.get(name);
252
+ if (!meta?.versions) {
253
+ failed.push(name);
254
+ continue;
255
+ }
256
+ const versions = Object.keys(meta.versions);
257
+ const range = allowedRange(name);
258
+ const latest = meta["dist-tags"]?.["latest"];
259
+ const follow = chosen.get("react-router");
260
+ const version = name.startsWith("@react-router/") && follow && versions.includes(follow) ? follow : choose(versions, range, latest);
261
+ if (version === undefined) {
262
+ unmatched.push(`${name}: no release within ${range ?? "any range"}; kept ${current}`);
263
+ continue;
264
+ }
265
+ chosen.set(name, version);
266
+ if (range !== undefined && latest && Bun.semver.order(latest, version) > 0 && !Bun.semver.satisfies(latest, range)) {
267
+ held.push(`${name}: kept to ${range}, where the newest is ${version}; npm's latest, ${latest}, is outside it`);
268
+ }
269
+ const next = `^${version}`;
270
+ if (next !== current)
271
+ moved.push(`${name} ${current} -> ${next}`);
272
+ deps[name] = next;
273
+ }
274
+ return { moved, held, failed, unmatched };
275
+ }
276
+
277
+ // src/templates/api.ts
278
+ function apiManifest(name, alxia) {
279
+ return {
280
+ name,
281
+ private: true,
282
+ type: "module",
283
+ scripts: {
284
+ dev: "bun --watch src/server.ts",
285
+ build: "bun build src/server.ts --target=bun --outdir=dist",
286
+ start: "bun dist/server.js",
287
+ test: "bun test",
288
+ typecheck: "tsc --noEmit"
289
+ },
290
+ dependencies: {
291
+ "@alxia/core": alxia["@alxia/core"],
292
+ zod: "^4.2.0"
293
+ },
294
+ devDependencies: {
295
+ "@alxia/client": alxia["@alxia/client"],
296
+ "@types/bun": "^1.4.2",
297
+ typescript: "^6.0.3"
298
+ }
299
+ };
300
+ }
301
+ var APP = `import { alxia, defineHook } from '@alxia/core';
302
+ import { z } from 'zod';
303
+
304
+ const Todo = z.object({ id: z.number(), title: z.string(), done: z.boolean() });
305
+ const NewTodo = z.object({ title: z.string().min(1) });
306
+
307
+ /** Set API_KEY in the environment: this default is for development. */
308
+ export const apiKey = Bun.env['API_KEY'] ?? 'dev-key';
309
+
310
+ // A hook of the routes it is given to: it answers 401 without the key, and
311
+ // that 401 joins the type of each, so the client reads it.
312
+ const requireKey = defineHook(({ request, reply }) =>
313
+ request.headers.get('x-api-key') === apiKey
314
+ ? undefined
315
+ : reply(401, { error: 'unauthorized' as const }),
316
+ );
317
+
318
+ const todos: z.infer<typeof Todo>[] = [];
319
+
320
+ export const app = alxia()
321
+ .decorate({ todos })
322
+ .post(
323
+ '/todos',
324
+ [requireKey],
325
+ { body: NewTodo, response: { 201: Todo } },
326
+ ({ body, todos, reply }) => {
327
+ const todo = { id: todos.length + 1, title: body.title, done: false };
328
+ todos.push(todo);
329
+ return reply.created(todo);
330
+ },
331
+ );
332
+
333
+ export type App = typeof app;
334
+ `;
335
+ var SERVER = `import { app } from './app';
336
+
337
+ const server = app.listen(Number(Bun.env['PORT'] ?? 3000));
338
+ console.log(\`listening on \${server.url}\`);
339
+ `;
340
+ var SPEC = `import { expect, test } from 'bun:test';
341
+ import { client } from '@alxia/client';
342
+ import { apiKey, app } from './app';
343
+
344
+ const json = { 'content-type': 'application/json', 'x-api-key': apiKey };
345
+
346
+ test('creates a todo from JSON', async () => {
347
+ const response = await app.request('/todos', {
348
+ method: 'POST',
349
+ headers: json,
350
+ body: JSON.stringify({ title: 'Write a route' }),
351
+ });
352
+ expect(response.status).toBe(201);
353
+ expect(await response.json()).toEqual({
354
+ id: expect.any(Number),
355
+ title: 'Write a route',
356
+ done: false,
357
+ });
358
+ });
359
+
360
+ test('refuses an empty title with a 400 naming it', async () => {
361
+ const response = await app.request('/todos', {
362
+ method: 'POST',
363
+ headers: json,
364
+ body: JSON.stringify({ title: '' }),
365
+ });
366
+ expect(response.status).toBe(400);
367
+ expect((await response.json()).issues[0].path).toEqual(['title']);
368
+ });
369
+
370
+ test('the typed client reads each status the route answers', async () => {
371
+ // Given the app itself, the client calls its fetch in process.
372
+ const api = client(app, { headers: { 'x-api-key': apiKey } });
373
+ const created = await api.post('/todos', { body: { title: 'Call it typed' } });
374
+ if (created.status !== 201) throw new Error(\`got \${created.status}\`);
375
+ expect(created.data.title).toBe('Call it typed'); // data is the Todo schema's type
376
+
377
+ const anonymous = await client(app).post('/todos', { body: { title: 'No key' } });
378
+ expect(anonymous.status).toBe(401);
379
+ if (anonymous.status === 401) expect(anonymous.data.error).toBe('unauthorized');
380
+ });
381
+ `;
382
+ var TSCONFIG = `{
383
+ "compilerOptions": {
384
+ "types": ["bun"],
385
+ "lib": ["ESNext", "DOM"],
386
+ "target": "ESNext",
387
+ "module": "ESNext",
388
+ "moduleDetection": "force",
389
+ "moduleResolution": "bundler",
390
+ "verbatimModuleSyntax": true,
391
+ "noEmit": true,
392
+ "skipLibCheck": true,
393
+ "resolveJsonModule": true,
394
+ "strict": true,
395
+ "noImplicitOverride": true,
396
+ "noImplicitReturns": true,
397
+ "noFallthroughCasesInSwitch": true,
398
+ "noUncheckedIndexedAccess": true,
399
+ "exactOptionalPropertyTypes": true,
400
+ "noPropertyAccessFromIndexSignature": true,
401
+ "noUnusedLocals": true,
402
+ "noUnusedParameters": true,
403
+ "forceConsistentCasingInFileNames": true
404
+ },
405
+ "include": ["src"]
406
+ }
407
+ `;
408
+ var GITIGNORE = `node_modules
409
+ dist
410
+ *.log
411
+ .DS_Store
412
+ .env
413
+ .env.local
414
+ `;
415
+ var readme = (name) => `# ${name}
416
+
417
+ An [alxia](https://github.com/softistx/alxia) app with
418
+ [Zod](https://zod.dev), made with \`bun create @alxia\`.
419
+
420
+ \`\`\`sh
421
+ bun dev # http://localhost:3000, restarted on every change
422
+ bun test # src/app.spec.ts: in process, and through the typed client
423
+ bun run typecheck
424
+ bun run build # dist/server.js, run with \`bun start\`
425
+ \`\`\`
426
+
427
+ \`\`\`sh
428
+ curl -X POST localhost:3000/todos \\
429
+ -H 'content-type: application/json' -H 'x-api-key: dev-key' \\
430
+ -d '{"title":"Write a route"}'
431
+ \`\`\`
432
+
433
+ - \`src/app.ts\`: the app. \`POST /todos\` validates its body with Zod, and
434
+ its own hook, \`requireKey\`, answers 401 without the \`x-api-key\` header.
435
+ Set \`API_KEY\` in the environment outside development.
436
+ - \`src/server.ts\`: listens on \`PORT\`, 3000 by default.
437
+ - \`src/app.spec.ts\`: \`app.request()\` and \`@alxia/client\`, no port.
438
+
439
+ Next: [Getting started](https://github.com/softistx/alxia/blob/develop/packages/core/docs/guide/getting-started.md)
440
+ and the rest of [\`@alxia/core\`'s guide](https://github.com/softistx/alxia/tree/develop/packages/core/docs).
441
+ `;
442
+ function apiFiles(name) {
443
+ return {
444
+ "src/app.ts": APP,
445
+ "src/server.ts": SERVER,
446
+ "src/app.spec.ts": SPEC,
447
+ "tsconfig.json": TSCONFIG,
448
+ ".gitignore": GITIGNORE,
449
+ "README.md": readme(name)
450
+ };
451
+ }
452
+
453
+ // src/write.ts
454
+ async function write(target, template, alxia, io) {
455
+ let manifest;
456
+ let files;
457
+ if (template === "api") {
458
+ manifest = apiManifest(packageName(target), alxia);
459
+ files = apiFiles(packageName(target));
460
+ } else {
461
+ await mkdir(dirname2(target), { recursive: true });
462
+ const code = await io.run([process.execPath, "x", ...scaffoldCommand(basename2(target))], dirname2(target));
463
+ if (code !== 0) {
464
+ throw new Error(`create-react-router exited with ${code}.`);
465
+ }
466
+ ({ manifest, files } = await alxiaLayer(target, alxia));
467
+ }
468
+ io.out("Resolving the newest versions alxia accepts...");
469
+ const bumped = await bumpDependencies(manifest, { url: registryUrl(io.env), ...io.fetch ? { fetch: io.fetch } : {} }, new Set(Object.keys(alxia)));
470
+ for (const line of bumped.moved)
471
+ io.out(` ${line}`);
472
+ for (const line of [...bumped.held, ...bumped.unmatched])
473
+ io.out(` ${line}`);
474
+ if (bumped.failed.length > 0) {
475
+ io.err(`${NAME}: warning: the registry did not answer for ${bumped.failed.join(", ")}; ` + "kept the versions the template ships.");
476
+ }
477
+ files["package.json"] = `${JSON.stringify(manifest, null, 2)}
478
+ `;
479
+ for (const [file, content] of Object.entries(files)) {
480
+ await Bun.write(join3(target, file), content);
481
+ }
482
+ return true;
483
+ }
484
+ // src/io.ts
485
+ function processIo() {
486
+ return {
487
+ out: (line) => console.log(line),
488
+ err: (line) => console.error(line),
489
+ ask: (question, fallback) => {
490
+ if (!process.stdin.isTTY)
491
+ return;
492
+ const answer = prompt(question, fallback);
493
+ return answer === null ? null : answer.trim() || fallback;
494
+ },
495
+ run: async (command, cwd) => Bun.spawn([...command], { cwd, stdio: ["inherit", "inherit", "inherit"] }).exited,
496
+ env: process.env
497
+ };
498
+ }
499
+
500
+ // src/main.ts
501
+ var USAGE = `Usage: bun create @alxia [dir] [options]
502
+
503
+ Writes a new alxia app in dir, which must be empty or not exist yet. Asks
504
+ for what is not given.
505
+
506
+ Options:
507
+ -t, --template <name> api an alxia app with Zod, a spec and the typed client
508
+ react-router React Router's official template, served by alxia
509
+ --no-install write the files, skip bun install
510
+ -h, --help show this help`;
511
+ async function main(args, cwd, io) {
512
+ const options = parseArgs(args);
513
+ if ("error" in options) {
514
+ io.err(`${NAME}: ${options.error}
515
+
516
+ ${USAGE}`);
517
+ return 1;
518
+ }
519
+ if (options.help) {
520
+ io.out(USAGE);
521
+ return 0;
522
+ }
523
+ const dir = options.dir ?? io.ask("Where should the project go?", "alxia-app");
524
+ if (dir === null) {
525
+ io.err(`${NAME}: cancelled, nothing written.`);
526
+ return 1;
527
+ }
528
+ if (dir === undefined) {
529
+ io.err(`${NAME}: no directory given, and no terminal to ask in.
530
+
531
+ ${USAGE}`);
532
+ return 1;
533
+ }
534
+ const template = options.template ?? askTemplate(io);
535
+ if (template === null) {
536
+ io.err(`${NAME}: cancelled, nothing written.`);
537
+ return 1;
538
+ }
539
+ if (typeof template === "object") {
540
+ io.err(`${NAME}: ${template.error}
541
+
542
+ ${USAGE}`);
543
+ return 1;
544
+ }
545
+ const target = resolve(cwd, dir);
546
+ const refused = await refusal(target, dir);
547
+ if (refused) {
548
+ io.err(`${NAME}: ${refused}`);
549
+ return 1;
550
+ }
551
+ const existed = await stat2(target).catch(() => {
552
+ return;
553
+ }) !== undefined;
554
+ const made = await alxiaRanges().then((alxia) => write(target, template, alxia, io)).catch(async (error) => {
555
+ await clean(target, existed);
556
+ io.err(`${NAME}: ${error instanceof ScaffoldChanged ? "" : "failed: "}${error instanceof Error ? error.message : String(error)}`);
557
+ return false;
558
+ });
559
+ if (!made)
560
+ return 1;
561
+ let installed = true;
562
+ if (options.install) {
563
+ io.out(`
564
+ Installing dependencies with bun install...`);
565
+ installed = await io.run([process.execPath, "install"], target) === 0;
566
+ if (!installed)
567
+ io.err(`${NAME}: bun install failed; the files are written.`);
568
+ }
569
+ io.out(done(relative(cwd, target) || ".", template, options.install && installed));
570
+ return installed ? 0 : 1;
571
+ }
572
+ function done(where, template, installed) {
573
+ const steps = [
574
+ where === "." ? undefined : `cd ${where}`,
575
+ installed ? undefined : "bun install",
576
+ "bun dev"
577
+ ].filter((step) => step !== undefined);
578
+ return `
579
+ Done: ${where === "." ? "this directory" : where} holds the ${template} template. Next:
580
+
581
+ ${steps.map((step) => ` ${step}`).join(`
582
+ `)}
583
+ `;
584
+ }
585
+ function askTemplate(io) {
586
+ const answer = io.ask(`Which template? ${TEMPLATES.join(" or ")}`, "api");
587
+ if (answer === null)
588
+ return null;
589
+ if (answer === undefined) {
590
+ return { error: "no --template given, and no terminal to ask in." };
591
+ }
592
+ return isTemplate(answer) ? answer : { error: `unknown template ${answer}: use ${TEMPLATES.join(" or ")}.` };
593
+ }
594
+
595
+ // src/index.ts
596
+ process.exitCode = await main(process.argv.slice(2), process.cwd(), processIo());
597
+
598
+ //# debugId=8B48AE430B9AACCF64756E2164756E21
599
+ //# sourceMappingURL=index.js.map