@jskit-ai/create-app 0.1.2 → 0.1.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jskit-ai/create-app",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Scaffold minimal JSKIT app shells.",
5
5
  "type": "module",
6
6
  "files": [
@@ -10,30 +10,33 @@
10
10
  "README.md"
11
11
  ],
12
12
  "scripts": {
13
- "test": "node --test"
13
+ "test": "node --test test/*.test.js"
14
14
  },
15
15
  "bin": {
16
16
  "jskit-create-app": "bin/jskit-create-app.js"
17
17
  },
18
18
  "exports": {
19
- ".": "./src/shared/index.js"
19
+ ".": "./src/index.js",
20
+ "./server": "./src/server/index.js",
21
+ "./client": "./src/client/index.js"
20
22
  },
21
23
  "dependencies": {},
22
24
  "engines": {
23
25
  "node": "20.x"
24
26
  },
25
27
  "publishConfig": {
26
- "access": "public"
28
+ "access": "public",
29
+ "registry": "https://registry.npmjs.org/"
27
30
  },
28
31
  "repository": {
29
32
  "type": "git",
30
33
  "url": "git+https://github.com/mobily-enterprises/jskit-ai.git",
31
- "directory": "packages/tooling/create-app"
34
+ "directory": "tooling/create-app"
32
35
  },
33
36
  "bugs": {
34
37
  "url": "https://github.com/mobily-enterprises/jskit-ai/issues"
35
38
  },
36
- "homepage": "https://github.com/mobily-enterprises/jskit-ai/tree/main/packages/tooling/create-app#readme",
39
+ "homepage": "https://github.com/mobily-enterprises/jskit-ai/tree/main/tooling/create-app#readme",
37
40
  "keywords": [
38
41
  "jskit",
39
42
  "scaffold",
package/README.md DELETED
@@ -1,24 +0,0 @@
1
- # @jskit-ai/create-app
2
-
3
- Scaffold a minimal JSKIT app shell from in-repo templates.
4
-
5
- ## Usage
6
-
7
- ```bash
8
- jskit-create-app my-app
9
- ```
10
-
11
- ```bash
12
- jskit-create-app --interactive
13
- ```
14
-
15
- ## Options
16
-
17
- - `--template <name>` template name under `templates/` (default `base-shell`)
18
- - `--title <text>` override the generated app title placeholder
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`
22
- - `--force` allow writes into non-empty target directories
23
- - `--dry-run` preview writes only
24
- - `--interactive` prompt for app values
@@ -1,5 +0,0 @@
1
- #!/usr/bin/env node
2
- import { runCliEntrypoint } from "../src/shared/cliEntrypoint.js";
3
- import { runCli } from "../src/shared/index.js";
4
-
5
- await runCliEntrypoint(runCli);
@@ -1,25 +0,0 @@
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 };
@@ -1,637 +0,0 @@
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 DEFAULT_DB_PROVIDER = "mysql";
11
- const INITIAL_BUNDLE_PRESETS = new Set(["none", "db", "db-auth"]);
12
- const DB_PROVIDERS = new Set(["mysql", "postgres"]);
13
- const ALLOWED_EXISTING_TARGET_ENTRIES = new Set([".git"]);
14
- const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
15
- const TEMPLATES_ROOT = path.join(PACKAGE_ROOT, "templates");
16
-
17
- function createCliError(message, { showUsage = false, exitCode = 1 } = {}) {
18
- const error = new Error(String(message || "Command failed."));
19
- error.showUsage = Boolean(showUsage);
20
- error.exitCode = Number.isInteger(exitCode) ? exitCode : 1;
21
- return error;
22
- }
23
-
24
- function toAppTitle(appName) {
25
- const words = String(appName)
26
- .trim()
27
- .split(/[-_]+/)
28
- .filter(Boolean)
29
- .map((word) => `${word.slice(0, 1).toUpperCase()}${word.slice(1)}`);
30
-
31
- return words.length > 0 ? words.join(" ") : "App";
32
- }
33
-
34
- function normalizeInitialBundlesPreset(value, { showUsage = true } = {}) {
35
- const normalized = String(value || DEFAULT_INITIAL_BUNDLES).trim().toLowerCase();
36
- if (INITIAL_BUNDLE_PRESETS.has(normalized)) {
37
- return normalized;
38
- }
39
-
40
- throw createCliError(
41
- `Invalid --initial-bundles value "${value}". Expected one of: none, db, db-auth.`,
42
- { showUsage }
43
- );
44
- }
45
-
46
- function normalizeDbProvider(value, { showUsage = true } = {}) {
47
- const normalized = String(value || DEFAULT_DB_PROVIDER).trim().toLowerCase();
48
- if (DB_PROVIDERS.has(normalized)) {
49
- return normalized;
50
- }
51
-
52
- throw createCliError(
53
- `Invalid --db-provider value "${value}". Expected one of: mysql, postgres.`,
54
- { showUsage }
55
- );
56
- }
57
-
58
- function buildInitialBundleCommands(initialBundles, dbProvider) {
59
- const normalizedPreset = normalizeInitialBundlesPreset(initialBundles, { showUsage: false });
60
- const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
61
-
62
- const commands = [];
63
- if (normalizedPreset === "db" || normalizedPreset === "db-auth") {
64
- commands.push(`npx jskit add db --provider ${normalizedProvider} --no-install`);
65
- }
66
- if (normalizedPreset === "db-auth") {
67
- commands.push("npx jskit add auth-base --no-install");
68
- }
69
-
70
- return commands;
71
- }
72
-
73
- function buildProgressiveBundleCommands(dbProvider) {
74
- const normalizedProvider = normalizeDbProvider(dbProvider, { showUsage: false });
75
- return [
76
- `npx jskit add db --provider ${normalizedProvider} --no-install`,
77
- "npx jskit add auth-base --no-install"
78
- ];
79
- }
80
-
81
- function validateAppName(appName, { showUsage = true } = {}) {
82
- if (!appName || typeof appName !== "string") {
83
- throw createCliError("Missing app name.", { showUsage });
84
- }
85
-
86
- if (!/^[a-z0-9][a-z0-9-]*$/.test(appName)) {
87
- throw createCliError(
88
- `Invalid app name "${appName}". Use lowercase letters, numbers, and dashes only.`,
89
- { showUsage }
90
- );
91
- }
92
- }
93
-
94
- function parseOptionWithValue(argv, index, optionName) {
95
- const nextValue = argv[index + 1];
96
- if (!nextValue || nextValue.startsWith("-")) {
97
- throw createCliError(`Option ${optionName} requires a value.`, {
98
- showUsage: true
99
- });
100
- }
101
- return {
102
- value: nextValue,
103
- nextIndex: index + 1
104
- };
105
- }
106
-
107
- function parseCliArgs(argv) {
108
- const args = Array.isArray(argv) ? argv : [];
109
- const options = {
110
- appName: null,
111
- appTitle: null,
112
- template: DEFAULT_TEMPLATE,
113
- target: null,
114
- initialBundles: DEFAULT_INITIAL_BUNDLES,
115
- dbProvider: DEFAULT_DB_PROVIDER,
116
- force: false,
117
- dryRun: false,
118
- help: false,
119
- interactive: false
120
- };
121
-
122
- const positionalArgs = [];
123
-
124
- for (let index = 0; index < args.length; index += 1) {
125
- const arg = String(args[index] || "");
126
-
127
- if (arg === "--help" || arg === "-h") {
128
- options.help = true;
129
- continue;
130
- }
131
-
132
- if (arg === "--force") {
133
- options.force = true;
134
- continue;
135
- }
136
-
137
- if (arg === "--dry-run") {
138
- options.dryRun = true;
139
- continue;
140
- }
141
-
142
- if (arg === "--interactive") {
143
- options.interactive = true;
144
- continue;
145
- }
146
-
147
- if (arg === "--template") {
148
- const { value, nextIndex } = parseOptionWithValue(args, index, "--template");
149
- options.template = value;
150
- index = nextIndex;
151
- continue;
152
- }
153
-
154
- if (arg.startsWith("--template=")) {
155
- options.template = arg.slice("--template=".length);
156
- continue;
157
- }
158
-
159
- if (arg === "--target") {
160
- const { value, nextIndex } = parseOptionWithValue(args, index, "--target");
161
- options.target = value;
162
- index = nextIndex;
163
- continue;
164
- }
165
-
166
- if (arg.startsWith("--target=")) {
167
- options.target = arg.slice("--target=".length);
168
- continue;
169
- }
170
-
171
- if (arg === "--title") {
172
- const { value, nextIndex } = parseOptionWithValue(args, index, "--title");
173
- options.appTitle = value;
174
- index = nextIndex;
175
- continue;
176
- }
177
-
178
- if (arg.startsWith("--title=")) {
179
- options.appTitle = arg.slice("--title=".length);
180
- continue;
181
- }
182
-
183
- if (arg === "--initial-bundles") {
184
- const { value, nextIndex } = parseOptionWithValue(args, index, "--initial-bundles");
185
- options.initialBundles = value;
186
- index = nextIndex;
187
- continue;
188
- }
189
-
190
- if (arg.startsWith("--initial-bundles=")) {
191
- options.initialBundles = arg.slice("--initial-bundles=".length);
192
- continue;
193
- }
194
-
195
- if (arg === "--db-provider") {
196
- const { value, nextIndex } = parseOptionWithValue(args, index, "--db-provider");
197
- options.dbProvider = value;
198
- index = nextIndex;
199
- continue;
200
- }
201
-
202
- if (arg.startsWith("--db-provider=")) {
203
- options.dbProvider = arg.slice("--db-provider=".length);
204
- continue;
205
- }
206
-
207
- if (arg.startsWith("-")) {
208
- throw createCliError(`Unknown option: ${arg}`, {
209
- showUsage: true
210
- });
211
- }
212
-
213
- positionalArgs.push(arg);
214
- }
215
-
216
- if (positionalArgs.length > 1) {
217
- throw createCliError("Only one <app-name> argument is allowed.", {
218
- showUsage: true
219
- });
220
- }
221
-
222
- if (positionalArgs.length === 1) {
223
- options.appName = positionalArgs[0];
224
- }
225
-
226
- if (!options.help && !options.interactive && positionalArgs.length !== 1) {
227
- throw createCliError("Expected exactly one <app-name> argument.", {
228
- showUsage: true
229
- });
230
- }
231
-
232
- return options;
233
- }
234
-
235
- function printUsage(stream = process.stderr) {
236
- stream.write("Usage: jskit-create-app [app-name] [options]\n");
237
- stream.write("\n");
238
- stream.write("Options:\n");
239
- stream.write(` --template <name> Template folder under templates/ (default: ${DEFAULT_TEMPLATE})\n`);
240
- stream.write(" --title <text> App title used for template replacements\n");
241
- stream.write(" --target <path> Target directory (default: ./<app-name>)\n");
242
- stream.write(" --initial-bundles <preset> Optional bundle preset: none | db | db-auth (default: none)\n");
243
- stream.write(" --db-provider <provider> Database provider for db presets: mysql | postgres (default: mysql)\n");
244
- stream.write(" --force Allow writing into a non-empty target directory\n");
245
- stream.write(" --dry-run Print planned writes without changing the filesystem\n");
246
- stream.write(" --interactive Prompt for app values instead of passing all flags\n");
247
- stream.write(" -h, --help Show this help\n");
248
- }
249
-
250
- function applyPlaceholders(source, replacements) {
251
- let output = String(source || "");
252
- for (const [placeholder, value] of Object.entries(replacements)) {
253
- output = output.split(placeholder).join(String(value));
254
- }
255
- return output;
256
- }
257
-
258
- async function resolveTemplateDirectory(templateName) {
259
- const cleanTemplate = String(templateName || "").trim();
260
- if (!cleanTemplate) {
261
- throw createCliError("Template name cannot be empty.", {
262
- showUsage: true
263
- });
264
- }
265
-
266
- const templateDir = path.join(TEMPLATES_ROOT, cleanTemplate);
267
-
268
- try {
269
- const templateStats = await stat(templateDir);
270
- if (!templateStats.isDirectory()) {
271
- throw createCliError(`Template "${cleanTemplate}" is not a directory.`);
272
- }
273
- } catch (error) {
274
- if (error?.code === "ENOENT") {
275
- throw createCliError(`Unknown template "${cleanTemplate}".`);
276
- }
277
- throw error;
278
- }
279
-
280
- return templateDir;
281
- }
282
-
283
- async function ensureTargetDirectoryState(targetDirectory, { force = false, dryRun = false } = {}) {
284
- let targetExists = false;
285
-
286
- try {
287
- const targetStats = await stat(targetDirectory);
288
- targetExists = true;
289
- if (!targetStats.isDirectory()) {
290
- throw createCliError(`Target path exists and is not a directory: ${targetDirectory}`);
291
- }
292
- } catch (error) {
293
- if (error?.code !== "ENOENT") {
294
- throw error;
295
- }
296
- }
297
-
298
- if (!targetExists) {
299
- if (!dryRun) {
300
- await mkdir(targetDirectory, { recursive: true });
301
- }
302
- return;
303
- }
304
-
305
- const entries = await readdir(targetDirectory);
306
- const blockingEntries = entries.filter((entry) => !ALLOWED_EXISTING_TARGET_ENTRIES.has(entry));
307
- if (blockingEntries.length > 0 && !force) {
308
- throw createCliError(
309
- `Target directory is not empty: ${targetDirectory}. Use --force to allow writing into it.`
310
- );
311
- }
312
- }
313
-
314
- function sortEntriesByName(entries) {
315
- return [...entries].sort((left, right) => left.name.localeCompare(right.name));
316
- }
317
-
318
- function mapTemplatePathToTargetPath(relativePath) {
319
- const pathSegments = String(relativePath || "")
320
- .split(path.sep)
321
- .map((segment) => (segment === "gitignore" ? ".gitignore" : segment));
322
- return pathSegments.join(path.sep);
323
- }
324
-
325
- async function writeTemplateFile(sourcePath, targetPath, replacements) {
326
- const sourceBody = await readFile(sourcePath, "utf8");
327
- const targetBody = applyPlaceholders(sourceBody, replacements);
328
-
329
- await mkdir(path.dirname(targetPath), { recursive: true });
330
- await writeFile(targetPath, targetBody, "utf8");
331
-
332
- const sourceStats = await stat(sourcePath);
333
- await chmod(targetPath, sourceStats.mode & 0o777);
334
- }
335
-
336
- async function copyTemplateDirectory({ templateDirectory, targetDirectory, replacements, dryRun }) {
337
- const touchedFiles = [];
338
-
339
- async function traverse(relativePath = "") {
340
- const sourceDirectory = path.join(templateDirectory, relativePath);
341
- const sourceEntries = sortEntriesByName(await readdir(sourceDirectory, { withFileTypes: true }));
342
-
343
- for (const entry of sourceEntries) {
344
- const entryRelativePath = relativePath ? path.join(relativePath, entry.name) : entry.name;
345
- const targetRelativePath = mapTemplatePathToTargetPath(entryRelativePath);
346
- const sourcePath = path.join(templateDirectory, entryRelativePath);
347
- const targetPath = path.join(targetDirectory, targetRelativePath);
348
-
349
- if (entry.isDirectory()) {
350
- if (!dryRun) {
351
- await mkdir(targetPath, { recursive: true });
352
- }
353
- await traverse(entryRelativePath);
354
- continue;
355
- }
356
-
357
- if (entry.isFile()) {
358
- touchedFiles.push(targetRelativePath);
359
- if (!dryRun) {
360
- await writeTemplateFile(sourcePath, targetPath, replacements);
361
- }
362
- continue;
363
- }
364
-
365
- throw createCliError(`Unsupported template entry type at ${entryRelativePath}.`);
366
- }
367
- }
368
-
369
- await traverse();
370
- return touchedFiles;
371
- }
372
-
373
- function toRelativeTargetLabel(cwd, targetDirectory) {
374
- const relativePath = path.relative(cwd, targetDirectory);
375
- if (!relativePath || relativePath === ".") {
376
- return ".";
377
- }
378
- if (relativePath.startsWith("..")) {
379
- return targetDirectory;
380
- }
381
- return `./${relativePath}`;
382
- }
383
-
384
- async function askQuestion(readline, label, defaultValue) {
385
- const suffix = defaultValue ? ` (${defaultValue})` : "";
386
- const response = await readline.question(`${label}${suffix}: `);
387
- const trimmed = response.trim();
388
- return trimmed || defaultValue;
389
- }
390
-
391
- async function askYesNoQuestion(readline, label, defaultValue) {
392
- const prompt = defaultValue ? "Y/n" : "y/N";
393
-
394
- while (true) {
395
- const response = await readline.question(`${label} [${prompt}]: `);
396
- const normalized = response.trim().toLowerCase();
397
- if (!normalized) {
398
- return defaultValue;
399
- }
400
- if (normalized === "y" || normalized === "yes") {
401
- return true;
402
- }
403
- if (normalized === "n" || normalized === "no") {
404
- return false;
405
- }
406
- }
407
- }
408
-
409
- function createReadlineInterface({ stdin = process.stdin, stdout = process.stdout } = {}) {
410
- return createInterface({
411
- input: stdin,
412
- output: stdout
413
- });
414
- }
415
-
416
- async function collectInteractiveOptions({
417
- parsed,
418
- stdout = process.stdout,
419
- stderr = process.stderr,
420
- stdin = process.stdin,
421
- readlineFactory = createReadlineInterface
422
- }) {
423
- const readline = readlineFactory({
424
- stdin,
425
- stdout
426
- });
427
-
428
- try {
429
- let appName = String(parsed.appName || "").trim();
430
- while (true) {
431
- appName = await askQuestion(readline, "App name", appName);
432
- try {
433
- validateAppName(appName, { showUsage: false });
434
- break;
435
- } catch (error) {
436
- stderr.write(`Error: ${error?.message || String(error)}\n`);
437
- }
438
- }
439
-
440
- const defaultTitle = String(parsed.appTitle || "").trim() || toAppTitle(appName);
441
- const appTitle = await askQuestion(readline, "App title", defaultTitle);
442
-
443
- const defaultTarget = String(parsed.target || "").trim() || appName;
444
- const target = await askQuestion(readline, "Target directory", defaultTarget);
445
-
446
- const defaultTemplate = String(parsed.template || "").trim() || DEFAULT_TEMPLATE;
447
- const template = await askQuestion(readline, "Template", defaultTemplate);
448
-
449
- const force = await askYesNoQuestion(
450
- readline,
451
- "Allow writing into non-empty target directories",
452
- Boolean(parsed.force)
453
- );
454
-
455
- let initialBundles = normalizeInitialBundlesPreset(parsed.initialBundles, { showUsage: false });
456
- while (true) {
457
- const candidate = await askQuestion(
458
- readline,
459
- "Initial bundle preset (none|db|db-auth)",
460
- initialBundles
461
- );
462
- try {
463
- initialBundles = normalizeInitialBundlesPreset(candidate, { showUsage: false });
464
- break;
465
- } catch (error) {
466
- stderr.write(`Error: ${error?.message || String(error)}\n`);
467
- }
468
- }
469
-
470
- let dbProvider = normalizeDbProvider(parsed.dbProvider, { showUsage: false });
471
- if (initialBundles === "db" || initialBundles === "db-auth") {
472
- while (true) {
473
- const candidate = await askQuestion(readline, "DB provider (mysql|postgres)", dbProvider);
474
- try {
475
- dbProvider = normalizeDbProvider(candidate, { showUsage: false });
476
- break;
477
- } catch (error) {
478
- stderr.write(`Error: ${error?.message || String(error)}\n`);
479
- }
480
- }
481
- }
482
-
483
- return {
484
- appName,
485
- appTitle,
486
- target,
487
- template,
488
- force,
489
- initialBundles,
490
- dbProvider
491
- };
492
- } finally {
493
- readline.close();
494
- }
495
- }
496
-
497
- export async function createApp({
498
- appName,
499
- appTitle = null,
500
- template = DEFAULT_TEMPLATE,
501
- target = null,
502
- initialBundles = DEFAULT_INITIAL_BUNDLES,
503
- dbProvider = DEFAULT_DB_PROVIDER,
504
- force = false,
505
- dryRun = false,
506
- cwd = process.cwd()
507
- }) {
508
- const resolvedAppName = String(appName || "").trim();
509
- validateAppName(resolvedAppName);
510
-
511
- const resolvedAppTitle = String(appTitle || "").trim() || toAppTitle(resolvedAppName);
512
- const resolvedInitialBundles = normalizeInitialBundlesPreset(initialBundles);
513
- const resolvedDbProvider = normalizeDbProvider(dbProvider);
514
-
515
- const resolvedCwd = path.resolve(cwd);
516
- const targetDirectory = path.resolve(resolvedCwd, target ? String(target) : resolvedAppName);
517
- const templateDirectory = await resolveTemplateDirectory(template);
518
-
519
- await ensureTargetDirectoryState(targetDirectory, {
520
- force,
521
- dryRun
522
- });
523
-
524
- const replacements = {
525
- __APP_NAME__: resolvedAppName,
526
- __APP_TITLE__: resolvedAppTitle
527
- };
528
-
529
- const touchedFiles = await copyTemplateDirectory({
530
- templateDirectory,
531
- targetDirectory,
532
- replacements,
533
- dryRun
534
- });
535
-
536
- return {
537
- appName: resolvedAppName,
538
- appTitle: resolvedAppTitle,
539
- template: String(template),
540
- initialBundles: resolvedInitialBundles,
541
- dbProvider: resolvedDbProvider,
542
- selectedBundleCommands: buildInitialBundleCommands(resolvedInitialBundles, resolvedDbProvider),
543
- progressiveBundleCommands: buildProgressiveBundleCommands(resolvedDbProvider),
544
- targetDirectory,
545
- dryRun,
546
- touchedFiles
547
- };
548
- }
549
-
550
- export async function runCli(
551
- argv,
552
- {
553
- stdout = process.stdout,
554
- stderr = process.stderr,
555
- stdin = process.stdin,
556
- cwd = process.cwd(),
557
- readlineFactory = createReadlineInterface
558
- } = {}
559
- ) {
560
- try {
561
- const parsed = parseCliArgs(argv);
562
-
563
- if (parsed.help) {
564
- printUsage(stdout);
565
- return 0;
566
- }
567
-
568
- const resolvedOptions = parsed.interactive
569
- ? {
570
- ...parsed,
571
- ...(await collectInteractiveOptions({
572
- parsed,
573
- stdout,
574
- stderr,
575
- stdin,
576
- readlineFactory
577
- }))
578
- }
579
- : parsed;
580
-
581
- const result = await createApp({
582
- appName: resolvedOptions.appName,
583
- appTitle: resolvedOptions.appTitle,
584
- template: resolvedOptions.template,
585
- target: resolvedOptions.target,
586
- initialBundles: resolvedOptions.initialBundles,
587
- dbProvider: resolvedOptions.dbProvider,
588
- force: resolvedOptions.force,
589
- dryRun: resolvedOptions.dryRun,
590
- cwd
591
- });
592
-
593
- const targetLabel = toRelativeTargetLabel(path.resolve(cwd), result.targetDirectory);
594
- if (result.dryRun) {
595
- stdout.write(
596
- `[dry-run] Would create app "${result.appName}" from template "${result.template}" at ${targetLabel}.\n`
597
- );
598
- } else {
599
- stdout.write(`Created app "${result.appName}" from template "${result.template}" at ${targetLabel}.\n`);
600
- }
601
-
602
- stdout.write(`${result.dryRun ? "Planned" : "Written"} files (${result.touchedFiles.length}):\n`);
603
- for (const filePath of result.touchedFiles) {
604
- stdout.write(`- ${filePath}\n`);
605
- }
606
-
607
- if (!result.dryRun) {
608
- stdout.write("\n");
609
- stdout.write("Next steps:\n");
610
- stdout.write(`- cd ${shellQuote(targetLabel)}\n`);
611
- stdout.write("- npm install\n");
612
- stdout.write("- npm run dev\n");
613
-
614
- stdout.write("\n");
615
- if (result.selectedBundleCommands.length > 0) {
616
- stdout.write(`Initial framework bundle commands (${result.initialBundles}):\n`);
617
- for (const command of result.selectedBundleCommands) {
618
- stdout.write(`- ${command}\n`);
619
- }
620
- } else {
621
- stdout.write("Add framework capabilities when ready:\n");
622
- for (const command of result.progressiveBundleCommands) {
623
- stdout.write(`- ${command}\n`);
624
- }
625
- }
626
- }
627
-
628
- return 0;
629
- } catch (error) {
630
- stderr.write(`Error: ${error?.message || String(error)}\n`);
631
- if (error?.showUsage) {
632
- stderr.write("\n");
633
- printUsage(stderr);
634
- }
635
- return Number.isInteger(error?.exitCode) ? error.exitCode : 1;
636
- }
637
- }
@@ -1 +0,0 @@
1
- web: npm run start
@@ -1,39 +0,0 @@
1
- # __APP_TITLE__
2
-
3
- Minimal JSKIT starter shell.
4
-
5
- ## What This Is
6
-
7
- This is the smallest practical JSKIT app host:
8
-
9
- - tiny Fastify server (`/api/v1/health`)
10
- - tiny Vue client shell
11
- - standardized scripts via `@jskit-ai/app-scripts`
12
-
13
- ## What This Is Not
14
-
15
- This app intentionally does not include:
16
-
17
- - db wiring
18
- - auth/workspace modules
19
- - billing/chat/social/ai modules
20
- - app-local framework composition engines
21
-
22
- Those are layered in later as framework packs/modules.
23
-
24
- ## Run
25
-
26
- ```bash
27
- npm install
28
- npm run dev
29
- npm run server
30
- npm run test
31
- npm run test:client
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
- ```
@@ -1,3 +0,0 @@
1
- import { createNodeVueFastifyScriptsConfig } from "@jskit-ai/app-scripts";
2
-
3
- export default createNodeVueFastifyScriptsConfig();
@@ -1,8 +0,0 @@
1
- import { startServer } from "../server.js";
2
-
3
- try {
4
- await startServer();
5
- } catch (error) {
6
- console.error("Failed to start __APP_NAME__ server:", error);
7
- process.exitCode = 1;
8
- }
@@ -1,10 +0,0 @@
1
- import { baseConfig, nodeConfig, webConfig } from "@jskit-ai/config-eslint";
2
-
3
- export default [
4
- {
5
- ignores: ["dist/**", "node_modules/**", "coverage/**"]
6
- },
7
- ...baseConfig,
8
- ...webConfig,
9
- ...nodeConfig
10
- ];
@@ -1,7 +0,0 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
2
- <rect width="64" height="64" rx="12" fill="#111827" />
3
- <path
4
- d="M20 16h24v8H28v20c0 6-4 10-10 10h-2v-8h2c2 0 2-1 2-3V16z"
5
- fill="#22d3ee"
6
- />
7
- </svg>
@@ -1,7 +0,0 @@
1
- node_modules/
2
- dist/
3
- coverage/
4
- test-results/
5
- .env
6
- .env.*
7
- !.env.example
@@ -1,13 +0,0 @@
1
- <!doctype html>
2
- <html lang="en">
3
- <head>
4
- <meta charset="UTF-8" />
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
- <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
7
- <title>__APP_TITLE__</title>
8
- </head>
9
- <body>
10
- <div id="app"></div>
11
- <script type="module" src="/src/%VITE_CLIENT_ENTRY%"></script>
12
- </body>
13
- </html>
@@ -1,39 +0,0 @@
1
- {
2
- "name": "__APP_NAME__",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "Minimal JSKIT base app (Fastify + Vue)",
7
- "engines": {
8
- "node": "20.x"
9
- },
10
- "bin": {
11
- "jskit": "node_modules/@jskit-ai/jskit/packages/tooling/jskit/bin/jskit.js"
12
- },
13
- "scripts": {
14
- "server": "jskit-app-scripts server",
15
- "start": "jskit-app-scripts start",
16
- "dev": "jskit-app-scripts dev",
17
- "build": "jskit-app-scripts build",
18
- "preview": "jskit-app-scripts preview",
19
- "lint": "jskit-app-scripts lint",
20
- "lint:process-env": "jskit-app-scripts lint:process-env",
21
- "test": "jskit-app-scripts test",
22
- "test:client": "jskit-app-scripts test:client"
23
- },
24
- "dependencies": {
25
- "@jskit-ai/app-scripts": "0.1.0",
26
- "@jskit-ai/server-runtime-core": "file:node_modules/@jskit-ai/jskit/packages/runtime/server-runtime-core",
27
- "@jskit-ai/surface-routing": "file:node_modules/@jskit-ai/jskit/packages/surface-routing",
28
- "fastify": "^5.7.4",
29
- "vue": "^3.5.13"
30
- },
31
- "devDependencies": {
32
- "@jskit-ai/config-eslint": "0.1.0",
33
- "@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
34
- "@vitejs/plugin-vue": "^5.2.1",
35
- "eslint": "^9.39.1",
36
- "vite": "^6.1.0",
37
- "vitest": "^4.0.18"
38
- }
39
- }
@@ -1,38 +0,0 @@
1
- {
2
- "name": "__APP_NAME__",
3
- "version": "0.1.0",
4
- "private": true,
5
- "type": "module",
6
- "description": "Minimal JSKIT base app (Fastify + Vue)",
7
- "engines": {
8
- "node": "20.x"
9
- },
10
- "bin": {
11
- "jskit": "node_modules/@jskit-ai/jskit/packages/tooling/jskit/bin/jskit.js"
12
- },
13
- "scripts": {
14
- "server": "jskit-app-scripts server",
15
- "start": "jskit-app-scripts start",
16
- "dev": "jskit-app-scripts dev",
17
- "build": "jskit-app-scripts build",
18
- "preview": "jskit-app-scripts preview",
19
- "lint": "jskit-app-scripts lint",
20
- "lint:process-env": "jskit-app-scripts lint:process-env",
21
- "test": "jskit-app-scripts test",
22
- "test:client": "jskit-app-scripts test:client"
23
- },
24
- "dependencies": {
25
- "@jskit-ai/app-scripts": "0.1.0",
26
- "@jskit-ai/server-runtime-core": "file:node_modules/@jskit-ai/jskit/packages/runtime/server-runtime-core",
27
- "fastify": "^5.7.4",
28
- "vue": "^3.5.13"
29
- },
30
- "devDependencies": {
31
- "@jskit-ai/config-eslint": "0.1.0",
32
- "@jskit-ai/jskit": "github:mobily-enterprises/jskit-ai",
33
- "@vitejs/plugin-vue": "^5.2.1",
34
- "eslint": "^9.39.1",
35
- "vite": "^6.1.0",
36
- "vitest": "^4.0.18"
37
- }
38
- }
@@ -1,16 +0,0 @@
1
- function toPort(value, fallback = 3000) {
2
- const parsed = Number.parseInt(String(value || "").trim(), 10);
3
- if (Number.isInteger(parsed) && parsed > 0) {
4
- return parsed;
5
- }
6
- return fallback;
7
- }
8
-
9
- function resolveRuntimeEnv() {
10
- return {
11
- PORT: toPort(process.env.PORT, 3000),
12
- HOST: String(process.env.HOST || "").trim() || "0.0.0.0"
13
- };
14
- }
15
-
16
- export { resolveRuntimeEnv };
@@ -1,98 +0,0 @@
1
- import Fastify from "fastify";
2
- import { resolveRuntimeEnv } from "./server/lib/runtimeEnv.js";
3
- import { registerApiRouteDefinitions } from "@jskit-ai/server-runtime-core/apiRouteRegistration";
4
- import { createServerRuntimeFromApp, applyContributedRuntimeLifecycle } from "@jskit-ai/server-runtime-core/serverContributions";
5
- import path from "node:path";
6
-
7
- function registerFallbackHealthRoute(app) {
8
- app.get("/api/v1/health", async () => {
9
- return {
10
- ok: true,
11
- app: "__APP_NAME__"
12
- };
13
- });
14
- }
15
-
16
- async function registerContributedRuntime(app, { appRoot, runtimeEnv }) {
17
- try {
18
- const composed = await createServerRuntimeFromApp({
19
- appRoot,
20
- strict: false,
21
- dependencies: {
22
- env: runtimeEnv,
23
- logger: app.log
24
- },
25
- routeConfig: {}
26
- });
27
-
28
- const routeCount = Array.isArray(composed.routes) ? composed.routes.length : 0;
29
- if (routeCount > 0) {
30
- registerApiRouteDefinitions(app, {
31
- routes: composed.routes
32
- });
33
- }
34
-
35
- const lifecycleResult = await applyContributedRuntimeLifecycle({
36
- app,
37
- runtimeResult: composed,
38
- dependencies: {
39
- env: runtimeEnv,
40
- logger: app.log
41
- }
42
- });
43
-
44
- app.log.info(
45
- {
46
- routeCount,
47
- pluginCount: lifecycleResult.pluginCount,
48
- workerCount: lifecycleResult.workerCount,
49
- onBootCount: lifecycleResult.onBootCount,
50
- packageOrder: composed.packageOrder
51
- },
52
- "Registered JSKIT contributed server runtime."
53
- );
54
-
55
- return {
56
- enabled: true,
57
- routeCount,
58
- pluginCount: lifecycleResult.pluginCount,
59
- workerCount: lifecycleResult.workerCount,
60
- onBootCount: lifecycleResult.onBootCount
61
- };
62
- } catch (error) {
63
- const message = String(error?.message || "");
64
- if (message.includes("Lock file not found:")) {
65
- return {
66
- enabled: false,
67
- routeCount: 0
68
- };
69
- }
70
- throw error;
71
- }
72
- }
73
-
74
- async function createServer() {
75
- const app = Fastify({ logger: true });
76
- const runtimeEnv = resolveRuntimeEnv();
77
- const appRoot = path.resolve(process.cwd());
78
- const contributed = await registerContributedRuntime(app, {
79
- appRoot,
80
- runtimeEnv
81
- });
82
- if (!contributed.enabled || contributed.routeCount < 1) {
83
- registerFallbackHealthRoute(app);
84
- }
85
-
86
- return app;
87
- }
88
-
89
- async function startServer(options = {}) {
90
- const runtimeEnv = resolveRuntimeEnv();
91
- const port = Number(options?.port) || runtimeEnv.PORT;
92
- const host = String(options?.host || "").trim() || runtimeEnv.HOST;
93
- const app = await createServer();
94
- await app.listen({ port, host });
95
- return app;
96
- }
97
-
98
- export { createServer, startServer };
@@ -1,24 +0,0 @@
1
- <script setup>
2
- import { onMounted, ref } from "vue";
3
-
4
- const appTitle = "__APP_TITLE__";
5
- const health = ref("loading...");
6
-
7
- onMounted(async () => {
8
- try {
9
- const response = await fetch("/api/v1/health");
10
- const payload = await response.json();
11
- health.value = payload?.ok ? "ok" : "unhealthy";
12
- } catch {
13
- health.value = "unreachable";
14
- }
15
- });
16
- </script>
17
-
18
- <template>
19
- <main style="font-family: sans-serif; max-width: 48rem; margin: 3rem auto; padding: 0 1rem;">
20
- <h1>{{ appTitle }}</h1>
21
- <p>Minimal starter shell is running.</p>
22
- <p><strong>Health:</strong> {{ health }}</p>
23
- </main>
24
- </template>
@@ -1,4 +0,0 @@
1
- import { createApp } from "vue";
2
- import App from "./App.vue";
3
-
4
- createApp(App).mount("#app");
@@ -1,7 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
-
3
- describe("__APP_NAME__ client smoke", () => {
4
- it("runs vitest in __APP_NAME__", () => {
5
- expect(true).toBe(true);
6
- });
7
- });
@@ -1,96 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import path from "node:path";
4
- import { fileURLToPath } from "node:url";
5
- import { access, readdir, readFile } from "node:fs/promises";
6
-
7
- const __filename = fileURLToPath(import.meta.url);
8
- const __dirname = path.dirname(__filename);
9
- const APP_ROOT = path.resolve(__dirname, "../..");
10
-
11
- const EXPECTED_RUNTIME_DEPENDENCIES = Object.freeze([
12
- "@jskit-ai/app-scripts",
13
- "@jskit-ai/server-runtime-core",
14
- "fastify",
15
- "vue"
16
- ]);
17
-
18
- const EXPECTED_DEV_DEPENDENCIES = Object.freeze([
19
- "@jskit-ai/config-eslint",
20
- "@jskit-ai/jskit",
21
- "@vitejs/plugin-vue",
22
- "eslint",
23
- "vite",
24
- "vitest"
25
- ]);
26
-
27
- const EXPECTED_TOP_LEVEL_ENTRIES = Object.freeze([
28
- "Procfile",
29
- "README.md",
30
- "app.scripts.config.mjs",
31
- "bin",
32
- "eslint.config.mjs",
33
- "favicon.svg",
34
- "gitignore",
35
- "index.html",
36
- "package.json",
37
- "package.json.ACTUAL_CORRECT",
38
- "server.js",
39
- "server",
40
- "src",
41
- "tests",
42
- "vite.config.mjs",
43
- "vite.shared.mjs"
44
- ]);
45
-
46
- async function readPackageJson() {
47
- const packageJsonPath = path.join(APP_ROOT, "package.json");
48
- const raw = await readFile(packageJsonPath, "utf8");
49
- return JSON.parse(raw);
50
- }
51
-
52
- function sortStrings(values) {
53
- return [...values].sort((left, right) => left.localeCompare(right));
54
- }
55
-
56
- function sortedKeys(record) {
57
- return sortStrings(Object.keys(record || {}));
58
- }
59
-
60
- async function readTopLevelEntries() {
61
- const entries = await readdir(APP_ROOT, { withFileTypes: true });
62
- const ignored = new Set([
63
- "node_modules",
64
- "dist",
65
- "coverage",
66
- "test-results",
67
- "package-lock.json",
68
- "pnpm-lock.yaml",
69
- "yarn.lock"
70
- ]);
71
- return entries
72
- .map((entry) => entry.name)
73
- .filter((name) => !name.startsWith("."))
74
- .filter((name) => !ignored.has(name));
75
- }
76
-
77
- test("minimal shell keeps strict dependency allowlist", async () => {
78
- const packageJson = await readPackageJson();
79
- assert.deepEqual(
80
- sortedKeys(packageJson.dependencies),
81
- [...EXPECTED_RUNTIME_DEPENDENCIES].sort((left, right) => left.localeCompare(right))
82
- );
83
- assert.deepEqual(
84
- sortedKeys(packageJson.devDependencies),
85
- [...EXPECTED_DEV_DEPENDENCIES].sort((left, right) => left.localeCompare(right))
86
- );
87
- });
88
-
89
- test("starter shell keeps a strict top-level footprint", async () => {
90
- const entries = await readTopLevelEntries();
91
- assert.deepEqual(sortStrings(entries), sortStrings(EXPECTED_TOP_LEVEL_ENTRIES));
92
- });
93
-
94
- test("legacy app.manifest scaffold is removed from starter shell", async () => {
95
- await assert.rejects(access(path.join(APP_ROOT, "framework/app.manifest.mjs")), /ENOENT/);
96
- });
@@ -1,19 +0,0 @@
1
- import assert from "node:assert/strict";
2
- import test from "node:test";
3
- import { createServer } from "../../server.js";
4
-
5
- test("GET /api/v1/health returns ok payload", async () => {
6
- const app = await createServer();
7
- const response = await app.inject({
8
- method: "GET",
9
- url: "/api/v1/health"
10
- });
11
-
12
- assert.equal(response.statusCode, 200);
13
- assert.deepEqual(response.json(), {
14
- ok: true,
15
- app: "__APP_NAME__"
16
- });
17
-
18
- await app.close();
19
- });
@@ -1,43 +0,0 @@
1
- import { defineConfig } from "vite";
2
- import vue from "@vitejs/plugin-vue";
3
- import { toPositiveInt } from "./vite.shared.mjs";
4
-
5
- const devPort = toPositiveInt(process.env.VITE_DEV_PORT, 5173);
6
- const apiProxyTarget = String(process.env.VITE_API_PROXY_TARGET || "").trim() || "http://localhost:3000";
7
- const clientEntry = (() => {
8
- const normalized = String(process.env.VITE_CLIENT_ENTRY || "").trim();
9
- if (!normalized) {
10
- return "/src/main.js";
11
- }
12
- if (normalized.startsWith("/")) {
13
- return normalized;
14
- }
15
- if (normalized.startsWith("src/")) {
16
- return `/${normalized}`;
17
- }
18
- return `/src/${normalized}`;
19
- })();
20
-
21
- export default defineConfig({
22
- plugins: [
23
- vue(),
24
- {
25
- name: "jskit-client-entry",
26
- transformIndexHtml(source) {
27
- return String(source || "").replace(/\/src\/main\.js/g, clientEntry);
28
- }
29
- }
30
- ],
31
- test: {
32
- include: ["tests/client/**/*.vitest.js"]
33
- },
34
- server: {
35
- port: devPort,
36
- proxy: {
37
- "/api": {
38
- target: apiProxyTarget,
39
- changeOrigin: true
40
- }
41
- }
42
- }
43
- });
@@ -1,9 +0,0 @@
1
- function toPositiveInt(value, fallback) {
2
- const parsed = Number.parseInt(String(value || "").trim(), 10);
3
- if (Number.isInteger(parsed) && parsed > 0) {
4
- return parsed;
5
- }
6
- return fallback;
7
- }
8
-
9
- export { toPositiveInt };