@serverkgg/bridge 0.1.4 → 0.1.6

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 (75) hide show
  1. package/package.json +3 -3
  2. package/src/bot/botCauses.test.ts +292 -0
  3. package/src/bot/botCauses.ts +58 -0
  4. package/src/bot/botDeploy.test.ts +260 -1
  5. package/src/bot/botDeploy.ts +125 -6
  6. package/src/bot/botState.ts +6 -0
  7. package/src/bot/detect/botFrameworks.ts +1 -0
  8. package/src/bot/detect/botInspectWarning.ts +4 -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 +35 -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 +443 -17
  50. package/src/bot/detect/detect.ts +375 -34
  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/manifest/manifest.ts +2 -1
  64. package/src/modules/bridge.ts +2 -0
  65. package/src/modules/bridgeModule.ts +5 -0
  66. package/src/probes/minecraftPing.test.ts +2 -1
  67. package/src/protocol/bridgeCrash.ts +49 -0
  68. package/src/protocol/bridgeEvents.ts +9 -0
  69. package/src/rcon/rconClient.test.ts +3 -2
  70. package/src/runtime/bridgeDriver.test.ts +45 -0
  71. package/src/runtime/bridgeDriver.ts +25 -0
  72. package/src/runtime/bridgeEvents.ts +2 -0
  73. package/src/runtime/bridgeSupervisor.test.ts +163 -3
  74. package/src/runtime/bridgeSupervisor.ts +42 -1
  75. 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,39 @@ 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 WINDOWS_COMMANDS: ReadonlySet<string> = new Set([
57
+ "cls",
58
+ ]);
59
+
60
+ const COMMAND_SEPARATOR = /&&|\|\||;|\|/;
61
+
62
+ const MANAGER_FIELD = /^(npm|pnpm|yarn|bun)@(\d+)/;
63
+
64
+ const ENGINE_MANAGERS: readonly string[] = [
65
+ "pnpm",
66
+ "bun",
67
+ "yarn",
68
+ ];
69
+
70
+ const YARN_BERRY = "yarn-berry";
71
+
72
+ const VERSION_NUMBER = /^v?(\d+(?:\.\d+)*)/;
73
+
74
+ const RANGE_COMPARATOR = /^(>=|<=|>|<|=|\^|~)?v?(\d+)(?:\.(\d+|x|\*))?(?:\.(\d+|x|\*))?$/;
75
+
76
+ const HYPHEN_RANGE = /(\S+)\s+-\s+(\S+)/g;
53
77
 
54
78
  const TOKEN_KEY = /TOKEN/i;
55
79
 
@@ -71,6 +95,14 @@ const MANAGERS: Readonly<Record<string, BotPackageManager>> = {
71
95
  npm: BotPackageManager.Npm,
72
96
  pnpm: BotPackageManager.Pnpm,
73
97
  yarn: BotPackageManager.Yarn,
98
+ [YARN_BERRY]: BotPackageManager.Yarn,
99
+ bun: BotPackageManager.Bun,
100
+ requirements: BotPackageManager.Pip,
101
+ uv: BotPackageManager.Uv,
102
+ poetry: BotPackageManager.Pip,
103
+ pdm: BotPackageManager.Pip,
104
+ pipenv: BotPackageManager.Pipenv,
105
+ project: BotPackageManager.Pip,
74
106
  };
75
107
 
76
108
  export interface PythonRequirement {
@@ -166,6 +198,8 @@ const FIXED_FILES: readonly string[] = [
166
198
  DETECT_TABLE.procfile.file,
167
199
  SERVERK_JSON,
168
200
  ...ENV_EXAMPLE_FILES,
201
+ ...DETECT_TABLE.node.versionFiles.map((entry) => entry.file),
202
+ ...DETECT_TABLE.python.versionFiles.map((entry) => entry.file),
169
203
  ];
170
204
 
171
205
  const sourceRank = (path: string) => {
@@ -366,6 +400,18 @@ const frameworksOf = (
366
400
  return declared.length > 0 ? declared : specs.filter((spec) => spec.modules.some((name) => imports.has(name)));
367
401
  };
368
402
 
403
+ const runsWindowsCommand = (command: string | null) => {
404
+ if (command === null) {
405
+ return false;
406
+ }
407
+
408
+ return command.split(COMMAND_SEPARATOR).some((segment) => {
409
+ const program = segment.trim().split(/\s+/)[0] ?? "";
410
+
411
+ return WINDOWS_COMMANDS.has(program.toLowerCase());
412
+ });
413
+ };
414
+
369
415
  const procfileCommand = (text: string | null): DetectedCommand | null => {
370
416
  if (text === null) {
371
417
  return null;
@@ -425,13 +471,29 @@ const nodeStartOf = (packageJson: PackageJson | null, has: (path: string) => boo
425
471
  : null;
426
472
  };
427
473
 
428
- const pythonStartOf = (has: (path: string) => boolean): DetectedCommand | null => {
474
+ const pythonStartOf = (has: (path: string) => boolean, paths: ReadonlySet<string>): DetectedCommand | null => {
429
475
  const file = DETECT_TABLE.python.startFiles.find(has);
430
476
 
431
- return file
477
+ if (file) {
478
+ return {
479
+ command: `python ${file}`,
480
+ source: file,
481
+ };
482
+ }
483
+
484
+ const packages = [
485
+ ...paths,
486
+ ]
487
+ .map((path) => PYTHON_PACKAGE_MAIN.exec(path)?.[1])
488
+ .filter((name): name is string => name !== undefined && !skipped(name) && !NOT_A_BOT_PACKAGE.has(name))
489
+ .sort(compareCodePoints);
490
+
491
+ const name = packages.length === 1 ? packages[0] : null;
492
+
493
+ return name
432
494
  ? {
433
- command: `python ${file}`,
434
- source: file,
495
+ command: `python -m ${name}`,
496
+ source: `${name}/__main__.py`,
435
497
  }
436
498
  : null;
437
499
  };
@@ -442,6 +504,243 @@ const nodeStartFileOf = (packageJson: PackageJson | null, has: (path: string) =>
442
504
  return (main !== null && has(main)) || DETECT_TABLE.node.startFiles.some(has);
443
505
  };
444
506
 
507
+ interface NodeManager {
508
+ key: string;
509
+ locked: boolean;
510
+ source: string;
511
+ }
512
+
513
+ const lockfileOf = (manager: string, has: (path: string) => boolean) => {
514
+ const lockManager = manager === YARN_BERRY ? "yarn" : manager;
515
+
516
+ return DETECT_TABLE.node.lockfiles.find((entry) => entry.manager === lockManager && has(entry.file)) ?? null;
517
+ };
518
+
519
+ const nodeManagerOf = (packageJson: PackageJson | null, has: (path: string) => boolean): NodeManager => {
520
+ const declared = packageJson?.[DETECT_TABLE.node.managerField];
521
+ const field = typeof declared === "string" ? MANAGER_FIELD.exec(declared.trim()) : null;
522
+
523
+ if (field) {
524
+ const key = field[1] === "yarn" && Number(field[2]) >= 2 ? YARN_BERRY : (field[1] ?? "npm");
525
+ const lock = lockfileOf(key, has);
526
+
527
+ return {
528
+ key,
529
+ locked: lock !== null,
530
+ source: lock?.file ?? DETECT_TABLE.node.manifest,
531
+ };
532
+ }
533
+
534
+ for (const entry of DETECT_TABLE.node.lockfiles) {
535
+ if (entry.manager !== "npm" && has(entry.file)) {
536
+ return {
537
+ key: entry.manager === "yarn" && has(DETECT_TABLE.node.berryFile) ? YARN_BERRY : entry.manager,
538
+ locked: true,
539
+ source: entry.file,
540
+ };
541
+ }
542
+ }
543
+
544
+ if (has(DETECT_TABLE.node.berryFile)) {
545
+ return {
546
+ key: YARN_BERRY,
547
+ locked: false,
548
+ source: DETECT_TABLE.node.berryFile,
549
+ };
550
+ }
551
+
552
+ const engines = recordOf(packageJson?.engines);
553
+ const engine = ENGINE_MANAGERS.find((manager) => typeof engines[manager] === "string") ?? null;
554
+
555
+ if (engine) {
556
+ return {
557
+ key: engine,
558
+ locked: false,
559
+ source: DETECT_TABLE.node.manifest,
560
+ };
561
+ }
562
+
563
+ const npmLock = lockfileOf("npm", has);
564
+
565
+ return {
566
+ key: "npm",
567
+ locked: npmLock !== null,
568
+ source: npmLock?.file ?? DETECT_TABLE.node.manifest,
569
+ };
570
+ };
571
+
572
+ const nodeBuildOf = (packageJson: PackageJson | null, manager: string): DetectedCommand | null => {
573
+ const script = recordOf(packageJson?.scripts)[DETECT_TABLE.node.buildScript];
574
+
575
+ if (typeof script !== "string" || script.trim().length === 0) {
576
+ return null;
577
+ }
578
+
579
+ const run = DETECT_TABLE.node.runCommands[manager] ?? DETECT_TABLE.node.runCommands.npm ?? [];
580
+
581
+ return {
582
+ command: [
583
+ ...run,
584
+ DETECT_TABLE.node.buildScript,
585
+ ].join(" "),
586
+ source: DETECT_TABLE.node.manifest,
587
+ };
588
+ };
589
+
590
+ const versionFromFile = (text: string, spec: DetectVersionFile): string | null => {
591
+ for (const raw of text.split("\n")) {
592
+ let line = raw.trim();
593
+
594
+ if (line.length === 0 || line.startsWith("#")) {
595
+ continue;
596
+ }
597
+
598
+ if (spec.tool !== undefined) {
599
+ const [tool, ...rest] = line.split(/\s+/);
600
+
601
+ if (tool !== spec.tool) {
602
+ continue;
603
+ }
604
+
605
+ line = rest[0] ?? "";
606
+ }
607
+
608
+ if (spec.key !== undefined) {
609
+ const match = new RegExp(`^${spec.key}\\s*=\\s*"?([^"\\s]+)`).exec(line);
610
+
611
+ if (!match) {
612
+ continue;
613
+ }
614
+
615
+ line = match[1] ?? "";
616
+ }
617
+
618
+ if (spec.prefix !== undefined) {
619
+ if (!line.startsWith(spec.prefix)) {
620
+ return null;
621
+ }
622
+
623
+ line = line.slice(spec.prefix.length);
624
+ }
625
+
626
+ return VERSION_NUMBER.exec(line)?.[1] ?? null;
627
+ }
628
+
629
+ return null;
630
+ };
631
+
632
+ const versionFromFiles = (
633
+ specs: readonly DetectVersionFile[],
634
+ read: (path: string) => string | null,
635
+ parts: number,
636
+ ): string | null => {
637
+ for (const spec of specs) {
638
+ const text = read(spec.file);
639
+
640
+ if (text === null) {
641
+ continue;
642
+ }
643
+
644
+ const version = versionFromFile(text, spec);
645
+
646
+ if (version !== null) {
647
+ return version.split(".").slice(0, parts).join(".");
648
+ }
649
+ }
650
+
651
+ return null;
652
+ };
653
+
654
+ const comparatorAdmits = (comparator: string, major: number): boolean | null => {
655
+ const match = RANGE_COMPARATOR.exec(comparator);
656
+
657
+ if (!match) {
658
+ return null;
659
+ }
660
+
661
+ const base = Number(match[2]);
662
+ const minor = match[3];
663
+
664
+ switch (match[1] ?? "") {
665
+ case ">=":
666
+ return major >= base;
667
+ case ">":
668
+ return minor === undefined ? major > base : major >= base;
669
+ case "<":
670
+ return major < base || (major === base && minor !== undefined && minor !== "0");
671
+ case "<=":
672
+ return major <= base;
673
+ default:
674
+ return major === base;
675
+ }
676
+ };
677
+
678
+ const comparatorsOf = (clause: string) => {
679
+ return clause
680
+ .replace(HYPHEN_RANGE, ">=$1 <=$2")
681
+ .trim()
682
+ .split(/\s+/)
683
+ .filter((token) => token.length > 0);
684
+ };
685
+
686
+ const rangeAdmits = (range: string, major: number) => {
687
+ return range.split("||").some((clause) => {
688
+ const verdicts = comparatorsOf(clause).map((token) => comparatorAdmits(token, major));
689
+
690
+ return verdicts.length === 0 || verdicts.some((verdict) => verdict === null) || verdicts.every(Boolean);
691
+ });
692
+ };
693
+
694
+ const requestedMajorOf = (range: string): string | null => {
695
+ const comparators = range
696
+ .split("||")
697
+ .flatMap(comparatorsOf)
698
+ .map((token) => RANGE_COMPARATOR.exec(token))
699
+ .filter((match) => match !== null);
700
+ const lower = comparators.filter((match) => match[1] !== "<" && match[1] !== "<=").map((match) => Number(match[2]));
701
+
702
+ if (lower.length > 0) {
703
+ return String(Math.min(...lower));
704
+ }
705
+
706
+ const upper = comparators.map((match) => (match[1] === "<" ? Number(match[2]) - 1 : Number(match[2])));
707
+
708
+ return upper.length === 0 ? null : String(Math.max(...upper));
709
+ };
710
+
711
+ interface NodeRequirement {
712
+ version: string;
713
+ unmet: boolean;
714
+ }
715
+
716
+ const nodeRequirementOf = (
717
+ packageJson: PackageJson | null,
718
+ read: (path: string) => string | null,
719
+ ): NodeRequirement | null => {
720
+ const image = Number(DETECT_TABLE.node.defaultVersion);
721
+ const engine = recordOf(packageJson?.engines)[DETECT_TABLE.node.enginesKey];
722
+
723
+ if (typeof engine === "string" && engine.trim().length > 0) {
724
+ const version = requestedMajorOf(engine);
725
+
726
+ if (version !== null) {
727
+ return {
728
+ version,
729
+ unmet: !rangeAdmits(engine, image),
730
+ };
731
+ }
732
+ }
733
+
734
+ const version = versionFromFiles(DETECT_TABLE.node.versionFiles, read, 1);
735
+
736
+ return version === null
737
+ ? null
738
+ : {
739
+ version,
740
+ unmet: Number(version) !== image,
741
+ };
742
+ };
743
+
445
744
  const decideRuntime = (
446
745
  node: {
447
746
  manifest: boolean;
@@ -536,9 +835,13 @@ const committedWarnings = (paths: ReadonlySet<string>, warnings: Set<BotInspectW
536
835
 
537
836
  const name = segments.at(-1) ?? "";
538
837
 
539
- if (!skipped(path) && ENV_FILE.test(name) && !ENV_TEMPLATE_FILES.has(name)) {
838
+ if (!skipped(path) && isSecretEnvFile(name)) {
540
839
  warnings.add(BotInspectWarning.EnvCommitted);
541
840
  }
841
+
842
+ if (!skipped(path) && DATABASE_FILE.test(name)) {
843
+ warnings.add(BotInspectWarning.DatabaseCommitted);
844
+ }
542
845
  }
543
846
  };
544
847
 
@@ -657,7 +960,6 @@ export const detect = (input: DetectInput): DetectResult => {
657
960
  const packageText = read(DETECT_TABLE.node.manifest);
658
961
  const packageJson = packageText === null ? null : parseObject(packageText);
659
962
  const hasPackage = has(DETECT_TABLE.node.manifest);
660
- const nodeLock = DETECT_TABLE.node.lockfiles.find((entry) => has(entry.file)) ?? null;
661
963
  const pythonManifest = DETECT_TABLE.python.manifests.find((entry) => has(entry.file)) ?? null;
662
964
  const requirementsText = read(REQUIREMENTS_FILE);
663
965
  const pyprojectText = read(PYPROJECT_FILE);
@@ -716,37 +1018,54 @@ export const detect = (input: DetectInput): DetectResult => {
716
1018
 
717
1019
  let packageManager: BotPackageManager | null = null;
718
1020
  let install: DetectedCommand | null = null;
1021
+ let build: DetectedCommand | null = null;
1022
+ let nodeVersion: string | null = null;
1023
+ let pythonVersion: string | null = null;
719
1024
 
720
1025
  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;
1026
+ if (hasPackage) {
1027
+ const manager = nodeManagerOf(packageJson, has);
1028
+ const commands = manager.locked
1029
+ ? DETECT_TABLE.node.installCommands.locked
1030
+ : DETECT_TABLE.node.installCommands.unlocked;
1031
+
1032
+ packageManager = MANAGERS[manager.key] ?? null;
729
1033
  install = {
730
- command: (DETECT_TABLE.node.installCommands[DETECT_TABLE.node.unlockedManager] ?? []).join(" "),
731
- source: DETECT_TABLE.node.manifest,
1034
+ command: (commands[manager.key] ?? []).join(" "),
1035
+ source: manager.source,
732
1036
  };
733
- warnings.add(BotInspectWarning.NoLockfile);
1037
+ build = nodeBuildOf(packageJson, manager.key);
1038
+
1039
+ if (!manager.locked) {
1040
+ warnings.add(BotInspectWarning.NoLockfile);
1041
+ }
1042
+
1043
+ const requirement = nodeRequirementOf(packageJson, read);
1044
+
1045
+ nodeVersion = requirement?.version ?? null;
1046
+
1047
+ if (requirement?.unmet) {
1048
+ warnings.add(BotInspectWarning.NodeVersionUnmet);
1049
+ }
734
1050
  } else {
735
1051
  warnings.add(BotInspectWarning.NoManifest);
736
1052
  }
737
1053
  }
738
1054
 
739
1055
  if (runtime === BotRuntime.Python) {
1056
+ pythonVersion = versionFromFiles(DETECT_TABLE.python.versionFiles, read, 2);
1057
+
740
1058
  if (pythonManifest) {
741
- packageManager = BotPackageManager.Pip;
1059
+ packageManager = MANAGERS[pythonManifest.manager] ?? null;
742
1060
  install = {
743
- command: [
744
- "pip",
745
- ...(DETECT_TABLE.python.installArguments[pythonManifest.manager] ?? []),
746
- ].join(" "),
1061
+ command: (DETECT_TABLE.python.installCommands[pythonManifest.manager] ?? []).join(" "),
747
1062
  source: pythonManifest.file,
748
1063
  };
749
1064
 
1065
+ if (DETECT_TABLE.python.unhonouredManagers.includes(pythonManifest.manager)) {
1066
+ warnings.add(BotInspectWarning.LockfileNotHonoured);
1067
+ }
1068
+
750
1069
  const declared = pythonManifest.file === REQUIREMENTS_FILE ? requirements : pyproject;
751
1070
 
752
1071
  if (declared.some((requirement) => !requirement.pinned)) {
@@ -768,26 +1087,41 @@ export const detect = (input: DetectInput): DetectResult => {
768
1087
  };
769
1088
  }
770
1089
 
1090
+ if (serverk?.build) {
1091
+ build = {
1092
+ command: serverk.build,
1093
+ source: SERVERK_JSON,
1094
+ };
1095
+ }
1096
+
771
1097
  const procfile = procfileCommand(read(DETECT_TABLE.procfile.file));
772
1098
  const detectedStart =
773
1099
  runtime === BotRuntime.Node
774
1100
  ? nodeStartOf(packageJson, has)
775
1101
  : runtime === BotRuntime.Python
776
- ? pythonStartOf(has)
1102
+ ? pythonStartOf(has, paths)
777
1103
  : null;
778
1104
  const start: DetectedCommand | null = serverk?.start
779
1105
  ? {
780
1106
  command: serverk.start,
781
1107
  source: SERVERK_JSON,
782
1108
  }
783
- : (detectedStart ?? procfile);
1109
+ : (procfile ?? detectedStart);
784
1110
 
785
1111
  if (!start) {
786
1112
  warnings.add(BotInspectWarning.NoStart);
787
1113
  }
788
1114
 
789
- if (runtime === BotRuntime.Node && start && TYPESCRIPT_ENTRY.test(start.command)) {
790
- warnings.add(BotInspectWarning.TypescriptEntry);
1115
+ const buildScript = recordOf(packageJson?.scripts)[DETECT_TABLE.node.buildScript];
1116
+
1117
+ const commands = [
1118
+ start?.command ?? null,
1119
+ build?.command ?? null,
1120
+ build?.source === DETECT_TABLE.node.manifest && typeof buildScript === "string" ? buildScript : null,
1121
+ ];
1122
+
1123
+ if (commands.some(runsWindowsCommand)) {
1124
+ warnings.add(BotInspectWarning.WindowsCommand);
791
1125
  }
792
1126
 
793
1127
  committedWarnings(paths, warnings);
@@ -806,6 +1140,10 @@ export const detect = (input: DetectInput): DetectResult => {
806
1140
  }
807
1141
  }
808
1142
 
1143
+ for (const entry of input.skippedEnv === undefined ? [] : parseEnvExample(input.skippedEnv)) {
1144
+ env.add(entry.key, ENV_FILE, null, null);
1145
+ }
1146
+
809
1147
  for (const [path, text] of sources) {
810
1148
  for (const key of scanEnvKeys(path, text)) {
811
1149
  env.add(key, path, null, null);
@@ -834,7 +1172,10 @@ export const detect = (input: DetectInput): DetectResult => {
834
1172
  framework,
835
1173
  platform,
836
1174
  install,
1175
+ build,
837
1176
  start,
1177
+ nodeVersion,
1178
+ pythonVersion,
838
1179
  env: env.entries,
839
1180
  tokenKey: tokenKeyOf(platform, env.entries),
840
1181
  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", () => {