@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.
Files changed (3) hide show
  1. package/README.md +69 -11
  2. package/dist/index.mjs +257 -92
  3. package/package.json +4 -2
package/README.md CHANGED
@@ -1,27 +1,85 @@
1
1
  # @tenkit/cli
2
2
 
3
- Public Tenkit CLI implementation package.
3
+ Public CLI implementation for creating Tenkit Expo projects.
4
4
 
5
- Most users should run Tenkit through the package-manager create entrypoint:
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
- This package owns the real Public CLI implementation behind `create-tenkit`:
12
- parsing, prompts, create-flow orchestration, Template generation, dependency
13
- installation, git convenience policy, and final output.
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
- Tenkit creates Expo and React Native projects for white-label apps,
16
- multi-tenant products, App Variant builds, and Runtime Tenant experiences.
15
+ ## Highlights
17
16
 
18
- ## Direct CLI
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
- The package exposes a `tenkit` binary for direct usage:
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
- For public project creation, prefer `pnpm create tenkit@latest` so the package
27
- manager resolves the create entrypoint correctly.
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.1.1";
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
- stdio: options.stdio ?? "inherit"
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({ explicitGitMode, env, runCommand, targetDir }) {
98
- if (explicitGitMode === false || explicitGitMode === "none") return {
99
- mode: false,
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
- mode: false,
116
+ enabled: false,
104
117
  skippedReason: "git-unavailable"
105
118
  };
106
- if (await isInsideGitWorktree(runCommand, targetDir) && explicitGitMode === void 0) {
107
- if (!env.isInteractive || env.isCi) return {
108
- mode: false,
109
- skippedReason: "nested-worktree"
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({ explicitGitMode, env, runCommand, probeDir }) {
125
+ async function prepareInitialGitSetup({ enabled, runCommand, probeDir }) {
125
126
  const gitPlan = await resolveGitMode({
126
- explicitGitMode,
127
- env,
127
+ enabled,
128
128
  runCommand,
129
129
  targetDir: probeDir
130
130
  });
131
131
  return { async run(targetDir) {
132
- if (!gitPlan.mode) return {
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
- if (gitPlan.mode === "init") return {
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("- pnpm install");
175
- output.log("- pnpm run android");
176
- output.log("- pnpm run ios");
177
- output.log("- pnpm run web");
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("Dependency installation failed. Run pnpm install in the generated project.");
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
- const projectName = value.trim();
204
- if (projectName.length === 0) throw new Error("Project name is required.");
205
- if (projectName === "." || projectName === "..") throw new Error("Project name must be a child folder name.");
206
- if (isPathSeparatorPresent(projectName)) throw new Error("Project name must not contain path separators.");
207
- if (/[\0-\x1F<>:"|?*]/.test(projectName)) throw new Error("Project name contains characters that are unsafe for a project folder.");
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 validatePackageName(value) {
214
- const packageName = value.trim();
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 derivePackageName(projectName) {
224
- return validatePackageName(slugifyPackageName(projectName));
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 parseGitMode(value) {
236
- if (value === false) return false;
237
- if (value === void 0) return;
238
- if (value === "init" || value === "commit" || value === "none") return value;
239
- throw new Error("Git mode must be one of: init, commit, none.");
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
- return validateProjectName(answer);
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
- install: options.install !== false,
292
- git: parseGitMode(options.git),
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
- explicitGitMode: resolvedOptions.git,
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("Installing dependencies with pnpm...");
350
- const installResult = await runCommand("pnpm", ["install"], writeResult.targetDir);
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: parseGitMode(options.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 <mode>", "git behavior: init, commit, none").option("--no-git", "skip git initialization").option("--dry-run", "validate options and print the create plan without writing files").configureOutput({
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
- ...options,
564
+ ...promptOptions,
404
565
  validate(value) {
405
- return options.validate(value);
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
- ...options,
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
- return isCancel(answer) ? PROMPT_CANCELLED : answer;
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
- isCi: process.env.CI === "true",
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.1.1",
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.1.1"
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
  }