@serverkgg/bridge 0.1.3 → 0.1.5

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 (78) hide show
  1. package/package.json +1 -1
  2. package/rules/package.md +5 -0
  3. package/src/bot/botCauses.test.ts +284 -0
  4. package/src/bot/botCauses.ts +58 -0
  5. package/src/bot/botDeploy.test.ts +260 -1
  6. package/src/bot/botDeploy.ts +125 -6
  7. package/src/bot/botState.ts +6 -0
  8. package/src/bot/detect/botInspectWarning.ts +3 -1
  9. package/src/bot/detect/botPackageManager.ts +3 -0
  10. package/src/bot/detect/corpus/alexmubarakshin-ts-telegraf-boilerplate.json +37 -0
  11. package/src/bot/detect/corpus/alsarmad-discord-quran-bot.json +40 -0
  12. package/src/bot/detect/corpus/anasty17-mirror-leech-telegram-bot.json +50 -0
  13. package/src/bot/detect/corpus/anidiotsguide-guidebot.json +36 -0
  14. package/src/bot/detect/corpus/anonymousx1025-anonxmusic.json +45 -0
  15. package/src/bot/detect/corpus/bot-base-telegram-bot-template.json +35 -0
  16. package/src/bot/detect/corpus/bracketed-templates-discordjs.json +40 -0
  17. package/src/bot/detect/corpus/cog-creators-red-discordbot.json +49 -0
  18. package/src/bot/detect/corpus/cranberriez-discord.ts-bot-template.json +35 -0
  19. package/src/bot/detect/corpus/crazycat256-nextcord-bot-template.json +38 -0
  20. package/src/bot/detect/corpus/discord-tickets-bot.json +45 -0
  21. package/src/bot/detect/corpus/edjopato-telegram-typescript-bot-template.json +37 -0
  22. package/src/bot/detect/corpus/haniali3161-bot-adhkar-telegram.json +33 -0
  23. package/src/bot/detect/corpus/hellosverre-discord-ts-template.json +34 -0
  24. package/src/bot/detect/corpus/kevinnovak-discord-bot-typescript-template.json +43 -0
  25. package/src/bot/detect/corpus/kkrypt0nn-python-discord-bot-template.json +41 -0
  26. package/src/bot/detect/corpus/kolezka-telegram-grammy-template.json +37 -0
  27. package/src/bot/detect/corpus/latand-tgbot_template_v3.json +37 -0
  28. package/src/bot/detect/corpus/mahmoudbm77-zekr-telegram-bot.json +36 -0
  29. package/src/bot/detect/corpus/mericcan41-discordjs-v14-template-ts.json +33 -0
  30. package/src/bot/detect/corpus/modmail-dev-modmail.json +52 -0
  31. package/src/bot/detect/corpus/nezdemkovski-telegram-bot-boilerplate.json +39 -0
  32. package/src/bot/detect/corpus/notchum-discord-bot-template.json +39 -0
  33. package/src/bot/detect/corpus/python-discord-bot.json +44 -0
  34. package/src/bot/detect/corpus/python-telegram-bot-rules-bot.json +39 -0
  35. package/src/bot/detect/corpus/rajdave69-pycord-bot-template.json +31 -0
  36. package/src/bot/detect/corpus/rapptz-robodanny.json +36 -0
  37. package/src/bot/detect/corpus/ry2x-typescript-discord.js-v.14-template.json +35 -0
  38. package/src/bot/detect/corpus/saiteja-madha-discord-js-bot.json +45 -0
  39. package/src/bot/detect/corpus/seegreatness-discord.js-for-heroku.json +37 -0
  40. package/src/bot/detect/corpus/serverk-discord-node.json +36 -0
  41. package/src/bot/detect/corpus/serverk-discord-python.json +35 -0
  42. package/src/bot/detect/corpus/serverk-telegram-node.json +36 -0
  43. package/src/bot/detect/corpus/serverk-telegram-python.json +35 -0
  44. package/src/bot/detect/corpus/shuruhatik-quran-radio-bot-discord.json +33 -0
  45. package/src/bot/detect/corpus/skyra-project-skyra.json +46 -0
  46. package/src/bot/detect/corpus/tgbot-collection-ytdlbot.json +41 -0
  47. package/src/bot/detect/corpus/zeriodev-music-bot.json +38 -0
  48. package/src/bot/detect/detect.corpus.test.ts +77 -0
  49. package/src/bot/detect/detect.test.ts +414 -17
  50. package/src/bot/detect/detect.ts +347 -36
  51. package/src/bot/detect/detectResult.ts +4 -0
  52. package/src/bot/detect/detectTable.test.ts +22 -6
  53. package/src/bot/detect/detectTable.ts +187 -19
  54. package/src/bot/detect/detectedVersion.ts +5 -0
  55. package/src/bot/detect/envExample.test.ts +55 -1
  56. package/src/bot/detect/envExample.ts +14 -0
  57. package/src/bot/detect/index.ts +1 -0
  58. package/src/bot/detect/keys.test.ts +32 -1
  59. package/src/bot/detect/keys.ts +15 -0
  60. package/src/bot/detect/serverkJson.test.ts +2 -0
  61. package/src/bot/detect/serverkJson.ts +4 -1
  62. package/src/bot/index.ts +1 -0
  63. package/src/cli/validate.test.ts +127 -1
  64. package/src/cli/validate.ts +42 -0
  65. package/src/manifest/manifest.ts +2 -1
  66. package/src/modules/bridge.ts +2 -0
  67. package/src/modules/bridgeModule.ts +5 -0
  68. package/src/protocol/bridgeCrash.ts +49 -0
  69. package/src/protocol/bridgeEvents.ts +9 -0
  70. package/src/protocol/bridgePreview.ts +1 -0
  71. package/src/rcon/rconAccess.ts +1 -1
  72. package/src/runtime/bridgeDriver.test.ts +45 -0
  73. package/src/runtime/bridgeDriver.ts +25 -0
  74. package/src/runtime/bridgeEvents.ts +2 -0
  75. package/src/runtime/bridgeRegistry.test.ts +32 -0
  76. package/src/runtime/bridgeSupervisor.test.ts +163 -3
  77. package/src/runtime/bridgeSupervisor.ts +42 -1
  78. package/src/runtime/serve.ts +26 -5
@@ -6,8 +6,8 @@ import type { BotPlatform } from "./botPlatform";
6
6
  import { BOT_RUNTIME_PACKAGES, BotRuntime } from "./botRuntime";
7
7
  import { BOT_TOKEN_KEYS, platformOfKey } from "./botToken";
8
8
  import type { DetectEntry, DetectedCommand, DetectedEnvKey, DetectInput, DetectResult } from "./detectResult";
9
- import { DETECT_TABLE } from "./detectTable";
10
- import { ENV_EXAMPLE_FILES, parseEnvExample } from "./envExample";
9
+ import { DETECT_TABLE, type DetectVersionFile } from "./detectTable";
10
+ import { ENV_EXAMPLE_FILES, ENV_FILE, isSecretEnvFile, parseEnvExample } from "./envExample";
11
11
  import { isEnvKey } from "./envKey";
12
12
  import { isNodeSource, isPythonSource, scanEnvKeys } from "./envScan";
13
13
  import { BOT_PROTECTED_KEYS, BOT_PROVIDED_KEYS, isReservedBotKey } from "./keys";
@@ -41,15 +41,33 @@ const VENV_SEGMENTS: ReadonlySet<string> = new Set([
41
41
  "venv",
42
42
  ]);
43
43
 
44
- const ENV_FILE = /^\.env(?:\..+)?$/;
44
+ const DATABASE_FILE = /\.(?:sqlite3?|db)$/i;
45
45
 
46
- const ENV_TEMPLATE_FILES: ReadonlySet<string> = new Set([
47
- ...ENV_EXAMPLE_FILES,
48
- ".env.dist",
49
- ".env.defaults",
46
+ const PYTHON_PACKAGE_MAIN = /^([A-Za-z_]\w*)\/__main__\.py$/;
47
+
48
+ const NOT_A_BOT_PACKAGE: ReadonlySet<string> = new Set([
49
+ "tests",
50
+ "test",
51
+ "docs",
52
+ "examples",
53
+ "scripts",
50
54
  ]);
51
55
 
52
- const TYPESCRIPT_ENTRY = /(?:^|\s)node\s+(?:-\S+\s+)*\S+\.(?:ts|mts|cts)(?:\s|$)/;
56
+ const MANAGER_FIELD = /^(npm|pnpm|yarn|bun)@(\d+)/;
57
+
58
+ const ENGINE_MANAGERS: readonly string[] = [
59
+ "pnpm",
60
+ "bun",
61
+ "yarn",
62
+ ];
63
+
64
+ const YARN_BERRY = "yarn-berry";
65
+
66
+ const VERSION_NUMBER = /^v?(\d+(?:\.\d+)*)/;
67
+
68
+ const RANGE_COMPARATOR = /^(>=|<=|>|<|=|\^|~)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/;
69
+
70
+ const HYPHEN_RANGE = /(\S+)\s+-\s+(\S+)/g;
53
71
 
54
72
  const TOKEN_KEY = /TOKEN/i;
55
73
 
@@ -71,6 +89,14 @@ const MANAGERS: Readonly<Record<string, BotPackageManager>> = {
71
89
  npm: BotPackageManager.Npm,
72
90
  pnpm: BotPackageManager.Pnpm,
73
91
  yarn: BotPackageManager.Yarn,
92
+ [YARN_BERRY]: BotPackageManager.Yarn,
93
+ bun: BotPackageManager.Bun,
94
+ requirements: BotPackageManager.Pip,
95
+ uv: BotPackageManager.Uv,
96
+ poetry: BotPackageManager.Pip,
97
+ pdm: BotPackageManager.Pip,
98
+ pipenv: BotPackageManager.Pipenv,
99
+ project: BotPackageManager.Pip,
74
100
  };
75
101
 
76
102
  export interface PythonRequirement {
@@ -166,6 +192,8 @@ const FIXED_FILES: readonly string[] = [
166
192
  DETECT_TABLE.procfile.file,
167
193
  SERVERK_JSON,
168
194
  ...ENV_EXAMPLE_FILES,
195
+ ...DETECT_TABLE.node.versionFiles.map((entry) => entry.file),
196
+ ...DETECT_TABLE.python.versionFiles.map((entry) => entry.file),
169
197
  ];
170
198
 
171
199
  const sourceRank = (path: string) => {
@@ -425,13 +453,29 @@ const nodeStartOf = (packageJson: PackageJson | null, has: (path: string) => boo
425
453
  : null;
426
454
  };
427
455
 
428
- const pythonStartOf = (has: (path: string) => boolean): DetectedCommand | null => {
456
+ const pythonStartOf = (has: (path: string) => boolean, paths: ReadonlySet<string>): DetectedCommand | null => {
429
457
  const file = DETECT_TABLE.python.startFiles.find(has);
430
458
 
431
- return file
459
+ if (file) {
460
+ return {
461
+ command: `python ${file}`,
462
+ source: file,
463
+ };
464
+ }
465
+
466
+ const packages = [
467
+ ...paths,
468
+ ]
469
+ .map((path) => PYTHON_PACKAGE_MAIN.exec(path)?.[1])
470
+ .filter((name): name is string => name !== undefined && !skipped(name) && !NOT_A_BOT_PACKAGE.has(name))
471
+ .sort(compareCodePoints);
472
+
473
+ const name = packages.length === 1 ? packages[0] : null;
474
+
475
+ return name
432
476
  ? {
433
- command: `python ${file}`,
434
- source: file,
477
+ command: `python -m ${name}`,
478
+ source: `${name}/__main__.py`,
435
479
  }
436
480
  : null;
437
481
  };
@@ -442,6 +486,243 @@ const nodeStartFileOf = (packageJson: PackageJson | null, has: (path: string) =>
442
486
  return (main !== null && has(main)) || DETECT_TABLE.node.startFiles.some(has);
443
487
  };
444
488
 
489
+ interface NodeManager {
490
+ key: string;
491
+ locked: boolean;
492
+ source: string;
493
+ }
494
+
495
+ const lockfileOf = (manager: string, has: (path: string) => boolean) => {
496
+ const lockManager = manager === YARN_BERRY ? "yarn" : manager;
497
+
498
+ return DETECT_TABLE.node.lockfiles.find((entry) => entry.manager === lockManager && has(entry.file)) ?? null;
499
+ };
500
+
501
+ const nodeManagerOf = (packageJson: PackageJson | null, has: (path: string) => boolean): NodeManager => {
502
+ const declared = packageJson?.[DETECT_TABLE.node.managerField];
503
+ const field = typeof declared === "string" ? MANAGER_FIELD.exec(declared.trim()) : null;
504
+
505
+ if (field) {
506
+ const key = field[1] === "yarn" && Number(field[2]) >= 2 ? YARN_BERRY : (field[1] ?? "npm");
507
+ const lock = lockfileOf(key, has);
508
+
509
+ return {
510
+ key,
511
+ locked: lock !== null,
512
+ source: lock?.file ?? DETECT_TABLE.node.manifest,
513
+ };
514
+ }
515
+
516
+ for (const entry of DETECT_TABLE.node.lockfiles) {
517
+ if (entry.manager !== "npm" && has(entry.file)) {
518
+ return {
519
+ key: entry.manager === "yarn" && has(DETECT_TABLE.node.berryFile) ? YARN_BERRY : entry.manager,
520
+ locked: true,
521
+ source: entry.file,
522
+ };
523
+ }
524
+ }
525
+
526
+ if (has(DETECT_TABLE.node.berryFile)) {
527
+ return {
528
+ key: YARN_BERRY,
529
+ locked: false,
530
+ source: DETECT_TABLE.node.berryFile,
531
+ };
532
+ }
533
+
534
+ const engines = recordOf(packageJson?.engines);
535
+ const engine = ENGINE_MANAGERS.find((manager) => typeof engines[manager] === "string") ?? null;
536
+
537
+ if (engine) {
538
+ return {
539
+ key: engine,
540
+ locked: false,
541
+ source: DETECT_TABLE.node.manifest,
542
+ };
543
+ }
544
+
545
+ const npmLock = lockfileOf("npm", has);
546
+
547
+ return {
548
+ key: "npm",
549
+ locked: npmLock !== null,
550
+ source: npmLock?.file ?? DETECT_TABLE.node.manifest,
551
+ };
552
+ };
553
+
554
+ const nodeBuildOf = (packageJson: PackageJson | null, manager: string): DetectedCommand | null => {
555
+ const script = recordOf(packageJson?.scripts)[DETECT_TABLE.node.buildScript];
556
+
557
+ if (typeof script !== "string" || script.trim().length === 0) {
558
+ return null;
559
+ }
560
+
561
+ const run = DETECT_TABLE.node.runCommands[manager] ?? DETECT_TABLE.node.runCommands.npm ?? [];
562
+
563
+ return {
564
+ command: [
565
+ ...run,
566
+ DETECT_TABLE.node.buildScript,
567
+ ].join(" "),
568
+ source: DETECT_TABLE.node.manifest,
569
+ };
570
+ };
571
+
572
+ const versionFromFile = (text: string, spec: DetectVersionFile): string | null => {
573
+ for (const raw of text.split("\n")) {
574
+ let line = raw.trim();
575
+
576
+ if (line.length === 0 || line.startsWith("#")) {
577
+ continue;
578
+ }
579
+
580
+ if (spec.tool !== undefined) {
581
+ const [tool, ...rest] = line.split(/\s+/);
582
+
583
+ if (tool !== spec.tool) {
584
+ continue;
585
+ }
586
+
587
+ line = rest[0] ?? "";
588
+ }
589
+
590
+ if (spec.key !== undefined) {
591
+ const match = new RegExp(`^${spec.key}\\s*=\\s*"?([^"\\s]+)`).exec(line);
592
+
593
+ if (!match) {
594
+ continue;
595
+ }
596
+
597
+ line = match[1] ?? "";
598
+ }
599
+
600
+ if (spec.prefix !== undefined) {
601
+ if (!line.startsWith(spec.prefix)) {
602
+ return null;
603
+ }
604
+
605
+ line = line.slice(spec.prefix.length);
606
+ }
607
+
608
+ return VERSION_NUMBER.exec(line)?.[1] ?? null;
609
+ }
610
+
611
+ return null;
612
+ };
613
+
614
+ const versionFromFiles = (
615
+ specs: readonly DetectVersionFile[],
616
+ read: (path: string) => string | null,
617
+ parts: number,
618
+ ): string | null => {
619
+ for (const spec of specs) {
620
+ const text = read(spec.file);
621
+
622
+ if (text === null) {
623
+ continue;
624
+ }
625
+
626
+ const version = versionFromFile(text, spec);
627
+
628
+ if (version !== null) {
629
+ return version.split(".").slice(0, parts).join(".");
630
+ }
631
+ }
632
+
633
+ return null;
634
+ };
635
+
636
+ const comparatorAdmits = (comparator: string, major: number): boolean | null => {
637
+ const match = RANGE_COMPARATOR.exec(comparator);
638
+
639
+ if (!match) {
640
+ return null;
641
+ }
642
+
643
+ const base = Number(match[2]);
644
+ const minor = match[3];
645
+
646
+ switch (match[1] ?? "") {
647
+ case ">=":
648
+ return major >= base;
649
+ case ">":
650
+ return minor === undefined ? major > base : major >= base;
651
+ case "<":
652
+ return major < base || (major === base && minor !== undefined && minor !== "0");
653
+ case "<=":
654
+ return major <= base;
655
+ default:
656
+ return major === base;
657
+ }
658
+ };
659
+
660
+ const comparatorsOf = (clause: string) => {
661
+ return clause
662
+ .replace(HYPHEN_RANGE, ">=$1 <=$2")
663
+ .trim()
664
+ .split(/\s+/)
665
+ .filter((token) => token.length > 0);
666
+ };
667
+
668
+ const rangeAdmits = (range: string, major: number) => {
669
+ return range.split("||").some((clause) => {
670
+ const verdicts = comparatorsOf(clause).map((token) => comparatorAdmits(token, major));
671
+
672
+ return verdicts.length === 0 || verdicts.some((verdict) => verdict === null) || verdicts.every(Boolean);
673
+ });
674
+ };
675
+
676
+ const requestedMajorOf = (range: string): string | null => {
677
+ const comparators = range
678
+ .split("||")
679
+ .flatMap(comparatorsOf)
680
+ .map((token) => RANGE_COMPARATOR.exec(token))
681
+ .filter((match) => match !== null);
682
+ const lower = comparators.filter((match) => match[1] !== "<" && match[1] !== "<=").map((match) => Number(match[2]));
683
+
684
+ if (lower.length > 0) {
685
+ return String(Math.min(...lower));
686
+ }
687
+
688
+ const upper = comparators.map((match) => (match[1] === "<" ? Number(match[2]) - 1 : Number(match[2])));
689
+
690
+ return upper.length === 0 ? null : String(Math.max(...upper));
691
+ };
692
+
693
+ interface NodeRequirement {
694
+ version: string;
695
+ unmet: boolean;
696
+ }
697
+
698
+ const nodeRequirementOf = (
699
+ packageJson: PackageJson | null,
700
+ read: (path: string) => string | null,
701
+ ): NodeRequirement | null => {
702
+ const image = Number(DETECT_TABLE.node.defaultVersion);
703
+ const engine = recordOf(packageJson?.engines)[DETECT_TABLE.node.enginesKey];
704
+
705
+ if (typeof engine === "string" && engine.trim().length > 0) {
706
+ const version = requestedMajorOf(engine);
707
+
708
+ if (version !== null) {
709
+ return {
710
+ version,
711
+ unmet: !rangeAdmits(engine, image),
712
+ };
713
+ }
714
+ }
715
+
716
+ const version = versionFromFiles(DETECT_TABLE.node.versionFiles, read, 1);
717
+
718
+ return version === null
719
+ ? null
720
+ : {
721
+ version,
722
+ unmet: Number(version) !== image,
723
+ };
724
+ };
725
+
445
726
  const decideRuntime = (
446
727
  node: {
447
728
  manifest: boolean;
@@ -536,9 +817,13 @@ const committedWarnings = (paths: ReadonlySet<string>, warnings: Set<BotInspectW
536
817
 
537
818
  const name = segments.at(-1) ?? "";
538
819
 
539
- if (!skipped(path) && ENV_FILE.test(name) && !ENV_TEMPLATE_FILES.has(name)) {
820
+ if (!skipped(path) && isSecretEnvFile(name)) {
540
821
  warnings.add(BotInspectWarning.EnvCommitted);
541
822
  }
823
+
824
+ if (!skipped(path) && DATABASE_FILE.test(name)) {
825
+ warnings.add(BotInspectWarning.DatabaseCommitted);
826
+ }
542
827
  }
543
828
  };
544
829
 
@@ -657,7 +942,6 @@ export const detect = (input: DetectInput): DetectResult => {
657
942
  const packageText = read(DETECT_TABLE.node.manifest);
658
943
  const packageJson = packageText === null ? null : parseObject(packageText);
659
944
  const hasPackage = has(DETECT_TABLE.node.manifest);
660
- const nodeLock = DETECT_TABLE.node.lockfiles.find((entry) => has(entry.file)) ?? null;
661
945
  const pythonManifest = DETECT_TABLE.python.manifests.find((entry) => has(entry.file)) ?? null;
662
946
  const requirementsText = read(REQUIREMENTS_FILE);
663
947
  const pyprojectText = read(PYPROJECT_FILE);
@@ -716,37 +1000,54 @@ export const detect = (input: DetectInput): DetectResult => {
716
1000
 
717
1001
  let packageManager: BotPackageManager | null = null;
718
1002
  let install: DetectedCommand | null = null;
1003
+ let build: DetectedCommand | null = null;
1004
+ let nodeVersion: string | null = null;
1005
+ let pythonVersion: string | null = null;
719
1006
 
720
1007
  if (runtime === BotRuntime.Node) {
721
- if (nodeLock) {
722
- packageManager = MANAGERS[nodeLock.manager] ?? null;
723
- install = {
724
- command: (DETECT_TABLE.node.installCommands[nodeLock.manager] ?? []).join(" "),
725
- source: nodeLock.file,
726
- };
727
- } else if (hasPackage) {
728
- packageManager = BotPackageManager.Npm;
1008
+ if (hasPackage) {
1009
+ const manager = nodeManagerOf(packageJson, has);
1010
+ const commands = manager.locked
1011
+ ? DETECT_TABLE.node.installCommands.locked
1012
+ : DETECT_TABLE.node.installCommands.unlocked;
1013
+
1014
+ packageManager = MANAGERS[manager.key] ?? null;
729
1015
  install = {
730
- command: (DETECT_TABLE.node.installCommands[DETECT_TABLE.node.unlockedManager] ?? []).join(" "),
731
- source: DETECT_TABLE.node.manifest,
1016
+ command: (commands[manager.key] ?? []).join(" "),
1017
+ source: manager.source,
732
1018
  };
733
- warnings.add(BotInspectWarning.NoLockfile);
1019
+ build = nodeBuildOf(packageJson, manager.key);
1020
+
1021
+ if (!manager.locked) {
1022
+ warnings.add(BotInspectWarning.NoLockfile);
1023
+ }
1024
+
1025
+ const requirement = nodeRequirementOf(packageJson, read);
1026
+
1027
+ nodeVersion = requirement?.version ?? null;
1028
+
1029
+ if (requirement?.unmet) {
1030
+ warnings.add(BotInspectWarning.NodeVersionUnmet);
1031
+ }
734
1032
  } else {
735
1033
  warnings.add(BotInspectWarning.NoManifest);
736
1034
  }
737
1035
  }
738
1036
 
739
1037
  if (runtime === BotRuntime.Python) {
1038
+ pythonVersion = versionFromFiles(DETECT_TABLE.python.versionFiles, read, 2);
1039
+
740
1040
  if (pythonManifest) {
741
- packageManager = BotPackageManager.Pip;
1041
+ packageManager = MANAGERS[pythonManifest.manager] ?? null;
742
1042
  install = {
743
- command: [
744
- "pip",
745
- ...(DETECT_TABLE.python.installArguments[pythonManifest.manager] ?? []),
746
- ].join(" "),
1043
+ command: (DETECT_TABLE.python.installCommands[pythonManifest.manager] ?? []).join(" "),
747
1044
  source: pythonManifest.file,
748
1045
  };
749
1046
 
1047
+ if (DETECT_TABLE.python.unhonouredManagers.includes(pythonManifest.manager)) {
1048
+ warnings.add(BotInspectWarning.LockfileNotHonoured);
1049
+ }
1050
+
750
1051
  const declared = pythonManifest.file === REQUIREMENTS_FILE ? requirements : pyproject;
751
1052
 
752
1053
  if (declared.some((requirement) => !requirement.pinned)) {
@@ -768,28 +1069,31 @@ export const detect = (input: DetectInput): DetectResult => {
768
1069
  };
769
1070
  }
770
1071
 
1072
+ if (serverk?.build) {
1073
+ build = {
1074
+ command: serverk.build,
1075
+ source: SERVERK_JSON,
1076
+ };
1077
+ }
1078
+
771
1079
  const procfile = procfileCommand(read(DETECT_TABLE.procfile.file));
772
1080
  const detectedStart =
773
1081
  runtime === BotRuntime.Node
774
1082
  ? nodeStartOf(packageJson, has)
775
1083
  : runtime === BotRuntime.Python
776
- ? pythonStartOf(has)
1084
+ ? pythonStartOf(has, paths)
777
1085
  : null;
778
1086
  const start: DetectedCommand | null = serverk?.start
779
1087
  ? {
780
1088
  command: serverk.start,
781
1089
  source: SERVERK_JSON,
782
1090
  }
783
- : (detectedStart ?? procfile);
1091
+ : (procfile ?? detectedStart);
784
1092
 
785
1093
  if (!start) {
786
1094
  warnings.add(BotInspectWarning.NoStart);
787
1095
  }
788
1096
 
789
- if (runtime === BotRuntime.Node && start && TYPESCRIPT_ENTRY.test(start.command)) {
790
- warnings.add(BotInspectWarning.TypescriptEntry);
791
- }
792
-
793
1097
  committedWarnings(paths, warnings);
794
1098
 
795
1099
  const env = envCollector(warnings);
@@ -806,6 +1110,10 @@ export const detect = (input: DetectInput): DetectResult => {
806
1110
  }
807
1111
  }
808
1112
 
1113
+ for (const entry of input.skippedEnv === undefined ? [] : parseEnvExample(input.skippedEnv)) {
1114
+ env.add(entry.key, ENV_FILE, null, null);
1115
+ }
1116
+
809
1117
  for (const [path, text] of sources) {
810
1118
  for (const key of scanEnvKeys(path, text)) {
811
1119
  env.add(key, path, null, null);
@@ -834,7 +1142,10 @@ export const detect = (input: DetectInput): DetectResult => {
834
1142
  framework,
835
1143
  platform,
836
1144
  install,
1145
+ build,
837
1146
  start,
1147
+ nodeVersion,
1148
+ pythonVersion,
838
1149
  env: env.entries,
839
1150
  tokenKey: tokenKeyOf(platform, env.entries),
840
1151
  warnings: warningsInOrder(warnings),
@@ -12,6 +12,7 @@ export interface DetectEntry {
12
12
  export interface DetectInput {
13
13
  files: readonly DetectEntry[];
14
14
  contents: Readonly<Record<string, string>>;
15
+ skippedEnv?: string;
15
16
  }
16
17
 
17
18
  export interface DetectedCommand {
@@ -33,7 +34,10 @@ export interface DetectResult {
33
34
  framework: BotFramework | null;
34
35
  platform: BotPlatform | null;
35
36
  install: DetectedCommand | null;
37
+ build: DetectedCommand | null;
36
38
  start: DetectedCommand | null;
39
+ nodeVersion: string | null;
40
+ pythonVersion: string | null;
37
41
  env: DetectedEnvKey[];
38
42
  tokenKey: string | null;
39
43
  warnings: BotInspectWarning[];
@@ -9,20 +9,36 @@ describe("renderDetectTable", () => {
9
9
  const rendered = renderDetectTable();
10
10
 
11
11
  expect(rendered.endsWith("}\n")).toBe(true);
12
- expect(rendered).toContain('\t"version": 1,');
12
+ expect(rendered).toContain('\t"version": 2,');
13
13
  expect(JSON.parse(rendered)).toEqual(DETECT_TABLE);
14
14
  });
15
15
 
16
- it("names an install command for every manager a lockfile or manifest points at", () => {
17
- for (const lockfile of DETECT_TABLE.node.lockfiles) {
18
- expect(DETECT_TABLE.node.installCommands[lockfile.manager]).toBeDefined();
16
+ it("names a locked and an unlocked install and a run command for every Node manager", () => {
17
+ const managers = new Set([
18
+ ...DETECT_TABLE.node.lockfiles.map((lockfile) => lockfile.manager),
19
+ "yarn-berry",
20
+ ]);
21
+
22
+ for (const manager of managers) {
23
+ expect(DETECT_TABLE.node.installCommands.locked[manager]).toBeDefined();
24
+ expect(DETECT_TABLE.node.installCommands.unlocked[manager]).toBeDefined();
25
+ expect(DETECT_TABLE.node.runCommands[manager]).toBeDefined();
19
26
  }
20
27
 
21
- expect(DETECT_TABLE.node.installCommands[DETECT_TABLE.node.unlockedManager]).toBeDefined();
28
+ expect(Number(DETECT_TABLE.node.defaultVersion)).toBeGreaterThan(0);
29
+ });
22
30
 
31
+ it("names an install command for every Python manifest and lockfile, and the ones it does not honour", () => {
23
32
  for (const manifest of DETECT_TABLE.python.manifests) {
24
- expect(DETECT_TABLE.python.installArguments[manifest.manager]).toBeDefined();
33
+ expect(DETECT_TABLE.python.installCommands[manifest.manager]).toBeDefined();
25
34
  }
35
+
36
+ for (const manager of DETECT_TABLE.python.unhonouredManagers) {
37
+ expect(DETECT_TABLE.python.manifests.some((manifest) => manifest.manager === manager)).toBe(true);
38
+ }
39
+
40
+ expect(DETECT_TABLE.python.manifests[0]?.file).toBe("requirements.txt");
41
+ expect(DETECT_TABLE.python.defaultVersion).toMatch(/^\d+\.\d+$/);
26
42
  });
27
43
 
28
44
  it("carries the serverk.json rules the launchers need to agree with the parser", () => {