@tenkit/cli 0.1.1 → 0.2.0-next.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 +69 -11
- package/dist/index.mjs +257 -92
- package/package.json +4 -2
package/README.md
CHANGED
|
@@ -1,27 +1,85 @@
|
|
|
1
1
|
# @tenkit/cli
|
|
2
2
|
|
|
3
|
-
Public
|
|
3
|
+
Public CLI implementation for creating Tenkit Expo projects.
|
|
4
4
|
|
|
5
|
-
Most users should run Tenkit through the
|
|
5
|
+
Most users should run Tenkit through the create entrypoint:
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
pnpm create tenkit@latest
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
`@tenkit/cli` owns the real create flow behind `create-tenkit`: option parsing,
|
|
12
|
+
prompts, Template generation, dependency installation, initial git setup, and
|
|
13
|
+
final next steps.
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
multi-tenant products, App Variant builds, and Runtime Tenant experiences.
|
|
15
|
+
## Highlights
|
|
17
16
|
|
|
18
|
-
|
|
17
|
+
- Create a generated Expo project from a selected Tenkit Setup Type.
|
|
18
|
+
- Prompt for project name, Setup Type, App Variant customization, and Styling Choice.
|
|
19
|
+
- Support `pnpm`, `npm`, `npx`, Bun, and `bunx` launchers.
|
|
20
|
+
- Use the selected package manager for install commands, generated README
|
|
21
|
+
commands, and generated app-local commands.
|
|
22
|
+
- Treat install and git failures as follow-up work so successful generation is
|
|
23
|
+
not hidden by convenience-step failures.
|
|
19
24
|
|
|
20
|
-
|
|
25
|
+
## Usage
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
# Package-manager create entrypoints
|
|
29
|
+
pnpm create tenkit@latest
|
|
30
|
+
npm create tenkit@latest
|
|
31
|
+
npx create-tenkit@latest
|
|
32
|
+
bun create tenkit@latest
|
|
33
|
+
bunx create-tenkit@latest
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
Direct binary usage is also available:
|
|
21
37
|
|
|
22
38
|
```bash
|
|
23
39
|
tenkit --help
|
|
24
40
|
```
|
|
25
41
|
|
|
26
|
-
|
|
27
|
-
|
|
42
|
+
## Common Options
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
tenkit --name studio-app --setup white-label --yes
|
|
46
|
+
tenkit --name venue-network --setup runtime-tenants --yes
|
|
47
|
+
tenkit --name franchise-app --setup generic-standalone --yes
|
|
48
|
+
tenkit --name unistyles-app --setup white-label --styling unistyles --yes
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
tenkit --name demo --setup runtime-tenants --yes --no-install --no-git
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Supported public Setup Type slugs:
|
|
56
|
+
|
|
57
|
+
```text
|
|
58
|
+
white-label
|
|
59
|
+
runtime-tenants
|
|
60
|
+
generic-standalone
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Supported Styling Option Values:
|
|
64
|
+
|
|
65
|
+
```text
|
|
66
|
+
bare
|
|
67
|
+
uniwind
|
|
68
|
+
unistyles
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Bare remains the default when Styling is omitted or `--yes` accepts defaults.
|
|
72
|
+
|
|
73
|
+
Override package-manager detection when needed:
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
tenkit --package-manager pnpm
|
|
77
|
+
tenkit --package-manager npm
|
|
78
|
+
tenkit --package-manager bun
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Package Boundary
|
|
82
|
+
|
|
83
|
+
`@tenkit/cli` is the Public CLI implementation package. The `create-tenkit`
|
|
84
|
+
package is a thin create entrypoint that delegates here. Template source,
|
|
85
|
+
generation, and writer safety live in `@tenkit/template-generator`.
|
package/dist/index.mjs
CHANGED
|
@@ -4,8 +4,10 @@ import fs from "fs-extra";
|
|
|
4
4
|
import { dirname, resolve } from "pathe";
|
|
5
5
|
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
6
6
|
import { Command } from "commander";
|
|
7
|
-
import { SUPPORTED_PUBLIC_SETUP_SLUGS, formatSupportedGeneratedSetupTypes, generateProject, normalizeGeneratedSetupType, preflightWriteProject, writeProject } from "@tenkit/template-generator";
|
|
7
|
+
import { SUPPORTED_PUBLIC_SETUP_SLUGS, formatSupportedGeneratedSetupTypes, generateProject, normalizeGeneratedAccentColor, normalizeGeneratedSetupType, preflightWriteProject, writeProject } from "@tenkit/template-generator";
|
|
8
|
+
import { SUPPORTED_GENERATED_STYLING_CHOICES, normalizeGeneratedStylingChoice } from "@tenkit/template-generator/styling-definitions";
|
|
8
9
|
import { spawn } from "node:child_process";
|
|
10
|
+
import { deriveAppVariantIdentities, deriveAppVariantIdentity, derivePackageName, getGeneratedSetupTypeDefinition, normalizeProjectName, validatePackageName } from "@tenkit/template-generator/setup-type-definitions";
|
|
9
11
|
//#region src/adapters/workspace.ts
|
|
10
12
|
function isPackageJsonShape(value) {
|
|
11
13
|
return typeof value === "object" && value !== null;
|
|
@@ -36,9 +38,10 @@ function resolveRealPath(path) {
|
|
|
36
38
|
}
|
|
37
39
|
//#endregion
|
|
38
40
|
//#region src/constants.ts
|
|
39
|
-
const CLI_VERSION = "0.
|
|
41
|
+
const CLI_VERSION = "0.2.0-next.0";
|
|
40
42
|
const DEFAULT_PROJECT_NAME = "tenkit-app";
|
|
41
43
|
const DEFAULT_PUBLIC_SETUP_SLUG = "white-label";
|
|
44
|
+
const DEFAULT_STYLING_CHOICE = "bare";
|
|
42
45
|
const PROMPT_CANCELLED = Symbol("prompt-cancelled");
|
|
43
46
|
const SETUP_PROMPT_CHOICES = [
|
|
44
47
|
{
|
|
@@ -54,16 +57,33 @@ const SETUP_PROMPT_CHOICES = [
|
|
|
54
57
|
label: "Generic + Standalone Apps"
|
|
55
58
|
}
|
|
56
59
|
];
|
|
60
|
+
const STYLING_PROMPT_LABELS = {
|
|
61
|
+
bare: "Bare",
|
|
62
|
+
uniwind: "Uniwind",
|
|
63
|
+
unistyles: "Unistyles"
|
|
64
|
+
};
|
|
65
|
+
const STYLING_PROMPT_CHOICES = SUPPORTED_GENERATED_STYLING_CHOICES.map((value) => ({
|
|
66
|
+
value,
|
|
67
|
+
label: STYLING_PROMPT_LABELS[value]
|
|
68
|
+
}));
|
|
57
69
|
function supportedSetupValues() {
|
|
58
70
|
return SUPPORTED_PUBLIC_SETUP_SLUGS;
|
|
59
71
|
}
|
|
72
|
+
function supportedStylingValues() {
|
|
73
|
+
return SUPPORTED_GENERATED_STYLING_CHOICES;
|
|
74
|
+
}
|
|
60
75
|
//#endregion
|
|
61
76
|
//#region src/adapters/command-runner.ts
|
|
62
77
|
function defaultRunCommand(command, args, cwd, options = {}) {
|
|
63
78
|
return new Promise((resolveCommand) => {
|
|
79
|
+
const stdio = options.stdio ?? "inherit";
|
|
64
80
|
const child = spawn(command, [...args], {
|
|
65
81
|
cwd,
|
|
66
|
-
|
|
82
|
+
env: options.env ? {
|
|
83
|
+
...process.env,
|
|
84
|
+
...options.env
|
|
85
|
+
} : process.env,
|
|
86
|
+
stdio
|
|
67
87
|
});
|
|
68
88
|
child.on("error", () => {
|
|
69
89
|
resolveCommand({
|
|
@@ -80,13 +100,6 @@ function defaultRunCommand(command, args, cwd, options = {}) {
|
|
|
80
100
|
});
|
|
81
101
|
}
|
|
82
102
|
//#endregion
|
|
83
|
-
//#region src/errors.ts
|
|
84
|
-
var CreateFlowCancelledError = class extends Error {
|
|
85
|
-
constructor() {
|
|
86
|
-
super("Create cancelled.");
|
|
87
|
-
}
|
|
88
|
-
};
|
|
89
|
-
//#endregion
|
|
90
103
|
//#region src/adapters/git.ts
|
|
91
104
|
async function isGitAvailable(runCommand, cwd) {
|
|
92
105
|
return (await runCommand("git", ["--version"], cwd, { stdio: "ignore" })).ok;
|
|
@@ -94,64 +107,46 @@ async function isGitAvailable(runCommand, cwd) {
|
|
|
94
107
|
async function isInsideGitWorktree(runCommand, cwd) {
|
|
95
108
|
return (await runCommand("git", ["rev-parse", "--is-inside-work-tree"], cwd, { stdio: "ignore" })).ok;
|
|
96
109
|
}
|
|
97
|
-
async function resolveGitMode({
|
|
98
|
-
if (
|
|
99
|
-
|
|
110
|
+
async function resolveGitMode({ enabled, runCommand, targetDir }) {
|
|
111
|
+
if (!enabled) return {
|
|
112
|
+
enabled: false,
|
|
100
113
|
skippedReason: "disabled"
|
|
101
114
|
};
|
|
102
115
|
if (!await isGitAvailable(runCommand, targetDir)) return {
|
|
103
|
-
|
|
116
|
+
enabled: false,
|
|
104
117
|
skippedReason: "git-unavailable"
|
|
105
118
|
};
|
|
106
|
-
if (await isInsideGitWorktree(runCommand, targetDir)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
const answer = await env.prompts.confirm({
|
|
112
|
-
message: "Initialize a nested git repository?",
|
|
113
|
-
initialValue: false
|
|
114
|
-
});
|
|
115
|
-
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
116
|
-
if (!answer) return {
|
|
117
|
-
mode: false,
|
|
118
|
-
skippedReason: "nested-worktree"
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
if (explicitGitMode === "init") return { mode: "init" };
|
|
122
|
-
return { mode: "commit" };
|
|
119
|
+
if (await isInsideGitWorktree(runCommand, targetDir)) return {
|
|
120
|
+
enabled: false,
|
|
121
|
+
skippedReason: "nested-worktree"
|
|
122
|
+
};
|
|
123
|
+
return { enabled: true };
|
|
123
124
|
}
|
|
124
|
-
async function prepareInitialGitSetup({
|
|
125
|
+
async function prepareInitialGitSetup({ enabled, runCommand, probeDir }) {
|
|
125
126
|
const gitPlan = await resolveGitMode({
|
|
126
|
-
|
|
127
|
-
env,
|
|
127
|
+
enabled,
|
|
128
128
|
runCommand,
|
|
129
129
|
targetDir: probeDir
|
|
130
130
|
});
|
|
131
131
|
return { async run(targetDir) {
|
|
132
|
-
if (!gitPlan.
|
|
132
|
+
if (!gitPlan.enabled) return {
|
|
133
133
|
gitInitialized: false,
|
|
134
134
|
gitCommitted: false,
|
|
135
135
|
gitSkippedReason: gitPlan.skippedReason,
|
|
136
136
|
gitFailed: false
|
|
137
137
|
};
|
|
138
|
-
const initResult = await runCommand("git", ["init"], targetDir);
|
|
138
|
+
const initResult = await runCommand("git", ["init"], targetDir, { stdio: "ignore" });
|
|
139
139
|
const gitInitialized = initResult.ok;
|
|
140
140
|
if (!initResult.ok) return {
|
|
141
141
|
gitInitialized,
|
|
142
142
|
gitCommitted: false,
|
|
143
143
|
gitFailed: true
|
|
144
144
|
};
|
|
145
|
-
|
|
146
|
-
gitInitialized,
|
|
147
|
-
gitCommitted: false,
|
|
148
|
-
gitFailed: false
|
|
149
|
-
};
|
|
150
|
-
const commitResult = (await runCommand("git", ["add", "--all"], targetDir)).ok ? await runCommand("git", [
|
|
145
|
+
const commitResult = (await runCommand("git", ["add", "--all"], targetDir, { stdio: "ignore" })).ok ? await runCommand("git", [
|
|
151
146
|
"commit",
|
|
152
147
|
"-m",
|
|
153
148
|
"Initial commit"
|
|
154
|
-
], targetDir) : {
|
|
149
|
+
], targetDir, { stdio: "ignore" }) : {
|
|
155
150
|
ok: false,
|
|
156
151
|
code: 1
|
|
157
152
|
};
|
|
@@ -163,6 +158,35 @@ async function prepareInitialGitSetup({ explicitGitMode, env, runCommand, probeD
|
|
|
163
158
|
} };
|
|
164
159
|
}
|
|
165
160
|
//#endregion
|
|
161
|
+
//#region src/create/package-manager.ts
|
|
162
|
+
const SUPPORTED_PACKAGE_MANAGERS = [
|
|
163
|
+
"pnpm",
|
|
164
|
+
"npm",
|
|
165
|
+
"bun"
|
|
166
|
+
];
|
|
167
|
+
function normalizePackageManagerInput(packageManager) {
|
|
168
|
+
if (packageManager === void 0) return;
|
|
169
|
+
const normalized = packageManager.trim();
|
|
170
|
+
if (!normalized) throw new Error(`Package manager must be one of: ${SUPPORTED_PACKAGE_MANAGERS.join(", ")}.`);
|
|
171
|
+
if (isPublicCliPackageManager(normalized)) return normalized;
|
|
172
|
+
throw new Error(`Unsupported package manager ${JSON.stringify(packageManager)}. Expected one of: ${SUPPORTED_PACKAGE_MANAGERS.join(", ")}.`);
|
|
173
|
+
}
|
|
174
|
+
function detectPackageManager(userAgent) {
|
|
175
|
+
if (userAgent?.startsWith("pnpm")) return "pnpm";
|
|
176
|
+
if (userAgent?.startsWith("bun")) return "bun";
|
|
177
|
+
if (userAgent?.startsWith("npm")) return "npm";
|
|
178
|
+
return "pnpm";
|
|
179
|
+
}
|
|
180
|
+
function formatInstallCommand(packageManager) {
|
|
181
|
+
return `${packageManager} install`;
|
|
182
|
+
}
|
|
183
|
+
function formatRunCommand(packageManager, script) {
|
|
184
|
+
return `${packageManager} run ${script}`;
|
|
185
|
+
}
|
|
186
|
+
function isPublicCliPackageManager(value) {
|
|
187
|
+
return SUPPORTED_PACKAGE_MANAGERS.some((packageManager) => packageManager === value);
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
166
190
|
//#region src/create/create-messages.ts
|
|
167
191
|
function logFinalOutput(result, output) {
|
|
168
192
|
const projectShellArg = formatShellArg(result.projectName);
|
|
@@ -171,13 +195,13 @@ function logFinalOutput(result, output) {
|
|
|
171
195
|
output.log("");
|
|
172
196
|
output.log("Next steps:");
|
|
173
197
|
output.log(`- cd ${projectShellArg}`);
|
|
174
|
-
if (result.installFailed || !result.installed) output.log(
|
|
175
|
-
output.log(
|
|
176
|
-
output.log(
|
|
177
|
-
output.log(
|
|
198
|
+
if (result.installFailed || !result.installed) output.log(`- ${formatInstallCommand(result.packageManager)}`);
|
|
199
|
+
output.log(`- ${formatRunCommand(result.packageManager, "android")}`);
|
|
200
|
+
output.log(`- ${formatRunCommand(result.packageManager, "ios")}`);
|
|
201
|
+
output.log(`- ${formatRunCommand(result.packageManager, "web")}`);
|
|
178
202
|
if (result.installFailed) {
|
|
179
203
|
output.log("");
|
|
180
|
-
output.log(
|
|
204
|
+
output.log(`Dependency installation failed. Run ${formatInstallCommand(result.packageManager)} in the generated project.`);
|
|
181
205
|
}
|
|
182
206
|
if (result.gitSkippedReason === "git-unavailable") {
|
|
183
207
|
output.log("");
|
|
@@ -195,33 +219,32 @@ function formatShellArg(value) {
|
|
|
195
219
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
196
220
|
}
|
|
197
221
|
//#endregion
|
|
222
|
+
//#region src/errors.ts
|
|
223
|
+
var CreateFlowCancelledError = class extends Error {
|
|
224
|
+
constructor() {
|
|
225
|
+
super("Create cancelled.");
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
//#endregion
|
|
198
229
|
//#region src/create/validation.ts
|
|
199
|
-
function isPathSeparatorPresent(value) {
|
|
200
|
-
return value.includes("/") || value.includes("\\");
|
|
201
|
-
}
|
|
202
230
|
function validateProjectName(value) {
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
return projectName;
|
|
209
|
-
}
|
|
210
|
-
function slugifyPackageName(projectName) {
|
|
211
|
-
return projectName.trim().toLowerCase().replace(/['"]/g, "").replace(/[^a-z0-9._-]+/g, "-").replace(/^[._-]+|[._-]+$/g, "").replace(/-{2,}/g, "-");
|
|
231
|
+
try {
|
|
232
|
+
return normalizeProjectName(value);
|
|
233
|
+
} catch {
|
|
234
|
+
throw new Error("Project name must contain a usable Latin letter or number.");
|
|
235
|
+
}
|
|
212
236
|
}
|
|
213
|
-
function
|
|
214
|
-
|
|
215
|
-
if (packageName.length === 0) throw new Error("Package name is required.");
|
|
216
|
-
if (packageName.length > 214) throw new Error("Package name must be 214 characters or fewer.");
|
|
217
|
-
if (packageName !== packageName.toLowerCase()) throw new Error("Package name must be lowercase.");
|
|
218
|
-
if (isPathSeparatorPresent(packageName)) throw new Error("Package name must not contain path separators.");
|
|
219
|
-
if (packageName.startsWith(".") || packageName.startsWith("_")) throw new Error("Package name must not start with \".\" or \"_\".");
|
|
220
|
-
if (!/^[a-z0-9][a-z0-9._-]*$/.test(packageName)) throw new Error("Package name must contain only lowercase letters, numbers, \".\", \"_\", and \"-\".");
|
|
221
|
-
return packageName;
|
|
237
|
+
function normalizeAppVariantNameInput(value) {
|
|
238
|
+
return deriveAppVariantIdentity(value).displayName;
|
|
222
239
|
}
|
|
223
|
-
function
|
|
224
|
-
|
|
240
|
+
function normalizeAppVariantAccentInput(value) {
|
|
241
|
+
const accent = value.trim();
|
|
242
|
+
const normalizedInput = accent.startsWith("#") ? accent : `#${accent}`;
|
|
243
|
+
try {
|
|
244
|
+
const normalizedAccent = normalizeGeneratedAccentColor(normalizedInput);
|
|
245
|
+
if (normalizedAccent) return normalizedAccent;
|
|
246
|
+
} catch {}
|
|
247
|
+
throw new Error(`Invalid App Variant Accent ${JSON.stringify(accent)}. Expected a six-digit hex color such as "#208AEF".`);
|
|
225
248
|
}
|
|
226
249
|
function normalizeSetupInput(setup, setupType) {
|
|
227
250
|
if (setup !== void 0 && setupType !== void 0 && setup !== setupType) throw new Error("Use either --setup or --setup-type, not both with different values.");
|
|
@@ -232,11 +255,26 @@ function normalizeSetupInput(setup, setupType) {
|
|
|
232
255
|
throw new Error(`Unsupported Setup Type ${JSON.stringify(selectedSetup)}. Expected ${formatSupportedGeneratedSetupTypes()}.`);
|
|
233
256
|
}
|
|
234
257
|
}
|
|
235
|
-
function
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
258
|
+
function normalizeStylingInput(value) {
|
|
259
|
+
try {
|
|
260
|
+
return normalizeGeneratedStylingChoice(value);
|
|
261
|
+
} catch {
|
|
262
|
+
throw new Error(`Unsupported Styling Choice ${JSON.stringify(value)}. Expected one of: ${supportedStylingValues().join(", ")}.`);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
function normalizeAppVariantCustomization(setupType, appVariantNamesInput, appVariantAccentsInput) {
|
|
266
|
+
const definition = getGeneratedSetupTypeDefinition(setupType);
|
|
267
|
+
const appVariantNames = appVariantNamesInput === void 0 ? definition.appVariants.map(({ defaultName }) => defaultName) : appVariantNamesInput.split(",").map((name) => name.trim());
|
|
268
|
+
if (appVariantNames.some((name) => name.length === 0)) throw new Error("App Variant names must not contain empty items.");
|
|
269
|
+
if (appVariantNames.length !== definition.appVariants.length) throw new Error(`Expected exactly ${definition.appVariants.length} App Variant names for ${definition.publicSlug}.`);
|
|
270
|
+
deriveAppVariantIdentities(appVariantNames);
|
|
271
|
+
const rawAccents = appVariantAccentsInput === void 0 ? definition.appVariants.map(({ defaultAccent }) => defaultAccent) : appVariantAccentsInput.split(",").map((accent) => accent.trim());
|
|
272
|
+
if (rawAccents.some((accent) => accent.length === 0)) throw new Error("App Variant Accents must not contain empty items.");
|
|
273
|
+
if (rawAccents.length !== definition.appVariants.length) throw new Error(`Expected exactly ${definition.appVariants.length} App Variant Accents for ${definition.publicSlug}.`);
|
|
274
|
+
return {
|
|
275
|
+
appVariantNames,
|
|
276
|
+
appVariantAccents: rawAccents.map(normalizeAppVariantAccentInput)
|
|
277
|
+
};
|
|
240
278
|
}
|
|
241
279
|
//#endregion
|
|
242
280
|
//#region src/create/resolve-create-options.ts
|
|
@@ -258,7 +296,66 @@ async function readProjectName(options, env) {
|
|
|
258
296
|
}
|
|
259
297
|
});
|
|
260
298
|
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
261
|
-
|
|
299
|
+
const projectName = validateProjectName(answer);
|
|
300
|
+
env.output.log(`Project folder/package: ${projectName}`);
|
|
301
|
+
return projectName;
|
|
302
|
+
}
|
|
303
|
+
async function readAppVariantCustomization(setupType, options, env) {
|
|
304
|
+
if (options.appVariantNamesInput !== void 0 || options.appVariantAccentsInput !== void 0) return normalizeAppVariantCustomization(setupType, options.appVariantNamesInput, options.appVariantAccentsInput);
|
|
305
|
+
if (options.yes || !env.isInteractive) return normalizeAppVariantCustomization(setupType, void 0, void 0);
|
|
306
|
+
const customize = await env.prompts.confirm({
|
|
307
|
+
message: "Customize App Variant names and Accent colors?",
|
|
308
|
+
initialValue: false
|
|
309
|
+
});
|
|
310
|
+
if (customize === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
311
|
+
if (!customize) return normalizeAppVariantCustomization(setupType, void 0, void 0);
|
|
312
|
+
const definition = getGeneratedSetupTypeDefinition(setupType);
|
|
313
|
+
const appVariantNames = [];
|
|
314
|
+
const appVariantAccents = [];
|
|
315
|
+
for (const appVariant of definition.appVariants) {
|
|
316
|
+
const name = await env.prompts.text({
|
|
317
|
+
message: `App Variant name: ${appVariant.defaultName}`,
|
|
318
|
+
placeholder: appVariant.defaultName,
|
|
319
|
+
defaultValue: appVariant.defaultName,
|
|
320
|
+
validate(value) {
|
|
321
|
+
try {
|
|
322
|
+
normalizeAppVariantNameInput(value ?? "");
|
|
323
|
+
return;
|
|
324
|
+
} catch (error) {
|
|
325
|
+
return error instanceof Error ? error.message : String(error);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
if (name === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
330
|
+
const accent = await env.prompts.text({
|
|
331
|
+
message: `App Variant Accent: ${appVariant.defaultName}`,
|
|
332
|
+
placeholder: appVariant.defaultAccent,
|
|
333
|
+
defaultValue: appVariant.defaultAccent,
|
|
334
|
+
validate(value) {
|
|
335
|
+
try {
|
|
336
|
+
normalizeAppVariantAccentInput(value ?? "");
|
|
337
|
+
return;
|
|
338
|
+
} catch (error) {
|
|
339
|
+
return error instanceof Error ? error.message : String(error);
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
if (accent === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
344
|
+
appVariantNames.push(name);
|
|
345
|
+
appVariantAccents.push(accent);
|
|
346
|
+
}
|
|
347
|
+
return normalizeAppVariantCustomization(setupType, appVariantNames.join(","), appVariantAccents.join(","));
|
|
348
|
+
}
|
|
349
|
+
async function readStylingChoice(options, env) {
|
|
350
|
+
if (options.styling !== void 0) return normalizeStylingInput(options.styling);
|
|
351
|
+
if (options.yes || !env.isInteractive) return DEFAULT_STYLING_CHOICE;
|
|
352
|
+
const answer = await env.prompts.select({
|
|
353
|
+
message: "Styling Choice",
|
|
354
|
+
initialValue: DEFAULT_STYLING_CHOICE,
|
|
355
|
+
options: STYLING_PROMPT_CHOICES
|
|
356
|
+
});
|
|
357
|
+
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
358
|
+
return normalizeStylingInput(answer);
|
|
262
359
|
}
|
|
263
360
|
async function readSetupType(options, env) {
|
|
264
361
|
if (options.setup !== void 0 || options.setupType !== void 0) return normalizeSetupInput(options.setup, options.setupType);
|
|
@@ -272,6 +369,35 @@ async function readSetupType(options, env) {
|
|
|
272
369
|
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
273
370
|
return normalizeGeneratedSetupType(answer);
|
|
274
371
|
}
|
|
372
|
+
async function readPackageManager(options, env) {
|
|
373
|
+
const detectedPackageManager = detectPackageManager(env.packageManagerUserAgent);
|
|
374
|
+
if (options.packageManager !== void 0) {
|
|
375
|
+
const packageManager = normalizePackageManagerInput(options.packageManager);
|
|
376
|
+
if (!packageManager) throw new Error("Package manager is required.");
|
|
377
|
+
return packageManager;
|
|
378
|
+
}
|
|
379
|
+
if (options.yes || !env.isInteractive) return detectedPackageManager;
|
|
380
|
+
const answer = await env.prompts.select({
|
|
381
|
+
message: "Package manager",
|
|
382
|
+
initialValue: detectedPackageManager,
|
|
383
|
+
options: SUPPORTED_PACKAGE_MANAGERS.map((packageManager) => ({
|
|
384
|
+
value: packageManager,
|
|
385
|
+
label: packageManager
|
|
386
|
+
}))
|
|
387
|
+
});
|
|
388
|
+
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
389
|
+
return answer;
|
|
390
|
+
}
|
|
391
|
+
async function readEnabledChoice({ explicitValue, message, options, env }) {
|
|
392
|
+
if (explicitValue !== void 0) return explicitValue;
|
|
393
|
+
if (options.yes || !env.isInteractive) return true;
|
|
394
|
+
const answer = await env.prompts.confirm({
|
|
395
|
+
message,
|
|
396
|
+
initialValue: true
|
|
397
|
+
});
|
|
398
|
+
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
399
|
+
return answer;
|
|
400
|
+
}
|
|
275
401
|
async function assertTargetIsSafe(targetDir) {
|
|
276
402
|
if (!await fs.pathExists(targetDir)) return;
|
|
277
403
|
if (!(await fs.stat(targetDir)).isDirectory()) throw new Error(`Generated project target ${targetDir} exists but is not a directory.`);
|
|
@@ -280,16 +406,35 @@ async function assertTargetIsSafe(targetDir) {
|
|
|
280
406
|
async function resolveCreateOptions(options, env) {
|
|
281
407
|
const projectName = await readProjectName(options, env);
|
|
282
408
|
const setupType = await readSetupType(options, env);
|
|
409
|
+
const { appVariantNames, appVariantAccents } = await readAppVariantCustomization(setupType, options, env);
|
|
410
|
+
const stylingChoice = await readStylingChoice(options, env);
|
|
283
411
|
const packageName = options.packageName !== void 0 ? validatePackageName(options.packageName) : derivePackageName(projectName);
|
|
412
|
+
const packageManager = await readPackageManager(options, env);
|
|
413
|
+
const git = await readEnabledChoice({
|
|
414
|
+
explicitValue: options.git,
|
|
415
|
+
message: "Initialize Git?",
|
|
416
|
+
options,
|
|
417
|
+
env
|
|
418
|
+
});
|
|
419
|
+
const install = await readEnabledChoice({
|
|
420
|
+
explicitValue: options.install,
|
|
421
|
+
message: "Install dependencies?",
|
|
422
|
+
options,
|
|
423
|
+
env
|
|
424
|
+
});
|
|
284
425
|
const targetDir = resolve(env.cwd, projectName);
|
|
285
426
|
await assertTargetIsSafe(targetDir);
|
|
286
427
|
return {
|
|
287
428
|
projectName,
|
|
288
429
|
packageName,
|
|
289
430
|
setupType,
|
|
431
|
+
stylingChoice,
|
|
432
|
+
appVariantNames,
|
|
433
|
+
appVariantAccents,
|
|
290
434
|
targetDir,
|
|
291
|
-
|
|
292
|
-
|
|
435
|
+
packageManager,
|
|
436
|
+
install,
|
|
437
|
+
git,
|
|
293
438
|
dryRun: options.dryRun === true
|
|
294
439
|
};
|
|
295
440
|
}
|
|
@@ -305,8 +450,12 @@ async function runCreateFlow(options, env) {
|
|
|
305
450
|
}));
|
|
306
451
|
const tree = generate({
|
|
307
452
|
setupType: resolvedOptions.setupType,
|
|
453
|
+
stylingChoice: resolvedOptions.stylingChoice,
|
|
454
|
+
appVariantAccents: resolvedOptions.appVariantAccents,
|
|
455
|
+
appVariantNames: resolvedOptions.appVariantNames,
|
|
308
456
|
projectName: resolvedOptions.projectName,
|
|
309
|
-
packageName: resolvedOptions.packageName
|
|
457
|
+
packageName: resolvedOptions.packageName,
|
|
458
|
+
packageManager: resolvedOptions.packageManager
|
|
310
459
|
});
|
|
311
460
|
if (resolvedOptions.dryRun) {
|
|
312
461
|
await preflightWriteProject({
|
|
@@ -321,6 +470,10 @@ async function runCreateFlow(options, env) {
|
|
|
321
470
|
projectName: resolvedOptions.projectName,
|
|
322
471
|
packageName: resolvedOptions.packageName,
|
|
323
472
|
setupType: resolvedOptions.setupType,
|
|
473
|
+
stylingChoice: resolvedOptions.stylingChoice,
|
|
474
|
+
appVariantAccents: resolvedOptions.appVariantAccents,
|
|
475
|
+
appVariantNames: resolvedOptions.appVariantNames,
|
|
476
|
+
packageManager: resolvedOptions.packageManager,
|
|
324
477
|
installed: false,
|
|
325
478
|
installFailed: false,
|
|
326
479
|
gitInitialized: false,
|
|
@@ -333,8 +486,7 @@ async function runCreateFlow(options, env) {
|
|
|
333
486
|
}
|
|
334
487
|
const gitProbeCwd = await fs.pathExists(resolvedOptions.targetDir) ? resolvedOptions.targetDir : env.cwd;
|
|
335
488
|
const gitSetup = await prepareInitialGitSetup({
|
|
336
|
-
|
|
337
|
-
env,
|
|
489
|
+
enabled: resolvedOptions.git,
|
|
338
490
|
runCommand,
|
|
339
491
|
probeDir: gitProbeCwd
|
|
340
492
|
});
|
|
@@ -346,8 +498,8 @@ async function runCreateFlow(options, env) {
|
|
|
346
498
|
let installed = false;
|
|
347
499
|
let installFailed = false;
|
|
348
500
|
if (resolvedOptions.install) {
|
|
349
|
-
env.output.log(
|
|
350
|
-
const installResult = await runCommand(
|
|
501
|
+
env.output.log(`Installing dependencies with ${resolvedOptions.packageManager}...`);
|
|
502
|
+
const installResult = await runCommand(resolvedOptions.packageManager, ["install"], writeResult.targetDir, { stdio: "ignore" });
|
|
351
503
|
installed = installResult.ok;
|
|
352
504
|
installFailed = !installResult.ok;
|
|
353
505
|
}
|
|
@@ -358,6 +510,10 @@ async function runCreateFlow(options, env) {
|
|
|
358
510
|
projectName: resolvedOptions.projectName,
|
|
359
511
|
packageName: resolvedOptions.packageName,
|
|
360
512
|
setupType: resolvedOptions.setupType,
|
|
513
|
+
stylingChoice: resolvedOptions.stylingChoice,
|
|
514
|
+
appVariantAccents: resolvedOptions.appVariantAccents,
|
|
515
|
+
appVariantNames: resolvedOptions.appVariantNames,
|
|
516
|
+
packageManager: resolvedOptions.packageManager,
|
|
361
517
|
installed,
|
|
362
518
|
installFailed,
|
|
363
519
|
gitInitialized: gitResult.gitInitialized,
|
|
@@ -376,15 +532,19 @@ function normalizeCommanderOptions(options) {
|
|
|
376
532
|
packageName: options.packageName,
|
|
377
533
|
setup: options.setup,
|
|
378
534
|
setupType: options.setupType,
|
|
535
|
+
styling: options.styling,
|
|
536
|
+
appVariantNamesInput: options.variantNames,
|
|
537
|
+
appVariantAccentsInput: options.variantAccents,
|
|
538
|
+
packageManager: options.packageManager,
|
|
379
539
|
yes: options.yes,
|
|
380
540
|
install: options.install,
|
|
381
|
-
git:
|
|
541
|
+
git: options.git,
|
|
382
542
|
dryRun: options.dryRun
|
|
383
543
|
};
|
|
384
544
|
}
|
|
385
545
|
function createProgram(env) {
|
|
386
546
|
const program = new Command();
|
|
387
|
-
program.name("tenkit").description("Create a generated Tenkit Expo project.").version(CLI_VERSION).allowExcessArguments(false).option("--name <name>", `project folder name, defaults to ${DEFAULT_PROJECT_NAME} with --yes`).option("--package-name <name>", "generated package.json name override").option("-s, --setup <setup>", `public Setup slug: ${supportedSetupValues().join(", ")}`).option("--setup-type <setupType>", "canonical Setup Type ID or public Setup slug").option("--yes", "skip prompts and accept defaults").option("--no-install", "skip dependency installation").option("--git
|
|
547
|
+
program.name("tenkit").description("Create a generated Tenkit Expo project.").version(CLI_VERSION).allowExcessArguments(false).option("--name <name>", `project folder name, defaults to ${DEFAULT_PROJECT_NAME} with --yes`).option("--package-name <name>", "generated package.json name override").option("-s, --setup <setup>", `public Setup slug: ${supportedSetupValues().join(", ")}`).option("--setup-type <setupType>", "canonical Setup Type ID or public Setup slug").option("--styling <styling>", `Styling Choice: ${supportedStylingValues().join(", ")}`).option("--variant-names <names>", "ordered comma-separated App Variant names").option("--variant-accents <colors>", "ordered comma-separated App Variant Accent colors").option("--package-manager <manager>", `install and generated command package manager: ${SUPPORTED_PACKAGE_MANAGERS.join(", ")}`).option("--yes", "skip prompts and accept defaults").option("--install", "install dependencies").option("--no-install", "skip dependency installation").option("--git", "initialize Git and create Initial commit").option("--no-git", "skip git initialization").option("--dry-run", "validate options and print the create plan without writing files").configureOutput({
|
|
388
548
|
writeOut: (message) => env.output.log(message.trimEnd()),
|
|
389
549
|
writeErr: (message) => env.output.error(message.trimEnd())
|
|
390
550
|
}).exitOverride().showHelpAfterError().action(async (options) => {
|
|
@@ -399,23 +559,28 @@ function createProgram(env) {
|
|
|
399
559
|
function createPromptAdapter() {
|
|
400
560
|
return {
|
|
401
561
|
async text(options) {
|
|
562
|
+
const { defaultValue, validate, ...promptOptions } = options;
|
|
402
563
|
const answer = await text({
|
|
403
|
-
...
|
|
564
|
+
...promptOptions,
|
|
404
565
|
validate(value) {
|
|
405
|
-
return
|
|
566
|
+
return validate(value === "" || value === void 0 ? defaultValue : value);
|
|
406
567
|
}
|
|
407
568
|
});
|
|
408
|
-
return isCancel(answer) ? PROMPT_CANCELLED : String(answer);
|
|
569
|
+
return isCancel(answer) ? PROMPT_CANCELLED : answer === "" || answer === void 0 ? defaultValue : String(answer);
|
|
409
570
|
},
|
|
410
571
|
async select(options) {
|
|
411
572
|
const answer = await select({
|
|
412
|
-
|
|
573
|
+
message: options.message,
|
|
574
|
+
initialValue: options.initialValue,
|
|
413
575
|
options: options.options.map((option) => ({
|
|
414
576
|
value: option.value,
|
|
415
577
|
label: option.label
|
|
416
578
|
}))
|
|
417
579
|
});
|
|
418
|
-
|
|
580
|
+
if (isCancel(answer)) return PROMPT_CANCELLED;
|
|
581
|
+
const selectedOption = options.options.find((option) => option.value === answer);
|
|
582
|
+
if (!selectedOption) throw new Error(`Prompt returned unsupported selection ${JSON.stringify(answer)}.`);
|
|
583
|
+
return selectedOption.value;
|
|
419
584
|
},
|
|
420
585
|
async confirm(options) {
|
|
421
586
|
const answer = await confirm(options);
|
|
@@ -439,7 +604,7 @@ async function main(argv = process.argv.slice(2), io = process) {
|
|
|
439
604
|
cwd: process.env.INIT_CWD ?? process.cwd(),
|
|
440
605
|
workspaceRoot,
|
|
441
606
|
isInteractive: io.stdin.isTTY === true && io.stdout.isTTY === true,
|
|
442
|
-
|
|
607
|
+
packageManagerUserAgent: process.env.npm_config_user_agent,
|
|
443
608
|
output,
|
|
444
609
|
prompts: createPromptAdapter()
|
|
445
610
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tenkit/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0-next.0",
|
|
4
4
|
"description": "Public Tenkit CLI implementation package.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://github.com/brilliantinsane/tenkit#readme",
|
|
@@ -41,12 +41,13 @@
|
|
|
41
41
|
"commander": "^15.0.0",
|
|
42
42
|
"fs-extra": "^11.3.5",
|
|
43
43
|
"pathe": "^2.0.3",
|
|
44
|
-
"@tenkit/template-generator": "0.
|
|
44
|
+
"@tenkit/template-generator": "0.2.0-next.0"
|
|
45
45
|
},
|
|
46
46
|
"devDependencies": {
|
|
47
47
|
"@types/fs-extra": "^11.0.4",
|
|
48
48
|
"@types/node": "^25.9.3",
|
|
49
49
|
"tsdown": "^0.22.3",
|
|
50
|
+
"tsx": "^4.22.4",
|
|
50
51
|
"typescript": "~6.0.3",
|
|
51
52
|
"vitest": "^4.1.9"
|
|
52
53
|
},
|
|
@@ -61,6 +62,7 @@
|
|
|
61
62
|
"build": "pnpm -F @tenkit/template-generator build && tsdown src/index.ts --format esm --dts",
|
|
62
63
|
"test": "vitest run",
|
|
63
64
|
"typecheck": "tsc --noEmit --pretty false",
|
|
65
|
+
"verify:matrix": "pnpm -F @tenkit/template-generator build && tsx scripts/verify-generation-matrix.ts",
|
|
64
66
|
"pack:dry-run": "pnpm pack --dry-run"
|
|
65
67
|
}
|
|
66
68
|
}
|