@jskit-ai/create-app 0.1.3 → 0.1.7

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 (53) hide show
  1. package/bin/jskit-create-app.js +5 -0
  2. package/package.json +2 -3
  3. package/src/client/index.js +1 -0
  4. package/src/index.js +1 -0
  5. package/src/server/cliEntrypoint.js +25 -0
  6. package/src/server/index.js +632 -0
  7. package/templates/base-shell/.jskit/lock.json +31 -0
  8. package/templates/base-shell/Procfile +1 -0
  9. package/templates/base-shell/README.md +43 -0
  10. package/templates/base-shell/app.scripts.config.mjs +3 -0
  11. package/templates/base-shell/bin/server.js +8 -0
  12. package/templates/base-shell/config/public.js +30 -0
  13. package/templates/base-shell/config/server.js +1 -0
  14. package/templates/base-shell/config/surfaceAccessPolicies.js +12 -0
  15. package/templates/base-shell/eslint.config.mjs +10 -0
  16. package/templates/base-shell/favicon.svg +7 -0
  17. package/templates/base-shell/gitignore +9 -0
  18. package/templates/base-shell/index.html +13 -0
  19. package/templates/base-shell/jsconfig.json +8 -0
  20. package/templates/base-shell/package.json +64 -0
  21. package/templates/base-shell/packages/main/package.descriptor.mjs +55 -0
  22. package/templates/base-shell/packages/main/package.json +12 -0
  23. package/templates/base-shell/packages/main/src/client/index.js +13 -0
  24. package/templates/base-shell/packages/main/src/client/providers/MainClientProvider.js +33 -0
  25. package/templates/base-shell/packages/main/src/server/controllers/index.js +9 -0
  26. package/templates/base-shell/packages/main/src/server/index.js +1 -0
  27. package/templates/base-shell/packages/main/src/server/providers/MainServiceProvider.js +22 -0
  28. package/templates/base-shell/packages/main/src/server/routes/index.js +9 -0
  29. package/templates/base-shell/packages/main/src/server/services/index.js +9 -0
  30. package/templates/base-shell/packages/main/src/server/support/loadAppConfig.js +55 -0
  31. package/templates/base-shell/packages/main/src/shared/index.js +8 -0
  32. package/templates/base-shell/packages/main/src/shared/schemas/index.js +20 -0
  33. package/templates/base-shell/scripts/dev-bootstrap-jskit.sh +110 -0
  34. package/templates/base-shell/scripts/just_run_verde +37 -0
  35. package/templates/base-shell/scripts/link-local-jskit-packages.sh +90 -0
  36. package/templates/base-shell/scripts/update-jskit-packages.sh +73 -0
  37. package/templates/base-shell/scripts/verdaccio/config.yaml +26 -0
  38. package/templates/base-shell/scripts/verdaccio-reset-and-publish-packages.sh +314 -0
  39. package/templates/base-shell/server/lib/runtimeEnv.js +45 -0
  40. package/templates/base-shell/server/lib/surfaceRuntime.js +10 -0
  41. package/templates/base-shell/server.js +69 -0
  42. package/templates/base-shell/src/App.vue +13 -0
  43. package/templates/base-shell/src/main.js +90 -0
  44. package/templates/base-shell/src/pages/console/index.vue +12 -0
  45. package/templates/base-shell/src/pages/console.vue +13 -0
  46. package/templates/base-shell/src/pages/home/index.vue +12 -0
  47. package/templates/base-shell/src/pages/home.vue +13 -0
  48. package/templates/base-shell/src/views/NotFound.vue +13 -0
  49. package/templates/base-shell/tests/client/smoke.vitest.js +7 -0
  50. package/templates/base-shell/tests/server/minimalShell.validator.test.js +134 -0
  51. package/templates/base-shell/tests/server/smoke.test.js +16 -0
  52. package/templates/base-shell/vite.config.mjs +64 -0
  53. package/templates/base-shell/vite.shared.mjs +59 -0
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import { runCliEntrypoint } from "../src/server/cliEntrypoint.js";
3
+ import { runCli } from "../src/server/index.js";
4
+
5
+ await runCliEntrypoint(runCli);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/create-app",
3
- "version": "0.1.3",
3
+ "version": "0.1.7",
4
4
  "description": "Scaffold minimal JSKIT app shells.",
5
5
  "type": "module",
6
6
  "files": [
@@ -25,8 +25,7 @@
25
25
  "node": "20.x"
26
26
  },
27
27
  "publishConfig": {
28
- "access": "public",
29
- "registry": "https://registry.npmjs.org/"
28
+ "access": "public"
30
29
  },
31
30
  "repository": {
32
31
  "type": "git",
@@ -0,0 +1 @@
1
+ export {};
package/src/index.js ADDED
@@ -0,0 +1 @@
1
+ throw new Error("Use explicit entrypoint import for @jskit-ai/create-app: /server or /client.");
@@ -0,0 +1,25 @@
1
+ import process from "node:process";
2
+
3
+ function shellQuote(value) {
4
+ const raw = String(value ?? "");
5
+ if (!raw) {
6
+ return "''";
7
+ }
8
+ if (/^[A-Za-z0-9_./:=+,-]+$/.test(raw)) {
9
+ return raw;
10
+ }
11
+ return `'${raw.replace(/'/g, "'\\''")}'`;
12
+ }
13
+
14
+ async function runCliEntrypoint(runCli, argv = process.argv.slice(2)) {
15
+ if (typeof runCli !== "function") {
16
+ throw new TypeError("runCliEntrypoint requires a runCli function");
17
+ }
18
+
19
+ const exitCode = await runCli(argv);
20
+ if (exitCode !== 0) {
21
+ process.exit(exitCode);
22
+ }
23
+ }
24
+
25
+ export { shellQuote, runCliEntrypoint };
@@ -0,0 +1,632 @@
1
+ import { chmod, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import process from "node:process";
4
+ import { createInterface } from "node:readline/promises";
5
+ import { fileURLToPath } from "node:url";
6
+ import { shellQuote } from "./cliEntrypoint.js";
7
+
8
+ const DEFAULT_TEMPLATE = "base-shell";
9
+ const DEFAULT_INITIAL_BUNDLES = "none";
10
+ const INITIAL_BUNDLE_PRESETS = new Set(["none", "auth"]);
11
+ const TENANCY_MODES = new Set(["none", "personal", "workspace"]);
12
+ const ALLOWED_EXISTING_TARGET_ENTRIES = new Set([".git"]);
13
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
14
+ const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, "templates");
15
+
16
+ function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
17
+ const error = new Error(String(message || "Command failed."));
18
+ error.showUsage = Boolean(showUsage);
19
+ error.exitCode = Number.isInteger(exitCode) ? exitCode : 1;
20
+ return error;
21
+ }
22
+
23
+ function toAppTitle(appName) {
24
+ const words = String(appName)
25
+ .trim()
26
+ .split(/[-_]+/)
27
+ .filter(Boolean)
28
+ .map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`);
29
+
30
+ return words.length > 0 ? words.join(" ") : "App";
31
+ }
32
+
33
+ function normalizeInitialBundlesPreset(value, { showUsage = true } = {}) {
34
+ const normalized = String(value || DEFAULT_INITIAL_BUNDLES).trim().toLowerCase();
35
+ if (INITIAL_BUNDLE_PRESETS.has(normalized)) {
36
+ return normalized;
37
+ }
38
+
39
+ throw createCliError(
40
+ `Invalid --initial-bundles value "${value}". Expected one of: none, auth.`,
41
+ { showUsage }
42
+ );
43
+ }
44
+
45
+ function normalizeTenancyMode(value, { showUsage = true } = {}) {
46
+ const normalized = String(value || "").trim().toLowerCase();
47
+ if (TENANCY_MODES.has(normalized)) {
48
+ return normalized;
49
+ }
50
+
51
+ throw createCliError(
52
+ `Invalid --tenancy-mode value "${value}". Expected one of: none, personal, workspace.`,
53
+ { showUsage }
54
+ );
55
+ }
56
+
57
+ function buildInitialBundleCommands(initialBundles) {
58
+ const normalizedPreset = normalizeInitialBundlesPreset(initialBundles, { showUsage: false });
59
+
60
+ const commands = [];
61
+ if (normalizedPreset === "auth") {
62
+ commands.push("npx jskit add auth-base --no-install");
63
+ }
64
+
65
+ return commands;
66
+ }
67
+
68
+ function validateAppName(appName, { showUsage = true } = {}) {
69
+ if (!appName || typeof appName !== "string") {
70
+ throw createCliError("Missing app name.", { showUsage });
71
+ }
72
+
73
+ if (!/^[a-z0-9][a-z0-9-]*$/.test(appName)) {
74
+ throw createCliError(
75
+ `Invalid app name "${appName}". Use lowercase letters, numbers, and dashes only.`,
76
+ { showUsage }
77
+ );
78
+ }
79
+ }
80
+
81
+ function parseOptionWithValue(argv, index, optionName) {
82
+ const nextValue = argv[index + 1];
83
+ if (!nextValue || nextValue.startsWith("-")) {
84
+ throw createCliError(`Option ${optionName} requires a value.`, {
85
+ showUsage: true
86
+ });
87
+ }
88
+ return {
89
+ value: nextValue,
90
+ nextIndex: index + 1
91
+ };
92
+ }
93
+
94
+ function parseCliArgs(argv) {
95
+ const args = Array.isArray(argv) ? argv : [];
96
+ const options = {
97
+ appName: null,
98
+ appTitle: null,
99
+ template: DEFAULT_TEMPLATE,
100
+ target: null,
101
+ initialBundles: DEFAULT_INITIAL_BUNDLES,
102
+ tenancyMode: null,
103
+ force: false,
104
+ dryRun: false,
105
+ help: false,
106
+ interactive: false
107
+ };
108
+
109
+ const positionalArgs = [];
110
+
111
+ for (let index = 0; index < args.length; index += 1) {
112
+ const arg = String(args[index] || "");
113
+
114
+ if (arg === "--help" || arg === "-h") {
115
+ options.help = true;
116
+ continue;
117
+ }
118
+
119
+ if (arg === "--force") {
120
+ options.force = true;
121
+ continue;
122
+ }
123
+
124
+ if (arg === "--dry-run") {
125
+ options.dryRun = true;
126
+ continue;
127
+ }
128
+
129
+ if (arg === "--interactive") {
130
+ options.interactive = true;
131
+ continue;
132
+ }
133
+
134
+ if (arg === "--template") {
135
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--template");
136
+ options.template = value;
137
+ index = nextIndex;
138
+ continue;
139
+ }
140
+
141
+ if (arg.startsWith("--template=")) {
142
+ options.template = arg.slice("--template=".length);
143
+ continue;
144
+ }
145
+
146
+ if (arg === "--target") {
147
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--target");
148
+ options.target = value;
149
+ index = nextIndex;
150
+ continue;
151
+ }
152
+
153
+ if (arg.startsWith("--target=")) {
154
+ options.target = arg.slice("--target=".length);
155
+ continue;
156
+ }
157
+
158
+ if (arg === "--title") {
159
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--title");
160
+ options.appTitle = value;
161
+ index = nextIndex;
162
+ continue;
163
+ }
164
+
165
+ if (arg.startsWith("--title=")) {
166
+ options.appTitle = arg.slice("--title=".length);
167
+ continue;
168
+ }
169
+
170
+ if (arg === "--initial-bundles") {
171
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--initial-bundles");
172
+ options.initialBundles = value;
173
+ index = nextIndex;
174
+ continue;
175
+ }
176
+
177
+ if (arg.startsWith("--initial-bundles=")) {
178
+ options.initialBundles = arg.slice("--initial-bundles=".length);
179
+ continue;
180
+ }
181
+
182
+ if (arg === "--tenancy-mode") {
183
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--tenancy-mode");
184
+ options.tenancyMode = value;
185
+ index = nextIndex;
186
+ continue;
187
+ }
188
+
189
+ if (arg.startsWith("--tenancy-mode=")) {
190
+ options.tenancyMode = arg.slice("--tenancy-mode=".length);
191
+ continue;
192
+ }
193
+
194
+ if (arg.startsWith("-")) {
195
+ throw createCliError(`Unknown option: ${arg}`, {
196
+ showUsage: true
197
+ });
198
+ }
199
+
200
+ positionalArgs.push(arg);
201
+ }
202
+
203
+ if (positionalArgs.length > 1) {
204
+ throw createCliError("Only one <app-name> argument is allowed.", {
205
+ showUsage: true
206
+ });
207
+ }
208
+
209
+ if (positionalArgs.length === 1) {
210
+ options.appName = positionalArgs[0];
211
+ }
212
+
213
+ if (!options.help && !options.interactive && positionalArgs.length !== 1) {
214
+ throw createCliError("Expected exactly one <app-name> argument.", {
215
+ showUsage: true
216
+ });
217
+ }
218
+
219
+ return options;
220
+ }
221
+
222
+ function printUsage(stream = process.stderr) {
223
+ stream.write("Usage: jskit-create-app [app-name] [options]\n");
224
+ stream.write("\n");
225
+ stream.write("Options:\n");
226
+ stream.write(` --template <name> Template folder under templates/ (default: ${DEFAULT_TEMPLATE})\n`);
227
+ stream.write(" --title <text> App title used for template replacements\n");
228
+ stream.write(" --target <path> Target directory (default: ./<app-name>)\n");
229
+ stream.write(" --initial-bundles <preset> Optional bundle preset: none | auth (default: none)\n");
230
+ stream.write(" --tenancy-mode <mode> Optional config seed: none | personal | workspace\n");
231
+ stream.write(" --force Allow writing into a non-empty target directory\n");
232
+ stream.write(" --dry-run Print planned writes without changing the filesystem\n");
233
+ stream.write(" --interactive Prompt for app values instead of passing all flags\n");
234
+ stream.write(" -h, --help Show this help\n");
235
+ }
236
+
237
+ function applyPlaceholders(source, replacements) {
238
+ let output = String(source || "");
239
+ for (const [placeholder, value] of Object.entries(replacements)) {
240
+ output = output.split(placeholder).join(String(value));
241
+ }
242
+ return output;
243
+ }
244
+
245
+ function isPathWithinRoot(rootPath, candidatePath) {
246
+ const relativePath = path.relative(rootPath, candidatePath);
247
+ return relativePath === "" || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
248
+ }
249
+
250
+ function normalizeTemplatePathSegments(templateName) {
251
+ const rawTemplateName = String(templateName || "").trim();
252
+ if (!rawTemplateName) {
253
+ throw createCliError("Template name cannot be empty.", {
254
+ showUsage: true
255
+ });
256
+ }
257
+
258
+ const segments = rawTemplateName.split(/[\\/]+/).filter(Boolean);
259
+ if (segments.length < 1 || segments.some((segment) => segment === "." || segment === "..")) {
260
+ throw createCliError(`Invalid template "${rawTemplateName}".`);
261
+ }
262
+
263
+ return segments;
264
+ }
265
+
266
+ async function resolveTemplateDirectory(templateName) {
267
+ const templatePathSegments = normalizeTemplatePathSegments(templateName);
268
+ const cleanTemplate = templatePathSegments.join(path.sep);
269
+ const templateDir = path.resolve(TEMPLATES_ROOT, ...templatePathSegments);
270
+
271
+ if (!isPathWithinRoot(TEMPLATES_ROOT, templateDir)) {
272
+ throw createCliError(`Invalid template "${cleanTemplate}".`);
273
+ }
274
+
275
+ try {
276
+ const templateStats = await stat(templateDir);
277
+ if (!templateStats.isDirectory()) {
278
+ throw createCliError(`Template "${cleanTemplate}" is not a directory.`);
279
+ }
280
+ } catch (error) {
281
+ if (error?.code === "ENOENT") {
282
+ throw createCliError(`Unknown template "${cleanTemplate}".`);
283
+ }
284
+ throw error;
285
+ }
286
+
287
+ return templateDir;
288
+ }
289
+
290
+ async function ensureTargetDirectoryState(targetDirectory, { force = false, dryRun = false } = {}) {
291
+ let targetExists = false;
292
+
293
+ try {
294
+ const targetStats = await stat(targetDirectory);
295
+ targetExists = true;
296
+ if (!targetStats.isDirectory()) {
297
+ throw createCliError(`Target path exists and is not a directory: ${targetDirectory}`);
298
+ }
299
+ } catch (error) {
300
+ if (error?.code !== "ENOENT") {
301
+ throw error;
302
+ }
303
+ }
304
+
305
+ if (!targetExists) {
306
+ if (!dryRun) {
307
+ await mkdir(targetDirectory, { recursive: true });
308
+ }
309
+ return;
310
+ }
311
+
312
+ const entries = await readdir(targetDirectory);
313
+ const blockingEntries = entries.filter((entry) => !ALLOWED_EXISTING_TARGET_ENTRIES.has(entry));
314
+ if (blockingEntries.length > 0 && !force) {
315
+ throw createCliError(
316
+ `Target directory is not empty: ${targetDirectory}. Use --force to allow writing into it.`
317
+ );
318
+ }
319
+ }
320
+
321
+ function sortEntriesByName(entries) {
322
+ return [...entries].sort((left, right) => left.name.localeCompare(right.name));
323
+ }
324
+
325
+ function mapTemplatePathToTargetPath(relativePath) {
326
+ const pathSegments = String(relativePath || "")
327
+ .split(path.sep)
328
+ .map((segment) => (segment === "gitignore" ? ".gitignore" : segment));
329
+ return pathSegments.join(path.sep);
330
+ }
331
+
332
+ async function writeTemplateFile(sourcePath, targetPath, replacements) {
333
+ const sourceBody = await readFile(sourcePath, "utf8");
334
+ const targetBody = applyPlaceholders(sourceBody, replacements);
335
+
336
+ await mkdir(path.dirname(targetPath), { recursive: true });
337
+ await writeFile(targetPath, targetBody, "utf8");
338
+
339
+ const sourceStats = await stat(sourcePath);
340
+ await chmod(targetPath, sourceStats.mode & 0o777);
341
+ }
342
+
343
+ async function copyTemplateDirectory({ templateDirectory, targetDirectory, replacements, dryRun }) {
344
+ const touchedFiles = [];
345
+
346
+ async function traverse(relativePath = "") {
347
+ const sourceDirectory = path.join(templateDirectory, relativePath);
348
+ const sourceEntries = sortEntriesByName(await readdir(sourceDirectory, { withFileTypes: true }));
349
+
350
+ for (const entry of sourceEntries) {
351
+ const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
352
+ const targetRelativePath = mapTemplatePathToTargetPath(entryRelativePath);
353
+ const sourcePath = path.join(templateDirectory, entryRelativePath);
354
+ const targetPath = path.join(targetDirectory, targetRelativePath);
355
+
356
+ if (entry.isDirectory()) {
357
+ if (!dryRun) {
358
+ await mkdir(targetPath, { recursive: true });
359
+ }
360
+ await traverse(entryRelativePath);
361
+ continue;
362
+ }
363
+
364
+ if (entry.isFile()) {
365
+ touchedFiles.push(targetRelativePath);
366
+ if (!dryRun) {
367
+ await writeTemplateFile(sourcePath, targetPath, replacements);
368
+ }
369
+ continue;
370
+ }
371
+
372
+ throw createCliError(`Unsupported template entry type at ${entryRelativePath}.`);
373
+ }
374
+ }
375
+
376
+ await traverse();
377
+ return touchedFiles;
378
+ }
379
+
380
+ function toRelativeTargetLabel(cwd, targetDirectory) {
381
+ const relativePath = path.relative(cwd, targetDirectory);
382
+ if (!relativePath || relativePath === ".") {
383
+ return ".";
384
+ }
385
+ if (relativePath.startsWith("..")) {
386
+ return targetDirectory;
387
+ }
388
+ return `./${relativePath}`;
389
+ }
390
+
391
+ async function askQuestion(readline, label, defaultValue) {
392
+ const suffix = defaultValue ? ` (${defaultValue})` : "";
393
+ const response = await readline.question(`${label}${suffix}: `);
394
+ const trimmed = response.trim();
395
+ return trimmed || defaultValue;
396
+ }
397
+
398
+ async function askYesNoQuestion(readline, label, defaultValue) {
399
+ const prompt = defaultValue ? "Y/n" : "y/N";
400
+
401
+ while (true) {
402
+ const response = await readline.question(`${label} [${prompt}]: `);
403
+ const normalized = response.trim().toLowerCase();
404
+ if (!normalized) {
405
+ return defaultValue;
406
+ }
407
+ if (normalized === "y" || normalized === "yes") {
408
+ return true;
409
+ }
410
+ if (normalized === "n" || normalized === "no") {
411
+ return false;
412
+ }
413
+ }
414
+ }
415
+
416
+ function createReadlineInterface({ stdin = process.stdin, stdout = process.stdout } = {}) {
417
+ return createInterface({
418
+ input: stdin,
419
+ output: stdout
420
+ });
421
+ }
422
+
423
+ async function collectInteractiveOptions({
424
+ parsed,
425
+ stdout = process.stdout,
426
+ stderr = process.stderr,
427
+ stdin = process.stdin,
428
+ readlineFactory = createReadlineInterface
429
+ }) {
430
+ const readline = readlineFactory({
431
+ stdin,
432
+ stdout
433
+ });
434
+
435
+ try {
436
+ let appName = String(parsed.appName || "").trim();
437
+ while (true) {
438
+ appName = await askQuestion(readline, "App name", appName);
439
+ try {
440
+ validateAppName(appName, { showUsage: false });
441
+ break;
442
+ } catch (error) {
443
+ stderr.write(`Error: ${error?.message || String(error)}\n`);
444
+ }
445
+ }
446
+
447
+ const defaultTitle = String(parsed.appTitle || "").trim() || toAppTitle(appName);
448
+ const appTitle = await askQuestion(readline, "App title", defaultTitle);
449
+
450
+ const defaultTarget = String(parsed.target || "").trim() || appName;
451
+ const target = await askQuestion(readline, "Target directory", defaultTarget);
452
+
453
+ const defaultTemplate = String(parsed.template || "").trim() || DEFAULT_TEMPLATE;
454
+ const template = await askQuestion(readline, "Template", defaultTemplate);
455
+
456
+ const force = await askYesNoQuestion(
457
+ readline,
458
+ "Allow writing into non-empty target directories",
459
+ Boolean(parsed.force)
460
+ );
461
+
462
+ let initialBundles = normalizeInitialBundlesPreset(parsed.initialBundles, { showUsage: false });
463
+ while (true) {
464
+ const candidate = await askQuestion(
465
+ readline,
466
+ "Initial bundle preset (none|auth)",
467
+ initialBundles
468
+ );
469
+ try {
470
+ initialBundles = normalizeInitialBundlesPreset(candidate, { showUsage: false });
471
+ break;
472
+ } catch (error) {
473
+ stderr.write(`Error: ${error?.message || String(error)}\n`);
474
+ }
475
+ }
476
+
477
+ return {
478
+ appName,
479
+ appTitle,
480
+ target,
481
+ template,
482
+ force,
483
+ initialBundles
484
+ };
485
+ } finally {
486
+ readline.close();
487
+ }
488
+ }
489
+
490
+ export async function createApp({
491
+ appName,
492
+ appTitle = null,
493
+ template = DEFAULT_TEMPLATE,
494
+ target = null,
495
+ initialBundles = DEFAULT_INITIAL_BUNDLES,
496
+ tenancyMode = null,
497
+ force = false,
498
+ dryRun = false,
499
+ cwd = process.cwd()
500
+ }) {
501
+ const resolvedAppName = String(appName || "").trim();
502
+ validateAppName(resolvedAppName);
503
+
504
+ const resolvedAppTitle = String(appTitle || "").trim() || toAppTitle(resolvedAppName);
505
+ const resolvedInitialBundles = normalizeInitialBundlesPreset(initialBundles);
506
+ const resolvedTenancyMode =
507
+ tenancyMode == null || String(tenancyMode).trim() === ""
508
+ ? null
509
+ : normalizeTenancyMode(tenancyMode);
510
+
511
+ const resolvedCwd = path.resolve(cwd);
512
+ const targetDirectory = path.resolve(resolvedCwd, target ? String(target) : resolvedAppName);
513
+ const templateDirectory = await resolveTemplateDirectory(template);
514
+
515
+ await ensureTargetDirectoryState(targetDirectory, {
516
+ force,
517
+ dryRun
518
+ });
519
+
520
+ const replacements = {
521
+ __APP_NAME__: resolvedAppName,
522
+ __APP_TITLE__: resolvedAppTitle,
523
+ __TENANCY_MODE_LINE__: resolvedTenancyMode
524
+ ? `config.tenancyMode = "${resolvedTenancyMode}";\n`
525
+ : ""
526
+ };
527
+
528
+ const touchedFiles = await copyTemplateDirectory({
529
+ templateDirectory,
530
+ targetDirectory,
531
+ replacements,
532
+ dryRun
533
+ });
534
+
535
+ return {
536
+ appName: resolvedAppName,
537
+ appTitle: resolvedAppTitle,
538
+ template: String(template),
539
+ initialBundles: resolvedInitialBundles,
540
+ tenancyMode: resolvedTenancyMode,
541
+ selectedBundleCommands: buildInitialBundleCommands(resolvedInitialBundles),
542
+ targetDirectory,
543
+ dryRun,
544
+ touchedFiles
545
+ };
546
+ }
547
+
548
+ export async function runCli(
549
+ argv,
550
+ {
551
+ stdout = process.stdout,
552
+ stderr = process.stderr,
553
+ stdin = process.stdin,
554
+ cwd = process.cwd(),
555
+ readlineFactory = createReadlineInterface
556
+ } = {}
557
+ ) {
558
+ try {
559
+ const parsed = parseCliArgs(argv);
560
+
561
+ if (parsed.help) {
562
+ printUsage(stdout);
563
+ return 0;
564
+ }
565
+
566
+ const resolvedOptions = parsed.interactive
567
+ ? {
568
+ ...parsed,
569
+ ...(await collectInteractiveOptions({
570
+ parsed,
571
+ stdout,
572
+ stderr,
573
+ stdin,
574
+ readlineFactory
575
+ }))
576
+ }
577
+ : parsed;
578
+
579
+ const result = await createApp({
580
+ appName: resolvedOptions.appName,
581
+ appTitle: resolvedOptions.appTitle,
582
+ template: resolvedOptions.template,
583
+ target: resolvedOptions.target,
584
+ initialBundles: resolvedOptions.initialBundles,
585
+ tenancyMode: resolvedOptions.tenancyMode,
586
+ force: resolvedOptions.force,
587
+ dryRun: resolvedOptions.dryRun,
588
+ cwd
589
+ });
590
+
591
+ const targetLabel = toRelativeTargetLabel(path.resolve(cwd), result.targetDirectory);
592
+ if (result.dryRun) {
593
+ stdout.write(
594
+ `[dry-run] Would create app "${result.appName}" from template "${result.template}" at ${targetLabel}.\n`
595
+ );
596
+ } else {
597
+ stdout.write(`Created app "${result.appName}" from template "${result.template}" at ${targetLabel}.\n`);
598
+ }
599
+ stdout.write(`${result.dryRun ? "Planned" : "Written"} files (${result.touchedFiles.length}):\n`);
600
+ for (const filePath of result.touchedFiles) {
601
+ stdout.write(`- ${filePath}\n`);
602
+ }
603
+
604
+ if (!result.dryRun) {
605
+ stdout.write("\n");
606
+ stdout.write("Next steps:\n");
607
+ stdout.write(`- cd ${shellQuote(targetLabel)}\n`);
608
+ stdout.write("- npm install\n");
609
+ stdout.write("- npm run dev\n");
610
+
611
+ stdout.write("\n");
612
+ if (result.selectedBundleCommands.length > 0) {
613
+ stdout.write(`Initial framework bundle commands (${result.initialBundles}):\n`);
614
+ for (const command of result.selectedBundleCommands) {
615
+ stdout.write(`- ${command}\n`);
616
+ }
617
+ } else {
618
+ stdout.write("Add framework capabilities when ready:\n");
619
+ stdout.write("- npx jskit add auth-base --no-install\n");
620
+ }
621
+ }
622
+
623
+ return 0;
624
+ } catch (error) {
625
+ stderr.write(`Error: ${error?.message || String(error)}\n`);
626
+ if (error?.showUsage) {
627
+ stderr.write("\n");
628
+ printUsage(stderr);
629
+ }
630
+ return Number.isInteger(error?.exitCode) ? error.exitCode : 1;
631
+ }
632
+ }