@tenkit/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +6 -0
- package/dist/index.d.mts +9 -0
- package/dist/index.mjs +467 -0
- package/package.json +61 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Tenkit
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
type CliIo = {
|
|
3
|
+
stdout: Pick<NodeJS.WriteStream, 'write' | 'isTTY'>;
|
|
4
|
+
stderr: Pick<NodeJS.WriteStream, 'write'>;
|
|
5
|
+
stdin: Pick<NodeJS.ReadStream, 'isTTY'>;
|
|
6
|
+
};
|
|
7
|
+
declare function main(argv?: string[], io?: CliIo): Promise<number>;
|
|
8
|
+
//#endregion
|
|
9
|
+
export { main };
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import fs from "fs-extra";
|
|
4
|
+
import { dirname, resolve } from "pathe";
|
|
5
|
+
import { cancel, confirm, intro, isCancel, outro, select, text } from "@clack/prompts";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { SUPPORTED_PUBLIC_SETUP_SLUGS, formatSupportedGeneratedSetupTypes, generateProject, normalizeGeneratedSetupType, preflightWriteProject, writeProject } from "@tenkit/template-generator";
|
|
8
|
+
import { spawn } from "node:child_process";
|
|
9
|
+
//#region src/adapters/workspace.ts
|
|
10
|
+
function isPackageJsonShape(value) {
|
|
11
|
+
return typeof value === "object" && value !== null;
|
|
12
|
+
}
|
|
13
|
+
async function findTenkitWorkspaceRoot(startUrl) {
|
|
14
|
+
let current = dirname(fileURLToPath(startUrl));
|
|
15
|
+
while (true) {
|
|
16
|
+
const packageJsonPath = resolve(current, "package.json");
|
|
17
|
+
if (await fs.pathExists(packageJsonPath)) {
|
|
18
|
+
const packageJson = await fs.readJson(packageJsonPath);
|
|
19
|
+
if (isPackageJsonShape(packageJson) && packageJson.name === "tenkit-workspace") return current;
|
|
20
|
+
}
|
|
21
|
+
const parent = dirname(current);
|
|
22
|
+
if (parent === current) return;
|
|
23
|
+
current = parent;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function isDirectCliRun(entryUrl, argvEntry) {
|
|
27
|
+
if (!argvEntry) return false;
|
|
28
|
+
return resolveRealPath(fileURLToPath(entryUrl)) === resolveRealPath(argvEntry);
|
|
29
|
+
}
|
|
30
|
+
function resolveRealPath(path) {
|
|
31
|
+
try {
|
|
32
|
+
return fs.realpathSync(path);
|
|
33
|
+
} catch {
|
|
34
|
+
return resolve(path);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
//#endregion
|
|
38
|
+
//#region src/constants.ts
|
|
39
|
+
const CLI_VERSION = "0.1.0";
|
|
40
|
+
const DEFAULT_PROJECT_NAME = "tenkit-app";
|
|
41
|
+
const DEFAULT_PUBLIC_SETUP_SLUG = "white-label";
|
|
42
|
+
const PROMPT_CANCELLED = Symbol("prompt-cancelled");
|
|
43
|
+
const SETUP_PROMPT_CHOICES = [
|
|
44
|
+
{
|
|
45
|
+
value: "white-label",
|
|
46
|
+
label: "White Label Apps"
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
value: "runtime-tenants",
|
|
50
|
+
label: "Runtime Tenant App"
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
value: "generic-standalone",
|
|
54
|
+
label: "Generic + Standalone Apps"
|
|
55
|
+
}
|
|
56
|
+
];
|
|
57
|
+
function supportedSetupValues() {
|
|
58
|
+
return SUPPORTED_PUBLIC_SETUP_SLUGS;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
//#region src/adapters/command-runner.ts
|
|
62
|
+
function defaultRunCommand(command, args, cwd, options = {}) {
|
|
63
|
+
return new Promise((resolveCommand) => {
|
|
64
|
+
const child = spawn(command, [...args], {
|
|
65
|
+
cwd,
|
|
66
|
+
stdio: options.stdio ?? "inherit"
|
|
67
|
+
});
|
|
68
|
+
child.on("error", () => {
|
|
69
|
+
resolveCommand({
|
|
70
|
+
ok: false,
|
|
71
|
+
code: 1
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
child.on("close", (code) => {
|
|
75
|
+
resolveCommand({
|
|
76
|
+
ok: code === 0,
|
|
77
|
+
code: code ?? 1
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//#endregion
|
|
83
|
+
//#region src/errors.ts
|
|
84
|
+
var CreateFlowCancelledError = class extends Error {
|
|
85
|
+
constructor() {
|
|
86
|
+
super("Create cancelled.");
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
//#endregion
|
|
90
|
+
//#region src/adapters/git.ts
|
|
91
|
+
async function isGitAvailable(runCommand, cwd) {
|
|
92
|
+
return (await runCommand("git", ["--version"], cwd, { stdio: "ignore" })).ok;
|
|
93
|
+
}
|
|
94
|
+
async function isInsideGitWorktree(runCommand, cwd) {
|
|
95
|
+
return (await runCommand("git", ["rev-parse", "--is-inside-work-tree"], cwd, { stdio: "ignore" })).ok;
|
|
96
|
+
}
|
|
97
|
+
async function resolveGitMode({ explicitGitMode, env, runCommand, targetDir }) {
|
|
98
|
+
if (explicitGitMode === false || explicitGitMode === "none") return {
|
|
99
|
+
mode: false,
|
|
100
|
+
skippedReason: "disabled"
|
|
101
|
+
};
|
|
102
|
+
if (!await isGitAvailable(runCommand, targetDir)) return {
|
|
103
|
+
mode: false,
|
|
104
|
+
skippedReason: "git-unavailable"
|
|
105
|
+
};
|
|
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" };
|
|
123
|
+
}
|
|
124
|
+
async function prepareInitialGitSetup({ explicitGitMode, env, runCommand, probeDir }) {
|
|
125
|
+
const gitPlan = await resolveGitMode({
|
|
126
|
+
explicitGitMode,
|
|
127
|
+
env,
|
|
128
|
+
runCommand,
|
|
129
|
+
targetDir: probeDir
|
|
130
|
+
});
|
|
131
|
+
return { async run(targetDir) {
|
|
132
|
+
if (!gitPlan.mode) return {
|
|
133
|
+
gitInitialized: false,
|
|
134
|
+
gitCommitted: false,
|
|
135
|
+
gitSkippedReason: gitPlan.skippedReason,
|
|
136
|
+
gitFailed: false
|
|
137
|
+
};
|
|
138
|
+
const initResult = await runCommand("git", ["init"], targetDir);
|
|
139
|
+
const gitInitialized = initResult.ok;
|
|
140
|
+
if (!initResult.ok) return {
|
|
141
|
+
gitInitialized,
|
|
142
|
+
gitCommitted: false,
|
|
143
|
+
gitFailed: true
|
|
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", [
|
|
151
|
+
"commit",
|
|
152
|
+
"-m",
|
|
153
|
+
"Initial commit"
|
|
154
|
+
], targetDir) : {
|
|
155
|
+
ok: false,
|
|
156
|
+
code: 1
|
|
157
|
+
};
|
|
158
|
+
return {
|
|
159
|
+
gitInitialized,
|
|
160
|
+
gitCommitted: commitResult.ok,
|
|
161
|
+
gitFailed: !commitResult.ok
|
|
162
|
+
};
|
|
163
|
+
} };
|
|
164
|
+
}
|
|
165
|
+
//#endregion
|
|
166
|
+
//#region src/create/create-messages.ts
|
|
167
|
+
function logFinalOutput(result, output) {
|
|
168
|
+
const projectShellArg = formatShellArg(result.projectName);
|
|
169
|
+
output.log("");
|
|
170
|
+
output.log(result.status === "dry-run" ? "Tenkit create plan is valid." : "Your Tenkit project is ready.");
|
|
171
|
+
output.log("");
|
|
172
|
+
output.log("Next steps:");
|
|
173
|
+
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");
|
|
178
|
+
if (result.installFailed) {
|
|
179
|
+
output.log("");
|
|
180
|
+
output.log("Dependency installation failed. Run pnpm install in the generated project.");
|
|
181
|
+
}
|
|
182
|
+
if (result.gitSkippedReason === "git-unavailable") {
|
|
183
|
+
output.log("");
|
|
184
|
+
output.log("Git was not available. Run git init when ready.");
|
|
185
|
+
} else if (result.gitSkippedReason === "nested-worktree") {
|
|
186
|
+
output.log("");
|
|
187
|
+
output.log("Git initialization was skipped because the project is inside an existing git worktree.");
|
|
188
|
+
} else if (result.gitFailed) {
|
|
189
|
+
output.log("");
|
|
190
|
+
output.log("Git setup did not complete. Run git init, git add --all, and git commit -m \"Initial commit\" when ready.");
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function formatShellArg(value) {
|
|
194
|
+
if (/^[A-Za-z0-9._/-]+$/.test(value)) return value;
|
|
195
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
196
|
+
}
|
|
197
|
+
//#endregion
|
|
198
|
+
//#region src/create/validation.ts
|
|
199
|
+
function isPathSeparatorPresent(value) {
|
|
200
|
+
return value.includes("/") || value.includes("\\");
|
|
201
|
+
}
|
|
202
|
+
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, "-");
|
|
212
|
+
}
|
|
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;
|
|
222
|
+
}
|
|
223
|
+
function derivePackageName(projectName) {
|
|
224
|
+
return validatePackageName(slugifyPackageName(projectName));
|
|
225
|
+
}
|
|
226
|
+
function normalizeSetupInput(setup, setupType) {
|
|
227
|
+
if (setup !== void 0 && setupType !== void 0 && setup !== setupType) throw new Error("Use either --setup or --setup-type, not both with different values.");
|
|
228
|
+
const selectedSetup = setup ?? setupType ?? "white-label";
|
|
229
|
+
try {
|
|
230
|
+
return normalizeGeneratedSetupType(selectedSetup);
|
|
231
|
+
} catch {
|
|
232
|
+
throw new Error(`Unsupported Setup Type ${JSON.stringify(selectedSetup)}. Expected ${formatSupportedGeneratedSetupTypes()}.`);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
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.");
|
|
240
|
+
}
|
|
241
|
+
//#endregion
|
|
242
|
+
//#region src/create/resolve-create-options.ts
|
|
243
|
+
async function readProjectName(options, env) {
|
|
244
|
+
if (options.name !== void 0) return validateProjectName(options.name);
|
|
245
|
+
if (options.yes) return DEFAULT_PROJECT_NAME;
|
|
246
|
+
if (!env.isInteractive) throw new Error("Missing --name. Pass --name or use --yes to accept the default.");
|
|
247
|
+
const answer = await env.prompts.text({
|
|
248
|
+
message: "Project name",
|
|
249
|
+
initialValue: DEFAULT_PROJECT_NAME,
|
|
250
|
+
validate(value) {
|
|
251
|
+
try {
|
|
252
|
+
validateProjectName(value ?? "");
|
|
253
|
+
return;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
return error instanceof Error ? error.message : String(error);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
});
|
|
259
|
+
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
260
|
+
return validateProjectName(answer);
|
|
261
|
+
}
|
|
262
|
+
async function readSetupType(options, env) {
|
|
263
|
+
if (options.setup !== void 0 || options.setupType !== void 0) return normalizeSetupInput(options.setup, options.setupType);
|
|
264
|
+
if (options.yes) return normalizeGeneratedSetupType(DEFAULT_PUBLIC_SETUP_SLUG);
|
|
265
|
+
if (!env.isInteractive) throw new Error("Missing --setup. Pass --setup or use --yes to accept the default.");
|
|
266
|
+
const answer = await env.prompts.select({
|
|
267
|
+
message: "Setup Type",
|
|
268
|
+
initialValue: DEFAULT_PUBLIC_SETUP_SLUG,
|
|
269
|
+
options: SETUP_PROMPT_CHOICES
|
|
270
|
+
});
|
|
271
|
+
if (answer === PROMPT_CANCELLED) throw new CreateFlowCancelledError();
|
|
272
|
+
return normalizeGeneratedSetupType(answer);
|
|
273
|
+
}
|
|
274
|
+
async function assertTargetIsSafe(targetDir) {
|
|
275
|
+
if (!await fs.pathExists(targetDir)) return;
|
|
276
|
+
if (!(await fs.stat(targetDir)).isDirectory()) throw new Error(`Generated project target ${targetDir} exists but is not a directory.`);
|
|
277
|
+
if ((await fs.readdir(targetDir)).length > 0) throw new Error(`Generated project target ${targetDir} already exists and is not empty.`);
|
|
278
|
+
}
|
|
279
|
+
async function resolveCreateOptions(options, env) {
|
|
280
|
+
const projectName = await readProjectName(options, env);
|
|
281
|
+
const setupType = await readSetupType(options, env);
|
|
282
|
+
const packageName = options.packageName !== void 0 ? validatePackageName(options.packageName) : derivePackageName(projectName);
|
|
283
|
+
const targetDir = resolve(env.cwd, projectName);
|
|
284
|
+
await assertTargetIsSafe(targetDir);
|
|
285
|
+
return {
|
|
286
|
+
projectName,
|
|
287
|
+
packageName,
|
|
288
|
+
setupType,
|
|
289
|
+
targetDir,
|
|
290
|
+
install: options.install !== false,
|
|
291
|
+
git: parseGitMode(options.git),
|
|
292
|
+
dryRun: options.dryRun === true
|
|
293
|
+
};
|
|
294
|
+
}
|
|
295
|
+
//#endregion
|
|
296
|
+
//#region src/create/run-create.ts
|
|
297
|
+
async function runCreateFlow(options, env) {
|
|
298
|
+
const resolvedOptions = await resolveCreateOptions(options, env);
|
|
299
|
+
const runCommand = env.runCommand ?? defaultRunCommand;
|
|
300
|
+
const generate = env.generate ?? generateProject;
|
|
301
|
+
const write = env.write ?? ((writeOptions) => writeProject({
|
|
302
|
+
...writeOptions,
|
|
303
|
+
overwrite: "never"
|
|
304
|
+
}));
|
|
305
|
+
const tree = generate({
|
|
306
|
+
setupType: resolvedOptions.setupType,
|
|
307
|
+
projectName: resolvedOptions.projectName,
|
|
308
|
+
packageName: resolvedOptions.packageName
|
|
309
|
+
});
|
|
310
|
+
if (resolvedOptions.dryRun) {
|
|
311
|
+
await preflightWriteProject({
|
|
312
|
+
targetDir: resolvedOptions.targetDir,
|
|
313
|
+
tree,
|
|
314
|
+
overwrite: "never",
|
|
315
|
+
forbiddenTargetRoots: env.workspaceRoot ? [env.workspaceRoot] : []
|
|
316
|
+
});
|
|
317
|
+
const result = {
|
|
318
|
+
status: "dry-run",
|
|
319
|
+
targetDir: resolvedOptions.targetDir,
|
|
320
|
+
projectName: resolvedOptions.projectName,
|
|
321
|
+
packageName: resolvedOptions.packageName,
|
|
322
|
+
setupType: resolvedOptions.setupType,
|
|
323
|
+
installed: false,
|
|
324
|
+
installFailed: false,
|
|
325
|
+
gitInitialized: false,
|
|
326
|
+
gitCommitted: false,
|
|
327
|
+
gitSkippedReason: "dry-run",
|
|
328
|
+
gitFailed: false
|
|
329
|
+
};
|
|
330
|
+
logFinalOutput(result, env.output);
|
|
331
|
+
return result;
|
|
332
|
+
}
|
|
333
|
+
const gitProbeCwd = await fs.pathExists(resolvedOptions.targetDir) ? resolvedOptions.targetDir : env.cwd;
|
|
334
|
+
const gitSetup = await prepareInitialGitSetup({
|
|
335
|
+
explicitGitMode: resolvedOptions.git,
|
|
336
|
+
env,
|
|
337
|
+
runCommand,
|
|
338
|
+
probeDir: gitProbeCwd
|
|
339
|
+
});
|
|
340
|
+
const writeResult = await write({
|
|
341
|
+
targetDir: resolvedOptions.targetDir,
|
|
342
|
+
tree,
|
|
343
|
+
forbiddenTargetRoots: env.workspaceRoot ? [env.workspaceRoot] : []
|
|
344
|
+
});
|
|
345
|
+
let installed = false;
|
|
346
|
+
let installFailed = false;
|
|
347
|
+
if (resolvedOptions.install) {
|
|
348
|
+
env.output.log("Installing dependencies with pnpm...");
|
|
349
|
+
const installResult = await runCommand("pnpm", ["install"], writeResult.targetDir);
|
|
350
|
+
installed = installResult.ok;
|
|
351
|
+
installFailed = !installResult.ok;
|
|
352
|
+
}
|
|
353
|
+
const gitResult = await gitSetup.run(writeResult.targetDir);
|
|
354
|
+
const result = {
|
|
355
|
+
status: "created",
|
|
356
|
+
targetDir: writeResult.targetDir,
|
|
357
|
+
projectName: resolvedOptions.projectName,
|
|
358
|
+
packageName: resolvedOptions.packageName,
|
|
359
|
+
setupType: resolvedOptions.setupType,
|
|
360
|
+
installed,
|
|
361
|
+
installFailed,
|
|
362
|
+
gitInitialized: gitResult.gitInitialized,
|
|
363
|
+
gitCommitted: gitResult.gitCommitted,
|
|
364
|
+
gitSkippedReason: gitResult.gitSkippedReason,
|
|
365
|
+
gitFailed: gitResult.gitFailed
|
|
366
|
+
};
|
|
367
|
+
logFinalOutput(result, env.output);
|
|
368
|
+
return result;
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
//#region src/commands/create.ts
|
|
372
|
+
function normalizeCommanderOptions(options) {
|
|
373
|
+
return {
|
|
374
|
+
name: options.name,
|
|
375
|
+
packageName: options.packageName,
|
|
376
|
+
setup: options.setup,
|
|
377
|
+
setupType: options.setupType,
|
|
378
|
+
yes: options.yes,
|
|
379
|
+
install: options.install,
|
|
380
|
+
git: parseGitMode(options.git),
|
|
381
|
+
dryRun: options.dryRun
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
function createProgram(env) {
|
|
385
|
+
const program = new Command();
|
|
386
|
+
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({
|
|
387
|
+
writeOut: (message) => env.output.log(message.trimEnd()),
|
|
388
|
+
writeErr: (message) => env.output.error(message.trimEnd())
|
|
389
|
+
}).exitOverride().showHelpAfterError().action(async (options) => {
|
|
390
|
+
intro("Create Tenkit");
|
|
391
|
+
await runCreateFlow(normalizeCommanderOptions(options), env);
|
|
392
|
+
outro("Done");
|
|
393
|
+
});
|
|
394
|
+
return program;
|
|
395
|
+
}
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/prompts/create-prompts.ts
|
|
398
|
+
function createPromptAdapter() {
|
|
399
|
+
return {
|
|
400
|
+
async text(options) {
|
|
401
|
+
const answer = await text({
|
|
402
|
+
...options,
|
|
403
|
+
validate(value) {
|
|
404
|
+
return options.validate(value);
|
|
405
|
+
}
|
|
406
|
+
});
|
|
407
|
+
return isCancel(answer) ? PROMPT_CANCELLED : String(answer);
|
|
408
|
+
},
|
|
409
|
+
async select(options) {
|
|
410
|
+
const answer = await select({
|
|
411
|
+
...options,
|
|
412
|
+
options: options.options.map((option) => ({
|
|
413
|
+
value: option.value,
|
|
414
|
+
label: option.label
|
|
415
|
+
}))
|
|
416
|
+
});
|
|
417
|
+
return isCancel(answer) ? PROMPT_CANCELLED : answer;
|
|
418
|
+
},
|
|
419
|
+
async confirm(options) {
|
|
420
|
+
const answer = await confirm(options);
|
|
421
|
+
return isCancel(answer) ? PROMPT_CANCELLED : answer;
|
|
422
|
+
}
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
//#endregion
|
|
426
|
+
//#region src/cli.ts
|
|
427
|
+
async function main(argv = process.argv.slice(2), io = process) {
|
|
428
|
+
const output = {
|
|
429
|
+
log(message = "") {
|
|
430
|
+
io.stdout.write(`${message}\n`);
|
|
431
|
+
},
|
|
432
|
+
error(message) {
|
|
433
|
+
io.stderr.write(`${message}\n`);
|
|
434
|
+
}
|
|
435
|
+
};
|
|
436
|
+
const workspaceRoot = await findTenkitWorkspaceRoot(import.meta.url);
|
|
437
|
+
const program = createProgram({
|
|
438
|
+
cwd: process.env.INIT_CWD ?? process.cwd(),
|
|
439
|
+
workspaceRoot,
|
|
440
|
+
isInteractive: io.stdin.isTTY === true && io.stdout.isTTY === true,
|
|
441
|
+
isCi: process.env.CI === "true",
|
|
442
|
+
output,
|
|
443
|
+
prompts: createPromptAdapter()
|
|
444
|
+
});
|
|
445
|
+
try {
|
|
446
|
+
await program.parseAsync(argv, { from: "user" });
|
|
447
|
+
return 0;
|
|
448
|
+
} catch (error) {
|
|
449
|
+
if (error instanceof CreateFlowCancelledError) {
|
|
450
|
+
cancel(error.message);
|
|
451
|
+
return 130;
|
|
452
|
+
}
|
|
453
|
+
if (error && typeof error === "object" && "code" in error && error.code === "commander.helpDisplayed") return 0;
|
|
454
|
+
if (error && typeof error === "object" && "code" in error && error.code === "commander.version") return 0;
|
|
455
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
456
|
+
output.error(message);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
//#region src/index.ts
|
|
462
|
+
if (isDirectCliRun(import.meta.url, process.argv[1])) {
|
|
463
|
+
const exitCode = await main();
|
|
464
|
+
process.exitCode = exitCode;
|
|
465
|
+
}
|
|
466
|
+
//#endregion
|
|
467
|
+
export { main };
|
package/package.json
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tenkit/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Public Tenkit CLI implementation package.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/brilliantinsane/tenkit#readme",
|
|
7
|
+
"bugs": {
|
|
8
|
+
"url": "https://github.com/brilliantinsane/tenkit/issues"
|
|
9
|
+
},
|
|
10
|
+
"repository": {
|
|
11
|
+
"type": "git",
|
|
12
|
+
"url": "git+https://github.com/brilliantinsane/tenkit.git",
|
|
13
|
+
"directory": "packages/cli"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"cli",
|
|
17
|
+
"expo",
|
|
18
|
+
"react-native",
|
|
19
|
+
"tenkit",
|
|
20
|
+
"typescript"
|
|
21
|
+
],
|
|
22
|
+
"type": "module",
|
|
23
|
+
"bin": {
|
|
24
|
+
"tenkit": "./dist/index.mjs"
|
|
25
|
+
},
|
|
26
|
+
"exports": {
|
|
27
|
+
".": "./dist/index.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist",
|
|
31
|
+
"package.json",
|
|
32
|
+
"README.md"
|
|
33
|
+
],
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"@clack/prompts": "^1.6.0",
|
|
36
|
+
"commander": "^15.0.0",
|
|
37
|
+
"fs-extra": "^11.3.5",
|
|
38
|
+
"pathe": "^2.0.3",
|
|
39
|
+
"@tenkit/template-generator": "0.1.0"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/fs-extra": "^11.0.4",
|
|
43
|
+
"@types/node": "^25.9.3",
|
|
44
|
+
"tsdown": "^0.22.3",
|
|
45
|
+
"typescript": "~6.0.3",
|
|
46
|
+
"vitest": "^4.1.9"
|
|
47
|
+
},
|
|
48
|
+
"publishConfig": {
|
|
49
|
+
"access": "public",
|
|
50
|
+
"provenance": true
|
|
51
|
+
},
|
|
52
|
+
"engines": {
|
|
53
|
+
"node": ">=22.14.0"
|
|
54
|
+
},
|
|
55
|
+
"scripts": {
|
|
56
|
+
"build": "pnpm -F @tenkit/template-generator build && tsdown src/index.ts --format esm --dts",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"typecheck": "tsc --noEmit --pretty false",
|
|
59
|
+
"pack:dry-run": "pnpm pack --dry-run"
|
|
60
|
+
}
|
|
61
|
+
}
|