create-zerotal 1.6.3 → 1.7.2
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/CHANGELOG.md +21 -0
- package/package.json +1 -1
- package/src/args.ts +192 -0
- package/src/index.ts +44 -13
- package/src/scaffold.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,27 @@ follows the Zerotal monorepo's unified versioning.
|
|
|
8
8
|
|
|
9
9
|
## [Unreleased]
|
|
10
10
|
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **A non-interactive mode, so the scaffolder can be scripted.** Every prompt now has a flag —
|
|
14
|
+
`--template`, `--db`, `--name` — plus `--yes` to take the defaults for anything unset,
|
|
15
|
+
`--no-install`, `--help` and `--version`. `bun create zerotal my-app` is unchanged.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **With no TTY the scaffolder hung instead of failing.** It read `process.argv[2]` as the project
|
|
20
|
+
name and asked for everything else through `readline`, which waits for a line that never arrives
|
|
21
|
+
in CI, in a pipeline, or under an agent. So it did not error and did not exit — it held the job
|
|
22
|
+
open until something timed it out. The workaround in this repo was to bypass the CLI entirely and
|
|
23
|
+
call `scaffold()` directly, which is the shape of an admission.
|
|
24
|
+
|
|
25
|
+
It now detects the absent terminal and refuses, naming the flag that would have answered the
|
|
26
|
+
question, with a non-zero exit code. Unknown flags are refused too rather than dropped: silently
|
|
27
|
+
ignoring `--tempalte=api` means asking again for something the caller thought they had given,
|
|
28
|
+
which in CI is the hang all over again. A failed `bun install` also exits non-zero when there is
|
|
29
|
+
no terminal — a half-built project reported as a success is worse than one that stops.
|
|
30
|
+
|
|
31
|
+
|
|
11
32
|
## [1.6.3] — 2026-08-15
|
|
12
33
|
|
|
13
34
|
### Added
|
package/package.json
CHANGED
package/src/args.ts
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import type { Database, Template } from './scaffold.ts';
|
|
2
|
+
|
|
3
|
+
export const TEMPLATES: readonly Template[] = ['api', 'admin', 'flow', 'react', 'vue', 'minimal'];
|
|
4
|
+
export const DATABASES: readonly Database[] = ['sqlite', 'postgres', 'mysql'];
|
|
5
|
+
|
|
6
|
+
/** Defaults, matching the first option of each prompt. */
|
|
7
|
+
export const DEFAULT_TEMPLATE: Template = 'api';
|
|
8
|
+
export const DEFAULT_DATABASE: Database = 'sqlite';
|
|
9
|
+
|
|
10
|
+
export interface ParsedArgs {
|
|
11
|
+
name?: string;
|
|
12
|
+
template?: string;
|
|
13
|
+
db?: string;
|
|
14
|
+
/** Take defaults for anything not supplied, instead of asking. */
|
|
15
|
+
yes: boolean;
|
|
16
|
+
install: boolean;
|
|
17
|
+
help: boolean;
|
|
18
|
+
version: boolean;
|
|
19
|
+
/** Flags that are not ours — reported rather than ignored. */
|
|
20
|
+
unknown: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parse the command line.
|
|
25
|
+
*
|
|
26
|
+
* Supports `--flag value` and `--flag=value`, plus `-t`/`-y`/`-h` shorthands, so
|
|
27
|
+
* neither habit is wrong. The first non-flag argument is the project name, which
|
|
28
|
+
* keeps `bun create zerotal my-app` working exactly as before.
|
|
29
|
+
*
|
|
30
|
+
* Unknown flags are collected rather than dropped. Silently ignoring
|
|
31
|
+
* `--tempalte=api` and then interactively asking for the template is the worst
|
|
32
|
+
* of both worlds: in CI it hangs, and a human is asked a question they thought
|
|
33
|
+
* they had answered.
|
|
34
|
+
*/
|
|
35
|
+
export function parseArgs(argv: readonly string[]): ParsedArgs {
|
|
36
|
+
const out: ParsedArgs = { yes: false, install: true, help: false, version: false, unknown: [] };
|
|
37
|
+
const takesValue = new Set(['name', 'template', 'db', 't']);
|
|
38
|
+
|
|
39
|
+
for (let i = 0; i < argv.length; i++) {
|
|
40
|
+
const arg = argv[i]!;
|
|
41
|
+
|
|
42
|
+
if (!arg.startsWith('-')) {
|
|
43
|
+
out.name ??= arg;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const [rawKey, inlineValue] = arg.replace(/^--?/, '').split(/=(.*)/s, 2) as [string, string?];
|
|
48
|
+
const key = rawKey === 't' ? 'template' : rawKey === 'y' ? 'yes' : rawKey === 'h' ? 'help' : rawKey;
|
|
49
|
+
|
|
50
|
+
// `--no-install` — the standard spelling for negating a boolean flag.
|
|
51
|
+
if (key === 'no-install') {
|
|
52
|
+
out.install = false;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (key === 'yes') {
|
|
56
|
+
out.yes = true;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (key === 'help') {
|
|
60
|
+
out.help = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (key === 'version' || key === 'v') {
|
|
64
|
+
out.version = true;
|
|
65
|
+
continue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (takesValue.has(key === 'template' ? 'template' : key)) {
|
|
69
|
+
// `--template api` as well as `--template=api`; a following flag is not a
|
|
70
|
+
// value, so `--template --yes` is a missing value rather than the string
|
|
71
|
+
// "--yes".
|
|
72
|
+
const next = argv[i + 1];
|
|
73
|
+
const value = inlineValue ?? (next && !next.startsWith('-') ? (i++, next) : undefined);
|
|
74
|
+
// Assigned only when present: `exactOptionalPropertyTypes` treats an
|
|
75
|
+
// explicit `undefined` as different from an absent key, and "absent" is
|
|
76
|
+
// exactly what a flag with no value means.
|
|
77
|
+
if (value !== undefined) {
|
|
78
|
+
if (key === 'name') out.name = value;
|
|
79
|
+
else if (key === 'template') out.template = value;
|
|
80
|
+
else if (key === 'db') out.db = value;
|
|
81
|
+
}
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
out.unknown.push(arg);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return out;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export interface Resolution {
|
|
92
|
+
ok: boolean;
|
|
93
|
+
/** Populated when `ok` — everything the scaffolder needs. */
|
|
94
|
+
name?: string | undefined;
|
|
95
|
+
template?: Template | undefined;
|
|
96
|
+
db?: Database | undefined;
|
|
97
|
+
install?: boolean | undefined;
|
|
98
|
+
/** Questions still to ask. Empty in non-interactive mode. */
|
|
99
|
+
askFor?: Array<'name' | 'template' | 'db'>;
|
|
100
|
+
/** Populated when `!ok` — a message a human or a CI log can act on. */
|
|
101
|
+
error?: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Turn parsed flags into either a plan or a refusal.
|
|
106
|
+
*
|
|
107
|
+
* The rule that matters: **never wait on stdin that is not there.** Without a
|
|
108
|
+
* TTY the prompts' `readline` never receives a line, so the old scaffolder did
|
|
109
|
+
* not fail in CI — it hung, forever, holding the job open until the runner timed
|
|
110
|
+
* it out. A missing answer is now an error with the flag that would have
|
|
111
|
+
* supplied it.
|
|
112
|
+
*
|
|
113
|
+
* `--yes` takes the defaults for whatever is unspecified, which is what makes
|
|
114
|
+
* the tool scriptable in one word.
|
|
115
|
+
*/
|
|
116
|
+
export function resolveOptions(args: ParsedArgs, interactive: boolean): Resolution {
|
|
117
|
+
if (args.unknown.length) {
|
|
118
|
+
return { ok: false, error: `Unknown option${args.unknown.length > 1 ? 's' : ''}: ${args.unknown.join(', ')}` };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (args.template !== undefined && !TEMPLATES.includes(args.template as Template)) {
|
|
122
|
+
return { ok: false, error: `Unknown template "${args.template}". Choose one of: ${TEMPLATES.join(', ')}` };
|
|
123
|
+
}
|
|
124
|
+
if (args.db !== undefined && !DATABASES.includes(args.db as Database)) {
|
|
125
|
+
return { ok: false, error: `Unknown database "${args.db}". Choose one of: ${DATABASES.join(', ')}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const template = (args.template as Template | undefined);
|
|
129
|
+
const db = (args.db as Database | undefined);
|
|
130
|
+
|
|
131
|
+
if (interactive && !args.yes) {
|
|
132
|
+
const askFor: Array<'name' | 'template' | 'db'> = [];
|
|
133
|
+
if (!args.name) askFor.push('name');
|
|
134
|
+
if (!template) askFor.push('template');
|
|
135
|
+
// Only the API template offers a database choice; the rest have no DB config.
|
|
136
|
+
if (!db && (template === undefined || template === 'api')) askFor.push('db');
|
|
137
|
+
return { ok: true, name: args.name, template, db, install: args.install, askFor };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Non-interactive from here: everything must already be known, or defaulted.
|
|
141
|
+
if (!args.name) {
|
|
142
|
+
return {
|
|
143
|
+
ok: false,
|
|
144
|
+
error:
|
|
145
|
+
'A project name is required when there is no terminal to ask on.\n' +
|
|
146
|
+
' bunx create-zerotal my-app --template=api --yes',
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
if (!template && !args.yes) {
|
|
150
|
+
return {
|
|
151
|
+
ok: false,
|
|
152
|
+
error:
|
|
153
|
+
'No template given, and no terminal to ask on.\n' +
|
|
154
|
+
` Pass --template=<${TEMPLATES.join('|')}>, or --yes to take the default (${DEFAULT_TEMPLATE}).`,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const chosen = template ?? DEFAULT_TEMPLATE;
|
|
159
|
+
return {
|
|
160
|
+
ok: true,
|
|
161
|
+
name: args.name,
|
|
162
|
+
template: chosen,
|
|
163
|
+
db: chosen === 'api' ? (db ?? DEFAULT_DATABASE) : DEFAULT_DATABASE,
|
|
164
|
+
install: args.install,
|
|
165
|
+
askFor: [],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/** The `--help` text. */
|
|
170
|
+
export function helpText(): string {
|
|
171
|
+
return [
|
|
172
|
+
'create-zerotal — scaffold a Zerotal application',
|
|
173
|
+
'',
|
|
174
|
+
'Usage',
|
|
175
|
+
' bun create zerotal <name> [options]',
|
|
176
|
+
' bunx create-zerotal <name> [options]',
|
|
177
|
+
'',
|
|
178
|
+
'Options',
|
|
179
|
+
` -t, --template <name> ${TEMPLATES.join(' | ')}`,
|
|
180
|
+
` --db <name> ${DATABASES.join(' | ')} (api template only)`,
|
|
181
|
+
' -y, --yes Take defaults for anything not given; never prompt',
|
|
182
|
+
' --no-install Skip bun install',
|
|
183
|
+
' -h, --help Show this message',
|
|
184
|
+
' -v, --version Print the scaffolder version',
|
|
185
|
+
'',
|
|
186
|
+
'Non-interactive',
|
|
187
|
+
' With no TTY (CI, an agent, a pipe) the scaffolder never prompts. Supply the',
|
|
188
|
+
' answers as flags, or pass --yes to take the defaults.',
|
|
189
|
+
'',
|
|
190
|
+
' bunx create-zerotal my-app --template=api --db=postgres --yes',
|
|
191
|
+
].join('\n');
|
|
192
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
import { resolve } from 'node:path';
|
|
11
11
|
import { printBanner, ask, choose, log, info, warn, step, dim, c } from './prompts.ts';
|
|
12
12
|
import { scaffold, install, type Template, type Database } from './scaffold.ts';
|
|
13
|
+
import { parseArgs, resolveOptions, helpText, TEMPLATES, DATABASES } from './args.ts';
|
|
13
14
|
import { newerScaffolderVersion } from './staleness.ts';
|
|
14
15
|
|
|
15
16
|
/** This scaffolder's own version, read from the manifest that ships beside it. */
|
|
@@ -18,6 +19,28 @@ const ZT_SELF_VERSION: string = (
|
|
|
18
19
|
).version;
|
|
19
20
|
|
|
20
21
|
async function main(): Promise<void> {
|
|
22
|
+
const args = parseArgs(process.argv.slice(2));
|
|
23
|
+
|
|
24
|
+
if (args.help) {
|
|
25
|
+
process.stdout.write(helpText() + '\n');
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
if (args.version) {
|
|
29
|
+
process.stdout.write(ZT_SELF_VERSION + '\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Whether there is anybody to answer a question. Without this the prompts'
|
|
34
|
+
// readline waits on a line that never comes, so a scaffolder run in CI or by
|
|
35
|
+
// an agent did not fail — it hung until something killed it.
|
|
36
|
+
const interactive = Boolean(process.stdin.isTTY);
|
|
37
|
+
const plan = resolveOptions(args, interactive);
|
|
38
|
+
if (!plan.ok) {
|
|
39
|
+
process.stderr.write(`\x1b[31mError: ${plan.error}\x1b[0m\n\n${helpText()}\n`);
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
const asking = new Set(plan.askFor ?? []);
|
|
43
|
+
|
|
21
44
|
// Started before the banner and awaited after the prompts, so the check costs
|
|
22
45
|
// no perceived time at all — the answer arrives while a human is reading.
|
|
23
46
|
const newerScaffolder = newerScaffolderVersion(ZT_SELF_VERSION);
|
|
@@ -25,9 +48,9 @@ async function main(): Promise<void> {
|
|
|
25
48
|
printBanner();
|
|
26
49
|
|
|
27
50
|
// ── Project name ────────────────────────────────────────────────────────────
|
|
28
|
-
const
|
|
29
|
-
|
|
30
|
-
|
|
51
|
+
const name = asking.has('name')
|
|
52
|
+
? await ask('Project name', 'my-zerotal-app')
|
|
53
|
+
: plan.name!;
|
|
31
54
|
|
|
32
55
|
const target = resolve(process.cwd(), name);
|
|
33
56
|
|
|
@@ -40,7 +63,9 @@ async function main(): Promise<void> {
|
|
|
40
63
|
|
|
41
64
|
// ── Template ────────────────────────────────────────────────────────────────
|
|
42
65
|
log('');
|
|
43
|
-
const template =
|
|
66
|
+
const template: Template = !asking.has('template')
|
|
67
|
+
? plan.template!
|
|
68
|
+
: await choose<Template>('Template', [
|
|
44
69
|
{
|
|
45
70
|
value: 'api',
|
|
46
71
|
label: 'API',
|
|
@@ -75,8 +100,8 @@ async function main(): Promise<void> {
|
|
|
75
100
|
|
|
76
101
|
// ── Database ─────────────────────────────────────────────────────────────────
|
|
77
102
|
// Only the API template ships a database config; minimal/flow have no DB.
|
|
78
|
-
let db: Database = 'sqlite';
|
|
79
|
-
if (template === 'api') {
|
|
103
|
+
let db: Database = plan.db ?? 'sqlite';
|
|
104
|
+
if (template === 'api' && asking.has('db')) {
|
|
80
105
|
log('');
|
|
81
106
|
db = await choose<Database>('Database', [
|
|
82
107
|
{
|
|
@@ -118,13 +143,19 @@ async function main(): Promise<void> {
|
|
|
118
143
|
info(`Project files created`);
|
|
119
144
|
|
|
120
145
|
// ── Install ─────────────────────────────────────────────────────────────────
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
146
|
+
if (plan.install !== false) {
|
|
147
|
+
log('');
|
|
148
|
+
step('Installing dependencies…');
|
|
149
|
+
const ok = await install(target);
|
|
150
|
+
if (ok) {
|
|
151
|
+
info(`Dependencies installed`);
|
|
152
|
+
} else {
|
|
153
|
+
warn(`bun install failed — run it manually inside the project`);
|
|
154
|
+
// A failed install is a broken project, and a script that carried on
|
|
155
|
+
// regardless would report success for one. A human has the message above
|
|
156
|
+
// and the directory in front of them; CI needs the exit code.
|
|
157
|
+
if (!interactive) process.exit(1);
|
|
158
|
+
}
|
|
128
159
|
}
|
|
129
160
|
|
|
130
161
|
// ── Done ─────────────────────────────────────────────────────────────────────
|
package/src/scaffold.ts
CHANGED
|
@@ -13,7 +13,7 @@ export type Template = 'minimal' | 'api' | 'admin' | 'flow' | 'react' | 'vue';
|
|
|
13
13
|
// "^1.1.0" found for specifier "zerotal"` — the first thing anyone trying the
|
|
14
14
|
// framework saw. `scaffold.test.ts` now asserts the two agree, so CI fails rather
|
|
15
15
|
// than the user's install.
|
|
16
|
-
export const ZT_VERSION = "^1.
|
|
16
|
+
export const ZT_VERSION = "^1.7.2";
|
|
17
17
|
|
|
18
18
|
export interface ScaffoldOptions {
|
|
19
19
|
name: string;
|