perf-skill 0.0.1 → 0.2.0
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/LICENSE +21 -0
- package/README.md +414 -0
- package/SKILL.md +238 -0
- package/dist/cli/main.d.ts +6 -0
- package/dist/cli/main.d.ts.map +1 -0
- package/dist/cli/main.js +353 -0
- package/dist/cli/main.js.map +1 -0
- package/dist/cli/options.d.ts +37 -0
- package/dist/cli/options.d.ts.map +1 -0
- package/dist/cli/options.js +54 -0
- package/dist/cli/options.js.map +1 -0
- package/dist/convert/converter.d.ts +39 -0
- package/dist/convert/converter.d.ts.map +1 -0
- package/dist/convert/converter.js +99 -0
- package/dist/convert/converter.js.map +1 -0
- package/dist/convert/extract.d.ts +32 -0
- package/dist/convert/extract.d.ts.map +1 -0
- package/dist/convert/extract.js +235 -0
- package/dist/convert/extract.js.map +1 -0
- package/dist/convert/index.d.ts +7 -0
- package/dist/convert/index.d.ts.map +1 -0
- package/dist/convert/index.js +7 -0
- package/dist/convert/index.js.map +1 -0
- package/dist/convert/sanitize.d.ts +60 -0
- package/dist/convert/sanitize.d.ts.map +1 -0
- package/dist/convert/sanitize.js +169 -0
- package/dist/convert/sanitize.js.map +1 -0
- package/dist/diff/engine.d.ts +76 -0
- package/dist/diff/engine.d.ts.map +1 -0
- package/dist/diff/engine.js +386 -0
- package/dist/diff/engine.js.map +1 -0
- package/dist/diff/index.d.ts +6 -0
- package/dist/diff/index.d.ts.map +1 -0
- package/dist/diff/index.js +6 -0
- package/dist/diff/index.js.map +1 -0
- package/dist/diff/markdown.d.ts +16 -0
- package/dist/diff/markdown.d.ts.map +1 -0
- package/dist/diff/markdown.js +342 -0
- package/dist/diff/markdown.js.map +1 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +247 -0
- package/dist/index.js.map +1 -0
- package/dist/llm/client.d.ts +39 -0
- package/dist/llm/client.d.ts.map +1 -0
- package/dist/llm/client.js +270 -0
- package/dist/llm/client.js.map +1 -0
- package/dist/llm/index.d.ts +8 -0
- package/dist/llm/index.d.ts.map +1 -0
- package/dist/llm/index.js +8 -0
- package/dist/llm/index.js.map +1 -0
- package/dist/llm/prompt.d.ts +32 -0
- package/dist/llm/prompt.d.ts.map +1 -0
- package/dist/llm/prompt.js +146 -0
- package/dist/llm/prompt.js.map +1 -0
- package/dist/llm/schema.d.ts +150 -0
- package/dist/llm/schema.d.ts.map +1 -0
- package/dist/llm/schema.js +131 -0
- package/dist/llm/schema.js.map +1 -0
- package/dist/llm/validate.d.ts +33 -0
- package/dist/llm/validate.d.ts.map +1 -0
- package/dist/llm/validate.js +241 -0
- package/dist/llm/validate.js.map +1 -0
- package/dist/profile/duration.d.ts +2 -0
- package/dist/profile/duration.d.ts.map +1 -0
- package/dist/profile/duration.js +24 -0
- package/dist/profile/duration.js.map +1 -0
- package/dist/profile/preload.d.ts +2 -0
- package/dist/profile/preload.d.ts.map +1 -0
- package/dist/profile/preload.js +100 -0
- package/dist/profile/preload.js.map +1 -0
- package/dist/profile/runner.d.ts +22 -0
- package/dist/profile/runner.d.ts.map +1 -0
- package/dist/profile/runner.js +88 -0
- package/dist/profile/runner.js.map +1 -0
- package/dist/server/http.d.ts +27 -0
- package/dist/server/http.d.ts.map +1 -0
- package/dist/server/http.js +285 -0
- package/dist/server/http.js.map +1 -0
- package/dist/server/utils.d.ts +15 -0
- package/dist/server/utils.d.ts.map +1 -0
- package/dist/server/utils.js +71 -0
- package/dist/server/utils.js.map +1 -0
- package/dist/skill/handler.d.ts +77 -0
- package/dist/skill/handler.d.ts.map +1 -0
- package/dist/skill/handler.js +91 -0
- package/dist/skill/handler.js.map +1 -0
- package/dist/skill/index.d.ts +6 -0
- package/dist/skill/index.d.ts.map +1 -0
- package/dist/skill/index.js +6 -0
- package/dist/skill/index.js.map +1 -0
- package/dist/skill/manifest.d.ts +17 -0
- package/dist/skill/manifest.d.ts.map +1 -0
- package/dist/skill/manifest.js +231 -0
- package/dist/skill/manifest.js.map +1 -0
- package/dist/types.d.ts +260 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +5 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/fs.d.ts +52 -0
- package/dist/utils/fs.d.ts.map +1 -0
- package/dist/utils/fs.js +106 -0
- package/dist/utils/fs.js.map +1 -0
- package/dist/utils/index.d.ts +7 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +7 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/limits.d.ts +38 -0
- package/dist/utils/limits.d.ts.map +1 -0
- package/dist/utils/limits.js +86 -0
- package/dist/utils/limits.js.map +1 -0
- package/dist/utils/logger.d.ts +21 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +82 -0
- package/dist/utils/logger.js.map +1 -0
- package/package.json +70 -6
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { parseDurationMs } from "./duration.js";
|
|
4
|
+
function parsePositiveInt(value, fallback) {
|
|
5
|
+
if (!value)
|
|
6
|
+
return fallback;
|
|
7
|
+
const parsed = Number.parseInt(value, 10);
|
|
8
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
9
|
+
return fallback;
|
|
10
|
+
return parsed;
|
|
11
|
+
}
|
|
12
|
+
const durationMs = parseDurationMs(process.env.PERF_SKILL_DURATION_MS, 10_000);
|
|
13
|
+
const profileOut = resolve(process.env.PERF_SKILL_PROFILE_OUT ?? "cpu.pb.gz");
|
|
14
|
+
const profileType = process.env.PERF_SKILL_PROFILE_TYPE ?? "cpu";
|
|
15
|
+
const logEnabled = process.env.PERF_SKILL_PROFILE_LOG !== "0";
|
|
16
|
+
const heapEnabled = process.env.PERF_SKILL_ENABLE_HEAP === "1";
|
|
17
|
+
const heapOut = resolve(process.env.PERF_SKILL_HEAP_OUT ?? "heap.pb.gz");
|
|
18
|
+
const heapIntervalBytes = parsePositiveInt(process.env.PERF_SKILL_HEAP_INTERVAL_BYTES, 512 * 1024);
|
|
19
|
+
const heapStackDepth = parsePositiveInt(process.env.PERF_SKILL_HEAP_STACK_DEPTH, 64);
|
|
20
|
+
let pprof;
|
|
21
|
+
try {
|
|
22
|
+
pprof = await import("@datadog/pprof");
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
console.error("[perf-skill] Failed to load @datadog/pprof. " +
|
|
26
|
+
"Ensure the package is installed and supported on this platform.");
|
|
27
|
+
if (error instanceof Error) {
|
|
28
|
+
console.error(`[perf-skill] ${error.message}`);
|
|
29
|
+
}
|
|
30
|
+
process.exit(1);
|
|
31
|
+
}
|
|
32
|
+
if (profileType !== "cpu" && logEnabled) {
|
|
33
|
+
console.error(`[perf-skill] Unsupported profile type: ${profileType}. Using cpu.`);
|
|
34
|
+
}
|
|
35
|
+
let stopped = false;
|
|
36
|
+
let heapStarted = false;
|
|
37
|
+
function stopAndWrite(reason) {
|
|
38
|
+
if (stopped)
|
|
39
|
+
return;
|
|
40
|
+
stopped = true;
|
|
41
|
+
try {
|
|
42
|
+
const profile = pprof.time.stop();
|
|
43
|
+
const buf = pprof.encodeSync(profile);
|
|
44
|
+
mkdirSync(dirname(profileOut), { recursive: true });
|
|
45
|
+
writeFileSync(profileOut, buf);
|
|
46
|
+
if (logEnabled) {
|
|
47
|
+
console.error(`[perf-skill] Wrote CPU profile (${reason}) to ${profileOut}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
catch (error) {
|
|
51
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
52
|
+
console.error(`[perf-skill] Failed to write profile: ${message}`);
|
|
53
|
+
}
|
|
54
|
+
if (heapEnabled && heapStarted) {
|
|
55
|
+
try {
|
|
56
|
+
const heapProfile = pprof.heap.profile();
|
|
57
|
+
const buf = pprof.encodeSync(heapProfile);
|
|
58
|
+
mkdirSync(dirname(heapOut), { recursive: true });
|
|
59
|
+
writeFileSync(heapOut, buf);
|
|
60
|
+
if (logEnabled) {
|
|
61
|
+
console.error(`[perf-skill] Wrote heap profile (${reason}) to ${heapOut}`);
|
|
62
|
+
}
|
|
63
|
+
pprof.heap.stop();
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
67
|
+
console.error(`[perf-skill] Failed to write heap profile: ${message}`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
pprof.time.start({});
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
76
|
+
console.error(`[perf-skill] Failed to start profiler: ${message}`);
|
|
77
|
+
process.exit(1);
|
|
78
|
+
}
|
|
79
|
+
if (heapEnabled) {
|
|
80
|
+
try {
|
|
81
|
+
pprof.heap.start(heapIntervalBytes, heapStackDepth);
|
|
82
|
+
heapStarted = true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
console.error(`[perf-skill] Failed to start heap profiler: ${message}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
const timer = setTimeout(() => stopAndWrite("duration"), durationMs);
|
|
90
|
+
timer.unref();
|
|
91
|
+
process.once("beforeExit", () => stopAndWrite("beforeExit"));
|
|
92
|
+
process.once("SIGINT", () => {
|
|
93
|
+
stopAndWrite("SIGINT");
|
|
94
|
+
process.exit(130);
|
|
95
|
+
});
|
|
96
|
+
process.once("SIGTERM", () => {
|
|
97
|
+
stopAndWrite("SIGTERM");
|
|
98
|
+
process.exit(143);
|
|
99
|
+
});
|
|
100
|
+
//# sourceMappingURL=preload.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preload.js","sourceRoot":"","sources":["../../src/profile/preload.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
export interface RunProfileOptions {
|
|
2
|
+
entryPath: string;
|
|
3
|
+
entryArgs?: string[];
|
|
4
|
+
durationMs?: number;
|
|
5
|
+
outPath?: string;
|
|
6
|
+
enableHeap?: boolean;
|
|
7
|
+
heapOutPath?: string;
|
|
8
|
+
heapIntervalBytes?: number;
|
|
9
|
+
heapStackDepth?: number;
|
|
10
|
+
cwd?: string;
|
|
11
|
+
env?: NodeJS.ProcessEnv;
|
|
12
|
+
}
|
|
13
|
+
export declare function resolvePreloadPath(): {
|
|
14
|
+
preloadPath: string;
|
|
15
|
+
needsTsx: boolean;
|
|
16
|
+
};
|
|
17
|
+
export declare function runCpuProfile(options: RunProfileOptions): Promise<{
|
|
18
|
+
profilePath: string;
|
|
19
|
+
heapProfilePath?: string;
|
|
20
|
+
}>;
|
|
21
|
+
export declare function parseDurationInput(value: string | undefined): number;
|
|
22
|
+
//# sourceMappingURL=runner.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/profile/runner.ts"],"names":[],"mappings":"AAOA,MAAM,WAAW,iBAAiB;IAChC,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;CACzB;AAaD,wBAAgB,kBAAkB,IAAI;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,CAY/E;AAOD,wBAAsB,aAAa,CACjC,OAAO,EAAE,iBAAiB,GACzB,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,eAAe,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA6D5D;AAED,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAEpE"}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { existsSync } from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import { resolve, extname } from "node:path";
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
6
|
+
import { parseDurationMs } from "./duration.js";
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
function hasTsx() {
|
|
9
|
+
try {
|
|
10
|
+
require.resolve("tsx");
|
|
11
|
+
return true;
|
|
12
|
+
}
|
|
13
|
+
catch {
|
|
14
|
+
return false;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
export function resolvePreloadPath() {
|
|
18
|
+
const jsPath = fileURLToPath(new URL("./preload.js", import.meta.url));
|
|
19
|
+
if (existsSync(jsPath)) {
|
|
20
|
+
return { preloadPath: jsPath, needsTsx: false };
|
|
21
|
+
}
|
|
22
|
+
const tsPath = fileURLToPath(new URL("./preload.ts", import.meta.url));
|
|
23
|
+
if (existsSync(tsPath)) {
|
|
24
|
+
return { preloadPath: tsPath, needsTsx: true };
|
|
25
|
+
}
|
|
26
|
+
throw new Error("Unable to locate perf-skill profile preload module.");
|
|
27
|
+
}
|
|
28
|
+
function entryNeedsTsx(entryPath) {
|
|
29
|
+
const ext = extname(entryPath).toLowerCase();
|
|
30
|
+
return ext === ".ts" || ext === ".tsx";
|
|
31
|
+
}
|
|
32
|
+
export async function runCpuProfile(options) {
|
|
33
|
+
const durationMs = options.durationMs ?? 10_000;
|
|
34
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
35
|
+
throw new Error("Invalid duration for profiling.");
|
|
36
|
+
}
|
|
37
|
+
const entryPath = resolve(options.entryPath);
|
|
38
|
+
const profilePath = resolve(options.outPath ?? "cpu.pb.gz");
|
|
39
|
+
const heapEnabled = options.enableHeap === true;
|
|
40
|
+
const heapProfilePath = heapEnabled
|
|
41
|
+
? resolve(options.heapOutPath ?? "heap.pb.gz")
|
|
42
|
+
: undefined;
|
|
43
|
+
const { preloadPath, needsTsx: preloadNeedsTsx } = resolvePreloadPath();
|
|
44
|
+
const entryUsesTsx = entryNeedsTsx(entryPath);
|
|
45
|
+
const needsTsx = preloadNeedsTsx || entryUsesTsx;
|
|
46
|
+
if (needsTsx && !hasTsx()) {
|
|
47
|
+
throw new Error("TypeScript entry requires the tsx package to be installed.");
|
|
48
|
+
}
|
|
49
|
+
const nodeArgs = [];
|
|
50
|
+
if (needsTsx) {
|
|
51
|
+
nodeArgs.push("--import", "tsx");
|
|
52
|
+
}
|
|
53
|
+
nodeArgs.push("--import", pathToFileURL(preloadPath).href, entryPath, ...(options.entryArgs ?? []));
|
|
54
|
+
const env = {
|
|
55
|
+
...process.env,
|
|
56
|
+
...options.env,
|
|
57
|
+
PERF_SKILL_DURATION_MS: String(durationMs),
|
|
58
|
+
PERF_SKILL_PROFILE_OUT: profilePath,
|
|
59
|
+
PERF_SKILL_PROFILE_TYPE: "cpu",
|
|
60
|
+
PERF_SKILL_ENABLE_HEAP: heapEnabled ? "1" : "0",
|
|
61
|
+
};
|
|
62
|
+
if (heapEnabled && heapProfilePath) {
|
|
63
|
+
env.PERF_SKILL_HEAP_OUT = heapProfilePath;
|
|
64
|
+
if (options.heapIntervalBytes !== undefined) {
|
|
65
|
+
env.PERF_SKILL_HEAP_INTERVAL_BYTES = String(options.heapIntervalBytes);
|
|
66
|
+
}
|
|
67
|
+
if (options.heapStackDepth !== undefined) {
|
|
68
|
+
env.PERF_SKILL_HEAP_STACK_DEPTH = String(options.heapStackDepth);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const exitCode = await new Promise((resolvePromise, reject) => {
|
|
72
|
+
const child = spawn(process.execPath, nodeArgs, {
|
|
73
|
+
stdio: "inherit",
|
|
74
|
+
cwd: options.cwd ?? process.cwd(),
|
|
75
|
+
env,
|
|
76
|
+
});
|
|
77
|
+
child.on("error", reject);
|
|
78
|
+
child.on("exit", (code) => resolvePromise(code ?? 1));
|
|
79
|
+
});
|
|
80
|
+
if (exitCode !== 0) {
|
|
81
|
+
throw new Error(`Profile run failed with exit code ${exitCode}.`);
|
|
82
|
+
}
|
|
83
|
+
return { profilePath, heapProfilePath };
|
|
84
|
+
}
|
|
85
|
+
export function parseDurationInput(value) {
|
|
86
|
+
return parseDurationMs(value, 10_000);
|
|
87
|
+
}
|
|
88
|
+
//# sourceMappingURL=runner.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"runner.js","sourceRoot":"","sources":["../../src/profile/runner.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API server for perf-skill
|
|
3
|
+
*/
|
|
4
|
+
import { type FastifyInstance } from "fastify";
|
|
5
|
+
import { analyze, diff } from "../index.js";
|
|
6
|
+
export interface ServerOptions {
|
|
7
|
+
port?: number;
|
|
8
|
+
host?: string;
|
|
9
|
+
maxFileSize?: number;
|
|
10
|
+
enableCors?: boolean;
|
|
11
|
+
corsOrigin?: string | string[] | boolean;
|
|
12
|
+
enableHelmet?: boolean;
|
|
13
|
+
enableRateLimit?: boolean;
|
|
14
|
+
rateLimitMax?: number;
|
|
15
|
+
rateLimitWindowMs?: number;
|
|
16
|
+
analyzeFn?: typeof analyze;
|
|
17
|
+
diffFn?: typeof diff;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Create and configure Fastify server
|
|
21
|
+
*/
|
|
22
|
+
export declare function createServer(options?: ServerOptions): Promise<FastifyInstance>;
|
|
23
|
+
/**
|
|
24
|
+
* Start the HTTP server
|
|
25
|
+
*/
|
|
26
|
+
export declare function startServer(options?: ServerOptions): Promise<FastifyInstance>;
|
|
27
|
+
//# sourceMappingURL=http.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAgB,EAAE,KAAK,eAAe,EAAE,MAAM,SAAS,CAAC;AAKxD,OAAO,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAY5C,MAAM,WAAW,aAAa;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,OAAO,CAAC;IACzC,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,eAAe,CAAC,EAAE,OAAO,CAAC;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,iBAAiB,CAAC,EAAE,MAAM,CAAC;IAC3B,SAAS,CAAC,EAAE,OAAO,OAAO,CAAC;IAC3B,MAAM,CAAC,EAAE,OAAO,IAAI,CAAC;CACtB;AAED;;GAEG;AACH,wBAAsB,YAAY,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAqSxF;AAED;;GAEG;AACH,wBAAsB,WAAW,CAAC,OAAO,GAAE,aAAkB,GAAG,OAAO,CAAC,eAAe,CAAC,CAiBvF"}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* HTTP API server for perf-skill
|
|
3
|
+
*/
|
|
4
|
+
import Fastify from "fastify";
|
|
5
|
+
import multipart from "@fastify/multipart";
|
|
6
|
+
import cors from "@fastify/cors";
|
|
7
|
+
import helmet from "@fastify/helmet";
|
|
8
|
+
import rateLimit from "@fastify/rate-limit";
|
|
9
|
+
import { analyze, diff } from "../index.js";
|
|
10
|
+
import { logger } from "../utils/logger.js";
|
|
11
|
+
import { checkSizeLimit, DEFAULT_LIMITS } from "../utils/limits.js";
|
|
12
|
+
import { cleanupUploadedFiles, parseOptionsField, parseBoolean, parseNumber, resolveMultipartFile, } from "./utils.js";
|
|
13
|
+
/**
|
|
14
|
+
* Create and configure Fastify server
|
|
15
|
+
*/
|
|
16
|
+
export async function createServer(options = {}) {
|
|
17
|
+
const server = Fastify({
|
|
18
|
+
logger: process.env.LOG_FORMAT === "json",
|
|
19
|
+
});
|
|
20
|
+
const analyzeFn = options.analyzeFn ?? analyze;
|
|
21
|
+
const diffFn = options.diffFn ?? diff;
|
|
22
|
+
const corsEnabled = options.enableCors ?? parseBoolean(process.env.CORS_ENABLED, true);
|
|
23
|
+
const helmetEnabled = options.enableHelmet ?? parseBoolean(process.env.HELMET_ENABLED, true);
|
|
24
|
+
const rateLimitEnabled = options.enableRateLimit ?? parseBoolean(process.env.RATE_LIMIT_ENABLED, true);
|
|
25
|
+
if (corsEnabled) {
|
|
26
|
+
const originValue = options.corsOrigin ?? process.env.CORS_ORIGIN ?? true;
|
|
27
|
+
const origin = typeof originValue === "string"
|
|
28
|
+
? originValue === "*"
|
|
29
|
+
? true
|
|
30
|
+
: originValue.split(",").map((item) => item.trim()).filter(Boolean)
|
|
31
|
+
: originValue;
|
|
32
|
+
await server.register(cors, { origin });
|
|
33
|
+
}
|
|
34
|
+
if (helmetEnabled) {
|
|
35
|
+
await server.register(helmet);
|
|
36
|
+
}
|
|
37
|
+
if (rateLimitEnabled) {
|
|
38
|
+
const max = options.rateLimitMax ?? parseNumber(process.env.RATE_LIMIT_MAX, 60);
|
|
39
|
+
const timeWindow = options.rateLimitWindowMs ?? parseNumber(process.env.RATE_LIMIT_WINDOW_MS, 60_000);
|
|
40
|
+
if (max > 0) {
|
|
41
|
+
await server.register(rateLimit, { max, timeWindow });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Register multipart plugin for file uploads
|
|
45
|
+
await server.register(multipart, {
|
|
46
|
+
attachFieldsToBody: true,
|
|
47
|
+
limits: {
|
|
48
|
+
fileSize: options.maxFileSize ?? DEFAULT_LIMITS.maxProfileBytes,
|
|
49
|
+
},
|
|
50
|
+
});
|
|
51
|
+
// Health check
|
|
52
|
+
server.get("/health", async () => {
|
|
53
|
+
return { status: "ok", timestamp: new Date().toISOString() };
|
|
54
|
+
});
|
|
55
|
+
// API info
|
|
56
|
+
server.get("/v1", async () => {
|
|
57
|
+
return {
|
|
58
|
+
name: "perf-skill",
|
|
59
|
+
version: "1.0.0",
|
|
60
|
+
endpoints: {
|
|
61
|
+
analyze: "POST /v1/pprof/analyze",
|
|
62
|
+
diff: "POST /v1/pprof/diff",
|
|
63
|
+
convert: "POST /v1/pprof/convert",
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
});
|
|
67
|
+
// Analyze endpoint
|
|
68
|
+
server.post("/v1/pprof/analyze", async (request, reply) => {
|
|
69
|
+
const startTime = performance.now();
|
|
70
|
+
try {
|
|
71
|
+
// Handle multipart file upload
|
|
72
|
+
const data = await resolveMultipartFile(request, "file");
|
|
73
|
+
if (!data) {
|
|
74
|
+
return reply.status(400).send({
|
|
75
|
+
success: false,
|
|
76
|
+
error: {
|
|
77
|
+
code: "MISSING_FILE",
|
|
78
|
+
message: "Profile file is required",
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
const profileBuffer = await data.toBuffer();
|
|
83
|
+
checkSizeLimit(profileBuffer, DEFAULT_LIMITS.maxProfileBytes, "Profile");
|
|
84
|
+
let options;
|
|
85
|
+
try {
|
|
86
|
+
options = parseOptionsField(request.body?.options);
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
return reply.status(400).send({
|
|
90
|
+
success: false,
|
|
91
|
+
error: {
|
|
92
|
+
code: "INVALID_OPTIONS",
|
|
93
|
+
message: error instanceof Error ? error.message : "Invalid options",
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
// Default to not including source in server mode for security
|
|
98
|
+
if (options.includeSource === undefined) {
|
|
99
|
+
options.includeSource = false;
|
|
100
|
+
}
|
|
101
|
+
logger.info("Analyze request received", {
|
|
102
|
+
filename: data.filename,
|
|
103
|
+
size: profileBuffer.length,
|
|
104
|
+
mode: options.mode,
|
|
105
|
+
});
|
|
106
|
+
const result = await analyzeFn(profileBuffer, options);
|
|
107
|
+
const response = {
|
|
108
|
+
success: true,
|
|
109
|
+
data: result,
|
|
110
|
+
};
|
|
111
|
+
logger.info("Analyze request completed", {
|
|
112
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
113
|
+
hotspotsCount: result.hotspots.length,
|
|
114
|
+
});
|
|
115
|
+
return response;
|
|
116
|
+
}
|
|
117
|
+
catch (error) {
|
|
118
|
+
logger.error("Analyze request failed", {
|
|
119
|
+
error: error instanceof Error ? error.message : String(error),
|
|
120
|
+
});
|
|
121
|
+
return reply.status(500).send({
|
|
122
|
+
success: false,
|
|
123
|
+
error: {
|
|
124
|
+
code: "ANALYSIS_ERROR",
|
|
125
|
+
message: error instanceof Error ? error.message : "Analysis failed",
|
|
126
|
+
},
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
// Convert-only endpoint (no LLM)
|
|
131
|
+
server.post("/v1/pprof/convert", async (request, reply) => {
|
|
132
|
+
try {
|
|
133
|
+
const data = await resolveMultipartFile(request, "file");
|
|
134
|
+
if (!data) {
|
|
135
|
+
return reply.status(400).send({
|
|
136
|
+
success: false,
|
|
137
|
+
error: {
|
|
138
|
+
code: "MISSING_FILE",
|
|
139
|
+
message: "Profile file is required",
|
|
140
|
+
},
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
const profileBuffer = await data.toBuffer();
|
|
144
|
+
checkSizeLimit(profileBuffer, DEFAULT_LIMITS.maxProfileBytes, "Profile");
|
|
145
|
+
let parsedOptions;
|
|
146
|
+
try {
|
|
147
|
+
parsedOptions = parseOptionsField(request.body?.options);
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
return reply.status(400).send({
|
|
151
|
+
success: false,
|
|
152
|
+
error: {
|
|
153
|
+
code: "INVALID_OPTIONS",
|
|
154
|
+
message: error instanceof Error ? error.message : "Invalid options",
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
const options = {
|
|
159
|
+
...parsedOptions,
|
|
160
|
+
mode: "convert-only",
|
|
161
|
+
includeSource: false,
|
|
162
|
+
};
|
|
163
|
+
const result = await analyzeFn(profileBuffer, options);
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
data: result,
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
catch (error) {
|
|
170
|
+
return reply.status(500).send({
|
|
171
|
+
success: false,
|
|
172
|
+
error: {
|
|
173
|
+
code: "CONVERT_ERROR",
|
|
174
|
+
message: error instanceof Error ? error.message : "Conversion failed",
|
|
175
|
+
},
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
});
|
|
179
|
+
// Diff endpoint
|
|
180
|
+
server.post("/v1/pprof/diff", async (request, reply) => {
|
|
181
|
+
const startTime = performance.now();
|
|
182
|
+
let files;
|
|
183
|
+
try {
|
|
184
|
+
// Expect two files: base and current
|
|
185
|
+
files = await request.saveRequestFiles();
|
|
186
|
+
if (files.length < 2) {
|
|
187
|
+
return reply.status(400).send({
|
|
188
|
+
success: false,
|
|
189
|
+
error: {
|
|
190
|
+
code: "MISSING_FILES",
|
|
191
|
+
message: "Two profile files required (base and current)",
|
|
192
|
+
},
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const baseFile = files.find(f => f.fieldname === "base") || files[0];
|
|
196
|
+
const currentFile = files.find(f => f.fieldname === "current") || files[1];
|
|
197
|
+
const { readFile } = await import("node:fs/promises");
|
|
198
|
+
const baseBuffer = await readFile(baseFile.filepath);
|
|
199
|
+
const currentBuffer = await readFile(currentFile.filepath);
|
|
200
|
+
checkSizeLimit(baseBuffer, DEFAULT_LIMITS.maxProfileBytes, "Base profile");
|
|
201
|
+
checkSizeLimit(currentBuffer, DEFAULT_LIMITS.maxProfileBytes, "Current profile");
|
|
202
|
+
let options;
|
|
203
|
+
try {
|
|
204
|
+
options = parseOptionsField(request.body?.options);
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
return reply.status(400).send({
|
|
208
|
+
success: false,
|
|
209
|
+
error: {
|
|
210
|
+
code: "INVALID_OPTIONS",
|
|
211
|
+
message: error instanceof Error ? error.message : "Invalid options",
|
|
212
|
+
},
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
logger.info("Diff request received", {
|
|
216
|
+
baseSize: baseBuffer.length,
|
|
217
|
+
currentSize: currentBuffer.length,
|
|
218
|
+
});
|
|
219
|
+
const result = await diffFn(baseBuffer, currentBuffer, options);
|
|
220
|
+
logger.info("Diff request completed", {
|
|
221
|
+
durationMs: Math.round(performance.now() - startTime),
|
|
222
|
+
regressions: result.regressions.length,
|
|
223
|
+
improvements: result.improvements.length,
|
|
224
|
+
});
|
|
225
|
+
return {
|
|
226
|
+
success: true,
|
|
227
|
+
data: result,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
catch (error) {
|
|
231
|
+
logger.error("Diff request failed", {
|
|
232
|
+
error: error instanceof Error ? error.message : String(error),
|
|
233
|
+
});
|
|
234
|
+
return reply.status(500).send({
|
|
235
|
+
success: false,
|
|
236
|
+
error: {
|
|
237
|
+
code: "DIFF_ERROR",
|
|
238
|
+
message: error instanceof Error ? error.message : "Diff analysis failed",
|
|
239
|
+
},
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
finally {
|
|
243
|
+
if (files) {
|
|
244
|
+
await cleanupUploadedFiles(files);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
});
|
|
248
|
+
// Error handler
|
|
249
|
+
server.setErrorHandler((error, request, reply) => {
|
|
250
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
251
|
+
logger.error("Server error", {
|
|
252
|
+
error: message,
|
|
253
|
+
url: request.url,
|
|
254
|
+
});
|
|
255
|
+
reply.status(500).send({
|
|
256
|
+
success: false,
|
|
257
|
+
error: {
|
|
258
|
+
code: "INTERNAL_ERROR",
|
|
259
|
+
message: "Internal server error",
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
return server;
|
|
264
|
+
}
|
|
265
|
+
/**
|
|
266
|
+
* Start the HTTP server
|
|
267
|
+
*/
|
|
268
|
+
export async function startServer(options = {}) {
|
|
269
|
+
const server = await createServer(options);
|
|
270
|
+
const port = options.port ?? 3000;
|
|
271
|
+
const host = options.host ?? "0.0.0.0";
|
|
272
|
+
try {
|
|
273
|
+
await server.listen({ port, host });
|
|
274
|
+
console.log(`perf-skill server listening on http://${host}:${port}`);
|
|
275
|
+
console.log(`API docs: http://${host}:${port}/v1`);
|
|
276
|
+
return server;
|
|
277
|
+
}
|
|
278
|
+
catch (error) {
|
|
279
|
+
logger.error("Failed to start server", {
|
|
280
|
+
error: error instanceof Error ? error.message : String(error),
|
|
281
|
+
});
|
|
282
|
+
throw error;
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
//# sourceMappingURL=http.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"http.js","sourceRoot":"","sources":["../../src/server/http.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server utilities for multipart handling
|
|
3
|
+
*/
|
|
4
|
+
import type { MultipartFile } from "@fastify/multipart";
|
|
5
|
+
export declare function parseOptionsField<T extends object>(input: unknown): T;
|
|
6
|
+
export declare function cleanupUploadedFiles(files: Array<{
|
|
7
|
+
filepath?: string | null;
|
|
8
|
+
}>): Promise<void>;
|
|
9
|
+
export declare function resolveMultipartFile(request: {
|
|
10
|
+
file: () => Promise<MultipartFile | undefined>;
|
|
11
|
+
body?: Record<string, unknown>;
|
|
12
|
+
}, fieldName: string): Promise<MultipartFile | undefined>;
|
|
13
|
+
export declare function parseBoolean(value: string | undefined, defaultValue: boolean): boolean;
|
|
14
|
+
export declare function parseNumber(value: string | undefined, defaultValue: number): number;
|
|
15
|
+
//# sourceMappingURL=utils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../src/server/utils.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAExD,wBAAgB,iBAAiB,CAAC,CAAC,SAAS,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CA2BrE;AAED,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,KAAK,CAAC;IAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE,CAAC,GACzC,OAAO,CAAC,IAAI,CAAC,CAiBf;AAED,wBAAsB,oBAAoB,CACxC,OAAO,EAAE;IAAE,IAAI,EAAE,MAAM,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAAC;IAAC,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;CAAE,EAC3F,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,aAAa,GAAG,SAAS,CAAC,CAapC;AAED,wBAAgB,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,OAAO,GAAG,OAAO,CAMtF;AAED,wBAAgB,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,YAAY,EAAE,MAAM,GAAG,MAAM,CAInF"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server utilities for multipart handling
|
|
3
|
+
*/
|
|
4
|
+
import { rm } from "node:fs/promises";
|
|
5
|
+
import { logger } from "../utils/logger.js";
|
|
6
|
+
export function parseOptionsField(input) {
|
|
7
|
+
if (input === undefined || input === null) {
|
|
8
|
+
return {};
|
|
9
|
+
}
|
|
10
|
+
if (typeof input === "string") {
|
|
11
|
+
const trimmed = input.trim();
|
|
12
|
+
if (!trimmed) {
|
|
13
|
+
return {};
|
|
14
|
+
}
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(trimmed);
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`Invalid options JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
if (typeof input === "object") {
|
|
23
|
+
if (input && "value" in input) {
|
|
24
|
+
return parseOptionsField(input.value);
|
|
25
|
+
}
|
|
26
|
+
return input;
|
|
27
|
+
}
|
|
28
|
+
throw new Error("Invalid options field type");
|
|
29
|
+
}
|
|
30
|
+
export async function cleanupUploadedFiles(files) {
|
|
31
|
+
const paths = files
|
|
32
|
+
.map((file) => file.filepath)
|
|
33
|
+
.filter((path) => Boolean(path));
|
|
34
|
+
const results = await Promise.allSettled(paths.map((path) => rm(path, { force: true })));
|
|
35
|
+
results.forEach((result, index) => {
|
|
36
|
+
if (result.status === "rejected") {
|
|
37
|
+
logger.warn("Failed to cleanup uploaded file", {
|
|
38
|
+
path: paths[index],
|
|
39
|
+
error: result.reason instanceof Error ? result.reason.message : String(result.reason),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
export async function resolveMultipartFile(request, fieldName) {
|
|
45
|
+
const body = request.body;
|
|
46
|
+
const entry = body?.[fieldName];
|
|
47
|
+
if (Array.isArray(entry)) {
|
|
48
|
+
return entry[0];
|
|
49
|
+
}
|
|
50
|
+
if (entry && typeof entry === "object" && entry.type === "file") {
|
|
51
|
+
return entry;
|
|
52
|
+
}
|
|
53
|
+
return request.file();
|
|
54
|
+
}
|
|
55
|
+
export function parseBoolean(value, defaultValue) {
|
|
56
|
+
if (value === undefined)
|
|
57
|
+
return defaultValue;
|
|
58
|
+
const normalized = value.trim().toLowerCase();
|
|
59
|
+
if (["1", "true", "yes", "on"].includes(normalized))
|
|
60
|
+
return true;
|
|
61
|
+
if (["0", "false", "no", "off"].includes(normalized))
|
|
62
|
+
return false;
|
|
63
|
+
return defaultValue;
|
|
64
|
+
}
|
|
65
|
+
export function parseNumber(value, defaultValue) {
|
|
66
|
+
if (value === undefined)
|
|
67
|
+
return defaultValue;
|
|
68
|
+
const parsed = Number(value);
|
|
69
|
+
return Number.isFinite(parsed) ? parsed : defaultValue;
|
|
70
|
+
}
|
|
71
|
+
//# sourceMappingURL=utils.js.map
|