@serverkgg/bridge 0.0.21 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +3 -1
- package/src/bot/botDeploy.test.ts +363 -0
- package/src/bot/botDeploy.ts +136 -0
- package/src/bot/botEnvironment.test.ts +189 -0
- package/src/bot/botEnvironment.ts +141 -0
- package/src/bot/botIdentity.test.ts +251 -0
- package/src/bot/botIdentity.ts +140 -0
- package/src/bot/botPanel.test.ts +216 -0
- package/src/bot/botPanel.ts +121 -0
- package/src/bot/botState.test.ts +45 -0
- package/src/bot/botState.ts +54 -0
- package/src/bot/botTerminal.test.ts +79 -0
- package/src/bot/botTerminal.ts +62 -0
- package/src/bot/botVariables.test.ts +193 -0
- package/src/bot/botVariables.ts +73 -0
- package/src/bot/detect/botFramework.ts +23 -0
- package/src/bot/detect/botFrameworks.ts +198 -0
- package/src/bot/detect/botInspectWarning.ts +15 -0
- package/src/bot/detect/botPackageManager.ts +6 -0
- package/src/bot/detect/botPlatform.ts +10 -0
- package/src/bot/detect/botRuntime.ts +15 -0
- package/src/bot/detect/botToken.test.ts +71 -0
- package/src/bot/detect/botToken.ts +126 -0
- package/src/bot/detect/detect.test.ts +543 -0
- package/src/bot/detect/detect.ts +842 -0
- package/src/bot/detect/detectResult.ts +40 -0
- package/src/bot/detect/detectTable.test.ts +72 -0
- package/src/bot/detect/detectTable.ts +166 -0
- package/src/bot/detect/envExample.test.ts +29 -0
- package/src/bot/detect/envExample.ts +48 -0
- package/src/bot/detect/envKey.ts +7 -0
- package/src/bot/detect/envScan.test.ts +39 -0
- package/src/bot/detect/envScan.ts +87 -0
- package/src/bot/detect/index.ts +17 -0
- package/src/bot/detect/inferBotPlatform.test.ts +60 -0
- package/src/bot/detect/inferBotPlatform.ts +28 -0
- package/src/bot/detect/keys.test.ts +28 -0
- package/src/bot/detect/keys.ts +64 -0
- package/src/bot/detect/sensitiveKey.test.ts +91 -0
- package/src/bot/detect/sensitiveKey.ts +44 -0
- package/src/bot/detect/serverkJson.test.ts +88 -0
- package/src/bot/detect/serverkJson.ts +174 -0
- package/src/bot/index.ts +8 -0
- package/src/cli/detectTable.test.ts +66 -0
- package/src/cli/detectTable.ts +43 -0
- package/src/cli/main.ts +12 -4
- package/src/cli/renderedFile.ts +35 -0
- package/src/cli/schema.ts +7 -26
- package/src/cli/validate.test.ts +210 -2
- package/src/cli/validate.ts +71 -7
- package/src/context/bridgeContext.ts +11 -0
- package/src/core/bridgePort.test.ts +45 -0
- package/src/core/bridgePort.ts +26 -0
- package/src/guides/compile.ts +2 -2
- package/src/manifest/index.ts +1 -0
- package/src/manifest/kind.ts +5 -0
- package/src/manifest/manifest.test.ts +630 -3
- package/src/manifest/manifest.ts +391 -87
- package/src/manifest/runtime.ts +2 -2
- package/src/manifest/source.ts +4 -4
- package/src/modules/bridgeLifecycleSubject.ts +8 -0
- package/src/modules/bridgeModule.ts +6 -0
- package/src/modules/bridgeStopSignal.ts +13 -0
- package/src/modules/index.ts +2 -0
- package/src/panel/bridgeDetail.ts +2 -0
- package/src/protocol/bridgeConfig.ts +2 -0
- package/src/runtime/bridgeConfigReader.test.ts +80 -0
- package/src/runtime/bridgeConfigReader.ts +20 -0
- package/src/runtime/bridgeContext.test.ts +241 -0
- package/src/runtime/bridgeContext.ts +35 -9
- package/src/runtime/bridgeDriver.test.ts +132 -1
- package/src/runtime/bridgeDriver.ts +54 -1
- package/src/runtime/bridgeFiles.test.ts +62 -0
- package/src/runtime/bridgeFiles.ts +12 -2
- package/src/runtime/bridgeLiveConfig.ts +18 -0
- package/src/runtime/bridgeRedaction.test.ts +410 -0
- package/src/runtime/bridgeRedaction.ts +214 -0
- package/src/runtime/bridgeRegistry.test.ts +3 -0
- package/src/runtime/bridgeRegistry.ts +15 -0
- package/src/runtime/bridgeServer.test.ts +130 -0
- package/src/runtime/bridgeServer.ts +24 -7
- package/src/runtime/bridgeSetupHandlers.test.ts +88 -0
- package/src/runtime/bridgeSetupHandlers.ts +16 -6
- package/src/runtime/bridgeSupervisor.test.ts +670 -2
- package/src/runtime/bridgeSupervisor.ts +219 -67
- package/src/runtime/serve.ts +47 -13
|
@@ -0,0 +1,842 @@
|
|
|
1
|
+
import type { BotFramework } from "./botFramework";
|
|
2
|
+
import { BOT_FRAMEWORKS, type BotFrameworkSpec, botFrameworkSpec } from "./botFrameworks";
|
|
3
|
+
import { BotInspectWarning } from "./botInspectWarning";
|
|
4
|
+
import { BotPackageManager } from "./botPackageManager";
|
|
5
|
+
import type { BotPlatform } from "./botPlatform";
|
|
6
|
+
import { BOT_RUNTIME_PACKAGES, BotRuntime } from "./botRuntime";
|
|
7
|
+
import { BOT_TOKEN_KEYS, platformOfKey } from "./botToken";
|
|
8
|
+
import type { DetectEntry, DetectedCommand, DetectedEnvKey, DetectInput, DetectResult } from "./detectResult";
|
|
9
|
+
import { DETECT_TABLE } from "./detectTable";
|
|
10
|
+
import { ENV_EXAMPLE_FILES, parseEnvExample } from "./envExample";
|
|
11
|
+
import { isEnvKey } from "./envKey";
|
|
12
|
+
import { isNodeSource, isPythonSource, scanEnvKeys } from "./envScan";
|
|
13
|
+
import { BOT_PROTECTED_KEYS, BOT_PROVIDED_KEYS, isReservedBotKey } from "./keys";
|
|
14
|
+
import { isSensitiveKey } from "./sensitiveKey";
|
|
15
|
+
import { parseServerkJson, SERVERK_JSON, type ServerkJson } from "./serverkJson";
|
|
16
|
+
|
|
17
|
+
export const DETECT_SOURCE_FILE_LIMIT = 40;
|
|
18
|
+
|
|
19
|
+
export const DETECT_SOURCE_MAX_BYTES = 65_536;
|
|
20
|
+
|
|
21
|
+
export const DETECT_FILE_MAX_BYTES = 262_144;
|
|
22
|
+
|
|
23
|
+
export const DETECT_ENV_LIMIT = 100;
|
|
24
|
+
|
|
25
|
+
export const REQUIREMENTS_FILE = "requirements.txt";
|
|
26
|
+
|
|
27
|
+
export const PYPROJECT_FILE = "pyproject.toml";
|
|
28
|
+
|
|
29
|
+
const SKIPPED_SEGMENTS: ReadonlySet<string> = new Set([
|
|
30
|
+
"node_modules",
|
|
31
|
+
".venv",
|
|
32
|
+
"venv",
|
|
33
|
+
"__pycache__",
|
|
34
|
+
".git",
|
|
35
|
+
".serverk",
|
|
36
|
+
"site-packages",
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
const VENV_SEGMENTS: ReadonlySet<string> = new Set([
|
|
40
|
+
".venv",
|
|
41
|
+
"venv",
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
const ENV_FILE = /^\.env(?:\..+)?$/;
|
|
45
|
+
|
|
46
|
+
const ENV_TEMPLATE_FILES: ReadonlySet<string> = new Set([
|
|
47
|
+
...ENV_EXAMPLE_FILES,
|
|
48
|
+
".env.dist",
|
|
49
|
+
".env.defaults",
|
|
50
|
+
]);
|
|
51
|
+
|
|
52
|
+
const TYPESCRIPT_ENTRY = /(?:^|\s)node\s+(?:-\S+\s+)*\S+\.(?:ts|mts|cts)(?:\s|$)/;
|
|
53
|
+
|
|
54
|
+
const TOKEN_KEY = /TOKEN/i;
|
|
55
|
+
|
|
56
|
+
const BOT_KEY = /BOT/i;
|
|
57
|
+
|
|
58
|
+
const NODE_IMPORT = /(?:\brequire\s*\(\s*|\bfrom\s+|\bimport\s*\(\s*|\bimport\s+)(["'])([^"'\n]+)\1/g;
|
|
59
|
+
|
|
60
|
+
const PYTHON_IMPORT = /^[ \t]*(?:from[ \t]+([A-Za-z_]\w*)|import[ \t]+([A-Za-z_]\w*))/gm;
|
|
61
|
+
|
|
62
|
+
const REQUIREMENT_NAME = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(.*)$/;
|
|
63
|
+
|
|
64
|
+
const REQUIREMENT_SPECIFIER = /[=<>~!@]/;
|
|
65
|
+
|
|
66
|
+
const TOML_HEADER = /^\s*\[/;
|
|
67
|
+
|
|
68
|
+
const TOML_KEY = /^\s*([A-Za-z0-9_.-]+|"[^"]+")\s*=\s*(.*)$/;
|
|
69
|
+
|
|
70
|
+
const MANAGERS: Readonly<Record<string, BotPackageManager>> = {
|
|
71
|
+
npm: BotPackageManager.Npm,
|
|
72
|
+
pnpm: BotPackageManager.Pnpm,
|
|
73
|
+
yarn: BotPackageManager.Yarn,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export interface PythonRequirement {
|
|
77
|
+
name: string;
|
|
78
|
+
pinned: boolean;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
type PackageJson = Record<string, unknown>;
|
|
82
|
+
|
|
83
|
+
const normalizePath = (path: string) => {
|
|
84
|
+
return path
|
|
85
|
+
.replaceAll("\\", "/")
|
|
86
|
+
.replace(/^(?:\.\/)+/, "")
|
|
87
|
+
.replace(/^\/+/, "")
|
|
88
|
+
.replace(/\/+$/, "");
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const segmentsOf = (path: string) => {
|
|
92
|
+
return path.split("/");
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const skipped = (path: string) => {
|
|
96
|
+
return segmentsOf(path).some((segment) => SKIPPED_SEGMENTS.has(segment));
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const depthOf = (path: string) => {
|
|
100
|
+
return segmentsOf(path).length;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
export const compareCodePoints = (left: string, right: string): number => {
|
|
104
|
+
const leftPoints = left[Symbol.iterator]();
|
|
105
|
+
const rightPoints = right[Symbol.iterator]();
|
|
106
|
+
|
|
107
|
+
for (;;) {
|
|
108
|
+
const a = leftPoints.next();
|
|
109
|
+
const b = rightPoints.next();
|
|
110
|
+
|
|
111
|
+
if (a.done || b.done) {
|
|
112
|
+
return a.done === b.done ? 0 : a.done ? -1 : 1;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const difference = (a.value.codePointAt(0) ?? 0) - (b.value.codePointAt(0) ?? 0);
|
|
116
|
+
|
|
117
|
+
if (difference !== 0) {
|
|
118
|
+
return difference;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const relativeFile = (path: string) => {
|
|
124
|
+
const trimmed = path.trim();
|
|
125
|
+
|
|
126
|
+
if (trimmed.startsWith("/")) {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const parts: string[] = [];
|
|
131
|
+
|
|
132
|
+
for (const part of trimmed.split("/")) {
|
|
133
|
+
if (part === "" || part === ".") {
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (part === "..") {
|
|
138
|
+
if (parts.length === 0) {
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
parts.pop();
|
|
143
|
+
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
parts.push(part);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return parts.length === 0 ? null : parts.join("/");
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
const fitsSize = (size: number | undefined, limit: number) => {
|
|
154
|
+
return size === undefined || size <= limit;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
const START_FILES: readonly string[] = [
|
|
158
|
+
...DETECT_TABLE.node.startFiles,
|
|
159
|
+
...DETECT_TABLE.python.startFiles,
|
|
160
|
+
];
|
|
161
|
+
|
|
162
|
+
const FIXED_FILES: readonly string[] = [
|
|
163
|
+
DETECT_TABLE.node.manifest,
|
|
164
|
+
REQUIREMENTS_FILE,
|
|
165
|
+
PYPROJECT_FILE,
|
|
166
|
+
DETECT_TABLE.procfile.file,
|
|
167
|
+
SERVERK_JSON,
|
|
168
|
+
...ENV_EXAMPLE_FILES,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
const sourceRank = (path: string) => {
|
|
172
|
+
const start = START_FILES.indexOf(path);
|
|
173
|
+
|
|
174
|
+
return start === -1 ? START_FILES.length : start;
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
export const detectWantedFiles = (files: readonly DetectEntry[]): string[] => {
|
|
178
|
+
const sizes = new Map<string, number | undefined>();
|
|
179
|
+
|
|
180
|
+
for (const entry of files) {
|
|
181
|
+
const path = normalizePath(entry.path);
|
|
182
|
+
|
|
183
|
+
if (path.length > 0) {
|
|
184
|
+
sizes.set(path, entry.size);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const fixed = FIXED_FILES.filter((path) => sizes.has(path) && fitsSize(sizes.get(path), DETECT_FILE_MAX_BYTES));
|
|
189
|
+
|
|
190
|
+
const sources = [
|
|
191
|
+
...sizes,
|
|
192
|
+
]
|
|
193
|
+
.filter(([path, size]) => {
|
|
194
|
+
return (isNodeSource(path) || isPythonSource(path)) && !skipped(path) && fitsSize(size, DETECT_SOURCE_MAX_BYTES);
|
|
195
|
+
})
|
|
196
|
+
.map(([path]) => path)
|
|
197
|
+
.sort((a, b) => sourceRank(a) - sourceRank(b) || depthOf(a) - depthOf(b) || compareCodePoints(a, b))
|
|
198
|
+
.slice(0, DETECT_SOURCE_FILE_LIMIT);
|
|
199
|
+
|
|
200
|
+
return [
|
|
201
|
+
...fixed,
|
|
202
|
+
...sources,
|
|
203
|
+
];
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
const parseObject = (text: string): PackageJson | null => {
|
|
207
|
+
try {
|
|
208
|
+
const parsed: unknown = JSON.parse(text);
|
|
209
|
+
|
|
210
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? (parsed as PackageJson) : null;
|
|
211
|
+
} catch {
|
|
212
|
+
return null;
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
const recordOf = (value: unknown): Record<string, unknown> => {
|
|
217
|
+
return typeof value === "object" && value !== null && !Array.isArray(value) ? (value as Record<string, unknown>) : {};
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
const nodeDependencies = (packageJson: PackageJson | null) => {
|
|
221
|
+
return new Set(
|
|
222
|
+
[
|
|
223
|
+
"dependencies",
|
|
224
|
+
"devDependencies",
|
|
225
|
+
"optionalDependencies",
|
|
226
|
+
].flatMap((field) => Object.keys(recordOf(packageJson?.[field]))),
|
|
227
|
+
);
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const normalizePythonName = (name: string) => {
|
|
231
|
+
return name.toLowerCase().replace(/[-_.]+/g, "-");
|
|
232
|
+
};
|
|
233
|
+
|
|
234
|
+
const requirementOf = (line: string): PythonRequirement | null => {
|
|
235
|
+
const text = line.split(";")[0]?.trim() ?? "";
|
|
236
|
+
|
|
237
|
+
if (
|
|
238
|
+
text.length === 0
|
|
239
|
+
|| text.startsWith("-")
|
|
240
|
+
|| text.includes("://")
|
|
241
|
+
|| text.startsWith(".")
|
|
242
|
+
|| text.startsWith("/")
|
|
243
|
+
) {
|
|
244
|
+
return null;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const match = REQUIREMENT_NAME.exec(text);
|
|
248
|
+
|
|
249
|
+
if (!match?.[1]) {
|
|
250
|
+
return null;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return {
|
|
254
|
+
name: normalizePythonName(match[1]),
|
|
255
|
+
pinned: REQUIREMENT_SPECIFIER.test(match[2] ?? ""),
|
|
256
|
+
};
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
export const parseRequirements = (text: string): PythonRequirement[] => {
|
|
260
|
+
return text
|
|
261
|
+
.split(/\r?\n/)
|
|
262
|
+
.map((line) => line.replace(/(?:^|\s)#.*$/, ""))
|
|
263
|
+
.map(requirementOf)
|
|
264
|
+
.filter((requirement) => requirement !== null);
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const tomlSection = (text: string, name: string) => {
|
|
268
|
+
const lines = text.split(/\r?\n/);
|
|
269
|
+
const start = lines.findIndex((line) => line.trim() === `[${name}]`);
|
|
270
|
+
|
|
271
|
+
if (start === -1) {
|
|
272
|
+
return "";
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const rest = lines.slice(start + 1);
|
|
276
|
+
const end = rest.findIndex((line) => TOML_HEADER.test(line));
|
|
277
|
+
|
|
278
|
+
return (end === -1 ? rest : rest.slice(0, end)).join("\n");
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
export const parsePyprojectDependencies = (text: string): PythonRequirement[] => {
|
|
282
|
+
const project = tomlSection(text, "project");
|
|
283
|
+
const listed = /(?:^|\n)\s*dependencies\s*=\s*\[([\s\S]*?)\]/.exec(project)?.[1] ?? "";
|
|
284
|
+
const declared = [
|
|
285
|
+
...listed.matchAll(/"([^"]+)"|'([^']+)'/g),
|
|
286
|
+
]
|
|
287
|
+
.map((match) => requirementOf(match[1] ?? match[2] ?? ""))
|
|
288
|
+
.filter((requirement) => requirement !== null);
|
|
289
|
+
|
|
290
|
+
const poetry = tomlSection(text, "tool.poetry.dependencies")
|
|
291
|
+
.split(/\r?\n/)
|
|
292
|
+
.map((line) => TOML_KEY.exec(line))
|
|
293
|
+
.filter((match) => match !== null)
|
|
294
|
+
.map((match) => {
|
|
295
|
+
const name = normalizePythonName((match[1] ?? "").replaceAll('"', ""));
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
name,
|
|
299
|
+
pinned: !/^["']\*["']$/.test((match[2] ?? "").trim()),
|
|
300
|
+
};
|
|
301
|
+
})
|
|
302
|
+
.filter((requirement) => requirement.name !== "python");
|
|
303
|
+
|
|
304
|
+
return [
|
|
305
|
+
...declared,
|
|
306
|
+
...poetry,
|
|
307
|
+
];
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
const packageNameOf = (specifier: string) => {
|
|
311
|
+
if (specifier.startsWith(".") || specifier.startsWith("/") || specifier.startsWith("node:")) {
|
|
312
|
+
return null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const parts = specifier.split("/");
|
|
316
|
+
|
|
317
|
+
return specifier.startsWith("@") ? parts.slice(0, 2).join("/") : (parts[0] ?? null);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const importedModules = (
|
|
321
|
+
sources: readonly [
|
|
322
|
+
string,
|
|
323
|
+
string,
|
|
324
|
+
][],
|
|
325
|
+
) => {
|
|
326
|
+
const node = new Set<string>();
|
|
327
|
+
const python = new Set<string>();
|
|
328
|
+
|
|
329
|
+
for (const [path, text] of sources) {
|
|
330
|
+
if (isNodeSource(path)) {
|
|
331
|
+
for (const match of text.matchAll(NODE_IMPORT)) {
|
|
332
|
+
const name = packageNameOf(match[2] ?? "");
|
|
333
|
+
|
|
334
|
+
if (name) {
|
|
335
|
+
node.add(name);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
if (isPythonSource(path)) {
|
|
341
|
+
for (const match of text.matchAll(PYTHON_IMPORT)) {
|
|
342
|
+
const name = match[1] ?? match[2];
|
|
343
|
+
|
|
344
|
+
if (name) {
|
|
345
|
+
python.add(name);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
return {
|
|
352
|
+
node,
|
|
353
|
+
python,
|
|
354
|
+
};
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const frameworksOf = (
|
|
358
|
+
runtime: BotRuntime,
|
|
359
|
+
dependencies: ReadonlySet<string>,
|
|
360
|
+
imports: ReadonlySet<string>,
|
|
361
|
+
normalize: (name: string) => string,
|
|
362
|
+
) => {
|
|
363
|
+
const specs = BOT_FRAMEWORKS.filter((spec) => spec.runtime === runtime);
|
|
364
|
+
const declared = specs.filter((spec) => spec.packages.some((name) => dependencies.has(normalize(name))));
|
|
365
|
+
|
|
366
|
+
return declared.length > 0 ? declared : specs.filter((spec) => spec.modules.some((name) => imports.has(name)));
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
const procfileCommand = (text: string | null): DetectedCommand | null => {
|
|
370
|
+
if (text === null) {
|
|
371
|
+
return null;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
const line = new RegExp(DETECT_TABLE.procfile.line);
|
|
375
|
+
const entries = new Map<string, string>();
|
|
376
|
+
|
|
377
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
378
|
+
const match = line.exec(raw.trim());
|
|
379
|
+
|
|
380
|
+
if (match?.[1] && match[2] && !entries.has(match[1])) {
|
|
381
|
+
entries.set(match[1], match[2].trim());
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
for (const process of DETECT_TABLE.procfile.processes) {
|
|
386
|
+
const command = entries.get(process);
|
|
387
|
+
|
|
388
|
+
if (command) {
|
|
389
|
+
return {
|
|
390
|
+
command,
|
|
391
|
+
source: DETECT_TABLE.procfile.file,
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
return null;
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
const nodeStartOf = (packageJson: PackageJson | null, has: (path: string) => boolean): DetectedCommand | null => {
|
|
400
|
+
const script = recordOf(packageJson?.scripts).start;
|
|
401
|
+
|
|
402
|
+
if (typeof script === "string" && script.trim().length > 0) {
|
|
403
|
+
return {
|
|
404
|
+
command: script.trim(),
|
|
405
|
+
source: DETECT_TABLE.node.manifest,
|
|
406
|
+
};
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const main = typeof packageJson?.main === "string" ? relativeFile(packageJson.main) : null;
|
|
410
|
+
|
|
411
|
+
if (main && has(main)) {
|
|
412
|
+
return {
|
|
413
|
+
command: `node ${main}`,
|
|
414
|
+
source: DETECT_TABLE.node.manifest,
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
const file = DETECT_TABLE.node.startFiles.find(has);
|
|
419
|
+
|
|
420
|
+
return file
|
|
421
|
+
? {
|
|
422
|
+
command: `node ${file}`,
|
|
423
|
+
source: file,
|
|
424
|
+
}
|
|
425
|
+
: null;
|
|
426
|
+
};
|
|
427
|
+
|
|
428
|
+
const pythonStartOf = (has: (path: string) => boolean): DetectedCommand | null => {
|
|
429
|
+
const file = DETECT_TABLE.python.startFiles.find(has);
|
|
430
|
+
|
|
431
|
+
return file
|
|
432
|
+
? {
|
|
433
|
+
command: `python ${file}`,
|
|
434
|
+
source: file,
|
|
435
|
+
}
|
|
436
|
+
: null;
|
|
437
|
+
};
|
|
438
|
+
|
|
439
|
+
const nodeStartFileOf = (packageJson: PackageJson | null, has: (path: string) => boolean) => {
|
|
440
|
+
const main = typeof packageJson?.main === "string" ? relativeFile(packageJson.main) : null;
|
|
441
|
+
|
|
442
|
+
return (main !== null && has(main)) || DETECT_TABLE.node.startFiles.some(has);
|
|
443
|
+
};
|
|
444
|
+
|
|
445
|
+
const decideRuntime = (
|
|
446
|
+
node: {
|
|
447
|
+
manifest: boolean;
|
|
448
|
+
frameworks: number;
|
|
449
|
+
startFile: boolean;
|
|
450
|
+
},
|
|
451
|
+
python: {
|
|
452
|
+
manifest: boolean;
|
|
453
|
+
frameworks: number;
|
|
454
|
+
startFile: boolean;
|
|
455
|
+
},
|
|
456
|
+
) => {
|
|
457
|
+
const nodeSignal = node.manifest || node.frameworks > 0 || node.startFile;
|
|
458
|
+
const pythonSignal = python.manifest || python.frameworks > 0 || python.startFile;
|
|
459
|
+
|
|
460
|
+
if (!(nodeSignal && pythonSignal)) {
|
|
461
|
+
return nodeSignal ? BotRuntime.Node : pythonSignal ? BotRuntime.Python : null;
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const tiebreaks: [
|
|
465
|
+
boolean,
|
|
466
|
+
boolean,
|
|
467
|
+
][] = [
|
|
468
|
+
[
|
|
469
|
+
node.frameworks > 0,
|
|
470
|
+
python.frameworks > 0,
|
|
471
|
+
],
|
|
472
|
+
[
|
|
473
|
+
node.manifest,
|
|
474
|
+
python.manifest,
|
|
475
|
+
],
|
|
476
|
+
[
|
|
477
|
+
node.startFile,
|
|
478
|
+
python.startFile,
|
|
479
|
+
],
|
|
480
|
+
];
|
|
481
|
+
|
|
482
|
+
for (const [nodeWins, pythonWins] of tiebreaks) {
|
|
483
|
+
if (nodeWins !== pythonWins) {
|
|
484
|
+
return nodeWins ? BotRuntime.Node : BotRuntime.Python;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
return null;
|
|
489
|
+
};
|
|
490
|
+
|
|
491
|
+
const uniquePlatforms = (specs: readonly BotFrameworkSpec[]) => {
|
|
492
|
+
return new Set(specs.map((spec) => spec.platform));
|
|
493
|
+
};
|
|
494
|
+
|
|
495
|
+
const onlyOf = <Value>(values: ReadonlySet<Value>) => {
|
|
496
|
+
return values.size === 1
|
|
497
|
+
? ([
|
|
498
|
+
...values,
|
|
499
|
+
][0] ?? null)
|
|
500
|
+
: null;
|
|
501
|
+
};
|
|
502
|
+
|
|
503
|
+
const tokenKeyOf = (platform: BotPlatform | null, env: readonly DetectedEnvKey[]) => {
|
|
504
|
+
const keys = env.map((entry) => entry.key);
|
|
505
|
+
const names = platform ? BOT_TOKEN_KEYS[platform] : Object.values(BOT_TOKEN_KEYS).flat();
|
|
506
|
+
const named = names.find((name) => keys.includes(name));
|
|
507
|
+
|
|
508
|
+
if (named) {
|
|
509
|
+
return named;
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
const tokens = env.filter((entry) => entry.sensitive && TOKEN_KEY.test(entry.key));
|
|
513
|
+
|
|
514
|
+
return (
|
|
515
|
+
tokens.find((entry) => platform !== null && platformOfKey(entry.key) === platform)?.key
|
|
516
|
+
?? tokens.find((entry) => platformOfKey(entry.key) === null && BOT_KEY.test(entry.key))?.key
|
|
517
|
+
?? null
|
|
518
|
+
);
|
|
519
|
+
};
|
|
520
|
+
|
|
521
|
+
const warningsInOrder = (found: ReadonlySet<BotInspectWarning>) => {
|
|
522
|
+
return Object.values(BotInspectWarning).filter((warning) => found.has(warning));
|
|
523
|
+
};
|
|
524
|
+
|
|
525
|
+
const committedWarnings = (paths: ReadonlySet<string>, warnings: Set<BotInspectWarning>) => {
|
|
526
|
+
for (const path of paths) {
|
|
527
|
+
const segments = segmentsOf(path);
|
|
528
|
+
|
|
529
|
+
if (segments.includes("node_modules")) {
|
|
530
|
+
warnings.add(BotInspectWarning.NodeModulesCommitted);
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (segments.some((segment) => VENV_SEGMENTS.has(segment))) {
|
|
534
|
+
warnings.add(BotInspectWarning.VenvCommitted);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const name = segments.at(-1) ?? "";
|
|
538
|
+
|
|
539
|
+
if (!skipped(path) && ENV_FILE.test(name) && !ENV_TEMPLATE_FILES.has(name)) {
|
|
540
|
+
warnings.add(BotInspectWarning.EnvCommitted);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
};
|
|
544
|
+
|
|
545
|
+
interface EnvCollector {
|
|
546
|
+
add(key: string, source: string, sensitive: boolean | null, example: string | null): void;
|
|
547
|
+
entries: DetectedEnvKey[];
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
const envCollector = (warnings: Set<BotInspectWarning>): EnvCollector => {
|
|
551
|
+
const seen = new Set<string>();
|
|
552
|
+
const entries: DetectedEnvKey[] = [];
|
|
553
|
+
|
|
554
|
+
return {
|
|
555
|
+
entries,
|
|
556
|
+
add(key, source, sensitive, example) {
|
|
557
|
+
if (seen.has(key) || !isEnvKey(key) || entries.length >= DETECT_ENV_LIMIT) {
|
|
558
|
+
return;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
seen.add(key);
|
|
562
|
+
|
|
563
|
+
if (isReservedBotKey(key)) {
|
|
564
|
+
if (!BOT_PROTECTED_KEYS.includes(key) && !BOT_PROVIDED_KEYS.includes(key)) {
|
|
565
|
+
warnings.add(BotInspectWarning.ReservedEnvKey);
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
const secret = sensitive ?? isSensitiveKey(key);
|
|
572
|
+
const shown = example?.trim() ?? "";
|
|
573
|
+
|
|
574
|
+
entries.push({
|
|
575
|
+
key,
|
|
576
|
+
sensitive: secret,
|
|
577
|
+
source,
|
|
578
|
+
example: secret || shown.length === 0 ? null : shown,
|
|
579
|
+
});
|
|
580
|
+
},
|
|
581
|
+
};
|
|
582
|
+
};
|
|
583
|
+
|
|
584
|
+
const frameworkOf = (
|
|
585
|
+
serverk: ServerkJson | null,
|
|
586
|
+
candidates: readonly BotFrameworkSpec[],
|
|
587
|
+
runtime: BotRuntime | null,
|
|
588
|
+
warnings: Set<BotInspectWarning>,
|
|
589
|
+
): BotFramework | null => {
|
|
590
|
+
if (serverk?.framework) {
|
|
591
|
+
const spec = botFrameworkSpec(serverk.framework);
|
|
592
|
+
const detected =
|
|
593
|
+
candidates.length > 0 && !candidates.some((candidate) => candidate.framework === serverk.framework);
|
|
594
|
+
|
|
595
|
+
if (detected || (runtime !== null && spec?.runtime !== runtime)) {
|
|
596
|
+
warnings.add(BotInspectWarning.ManifestDisagrees);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return serverk.framework;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
return uniquePlatforms(candidates).size === 1 ? (candidates[0]?.framework ?? null) : null;
|
|
603
|
+
};
|
|
604
|
+
|
|
605
|
+
const platformOf = (
|
|
606
|
+
serverk: ServerkJson | null,
|
|
607
|
+
framework: BotFramework | null,
|
|
608
|
+
candidates: readonly BotFrameworkSpec[],
|
|
609
|
+
keyPlatforms: ReadonlySet<BotPlatform>,
|
|
610
|
+
warnings: Set<BotInspectWarning>,
|
|
611
|
+
): BotPlatform | null => {
|
|
612
|
+
const frameworkPlatforms = uniquePlatforms(candidates);
|
|
613
|
+
|
|
614
|
+
if (serverk?.platform) {
|
|
615
|
+
if (frameworkPlatforms.size > 0 && !frameworkPlatforms.has(serverk.platform)) {
|
|
616
|
+
warnings.add(BotInspectWarning.ManifestDisagrees);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
return serverk.platform;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
if (framework) {
|
|
623
|
+
return botFrameworkSpec(framework)?.platform ?? null;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (frameworkPlatforms.size > 1 || (frameworkPlatforms.size === 0 && keyPlatforms.size > 1)) {
|
|
627
|
+
warnings.add(BotInspectWarning.TwoPlatforms);
|
|
628
|
+
|
|
629
|
+
return null;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
return onlyOf(frameworkPlatforms) ?? onlyOf(keyPlatforms);
|
|
633
|
+
};
|
|
634
|
+
|
|
635
|
+
export const detect = (input: DetectInput): DetectResult => {
|
|
636
|
+
const contents = new Map<string, string>();
|
|
637
|
+
|
|
638
|
+
for (const [path, text] of Object.entries(input.contents)) {
|
|
639
|
+
contents.set(normalizePath(path), text);
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
const paths = new Set<string>([
|
|
643
|
+
...input.files.map((entry) => normalizePath(entry.path)).filter((path) => path.length > 0),
|
|
644
|
+
...contents.keys(),
|
|
645
|
+
]);
|
|
646
|
+
const has = (path: string) => paths.has(path);
|
|
647
|
+
const read = (path: string) => contents.get(path) ?? null;
|
|
648
|
+
const warnings = new Set<BotInspectWarning>();
|
|
649
|
+
|
|
650
|
+
const serverkText = read(SERVERK_JSON);
|
|
651
|
+
const serverk = serverkText === null ? null : parseServerkJson(serverkText);
|
|
652
|
+
|
|
653
|
+
if (serverkText !== null && serverk === null) {
|
|
654
|
+
warnings.add(BotInspectWarning.ManifestInvalid);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
const packageText = read(DETECT_TABLE.node.manifest);
|
|
658
|
+
const packageJson = packageText === null ? null : parseObject(packageText);
|
|
659
|
+
const hasPackage = has(DETECT_TABLE.node.manifest);
|
|
660
|
+
const nodeLock = DETECT_TABLE.node.lockfiles.find((entry) => has(entry.file)) ?? null;
|
|
661
|
+
const pythonManifest = DETECT_TABLE.python.manifests.find((entry) => has(entry.file)) ?? null;
|
|
662
|
+
const requirementsText = read(REQUIREMENTS_FILE);
|
|
663
|
+
const pyprojectText = read(PYPROJECT_FILE);
|
|
664
|
+
const requirements = requirementsText === null ? [] : parseRequirements(requirementsText);
|
|
665
|
+
const pyproject = pyprojectText === null ? [] : parsePyprojectDependencies(pyprojectText);
|
|
666
|
+
const sources = [
|
|
667
|
+
...contents,
|
|
668
|
+
]
|
|
669
|
+
.filter(([path]) => !skipped(path) && (isNodeSource(path) || isPythonSource(path)))
|
|
670
|
+
.sort(([a], [b]) => compareCodePoints(a, b));
|
|
671
|
+
const imports = importedModules(sources);
|
|
672
|
+
|
|
673
|
+
const nodeFrameworks = frameworksOf(BotRuntime.Node, nodeDependencies(packageJson), imports.node, (name) => name);
|
|
674
|
+
const pythonFrameworks = frameworksOf(
|
|
675
|
+
BotRuntime.Python,
|
|
676
|
+
new Set(
|
|
677
|
+
[
|
|
678
|
+
...requirements,
|
|
679
|
+
...pyproject,
|
|
680
|
+
].map((requirement) => requirement.name),
|
|
681
|
+
),
|
|
682
|
+
imports.python,
|
|
683
|
+
normalizePythonName,
|
|
684
|
+
);
|
|
685
|
+
|
|
686
|
+
if (hasPackage && pythonManifest) {
|
|
687
|
+
warnings.add(BotInspectWarning.TwoRuntimes);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const codeRuntime = decideRuntime(
|
|
691
|
+
{
|
|
692
|
+
manifest: hasPackage,
|
|
693
|
+
frameworks: nodeFrameworks.length,
|
|
694
|
+
startFile: nodeStartFileOf(packageJson, has),
|
|
695
|
+
},
|
|
696
|
+
{
|
|
697
|
+
manifest: pythonManifest !== null,
|
|
698
|
+
frameworks: pythonFrameworks.length,
|
|
699
|
+
startFile: DETECT_TABLE.python.startFiles.some(has),
|
|
700
|
+
},
|
|
701
|
+
);
|
|
702
|
+
|
|
703
|
+
if (
|
|
704
|
+
codeRuntime === null
|
|
705
|
+
&& (hasPackage || nodeFrameworks.length > 0)
|
|
706
|
+
&& (pythonManifest || pythonFrameworks.length > 0)
|
|
707
|
+
) {
|
|
708
|
+
warnings.add(BotInspectWarning.TwoRuntimes);
|
|
709
|
+
}
|
|
710
|
+
|
|
711
|
+
const runtime = serverk?.runtime ?? codeRuntime;
|
|
712
|
+
|
|
713
|
+
if (serverk?.runtime && codeRuntime && serverk.runtime !== codeRuntime) {
|
|
714
|
+
warnings.add(BotInspectWarning.ManifestDisagrees);
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
let packageManager: BotPackageManager | null = null;
|
|
718
|
+
let install: DetectedCommand | null = null;
|
|
719
|
+
|
|
720
|
+
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;
|
|
729
|
+
install = {
|
|
730
|
+
command: (DETECT_TABLE.node.installCommands[DETECT_TABLE.node.unlockedManager] ?? []).join(" "),
|
|
731
|
+
source: DETECT_TABLE.node.manifest,
|
|
732
|
+
};
|
|
733
|
+
warnings.add(BotInspectWarning.NoLockfile);
|
|
734
|
+
} else {
|
|
735
|
+
warnings.add(BotInspectWarning.NoManifest);
|
|
736
|
+
}
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
if (runtime === BotRuntime.Python) {
|
|
740
|
+
if (pythonManifest) {
|
|
741
|
+
packageManager = BotPackageManager.Pip;
|
|
742
|
+
install = {
|
|
743
|
+
command: [
|
|
744
|
+
"pip",
|
|
745
|
+
...(DETECT_TABLE.python.installArguments[pythonManifest.manager] ?? []),
|
|
746
|
+
].join(" "),
|
|
747
|
+
source: pythonManifest.file,
|
|
748
|
+
};
|
|
749
|
+
|
|
750
|
+
const declared = pythonManifest.file === REQUIREMENTS_FILE ? requirements : pyproject;
|
|
751
|
+
|
|
752
|
+
if (declared.some((requirement) => !requirement.pinned)) {
|
|
753
|
+
warnings.add(BotInspectWarning.UnpinnedRequirements);
|
|
754
|
+
}
|
|
755
|
+
} else {
|
|
756
|
+
warnings.add(BotInspectWarning.NoManifest);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (runtime === null) {
|
|
761
|
+
warnings.add(BotInspectWarning.NoManifest);
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
if (serverk?.install) {
|
|
765
|
+
install = {
|
|
766
|
+
command: serverk.install,
|
|
767
|
+
source: SERVERK_JSON,
|
|
768
|
+
};
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
const procfile = procfileCommand(read(DETECT_TABLE.procfile.file));
|
|
772
|
+
const detectedStart =
|
|
773
|
+
runtime === BotRuntime.Node
|
|
774
|
+
? nodeStartOf(packageJson, has)
|
|
775
|
+
: runtime === BotRuntime.Python
|
|
776
|
+
? pythonStartOf(has)
|
|
777
|
+
: null;
|
|
778
|
+
const start: DetectedCommand | null = serverk?.start
|
|
779
|
+
? {
|
|
780
|
+
command: serverk.start,
|
|
781
|
+
source: SERVERK_JSON,
|
|
782
|
+
}
|
|
783
|
+
: (detectedStart ?? procfile);
|
|
784
|
+
|
|
785
|
+
if (!start) {
|
|
786
|
+
warnings.add(BotInspectWarning.NoStart);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
if (runtime === BotRuntime.Node && start && TYPESCRIPT_ENTRY.test(start.command)) {
|
|
790
|
+
warnings.add(BotInspectWarning.TypescriptEntry);
|
|
791
|
+
}
|
|
792
|
+
|
|
793
|
+
committedWarnings(paths, warnings);
|
|
794
|
+
|
|
795
|
+
const env = envCollector(warnings);
|
|
796
|
+
|
|
797
|
+
for (const entry of serverk?.env ?? []) {
|
|
798
|
+
env.add(entry.key, SERVERK_JSON, entry.sensitive, entry.example);
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
for (const file of ENV_EXAMPLE_FILES) {
|
|
802
|
+
const text = read(file);
|
|
803
|
+
|
|
804
|
+
for (const entry of text === null ? [] : parseEnvExample(text)) {
|
|
805
|
+
env.add(entry.key, file, null, entry.value);
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
|
|
809
|
+
for (const [path, text] of sources) {
|
|
810
|
+
for (const key of scanEnvKeys(path, text)) {
|
|
811
|
+
env.add(key, path, null, null);
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
const candidates =
|
|
816
|
+
runtime === BotRuntime.Node
|
|
817
|
+
? nodeFrameworks
|
|
818
|
+
: runtime === BotRuntime.Python
|
|
819
|
+
? pythonFrameworks
|
|
820
|
+
: [
|
|
821
|
+
...nodeFrameworks,
|
|
822
|
+
...pythonFrameworks,
|
|
823
|
+
];
|
|
824
|
+
const framework = frameworkOf(serverk, candidates, runtime, warnings);
|
|
825
|
+
const keyPlatforms = new Set(
|
|
826
|
+
env.entries.map((entry) => platformOfKey(entry.key)).filter((platform) => platform !== null),
|
|
827
|
+
);
|
|
828
|
+
const platform = platformOf(serverk, framework, candidates, keyPlatforms, warnings);
|
|
829
|
+
|
|
830
|
+
return {
|
|
831
|
+
runtime,
|
|
832
|
+
package: runtime ? BOT_RUNTIME_PACKAGES[runtime] : null,
|
|
833
|
+
packageManager,
|
|
834
|
+
framework,
|
|
835
|
+
platform,
|
|
836
|
+
install,
|
|
837
|
+
start,
|
|
838
|
+
env: env.entries,
|
|
839
|
+
tokenKey: tokenKeyOf(platform, env.entries),
|
|
840
|
+
warnings: warningsInOrder(warnings),
|
|
841
|
+
};
|
|
842
|
+
};
|