@jskit-ai/create-app 0.1.0 → 0.1.1

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 CHANGED
@@ -17,6 +17,8 @@ jskit-create-app --interactive
17
17
  - `--template <name>` template name under `templates/` (default `base-shell`)
18
18
  - `--title <text>` override the generated app title placeholder
19
19
  - `--target <path>` output directory (default `./<app-name>`)
20
+ - `--initial-bundles <preset>` optional framework preset: `none`, `db`, or `db-auth`
21
+ - `--db-provider <provider>` provider for `db` presets: `mysql` or `postgres`
20
22
  - `--force` allow writes into non-empty target directories
21
23
  - `--dry-run` preview writes only
22
24
  - `--interactive` prompt for app values
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
2
  import process from "node:process";
3
- import { runCli } from "../src/index.js";
3
+ import { runCli } from "../src/shared/index.js";
4
4
 
5
5
  const exitCode = await runCli(process.argv.slice(2));
6
6
  if (exitCode !== 0) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/create-app",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Scaffold minimal JSKIT app shells.",
5
5
  "type": "module",
6
6
  "files": [
@@ -16,7 +16,7 @@
16
16
  "jskit-create-app": "bin/jskit-create-app.js"
17
17
  },
18
18
  "exports": {
19
- ".": "./src/index.js"
19
+ ".": "./src/shared/index.js"
20
20
  },
21
21
  "engines": {
22
22
  "node": "20.x"
@@ -5,7 +5,12 @@ import { createInterface } from "node:readline/promises";
5
5
  import { fileURLToPath } from "node:url";
6
6
 
7
7
  const DEFAULT_TEMPLATE = "base-shell";
8
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
8
+ const DEFAULT_INITIAL_BUNDLES = "none";
9
+ const DEFAULT_DB_PROVIDER = "mysql";
10
+ const INITIAL_BUNDLE_PRESETS = new Set(["none", "db", "db-auth"]);
11
+ const DB_PROVIDERS = new Set(["mysql", "postgres"]);
12
+ const ALLOWED_EXISTING_TARGET_ENTRIES = new Set([".git"]);
13
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
9
14
  const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, "templates");
10
15
 
11
16
  function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
@@ -36,6 +41,53 @@ function toAppTitle(appName) {
36
41
  return words.length > 0 ? words.join(" ") : "App";
37
42
  }
38
43
 
44
+ function normalizeInitialBundlesPreset(value, { showUsage = true } = {}) {
45
+ const normalized = String(value || DEFAULT_INITIAL_BUNDLES).trim().toLowerCase();
46
+ if (INITIAL_BUNDLE_PRESETS.has(normalized)) {
47
+ return normalized;
48
+ }
49
+
50
+ throw createCliError(
51
+ `Invalid --initial-bundles value "${value}". Expected one of: none, db, db-auth.`,
52
+ { showUsage }
53
+ );
54
+ }
55
+
56
+ function normalizeDbProvider(value, { showUsage = true } = {}) {
57
+ const normalized = String(value || DEFAULT_DB_PROVIDER).trim().toLowerCase();
58
+ if (DB_PROVIDERS.has(normalized)) {
59
+ return normalized;
60
+ }
61
+
62
+ throw createCliError(
63
+ `Invalid --db-provider value "${value}". Expected one of: mysql, postgres.`,
64
+ { showUsage }
65
+ );
66
+ }
67
+
68
+ function buildInitialBundleCommands(initialBundles, dbProvider) {
69
+ const normalizedPreset = normalizeInitialBundlesPreset(initialBundles, { showUsage: false });
70
+ const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
71
+
72
+ const commands = [];
73
+ if (normalizedPreset === "db" || normalizedPreset === "db-auth") {
74
+ commands.push(`npx jskit add db --provider ${normalizedProvider} --no-install`);
75
+ }
76
+ if (normalizedPreset === "db-auth") {
77
+ commands.push("npx jskit add auth-base --no-install");
78
+ }
79
+
80
+ return commands;
81
+ }
82
+
83
+ function buildProgressiveBundleCommands(dbProvider) {
84
+ const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
85
+ return [
86
+ `npx jskit add db --provider ${normalizedProvider} --no-install`,
87
+ "npx jskit add auth-base --no-install"
88
+ ];
89
+ }
90
+
39
91
  function validateAppName(appName, { showUsage = true } = {}) {
40
92
  if (!appName || typeof appName !== "string") {
41
93
  throw createCliError("Missing app name.", { showUsage });
@@ -69,6 +121,8 @@ function parseCliArgs(argv) {
69
121
  appTitle: null,
70
122
  template: DEFAULT_TEMPLATE,
71
123
  target: null,
124
+ initialBundles: DEFAULT_INITIAL_BUNDLES,
125
+ dbProvider: DEFAULT_DB_PROVIDER,
72
126
  force: false,
73
127
  dryRun: false,
74
128
  help: false,
@@ -136,6 +190,30 @@ function parseCliArgs(argv) {
136
190
  continue;
137
191
  }
138
192
 
193
+ if (arg === "--initial-bundles") {
194
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--initial-bundles");
195
+ options.initialBundles = value;
196
+ index = nextIndex;
197
+ continue;
198
+ }
199
+
200
+ if (arg.startsWith("--initial-bundles=")) {
201
+ options.initialBundles = arg.slice("--initial-bundles=".length);
202
+ continue;
203
+ }
204
+
205
+ if (arg === "--db-provider") {
206
+ const { value, nextIndex } = parseOptionWithValue(args, index, "--db-provider");
207
+ options.dbProvider = value;
208
+ index = nextIndex;
209
+ continue;
210
+ }
211
+
212
+ if (arg.startsWith("--db-provider=")) {
213
+ options.dbProvider = arg.slice("--db-provider=".length);
214
+ continue;
215
+ }
216
+
139
217
  if (arg.startsWith("-")) {
140
218
  throw createCliError(`Unknown option: ${arg}`, {
141
219
  showUsage: true
@@ -171,6 +249,8 @@ function printUsage(stream = process.stderr) {
171
249
  stream.write(` --template <name> Template folder under templates/ (default: ${DEFAULT_TEMPLATE})\n`);
172
250
  stream.write(" --title <text> App title used for template replacements\n");
173
251
  stream.write(" --target <path> Target directory (default: ./<app-name>)\n");
252
+ stream.write(" --initial-bundles <preset> Optional bundle preset: none | db | db-auth (default: none)\n");
253
+ stream.write(" --db-provider <provider> Database provider for db presets: mysql | postgres (default: mysql)\n");
174
254
  stream.write(" --force Allow writing into a non-empty target directory\n");
175
255
  stream.write(" --dry-run Print planned writes without changing the filesystem\n");
176
256
  stream.write(" --interactive Prompt for app values instead of passing all flags\n");
@@ -233,7 +313,8 @@ async function ensureTargetDirectoryState(targetDirectory, { force = false, dryR
233
313
  }
234
314
 
235
315
  const entries = await readdir(targetDirectory);
236
- if (entries.length > 0 && !force) {
316
+ const blockingEntries = entries.filter((entry) => !ALLOWED_EXISTING_TARGET_ENTRIES.has(entry));
317
+ if (blockingEntries.length > 0 && !force) {
237
318
  throw createCliError(
238
319
  `Target directory is not empty: ${targetDirectory}. Use --force to allow writing into it.`
239
320
  );
@@ -335,11 +416,24 @@ async function askYesNoQuestion(readline, label, defaultValue) {
335
416
  }
336
417
  }
337
418
 
338
- async function collectInteractiveOptions({ parsed, stdout = process.stdout, stderr = process.stderr, stdin = process.stdin }) {
339
- const readline = createInterface({
419
+ function createReadlineInterface({ stdin = process.stdin, stdout = process.stdout } = {}) {
420
+ return createInterface({
340
421
  input: stdin,
341
422
  output: stdout
342
423
  });
424
+ }
425
+
426
+ async function collectInteractiveOptions({
427
+ parsed,
428
+ stdout = process.stdout,
429
+ stderr = process.stderr,
430
+ stdin = process.stdin,
431
+ readlineFactory = createReadlineInterface
432
+ }) {
433
+ const readline = readlineFactory({
434
+ stdin,
435
+ stdout
436
+ });
343
437
 
344
438
  try {
345
439
  let appName = String(parsed.appName || "").trim();
@@ -368,12 +462,42 @@ async function collectInteractiveOptions({ parsed, stdout = process.stdout, stde
368
462
  Boolean(parsed.force)
369
463
  );
370
464
 
465
+ let initialBundles = normalizeInitialBundlesPreset(parsed.initialBundles, { showUsage: false });
466
+ while (true) {
467
+ const candidate = await askQuestion(
468
+ readline,
469
+ "Initial bundle preset (none|db|db-auth)",
470
+ initialBundles
471
+ );
472
+ try {
473
+ initialBundles = normalizeInitialBundlesPreset(candidate, { showUsage: false });
474
+ break;
475
+ } catch (error) {
476
+ stderr.write(`Error: ${error?.message || String(error)}\n`);
477
+ }
478
+ }
479
+
480
+ let dbProvider = normalizeDbProvider(parsed.dbProvider, { showUsage: false });
481
+ if (initialBundles === "db" || initialBundles === "db-auth") {
482
+ while (true) {
483
+ const candidate = await askQuestion(readline, "DB provider (mysql|postgres)", dbProvider);
484
+ try {
485
+ dbProvider = normalizeDbProvider(candidate, { showUsage: false });
486
+ break;
487
+ } catch (error) {
488
+ stderr.write(`Error: ${error?.message || String(error)}\n`);
489
+ }
490
+ }
491
+ }
492
+
371
493
  return {
372
494
  appName,
373
495
  appTitle,
374
496
  target,
375
497
  template,
376
- force
498
+ force,
499
+ initialBundles,
500
+ dbProvider
377
501
  };
378
502
  } finally {
379
503
  readline.close();
@@ -385,6 +509,8 @@ export async function createApp({
385
509
  appTitle = null,
386
510
  template = DEFAULT_TEMPLATE,
387
511
  target = null,
512
+ initialBundles = DEFAULT_INITIAL_BUNDLES,
513
+ dbProvider = DEFAULT_DB_PROVIDER,
388
514
  force = false,
389
515
  dryRun = false,
390
516
  cwd = process.cwd()
@@ -393,6 +519,8 @@ export async function createApp({
393
519
  validateAppName(resolvedAppName);
394
520
 
395
521
  const resolvedAppTitle = String(appTitle || "").trim() || toAppTitle(resolvedAppName);
522
+ const resolvedInitialBundles = normalizeInitialBundlesPreset(initialBundles);
523
+ const resolvedDbProvider = normalizeDbProvider(dbProvider);
396
524
 
397
525
  const resolvedCwd = path.resolve(cwd);
398
526
  const targetDirectory = path.resolve(resolvedCwd, target ? String(target) : resolvedAppName);
@@ -419,6 +547,10 @@ export async function createApp({
419
547
  appName: resolvedAppName,
420
548
  appTitle: resolvedAppTitle,
421
549
  template: String(template),
550
+ initialBundles: resolvedInitialBundles,
551
+ dbProvider: resolvedDbProvider,
552
+ selectedBundleCommands: buildInitialBundleCommands(resolvedInitialBundles, resolvedDbProvider),
553
+ progressiveBundleCommands: buildProgressiveBundleCommands(resolvedDbProvider),
422
554
  targetDirectory,
423
555
  dryRun,
424
556
  touchedFiles
@@ -431,7 +563,8 @@ export async function runCli(
431
563
  stdout = process.stdout,
432
564
  stderr = process.stderr,
433
565
  stdin = process.stdin,
434
- cwd = process.cwd()
566
+ cwd = process.cwd(),
567
+ readlineFactory = createReadlineInterface
435
568
  } = {}
436
569
  ) {
437
570
  try {
@@ -449,7 +582,8 @@ export async function runCli(
449
582
  parsed,
450
583
  stdout,
451
584
  stderr,
452
- stdin
585
+ stdin,
586
+ readlineFactory
453
587
  }))
454
588
  }
455
589
  : parsed;
@@ -459,6 +593,8 @@ export async function runCli(
459
593
  appTitle: resolvedOptions.appTitle,
460
594
  template: resolvedOptions.template,
461
595
  target: resolvedOptions.target,
596
+ initialBundles: resolvedOptions.initialBundles,
597
+ dbProvider: resolvedOptions.dbProvider,
462
598
  force: resolvedOptions.force,
463
599
  dryRun: resolvedOptions.dryRun,
464
600
  cwd
@@ -484,6 +620,19 @@ export async function runCli(
484
620
  stdout.write(`- cd ${shellQuote(targetLabel)}\n`);
485
621
  stdout.write("- npm install\n");
486
622
  stdout.write("- npm run dev\n");
623
+
624
+ stdout.write("\n");
625
+ if (result.selectedBundleCommands.length > 0) {
626
+ stdout.write(`Initial framework bundle commands (${result.initialBundles}):\n`);
627
+ for (const command of result.selectedBundleCommands) {
628
+ stdout.write(`- ${command}\n`);
629
+ }
630
+ } else {
631
+ stdout.write("Add framework capabilities when ready:\n");
632
+ for (const command of result.progressiveBundleCommands) {
633
+ stdout.write(`- ${command}\n`);
634
+ }
635
+ }
487
636
  }
488
637
 
489
638
  return 0;
@@ -0,0 +1 @@
1
+ web: npm run start
@@ -9,7 +9,6 @@ This is the smallest practical JSKIT app host:
9
9
  - tiny Fastify server (`/api/v1/health`)
10
10
  - tiny Vue client shell
11
11
  - standardized scripts via `@jskit-ai/app-scripts`
12
- - framework composition seed file: `framework/app.manifest.mjs`
13
12
 
14
13
  ## What This Is Not
15
14
 
@@ -31,3 +30,10 @@ npm run server
31
30
  npm run test
32
31
  npm run test:client
33
32
  ```
33
+
34
+ ## Progressive Bundles
35
+
36
+ ```bash
37
+ npx jskit add db --provider mysql --no-install
38
+ npx jskit add auth-base --no-install
39
+ ```
@@ -7,6 +7,9 @@
7
7
  "engines": {
8
8
  "node": "20.x"
9
9
  },
10
+ "bin": {
11
+ "jskit": "node_modules/@jskit-ai/jskit/packages/tooling/jskit/bin/jskit.js"
12
+ },
10
13
  "scripts": {
11
14
  "server": "jskit-app-scripts server",
12
15
  "start": "jskit-app-scripts start",
@@ -25,6 +28,7 @@
25
28
  },
26
29
  "devDependencies": {
27
30
  "@jskit-ai/config-eslint": "0.1.0",
31
+ "@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
28
32
  "@vitejs/plugin-vue": "^5.2.1",
29
33
  "eslint": "^9.39.1",
30
34
  "vite": "^6.1.0",
@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
- import { readdir, readFile } from "node:fs/promises";
5
+ import { access, readdir, readFile } from "node:fs/promises";
6
6
 
7
7
  const __filename = fileURLToPath(import.meta.url);
8
8
  const __dirname = path.dirname(__filename);
@@ -16,6 +16,7 @@ const EXPECTED_RUNTIME_DEPENDENCIES = Object.freeze([
16
16
 
17
17
  const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
18
18
  "@jskit-ai/config-eslint",
19
+ "@jskit-ai/jskit",
19
20
  "@vitejs/plugin-vue",
20
21
  "eslint",
21
22
  "vite",
@@ -23,12 +24,12 @@ const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
23
24
  ]);
24
25
 
25
26
  const EXPECTED_TOP_LEVEL_ENTRIES = Object.freeze([
27
+ "Procfile",
26
28
  "README.md",
27
29
  "app.scripts.config.mjs",
28
30
  "bin",
29
31
  "eslint.config.mjs",
30
32
  "favicon.svg",
31
- "framework",
32
33
  "gitignore",
33
34
  "index.html",
34
35
  "package.json",
@@ -87,14 +88,6 @@ test("starter shell keeps a strict top-level footprint", async () => {
87
88
  assert.deepEqual(sortStrings(entries), sortStrings(EXPECTED_TOP_LEVEL_ENTRIES));
88
89
  });
89
90
 
90
- test("manifest scaffold exists with strict core-only defaults", async () => {
91
- const manifestModule = await import(path.join(APP_ROOT, "framework/app.manifest.mjs"));
92
- const manifest = manifestModule?.default;
93
-
94
- assert.equal(manifest.manifestVersion, 1);
95
- assert.equal(manifest.appId, "__APP_NAME__");
96
- assert.equal(manifest.profileId, "web-saas-default");
97
- assert.equal(manifest.mode, "strict");
98
- assert.equal(manifest.enforceProfileRequired, true);
99
- assert.deepEqual(manifest.optionalModulePacks, ["core"]);
91
+ test("legacy app.manifest scaffold is removed from starter shell", async () => {
92
+ await assert.rejects(access(path.join(APP_ROOT, "framework/app.manifest.mjs")), /ENOENT/);
100
93
  });
@@ -1,8 +0,0 @@
1
- export default Object.freeze({
2
- manifestVersion: 1,
3
- appId: "__APP_NAME__",
4
- profileId: "web-saas-default",
5
- mode: "strict",
6
- enforceProfileRequired: true,
7
- optionalModulePacks: ["core"]
8
- });