create-basalt 1.2.1 → 1.3.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/README.md CHANGED
@@ -1,3 +1,9 @@
1
+ <p align="center">
2
+ <a href="https://basaltkit-docs.pages.dev">
3
+ <img src="https://basaltkit-docs.pages.dev/social-card.png" alt="Basalt" width="440">
4
+ </a>
5
+ </p>
6
+
1
7
  # create-basalt
2
8
 
3
9
  Basalt project generator: a single command (`npm create basalt my-app`) creates a complete, ready-to-run SaaS application — typed API, authentication, multi-tenancy, and optionally billing, a web frontend, and the `basalt` CLI. It's the framework's starting point: use it whenever you want to start a new project.
@@ -40,6 +46,7 @@ Multi-tenancy? (Y/n) y
40
46
  Authentication? (Y/n) y
41
47
  Subscriptions / billing? (y/N) n
42
48
  Web UI (React + shadcn)? (y/N) n
49
+ MCP server (routes as AI-agent tools)? (y/N) n
43
50
  'basalt' CLI (code generators)? (y/N) n
44
51
  Install dependencies now? (y/N) y
45
52
  Initialize a git repository? (y/N) y
@@ -83,6 +90,7 @@ Usage: npm create basalt <name> [options]
83
90
  | `--billing` | off | Includes subscriptions/plans (`@basaltkit/subscriptions`, example `free` and `pro` plans) |
84
91
  | `--ui` | off | Generates the `web/` frontend (React + shadcn via `@basaltkit/admin-shadcn` + `@basaltkit/sdk`). **Forces pnpm** — see note below |
85
92
  | `--cli` | off | Generates the `basalt` CLI (`bin/basalt.ts`, `pnpm basalt` script, `make:*` generators from `@basaltkit/generator`) |
93
+ | `--mcp` | off | Exposes read-only routes as MCP tools (`@basaltkit/mcp`) over HTTP at `POST /mcp` — the overview and health endpoints are opted in via `meta.mcp` |
86
94
  | `--install` | off | Installs dependencies at the end (with the detected/chosen manager) |
87
95
  | `--git` | off | Runs `git init` + first commit ("Initial commit from create-basalt") |
88
96
  | `--pm=<manager>` | autodetect | Package manager: `pnpm` \| `npm` \| `yarn` \| `bun` |
@@ -196,6 +204,7 @@ Exported from `create-basalt` (in addition to the `create-basalt` executable):
196
204
  | `billing` | `boolean` | No | `false` | Include subscriptions |
197
205
  | `ui` | `boolean` | No | `false` | Generate the `web/` frontend |
198
206
  | `cli` | `boolean` | No | `false` | Generate the `basalt` CLI |
207
+ | `mcp` | `boolean` | No | `false` | Expose read-only routes as MCP tools at `/mcp` |
199
208
 
200
209
  `CreateProjectResult`:
201
210
 
package/dist/cli.d.ts CHANGED
@@ -1 +1,2 @@
1
1
  #!/usr/bin/env node
2
+ export {};
package/dist/cli.js CHANGED
@@ -1,17 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import {
3
- TargetNotEmptyError,
4
- WizardCancelledError,
5
- createProject,
6
- detectPackageManager,
7
- runWizard,
8
- ttyPrompter
9
- } from "./chunk-Z6RTS4I7.js";
10
-
11
- // src/cli.ts
12
- import { spawn } from "child_process";
13
- import { stdin, stdout } from "process";
14
- var USAGE = `Usage: npm create basalt <name> [options]
2
+ import { spawn } from 'node:child_process';
3
+ import { stdin, stdout } from 'node:process';
4
+ import { createProject, detectPackageManager, runWizard, ttyPrompter, TargetNotEmptyError, WizardCancelledError, } from './index.js';
5
+ const USAGE = `Usage: npm create basalt <name> [options]
15
6
 
16
7
  Options:
17
8
  --dir=<path> Target directory (default: ./<name>)
@@ -20,6 +11,7 @@ Options:
20
11
  --billing Include subscriptions/billing
21
12
  --ui Scaffold a web/ frontend (React + shadcn + SDK)
22
13
  --cli Scaffold the 'basalt' CLI (code generators + commands)
14
+ --mcp Expose read-only routes as MCP tools at /mcp
23
15
  --install Install dependencies after scaffolding
24
16
  --git Initialize a git repository with a first commit
25
17
  --pm=<manager> Package manager: pnpm | npm | yarn | bun (default: auto-detect)
@@ -29,121 +21,143 @@ Options:
29
21
  Run with no name in a terminal to be prompted interactively.
30
22
  `;
31
23
  function parseArgs(argv) {
32
- const flags2 = {
33
- tenancy: true,
34
- auth: true,
35
- billing: false,
36
- ui: false,
37
- cli: false,
38
- install: false,
39
- git: false,
40
- yes: false
41
- };
42
- for (const token of argv) {
43
- if (token === "--no-tenancy") flags2.tenancy = false;
44
- else if (token === "--no-auth") flags2.auth = false;
45
- else if (token === "--billing") flags2.billing = true;
46
- else if (token === "--ui") flags2.ui = true;
47
- else if (token === "--cli") flags2.cli = true;
48
- else if (token === "--install") flags2.install = true;
49
- else if (token === "--git") flags2.git = true;
50
- else if (token === "-y" || token === "--yes") flags2.yes = true;
51
- else if (token.startsWith("--dir=")) flags2.dir = token.slice("--dir=".length);
52
- else if (token.startsWith("--pm=")) flags2.pm = token.slice("--pm=".length);
53
- else if (token === "--help" || token === "-h") {
54
- stdout.write(USAGE);
55
- process.exit(0);
56
- } else if (!token.startsWith("--") && flags2.name === void 0) flags2.name = token;
57
- }
58
- return flags2;
24
+ const flags = {
25
+ tenancy: true,
26
+ auth: true,
27
+ billing: false,
28
+ ui: false,
29
+ cli: false,
30
+ mcp: false,
31
+ install: false,
32
+ git: false,
33
+ yes: false,
34
+ };
35
+ for (const token of argv) {
36
+ if (token === '--no-tenancy')
37
+ flags.tenancy = false;
38
+ else if (token === '--no-auth')
39
+ flags.auth = false;
40
+ else if (token === '--billing')
41
+ flags.billing = true;
42
+ else if (token === '--ui')
43
+ flags.ui = true;
44
+ else if (token === '--cli')
45
+ flags.cli = true;
46
+ else if (token === '--mcp')
47
+ flags.mcp = true;
48
+ else if (token === '--install')
49
+ flags.install = true;
50
+ else if (token === '--git')
51
+ flags.git = true;
52
+ else if (token === '-y' || token === '--yes')
53
+ flags.yes = true;
54
+ else if (token.startsWith('--dir='))
55
+ flags.dir = token.slice('--dir='.length);
56
+ else if (token.startsWith('--pm='))
57
+ flags.pm = token.slice('--pm='.length);
58
+ else if (token === '--help' || token === '-h') {
59
+ stdout.write(USAGE);
60
+ process.exit(0);
61
+ }
62
+ else if (!token.startsWith('--') && flags.name === undefined)
63
+ flags.name = token;
64
+ }
65
+ return flags;
59
66
  }
67
+ /** Runs a command inheriting stdio; resolves false on non-zero exit (never throws). */
60
68
  function run(command, args, cwd) {
61
- return new Promise((resolve) => {
62
- const child = spawn(command, args, {
63
- cwd,
64
- stdio: "inherit",
65
- shell: process.platform === "win32"
69
+ return new Promise((resolve) => {
70
+ const child = spawn(command, args, {
71
+ cwd,
72
+ stdio: 'inherit',
73
+ shell: process.platform === 'win32',
74
+ });
75
+ child.on('error', () => resolve(false));
76
+ child.on('close', (code) => resolve(code === 0));
66
77
  });
67
- child.on("error", () => resolve(false));
68
- child.on("close", (code) => resolve(code === 0));
69
- });
70
78
  }
79
+ /** Cleanly abort on Ctrl+C instead of dumping a Node AbortError stack trace. */
71
80
  function cancel() {
72
- stdout.write("\nCancelled.\n");
73
- process.exit(130);
81
+ stdout.write('\nCancelled.\n');
82
+ process.exit(130); // 128 + SIGINT
74
83
  }
75
- var flags = parseArgs(process.argv.slice(2));
76
- if (!flags.yes && flags.name === void 0 && stdin.isTTY) {
77
- try {
78
- const result = await runWizard(ttyPrompter(), {
79
- defaultPm: flags.pm ?? detectPackageManager()
80
- });
81
- flags.name = result.name;
82
- flags.tenancy = result.tenancy;
83
- flags.auth = result.auth;
84
- flags.billing = result.billing;
85
- flags.ui = result.ui;
86
- flags.cli = result.cli;
87
- flags.install = result.install;
88
- flags.git = result.git;
89
- flags.pm = result.pm;
90
- } catch (error) {
91
- if (error instanceof WizardCancelledError) cancel();
92
- throw error;
93
- }
84
+ const flags = parseArgs(process.argv.slice(2));
85
+ // Rich interactive wizard when nothing was specified in a terminal. CI, piped
86
+ // input, and `--yes` keep the flag-driven path.
87
+ if (!flags.yes && flags.name === undefined && stdin.isTTY) {
88
+ try {
89
+ const result = await runWizard(ttyPrompter(), {
90
+ defaultPm: flags.pm ?? detectPackageManager(),
91
+ });
92
+ flags.name = result.name;
93
+ flags.tenancy = result.tenancy;
94
+ flags.auth = result.auth;
95
+ flags.billing = result.billing;
96
+ flags.ui = result.ui;
97
+ flags.cli = result.cli;
98
+ flags.mcp = result.mcp;
99
+ flags.install = result.install;
100
+ flags.git = result.git;
101
+ flags.pm = result.pm;
102
+ }
103
+ catch (error) {
104
+ if (error instanceof WizardCancelledError)
105
+ cancel();
106
+ throw error;
107
+ }
94
108
  }
95
109
  if (!flags.name) {
96
- stdout.write(USAGE);
97
- process.exit(1);
110
+ stdout.write(USAGE);
111
+ process.exit(1);
98
112
  }
99
- var pm = flags.pm ?? detectPackageManager();
100
- if (flags.ui && pm !== "pnpm") {
101
- console.log(`Note: --ui projects are pnpm workspaces \u2014 using pnpm instead of ${pm}.`);
102
- pm = "pnpm";
113
+ let pm = flags.pm ?? detectPackageManager();
114
+ // The web/ frontend is a pnpm workspace member (pnpm-workspace.yaml); npm,
115
+ // yarn and bun can't install or run it, so --ui projects are pnpm-only.
116
+ if (flags.ui && pm !== 'pnpm') {
117
+ console.log(`Note: --ui projects are pnpm workspaces — using pnpm instead of ${pm}.`);
118
+ pm = 'pnpm';
103
119
  }
104
120
  try {
105
- const result = await createProject({
106
- name: flags.name,
107
- ...flags.dir ? { dir: flags.dir } : {},
108
- tenancy: flags.tenancy,
109
- auth: flags.auth,
110
- billing: flags.billing,
111
- ui: flags.ui,
112
- cli: flags.cli
113
- });
114
- console.log(`
115
- Created ${result.options.name} in ${result.dir}
116
- `);
117
- for (const file of result.files) console.log(` ${file}`);
118
- if (flags.git) {
119
- console.log("\nInitializing git repository\u2026");
120
- const ok = await run("git", ["init", "-q"], result.dir) && await run("git", ["add", "-A"], result.dir) && await run(
121
- "git",
122
- ["commit", "-q", "-m", "Initial commit from create-basalt"],
123
- result.dir
124
- );
125
- console.log(
126
- ok ? " git repository initialized." : " (skipped \u2014 git unavailable or already a repo)"
127
- );
128
- }
129
- if (flags.install) {
130
- console.log(`
131
- Installing dependencies with ${pm}\u2026`);
132
- const ok = await run(pm, ["install"], result.dir);
133
- if (!ok) console.log(` (install failed \u2014 run "${pm} install" yourself)`);
134
- }
135
- const steps = [`cd ${result.dir}`];
136
- if (!flags.install) steps.push(`${pm} install`);
137
- steps.push(`${pm} run dev${flags.ui ? " # API on :3000" : ""}`);
138
- if (flags.ui) steps.push(`pnpm --filter ${result.options.name}-web dev # UI on :5180`);
139
- console.log(`
140
- Next steps:
141
- ${steps.map((s) => ` ${s}`).join("\n")}
142
- `);
143
- } catch (error) {
144
- if (error instanceof TargetNotEmptyError) {
145
- console.error(error.message);
146
- process.exit(1);
147
- }
148
- throw error;
121
+ const result = await createProject({
122
+ name: flags.name,
123
+ ...(flags.dir ? { dir: flags.dir } : {}),
124
+ tenancy: flags.tenancy,
125
+ auth: flags.auth,
126
+ billing: flags.billing,
127
+ ui: flags.ui,
128
+ cli: flags.cli,
129
+ mcp: flags.mcp,
130
+ });
131
+ console.log(`\nCreated ${result.options.name} in ${result.dir}\n`);
132
+ for (const file of result.files)
133
+ console.log(` ${file}`);
134
+ if (flags.git) {
135
+ console.log('\nInitializing git repository…');
136
+ const ok = (await run('git', ['init', '-q'], result.dir)) &&
137
+ (await run('git', ['add', '-A'], result.dir)) &&
138
+ (await run('git', ['commit', '-q', '-m', 'Initial commit from create-basalt'], result.dir));
139
+ console.log(ok ? ' git repository initialized.' : ' (skipped — git unavailable or already a repo)');
140
+ }
141
+ if (flags.install) {
142
+ console.log(`\nInstalling dependencies with ${pm}…`);
143
+ const ok = await run(pm, ['install'], result.dir);
144
+ if (!ok)
145
+ console.log(` (install failed — run "${pm} install" yourself)`);
146
+ }
147
+ const steps = [`cd ${result.dir}`];
148
+ if (!flags.install)
149
+ steps.push(`${pm} install`);
150
+ steps.push(`${pm} run dev${flags.ui ? ' # API on :3000' : ''}`);
151
+ // `web` is wired as a pnpm workspace member (pnpm-workspace.yaml), so its dev
152
+ // server is launched with a pnpm filter regardless of the root package manager.
153
+ if (flags.ui)
154
+ steps.push(`pnpm --filter ${result.options.name}-web dev # UI on :5180`);
155
+ console.log(`\nNext steps:\n${steps.map((s) => ` ${s}`).join('\n')}\n`);
156
+ }
157
+ catch (error) {
158
+ if (error instanceof TargetNotEmptyError) {
159
+ console.error(error.message);
160
+ process.exit(1);
161
+ }
162
+ throw error;
149
163
  }
package/dist/index.d.ts CHANGED
@@ -1,110 +1,6 @@
1
- interface ProjectOptions {
2
- name: string;
3
- tenancy: boolean;
4
- auth: boolean;
5
- billing: boolean;
6
- /** Scaffold a web/ frontend (React + shadcn + SDK). */
7
- ui: boolean;
8
- /** Scaffold the `basalt` CLI entrypoint (code generators + built-in commands). */
9
- cli: boolean;
10
- }
11
-
12
- /** A selectable option. `value` is returned; `label`/`hint` are shown. */
13
- interface Choice<T> {
14
- value: T;
15
- label: string;
16
- hint?: string;
17
- }
18
- /**
19
- * The prompt surface the wizard talks to. Abstracted so the flow (`runWizard`)
20
- * is testable with a scripted implementation — the raw-mode TTY version is the
21
- * thin, untested I/O shell.
22
- */
23
- interface Prompter {
24
- intro(message: string): void;
25
- note(message: string): void;
26
- outro(message: string): void;
27
- text(opts: {
28
- message: string;
29
- placeholder?: string;
30
- initial?: string;
31
- validate?: (v: string) => string | undefined;
32
- }): Promise<string>;
33
- select<T>(opts: {
34
- message: string;
35
- choices: Choice<T>[];
36
- initial?: number;
37
- }): Promise<T>;
38
- multiselect<T>(opts: {
39
- message: string;
40
- choices: Choice<T>[];
41
- initial?: T[];
42
- }): Promise<T[]>;
43
- confirm(opts: {
44
- message: string;
45
- initial?: boolean;
46
- }): Promise<boolean>;
47
- }
48
- /** Thrown when the user aborts (Ctrl+C, or declines the final confirmation). */
49
- declare class WizardCancelledError extends Error {
50
- constructor();
51
- }
52
- /**
53
- * A rich, dependency-free terminal prompter: arrow-key `select`/`multiselect`
54
- * (raw mode), validated `text`, and `confirm`. Only usable in a TTY.
55
- */
56
- declare function ttyPrompter(): Prompter;
57
- /**
58
- * A scripted prompter for tests: each call dequeues the next answer of its kind.
59
- * `log` captures intro/note/outro lines so a test can assert the summary.
60
- */
61
- declare function scriptedPrompter(script: {
62
- text?: string[];
63
- select?: unknown[];
64
- multiselect?: unknown[][];
65
- confirm?: boolean[];
66
- }): Prompter & {
67
- log: string[];
68
- };
69
-
70
- type FeatureKey = 'tenancy' | 'auth' | 'billing' | 'ui' | 'cli';
71
- /** The feature toggles offered in the "custom" preset (and shown in the summary). */
72
- declare const FEATURES: Choice<FeatureKey>[];
73
- interface Preset {
74
- value: string;
75
- label: string;
76
- hint: string;
77
- /** null → let the user pick features (custom). */
78
- features: FeatureKey[] | null;
79
- }
80
- declare const PRESETS: Preset[];
81
- /** Validates a project name as an npm-installable package name. */
82
- declare function validateProjectName(name: string): string | undefined;
83
- interface WizardResult {
84
- name: string;
85
- tenancy: boolean;
86
- auth: boolean;
87
- billing: boolean;
88
- ui: boolean;
89
- cli: boolean;
90
- pm: PackageManager;
91
- install: boolean;
92
- git: boolean;
93
- }
94
- interface WizardOptions {
95
- /** Pre-filled name (from argv) — skips the name prompt when valid. */
96
- name?: string;
97
- /** Detected package manager, used as the default selection. */
98
- defaultPm?: PackageManager;
99
- }
100
- /**
101
- * The interactive scaffolding flow. Pure with respect to I/O — it drives the
102
- * injected {@link Prompter}, so tests script the answers and assert the result.
103
- * Throws {@link WizardCancelledError} if the user declines the final confirm.
104
- */
105
- declare function runWizard(prompter: Prompter, options?: WizardOptions): Promise<WizardResult>;
106
-
107
- interface CreateProjectInput {
1
+ import { type ProjectOptions } from './templates.js';
2
+ export type { ProjectOptions } from './templates.js';
3
+ export interface CreateProjectInput {
108
4
  name: string;
109
5
  /** Target directory. Default: ./<name> under cwd. */
110
6
  dir?: string;
@@ -115,22 +11,24 @@ interface CreateProjectInput {
115
11
  ui?: boolean;
116
12
  /** Scaffold the `basalt` CLI entrypoint (code generators + commands). Default: false. */
117
13
  cli?: boolean;
14
+ /** Expose opted-in routes as MCP tools over HTTP at `/mcp`. Default: false. */
15
+ mcp?: boolean;
118
16
  }
119
- interface CreateProjectResult {
17
+ export interface CreateProjectResult {
120
18
  dir: string;
121
19
  files: string[];
122
20
  options: ProjectOptions;
123
21
  }
124
- declare class TargetNotEmptyError extends Error {
22
+ export declare class TargetNotEmptyError extends Error {
125
23
  constructor(dir: string);
126
24
  }
127
- type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
25
+ export type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
128
26
  /**
129
27
  * Detects the package manager that invoked `npm create` / `npx`, via the
130
28
  * `npm_config_user_agent` env var npm/pnpm/yarn/bun all set. Falls back to npm.
131
29
  */
132
- declare function detectPackageManager(userAgent?: string | undefined): PackageManager;
30
+ export declare function detectPackageManager(userAgent?: string | undefined): PackageManager;
133
31
  /** Generates a ready-to-run Basalt app. Does not install dependencies. */
134
- declare function createProject(input: CreateProjectInput): Promise<CreateProjectResult>;
135
-
136
- export { type Choice, type CreateProjectInput, type CreateProjectResult, FEATURES, type FeatureKey, PRESETS, type PackageManager, type ProjectOptions, type Prompter, TargetNotEmptyError, WizardCancelledError, type WizardOptions, type WizardResult, createProject, detectPackageManager, runWizard, scriptedPrompter, ttyPrompter, validateProjectName };
32
+ export declare function createProject(input: CreateProjectInput): Promise<CreateProjectResult>;
33
+ export { runWizard, validateProjectName, PRESETS, FEATURES, type WizardResult, type WizardOptions, type FeatureKey } from './wizard.js';
34
+ export { ttyPrompter, scriptedPrompter, WizardCancelledError, type Prompter, type Choice } from './prompt.js';
package/dist/index.js CHANGED
@@ -1,24 +1,60 @@
1
- import {
2
- FEATURES,
3
- PRESETS,
4
- TargetNotEmptyError,
5
- WizardCancelledError,
6
- createProject,
7
- detectPackageManager,
8
- runWizard,
9
- scriptedPrompter,
10
- ttyPrompter,
11
- validateProjectName
12
- } from "./chunk-Z6RTS4I7.js";
13
- export {
14
- FEATURES,
15
- PRESETS,
16
- TargetNotEmptyError,
17
- WizardCancelledError,
18
- createProject,
19
- detectPackageManager,
20
- runWizard,
21
- scriptedPrompter,
22
- ttyPrompter,
23
- validateProjectName
24
- };
1
+ import { mkdir, readdir, writeFile } from 'node:fs/promises';
2
+ import { dirname, join, resolve } from 'node:path';
3
+ import { appTest, appTs, envExample, envTs, gitignore, mcpJson, basaltBin, packageJson, pnpmWorkspaceYaml, readme, routesTs, serverTs, tsconfigJson, } from './templates.js';
4
+ import { uiFiles } from './templates-ui.js';
5
+ export class TargetNotEmptyError extends Error {
6
+ constructor(dir) {
7
+ super(`Target directory "${dir}" already exists and is not empty.`);
8
+ this.name = 'TargetNotEmptyError';
9
+ }
10
+ }
11
+ /**
12
+ * Detects the package manager that invoked `npm create` / `npx`, via the
13
+ * `npm_config_user_agent` env var npm/pnpm/yarn/bun all set. Falls back to npm.
14
+ */
15
+ export function detectPackageManager(userAgent = process.env['npm_config_user_agent']) {
16
+ const name = userAgent?.split(' ')[0]?.split('/')[0];
17
+ if (name === 'pnpm' || name === 'yarn' || name === 'bun')
18
+ return name;
19
+ return 'npm';
20
+ }
21
+ /** Generates a ready-to-run Basalt app. Does not install dependencies. */
22
+ export async function createProject(input) {
23
+ const options = {
24
+ name: input.name,
25
+ tenancy: input.tenancy ?? true,
26
+ auth: input.auth ?? true,
27
+ billing: input.billing ?? false,
28
+ ui: input.ui ?? false,
29
+ cli: input.cli ?? false,
30
+ mcp: input.mcp ?? false,
31
+ };
32
+ const dir = resolve(input.dir ?? input.name);
33
+ const existing = await readdir(dir).catch(() => null);
34
+ if (existing && existing.length > 0)
35
+ throw new TargetNotEmptyError(dir);
36
+ const files = {
37
+ 'package.json': packageJson(options),
38
+ 'tsconfig.json': tsconfigJson(),
39
+ '.env.example': envExample(options),
40
+ '.gitignore': gitignore(),
41
+ 'README.md': readme(options),
42
+ 'pnpm-workspace.yaml': pnpmWorkspaceYaml(options),
43
+ 'src/env.ts': envTs(options),
44
+ 'src/app.ts': appTs(options),
45
+ 'src/routes.ts': routesTs(options),
46
+ 'src/server.ts': serverTs(),
47
+ 'tests/app.test.ts': appTest(options),
48
+ ...(options.cli ? { 'bin/basalt.ts': basaltBin() } : {}),
49
+ ...(options.mcp ? { '.mcp.json': mcpJson(options) } : {}),
50
+ ...(options.ui ? uiFiles(options) : {}),
51
+ };
52
+ for (const [path, content] of Object.entries(files)) {
53
+ const target = join(dir, path);
54
+ await mkdir(dirname(target), { recursive: true });
55
+ await writeFile(target, content);
56
+ }
57
+ return { dir, files: Object.keys(files).sort(), options };
58
+ }
59
+ export { runWizard, validateProjectName, PRESETS, FEATURES } from './wizard.js';
60
+ export { ttyPrompter, scriptedPrompter, WizardCancelledError } from './prompt.js';
@@ -0,0 +1,57 @@
1
+ /** A selectable option. `value` is returned; `label`/`hint` are shown. */
2
+ export interface Choice<T> {
3
+ value: T;
4
+ label: string;
5
+ hint?: string;
6
+ }
7
+ /**
8
+ * The prompt surface the wizard talks to. Abstracted so the flow (`runWizard`)
9
+ * is testable with a scripted implementation — the raw-mode TTY version is the
10
+ * thin, untested I/O shell.
11
+ */
12
+ export interface Prompter {
13
+ intro(message: string): void;
14
+ note(message: string): void;
15
+ outro(message: string): void;
16
+ text(opts: {
17
+ message: string;
18
+ placeholder?: string;
19
+ initial?: string;
20
+ validate?: (v: string) => string | undefined;
21
+ }): Promise<string>;
22
+ select<T>(opts: {
23
+ message: string;
24
+ choices: Choice<T>[];
25
+ initial?: number;
26
+ }): Promise<T>;
27
+ multiselect<T>(opts: {
28
+ message: string;
29
+ choices: Choice<T>[];
30
+ initial?: T[];
31
+ }): Promise<T[]>;
32
+ confirm(opts: {
33
+ message: string;
34
+ initial?: boolean;
35
+ }): Promise<boolean>;
36
+ }
37
+ /** Thrown when the user aborts (Ctrl+C, or declines the final confirmation). */
38
+ export declare class WizardCancelledError extends Error {
39
+ constructor();
40
+ }
41
+ /**
42
+ * A rich, dependency-free terminal prompter: arrow-key `select`/`multiselect`
43
+ * (raw mode), validated `text`, and `confirm`. Only usable in a TTY.
44
+ */
45
+ export declare function ttyPrompter(): Prompter;
46
+ /**
47
+ * A scripted prompter for tests: each call dequeues the next answer of its kind.
48
+ * `log` captures intro/note/outro lines so a test can assert the summary.
49
+ */
50
+ export declare function scriptedPrompter(script: {
51
+ text?: string[];
52
+ select?: unknown[];
53
+ multiselect?: unknown[][];
54
+ confirm?: boolean[];
55
+ }): Prompter & {
56
+ log: string[];
57
+ };