create-prisma-php-app 4.0.0-alpha.2 → 4.0.0-alpha.21

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 (59) hide show
  1. package/dist/.htaccess +54 -41
  2. package/dist/bootstrap.php +143 -98
  3. package/dist/index.js +264 -99
  4. package/dist/settings/auto-swagger-docs.ts +196 -95
  5. package/dist/settings/bs-config.ts +56 -58
  6. package/dist/settings/files-list.json +1 -1
  7. package/dist/settings/restart-mcp.ts +58 -0
  8. package/dist/settings/restart-websocket.ts +51 -45
  9. package/dist/settings/utils.ts +240 -0
  10. package/dist/src/Lib/AI/ChatGPTClient.php +147 -0
  11. package/dist/src/Lib/Auth/Auth.php +544 -0
  12. package/dist/src/Lib/Auth/AuthConfig.php +89 -0
  13. package/dist/src/Lib/CacheHandler.php +121 -0
  14. package/dist/src/Lib/ErrorHandler.php +322 -0
  15. package/dist/src/Lib/FileManager/UploadFile.php +383 -0
  16. package/dist/src/Lib/Headers/Boom.php +192 -0
  17. package/dist/src/Lib/IncludeTracker.php +59 -0
  18. package/dist/src/Lib/MCP/WeatherTools.php +104 -0
  19. package/dist/src/Lib/MCP/mcp-server.php +80 -0
  20. package/dist/src/Lib/MainLayout.php +230 -0
  21. package/dist/src/Lib/Middleware/AuthMiddleware.php +154 -0
  22. package/dist/src/Lib/Middleware/CorsMiddleware.php +145 -0
  23. package/dist/src/Lib/PHPMailer/Mailer.php +169 -0
  24. package/dist/src/Lib/PHPX/Exceptions/ComponentValidationException.php +49 -0
  25. package/dist/src/Lib/PHPX/Fragment.php +32 -0
  26. package/dist/src/Lib/PHPX/IPHPX.php +22 -0
  27. package/dist/src/Lib/PHPX/PHPX.php +287 -0
  28. package/dist/src/Lib/PHPX/TemplateCompiler.php +641 -0
  29. package/dist/src/Lib/PHPX/TwMerge.php +346 -0
  30. package/dist/src/Lib/PHPX/TypeCoercer.php +490 -0
  31. package/dist/src/Lib/PartialRenderer.php +40 -0
  32. package/dist/src/Lib/PrismaPHPSettings.php +181 -0
  33. package/dist/src/Lib/Request.php +479 -0
  34. package/dist/src/Lib/Security/RateLimiter.php +33 -0
  35. package/dist/src/Lib/Set.php +102 -0
  36. package/dist/src/Lib/StateManager.php +127 -0
  37. package/dist/src/Lib/Validator.php +752 -0
  38. package/dist/src/{Websocket → Lib/Websocket}/ConnectionManager.php +1 -1
  39. package/dist/src/Lib/Websocket/websocket-server.php +118 -0
  40. package/dist/src/app/error.php +1 -1
  41. package/dist/src/app/index.php +24 -5
  42. package/dist/src/app/js/index.js +1 -1
  43. package/dist/src/app/layout.php +2 -2
  44. package/package.json +1 -1
  45. package/dist/settings/restart-websocket.bat +0 -28
  46. package/dist/src/app/assets/images/prisma-php-black.svg +0 -6
  47. package/dist/websocket-server.php +0 -22
  48. package/vendor/autoload.php +0 -25
  49. package/vendor/composer/ClassLoader.php +0 -579
  50. package/vendor/composer/InstalledVersions.php +0 -359
  51. package/vendor/composer/LICENSE +0 -21
  52. package/vendor/composer/autoload_classmap.php +0 -10
  53. package/vendor/composer/autoload_namespaces.php +0 -9
  54. package/vendor/composer/autoload_psr4.php +0 -10
  55. package/vendor/composer/autoload_real.php +0 -38
  56. package/vendor/composer/autoload_static.php +0 -25
  57. package/vendor/composer/installed.json +0 -825
  58. package/vendor/composer/installed.php +0 -132
  59. package/vendor/composer/platform_check.php +0 -26
@@ -323,18 +323,153 @@ function generateSwaggerAnnotation(modelName: string, fields: Field[]): string {
323
323
  */`;
324
324
  }
325
325
 
326
- // Function to generate dynamic ID validation logic for update and find-by-ID routes
327
- function generateIdValidationLogic(idField: any) {
328
- const fieldType = idField.type.toLowerCase();
326
+ function isRequiredOnCreate(field: Field): boolean {
327
+ // Required if Prisma says required AND no DB default AND not generated/readOnly/updatedAt/id
328
+ return (
329
+ field.isRequired &&
330
+ !field.hasDefaultValue &&
331
+ !field.isGenerated &&
332
+ !field.isUpdatedAt &&
333
+ !field.isId &&
334
+ !field.isReadOnly
335
+ );
336
+ }
337
+
338
+ function phpRuleBodyForType(prismaTypeLower: string): string {
339
+ switch (prismaTypeLower) {
340
+ case "boolean":
341
+ return `
342
+ $b = Validator::boolean($v);
343
+ if ($b === null) return false;
344
+ $out = (bool)$b;
345
+ return true;`;
346
+
347
+ case "int":
348
+ case "bigint":
349
+ return `
350
+ $i = Validator::int($v);
351
+ if ($i === null) return false;
352
+ $out = $i;
353
+ return true;`;
354
+
355
+ case "float":
356
+ return `
357
+ $f = Validator::float($v);
358
+ if ($f === null) return false;
359
+ $out = $f;
360
+ return true;`;
361
+
362
+ case "decimal":
363
+ return `
364
+ $d = Validator::decimal($v);
365
+ if ($d === null) return false;
366
+ $out = (string)$d; // keep decimals canonical as string
367
+ return true;`;
368
+
369
+ case "datetime":
370
+ return `
371
+ $dt = Validator::dateTime($v, 'Y-m-d H:i:s');
372
+ if ($dt === null) return false;
373
+ $out = $dt;
374
+ return true;`;
375
+
376
+ case "json":
377
+ return `
378
+ if (is_string($v)) {
379
+ json_decode($v);
380
+ if (json_last_error() !== JSON_ERROR_NONE) return false;
381
+ $out = $v;
382
+ return true;
383
+ } else {
384
+ $enc = json_encode($v, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
385
+ if ($enc === false) return false;
386
+ $out = $enc;
387
+ return true;
388
+ }`;
389
+
390
+ case "uuid":
391
+ return `
392
+ $s = Validator::uuid($v);
393
+ if ($s === null) return false;
394
+ $out = $s;
395
+ return true;`;
396
+
397
+ case "cuid":
398
+ return `
399
+ $s = Validator::cuid($v);
400
+ if ($s === null) return false;
401
+ $out = $s;
402
+ return true;`;
403
+
404
+ case "cuid2":
405
+ return `
406
+ $s = Validator::cuid2($v);
407
+ if ($s === null) return false;
408
+ $out = $s;
409
+ return true;`;
410
+
411
+ case "string":
412
+ default:
413
+ return `
414
+ $s = Validator::string($v, false); // trim, no HTML escaping for DB
415
+ if ($s === '') return false;
416
+ $out = $s;
417
+ return true;`;
418
+ }
419
+ }
420
+
421
+ function generatePhpSchema(fields: Field[], forUpdate: boolean): string {
422
+ const entries = fields
423
+ .filter((f) => !shouldSkipField(f))
424
+ .map((f) => {
425
+ const t = f.type.toLowerCase();
426
+ const required = forUpdate ? false : isRequiredOnCreate(f);
427
+ const body = phpRuleBodyForType(t).trim();
428
+ return ` '${f.name}' => [
429
+ 'type' => '${t}',
430
+ 'required' => ${required ? "true" : "false"},
431
+ 'validate' => function($v, &$out) {
432
+ ${body}
433
+ },
434
+ ]`;
435
+ })
436
+ .join(",\n");
437
+ return `[\n${entries}\n]`;
438
+ }
329
439
 
330
- if (["cuid", "uuid", "autoincrement"].includes(idField.default?.name)) {
440
+ function idValidatorSnippet(idField: Field): string {
441
+ const t = idField.type.toLowerCase();
442
+ const def = (idField as any).default?.name?.toLowerCase?.() || "";
443
+
444
+ // numeric ids (int/bigint or autoincrement())
445
+ if (t === "int" || t === "bigint" || def === "autoincrement") {
446
+ return `
447
+ $__id = Validator::int($id);
448
+ if ($__id === null) { Boom::badRequest("Invalid ${idField.name}")->toResponse(); return; }
449
+ $id = $__id;`;
450
+ }
451
+
452
+ // uuid() default or explicit UUID type
453
+ if (t === "uuid" || def === "uuid") {
331
454
  return `
332
- if (!Validator::${fieldType}($id)) {
333
- Boom::badRequest("Invalid ${idField.name}")->toResponse();
334
- }`;
455
+ if (Validator::uuid($id) === null) { Boom::badRequest("Invalid ${idField.name}")->toResponse(); return; }`;
335
456
  }
336
457
 
337
- return ""; // No specific validation needed otherwise
458
+ // cuid() / cuid2() defaults
459
+ if (def === "cuid") {
460
+ return `
461
+ if (Validator::cuid($id) === null) { Boom::badRequest("Invalid ${idField.name}")->toResponse(); return; }`;
462
+ }
463
+ if (def === "cuid2") {
464
+ return `
465
+ if (Validator::cuid2($id) === null) { Boom::badRequest("Invalid ${idField.name}")->toResponse(); return; }`;
466
+ }
467
+
468
+ // fallback: non-empty string
469
+ return `
470
+ $__id = Validator::string($id, false);
471
+ if ($__id === '') { Boom::badRequest("Invalid ${idField.name}")->toResponse(); return; }
472
+ $id = $__id;`;
338
473
  }
339
474
 
340
475
  // Function to generate endpoints for a model
@@ -372,7 +507,7 @@ function generateEndpoints(modelName: string, fields: any[]): void {
372
507
  const idDir = `${baseDir}/[id]`;
373
508
  mkdirSync(resolve(__dirname, `../${idDir}`), { recursive: true });
374
509
  const idRoutePath = `${idDir}/route.php`;
375
- const idValidationLogic = generateIdValidationLogic(idField);
510
+ const idCheck = idValidatorSnippet(idField);
376
511
  const idRouteContent = `<?php
377
512
 
378
513
  use Lib\\Prisma\\Classes\\Prisma;
@@ -382,7 +517,7 @@ use Lib\\Request;
382
517
 
383
518
  $prisma = Prisma::getInstance();
384
519
  $id = Request::$dynamicParams->id ?? null;
385
- ${idValidationLogic}
520
+ ${idCheck}
386
521
 
387
522
  $${camelCaseModelName} = $prisma->${camelCaseModelName}->findUnique([
388
523
  'where' => [
@@ -406,6 +541,8 @@ echo json_encode($${camelCaseModelName});`;
406
541
  mkdirSync(resolve(__dirname, `../${createDir}`), { recursive: true });
407
542
  const createRoutePath = `${createDir}/route.php`;
408
543
 
544
+ const createSchema = generatePhpSchema(fieldsToCreateAndUpdate, false);
545
+
409
546
  const createRouteContent = `<?php
410
547
 
411
548
  use Lib\\Prisma\\Classes\\Prisma;
@@ -415,56 +552,39 @@ use Lib\\Request;
415
552
 
416
553
  $prisma = Prisma::getInstance();
417
554
 
418
- // Define fields with their types, required status, and validation functions
419
- $fieldsWithTypesAndStatus = [
420
- ${fieldsToCreateAndUpdate
421
- .map(
422
- (field) =>
423
- ` '${field.name}' => [
424
- 'type' => '${field.type.toLowerCase()}',
425
- 'required' => ${field.isRequired ? "true" : "false"},
426
- 'validate' => fn($value) => is_null($value) || $value === '' || Validator::${field.type.toLowerCase()}($value)
427
- ]`
428
- )
429
- .join(",\n")}
430
- ];
555
+ /** Schema: type-aware validate + normalize */
556
+ $schema = ${createSchema};
431
557
 
432
558
  $data = [];
433
- foreach ($fieldsWithTypesAndStatus as $field => $details) {
434
- $isRequired = $details['required'];
435
- $type = $details['type'];
436
- $validationFn = $details['validate'];
437
-
438
- // Check if the field is required and missing in the request
439
- if ($isRequired && !isset(Request::$params->$field)) {
440
- Boom::badRequest("Missing {$field}")->toResponse();
441
- }
442
-
443
- // Check if the field is present in the request
444
- if (isset(Request::$params->$field)) {
445
- $value = Request::$params->$field;
446
-
447
- // Validate the field using the validation function
448
- if (!$validationFn($value)) {
449
- Boom::badRequest("Invalid {$field}", ["Expected type '{$type}'"])->toResponse();
559
+ foreach ($schema as $field => $rule) {
560
+ $isRequired = $rule['required'] ?? false;
561
+
562
+ $has = is_object(Request::$params) && property_exists(Request::$params, $field);
563
+ if (!$has) {
564
+ if ($isRequired) {
565
+ Boom::badRequest("Missing {$field}")->toResponse();
566
+ return;
450
567
  }
568
+ continue;
569
+ }
451
570
 
452
- // Assign the validated value to the data array
453
- $data[$field] = $value;
571
+ $raw = Request::$params->$field;
572
+ $out = null;
573
+ if (!($rule['validate'])($raw, $out)) {
574
+ $type = $rule['type'] ?? 'unknown';
575
+ Boom::badRequest("Invalid {$field}", ["Expected type '{$type}'"])->toResponse();
576
+ return;
454
577
  }
578
+ $data[$field] = $out;
455
579
  }
456
580
 
457
- // Create the new record using the Prisma instance
458
- $new${modelName} = $prisma->${camelCaseModelName}->create([
459
- 'data' => $data
460
- ]);
581
+ $new${modelName} = $prisma->${camelCaseModelName}->create(['data' => $data]);
461
582
 
462
- // Handle potential internal server error
463
583
  if (!$new${modelName}) {
464
584
  Boom::internal()->toResponse();
585
+ return;
465
586
  }
466
587
 
467
- // Return the newly created record in JSON format
468
588
  echo json_encode($new${modelName});`;
469
589
 
470
590
  writeFileSync(
@@ -478,6 +598,8 @@ echo json_encode($new${modelName});`;
478
598
  mkdirSync(resolve(__dirname, `../${updateDir}`), { recursive: true });
479
599
  const updateRoutePath = `${updateDir}/route.php`;
480
600
 
601
+ const updateSchema = generatePhpSchema(fieldsToCreateAndUpdate, true);
602
+
481
603
  const updateRouteContent = `<?php
482
604
 
483
605
  use Lib\\Prisma\\Classes\\Prisma;
@@ -487,65 +609,44 @@ use Lib\\Request;
487
609
 
488
610
  $prisma = Prisma::getInstance();
489
611
  $id = Request::$dynamicParams->id ?? null;
612
+ ${idCheck}
490
613
 
491
- // Perform validation for the ID
492
- if (!Validator::int($id)) {
493
- Boom::badRequest("Invalid id")->toResponse();
494
- }
495
-
496
- // Define fields with their types, required status, and validation functions
497
- $fieldsWithTypesAndStatus = [
498
- ${fieldsToCreateAndUpdate
499
- .map(
500
- (field) =>
501
- ` '${field.name}' => [
502
- 'type' => '${field.type.toLowerCase()}',
503
- 'required' => ${field.isRequired ? "true" : "false"},
504
- 'validate' => fn($value) => is_null($value) || $value === '' || Validator::${field.type.toLowerCase()}($value)
505
- ]`
506
- )
507
- .join(",\n")}
508
- ];
509
-
614
+ /** Partial update: nothing is required, but at least one field must be present */
615
+ $schema = ${updateSchema};
510
616
  $data = [];
511
- foreach ($fieldsWithTypesAndStatus as $field => $details) {
512
- $isRequired = $details['required'];
513
- $type = $details['type'];
514
- $validationFn = $details['validate'];
515
-
516
- // Check if the field is required and missing in the request
517
- if ($isRequired && !isset(Request::$params->$field)) {
518
- Boom::badRequest("Missing {$field}")->toResponse();
617
+ $any = false;
618
+
619
+ foreach ($schema as $field => $rule) {
620
+ $has = is_object(Request::$params) && property_exists(Request::$params, $field);
621
+ if (!$has) continue;
622
+
623
+ $raw = Request::$params->$field;
624
+ $out = null;
625
+ if (!($rule['validate'])($raw, $out)) {
626
+ $type = $rule['type'] ?? 'unknown';
627
+ Boom::badRequest("Invalid {$field}", ["Expected type '{$type}'"])->toResponse();
628
+ return;
519
629
  }
630
+ $data[$field] = $out;
631
+ $any = true;
632
+ }
520
633
 
521
- // Check if the field is present in the request
522
- if (isset(Request::$params->$field)) {
523
- $value = Request::$params->$field;
524
-
525
- // Validate the field using the validation function
526
- if (!$validationFn($value)) {
527
- Boom::badRequest("Invalid {$field}", ["Expected type '{$type}'"])->toResponse();
528
- }
529
-
530
- // Assign the validated value to the data array
531
- $data[$field] = $value;
532
- }
634
+ if (!$any) {
635
+ Boom::badRequest("No fields to update")->toResponse();
636
+ return;
533
637
  }
534
638
 
535
- // Update the record
536
639
  $updated${modelName} = $prisma->${camelCaseModelName}->update([
537
640
  'where' => ['${idFieldName}' => $id],
538
- 'data' => $data,
641
+ 'data' => $data,
539
642
  ]);
540
643
 
541
- // Handle potential internal server error
542
644
  if (!$updated${modelName}) {
543
645
  Boom::notFound()->toResponse();
646
+ return;
544
647
  }
545
648
 
546
- // Return the updated record in JSON format
547
- echo json_encode($updated${modelName});
548
- `;
649
+ echo json_encode($updated${modelName});`;
549
650
 
550
651
  writeFileSync(
551
652
  resolve(__dirname, `../${updateRoutePath}`),
@@ -566,7 +667,7 @@ use Lib\\Request;
566
667
 
567
668
  $prisma = Prisma::getInstance();
568
669
  $id = Request::$dynamicParams->id ?? null;
569
- ${idValidationLogic}
670
+ ${idCheck}
570
671
 
571
672
  $deleted${modelName} = $prisma->${camelCaseModelName}->delete([
572
673
  'where' => [
@@ -708,7 +809,7 @@ await generateSwaggerDocs(modelsToGenerate);
708
809
  await swaggerConfig();
709
810
 
710
811
  if (prismaSchemaConfigJson.generatePhpClasses) {
711
- spawn("npx", ["php", "generate", "class"], {
812
+ spawn("npx", ["ppo", "generate"], {
712
813
  stdio: "inherit",
713
814
  shell: true,
714
815
  });
@@ -1,6 +1,5 @@
1
1
  import { createProxyMiddleware } from "http-proxy-middleware";
2
2
  import { writeFileSync } from "fs";
3
- import chokidar from "chokidar";
4
3
  import browserSync, { BrowserSyncInstance } from "browser-sync";
5
4
  import prismaPhpConfigJson from "../prisma-php.json";
6
5
  import { generateFileListJson } from "./files-list.js";
@@ -14,75 +13,79 @@ import {
14
13
  updateComponentImports,
15
14
  } from "./class-imports";
16
15
  import { checkComponentImports } from "./component-import-checker";
16
+ import { DebouncedWorker, createSrcWatcher, DEFAULT_AWF } from "./utils.js";
17
17
 
18
18
  const { __dirname } = getFileMeta();
19
19
 
20
20
  const bs: BrowserSyncInstance = browserSync.create();
21
21
 
22
- // Watch for file changes (create, delete, save)
23
- const watcher = chokidar.watch("src/app/**/*", {
24
- ignored: /(^|[\/\\])\../, // Ignore dotfiles
25
- persistent: true,
26
- usePolling: true,
27
- interval: 1000,
28
- });
22
+ // ---------- Watcher (whole ./src) ----------
23
+ const pipeline = new DebouncedWorker(
24
+ async () => {
25
+ await generateFileListJson();
26
+ await updateAllClassLogs();
27
+ await updateComponentImports();
29
28
 
30
- // On changes, generate file list and also update the class log
31
- const handleFileChange = async () => {
32
- await generateFileListJson();
33
- await updateAllClassLogs();
34
- await updateComponentImports();
29
+ // Scan all PHP files in the whole SRC tree
30
+ const phpFiles = await getAllPhpFiles(SRC_DIR);
31
+ for (const file of phpFiles) {
32
+ const rawFileImports = await analyzeImportsInFile(file);
35
33
 
36
- // Optionally, run the component check on each PHP file.
37
- const phpFiles = await getAllPhpFiles(SRC_DIR + "/app");
38
- for (const file of phpFiles) {
39
- const rawFileImports = await analyzeImportsInFile(file);
40
- // Convert Record<string, string> to Record<string, { className: string; filePath: string; importer?: string }[]>
41
- const fileImports: Record<
42
- string,
43
- | { className: string; filePath: string; importer?: string }[]
44
- | { className: string; filePath: string; importer?: string }
45
- > = {};
46
- for (const key in rawFileImports) {
47
- if (typeof rawFileImports[key] === "string") {
48
- fileImports[key] = [
49
- {
50
- className: key,
51
- filePath: rawFileImports[key],
52
- },
53
- ];
54
- } else {
55
- fileImports[key] = rawFileImports[key];
34
+ // Normalize to array-of-objects shape expected by the checker
35
+ const fileImports: Record<
36
+ string,
37
+ { className: string; filePath: string; importer?: string }[]
38
+ > = {};
39
+ for (const key in rawFileImports) {
40
+ const v = rawFileImports[key];
41
+ fileImports[key] = Array.isArray(v)
42
+ ? v
43
+ : [{ className: key, filePath: v }];
56
44
  }
45
+ await checkComponentImports(file, fileImports);
57
46
  }
58
- await checkComponentImports(file, fileImports);
59
- }
60
- };
61
47
 
62
- // Perform specific actions for file events
63
- watcher
64
- .on("add", handleFileChange)
65
- .on("change", handleFileChange)
66
- .on("unlink", handleFileChange);
48
+ // Reload after tasks complete
49
+ bs.reload();
50
+ },
51
+ 350,
52
+ "bs-pipeline"
53
+ );
54
+
55
+ // watch the entire src; we don’t need an extension filter here
56
+ createSrcWatcher(join(SRC_DIR, "**", "*"), {
57
+ onEvent: (_ev, _abs, rel) => pipeline.schedule(rel),
58
+ awaitWriteFinish: DEFAULT_AWF,
59
+ logPrefix: "watch",
60
+ usePolling: true,
61
+ interval: 1000,
62
+ });
67
63
 
68
- // BrowserSync initialization
64
+ // ---------- BrowserSync ----------
69
65
  bs.init(
70
66
  {
67
+ /**
68
+ * Proxy your PHP app (from prisma-php.json).
69
+ * Use object form to enable WebSocket proxying.
70
+ */
71
71
  proxy: "http://localhost:3000",
72
+
72
73
  middleware: [
73
- (_: any, res: any, next: any) => {
74
+ (_req, res, next) => {
74
75
  res.setHeader("Cache-Control", "no-cache, no-store, must-revalidate");
75
76
  res.setHeader("Pragma", "no-cache");
76
77
  res.setHeader("Expires", "0");
77
78
  next();
78
79
  },
80
+
79
81
  createProxyMiddleware({
80
82
  target: prismaPhpConfigJson.bsTarget,
81
83
  changeOrigin: true,
82
84
  pathRewrite: {},
83
85
  }),
84
86
  ],
85
- files: "src/**/*.*",
87
+
88
+ files: `${SRC_DIR}/**/*.*`, // still do file-level reloads as a safety net
86
89
  notify: false,
87
90
  open: false,
88
91
  ghostMode: false,
@@ -98,24 +101,19 @@ bs.init(
98
101
  return;
99
102
  }
100
103
 
101
- // Retrieve the active URLs from the BrowserSync instance
102
- const options = bsInstance.getOption("urls");
103
- const localUrl = options.get("local");
104
- const externalUrl = options.get("external");
105
- const uiUrl = options.get("ui");
106
- const uiExternalUrl = options.get("ui-external");
107
-
108
- // Construct the URLs dynamically
109
- const urls = {
110
- local: localUrl,
111
- external: externalUrl,
112
- ui: uiUrl,
113
- uiExternal: uiExternalUrl,
104
+ // Write live URLs for other tooling
105
+ const urls = bsInstance.getOption("urls");
106
+ const out = {
107
+ local: urls.get("local"),
108
+ external: urls.get("external"),
109
+ ui: urls.get("ui"),
110
+ uiExternal: urls.get("ui-external"),
114
111
  };
115
112
 
116
113
  writeFileSync(
117
114
  join(__dirname, "bs-config.json"),
118
- JSON.stringify(urls, null, 2)
115
+ JSON.stringify(out, null, 2)
119
116
  );
117
+ console.log("\n\x1b[90mPress Ctrl+C to stop.\x1b[0m\n");
120
118
  }
121
119
  );
@@ -1 +1 @@
1
- []
1
+ []
@@ -0,0 +1,58 @@
1
+ import { join } from "path";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname } from "path";
4
+ import {
5
+ createRestartableProcess,
6
+ createSrcWatcher,
7
+ DebouncedWorker,
8
+ DEFAULT_AWF,
9
+ onExit,
10
+ } from "./utils.js";
11
+
12
+ const __filename = fileURLToPath(import.meta.url);
13
+ const __dirname = dirname(__filename);
14
+
15
+ const phpPath = process.env.PHP_PATH || "php";
16
+ const serverScriptPath = join(
17
+ __dirname,
18
+ "..",
19
+ "src",
20
+ "Lib",
21
+ "MCP",
22
+ "mcp-server.php"
23
+ );
24
+ const watchRoot = join(__dirname, "..", "src");
25
+
26
+ // Restartable MCP server
27
+ const mcp = createRestartableProcess({
28
+ name: "MCP",
29
+ cmd: phpPath,
30
+ args: [serverScriptPath],
31
+ windowsKillTree: true,
32
+ });
33
+
34
+ mcp.start();
35
+
36
+ // Debounced restarter
37
+ const restarter = new DebouncedWorker(
38
+ async () => {
39
+ await mcp.restart("file change");
40
+ },
41
+ 250,
42
+ "mcp-restart"
43
+ );
44
+
45
+ // Watch ./src for relevant changes
46
+ createSrcWatcher(watchRoot, {
47
+ exts: [".php", ".ts", ".js", ".json"],
48
+ onEvent: (ev, _abs, rel) => restarter.schedule(`${ev}: ${rel}`),
49
+ awaitWriteFinish: DEFAULT_AWF,
50
+ logPrefix: "MCP watch",
51
+ usePolling: true,
52
+ interval: 1000,
53
+ });
54
+
55
+ // Graceful shutdown
56
+ onExit(async () => {
57
+ await mcp.stop();
58
+ });