@bpinternal/overwatch 0.1.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 +451 -0
- package/dist/actuators/actuator.d.ts +13 -0
- package/dist/actuators/actuator.js +30 -0
- package/dist/actuators/actuator.js.map +7 -0
- package/dist/actuators/agent-pr.d.ts +25 -0
- package/dist/actuators/agent-pr.js +119 -0
- package/dist/actuators/agent-pr.js.map +7 -0
- package/dist/actuators/comments.d.ts +3 -0
- package/dist/actuators/comments.js +42 -0
- package/dist/actuators/comments.js.map +7 -0
- package/dist/actuators/index.d.ts +54 -0
- package/dist/actuators/index.js +40 -0
- package/dist/actuators/index.js.map +7 -0
- package/dist/actuators/instructions.d.ts +9 -0
- package/dist/actuators/instructions.js +62 -0
- package/dist/actuators/instructions.js.map +7 -0
- package/dist/actuators.d.ts +72 -0
- package/dist/actuators.js +173 -0
- package/dist/actuators.js.map +7 -0
- package/dist/agents.d.ts +53 -0
- package/dist/agents.js +107 -0
- package/dist/agents.js.map +7 -0
- package/dist/claims.d.ts +16 -0
- package/dist/claims.js +51 -0
- package/dist/claims.js.map +7 -0
- package/dist/cli.d.ts +13 -0
- package/dist/cli.js +94 -0
- package/dist/cli.js.map +7 -0
- package/dist/control-loop.d.ts +50 -0
- package/dist/control-loop.js +285 -0
- package/dist/control-loop.js.map +7 -0
- package/dist/github.d.ts +98 -0
- package/dist/github.js +258 -0
- package/dist/github.js.map +7 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +7 -0
- package/dist/log.d.ts +22 -0
- package/dist/log.js +87 -0
- package/dist/log.js.map +7 -0
- package/dist/pickers/busiest-file.d.ts +9 -0
- package/dist/pickers/busiest-file.js +48 -0
- package/dist/pickers/busiest-file.js.map +7 -0
- package/dist/pickers/count.d.ts +6 -0
- package/dist/pickers/count.js +29 -0
- package/dist/pickers/count.js.map +7 -0
- package/dist/pickers/index.d.ts +12 -0
- package/dist/pickers/index.js +41 -0
- package/dist/pickers/index.js.map +7 -0
- package/dist/pickers.d.ts +21 -0
- package/dist/pickers.js +61 -0
- package/dist/pickers.js.map +7 -0
- package/dist/repo-handle.d.ts +35 -0
- package/dist/repo-handle.js +75 -0
- package/dist/repo-handle.js.map +7 -0
- package/dist/sandbox.d.ts +33 -0
- package/dist/sandbox.js +91 -0
- package/dist/sandbox.js.map +7 -0
- package/dist/sensors/ast-grep.d.ts +41 -0
- package/dist/sensors/ast-grep.js +65 -0
- package/dist/sensors/ast-grep.js.map +7 -0
- package/dist/sensors/grep.d.ts +31 -0
- package/dist/sensors/grep.js +39 -0
- package/dist/sensors/grep.js.map +7 -0
- package/dist/sensors/index.d.ts +27 -0
- package/dist/sensors/index.js +38 -0
- package/dist/sensors/index.js.map +7 -0
- package/dist/sensors/react-doctor.d.ts +43 -0
- package/dist/sensors/react-doctor.js +71 -0
- package/dist/sensors/react-doctor.js.map +7 -0
- package/dist/sensors/script.d.ts +7 -0
- package/dist/sensors/script.js +32 -0
- package/dist/sensors/script.js.map +7 -0
- package/dist/sensors.d.ts +117 -0
- package/dist/sensors.js +126 -0
- package/dist/sensors.js.map +7 -0
- package/dist/types.d.ts +119 -0
- package/dist/types.js +17 -0
- package/dist/types.js.map +7 -0
- package/package.json +64 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { RepoHandle } from "../repo-handle";
|
|
2
|
+
import type { Signal } from "../types";
|
|
3
|
+
/**
|
|
4
|
+
* A sensor that runs on the host but reads the repo through a {@link RepoHandle}
|
|
5
|
+
* proxying into the sandbox. Closures and imports work normally.
|
|
6
|
+
*/
|
|
7
|
+
export type SensorFn<TData = unknown> = (repo: RepoHandle) => Promise<Signal<TData>[]>;
|
|
8
|
+
/**
|
|
9
|
+
* A sensor that is a standalone script uploaded into the sandbox and executed there
|
|
10
|
+
* with the repo root as cwd. Create one with `sensors.script`.
|
|
11
|
+
*/
|
|
12
|
+
export interface SensorScript<TData = unknown> {
|
|
13
|
+
kind: "sensor-script";
|
|
14
|
+
/** Path on the host to a script whose default export is `() => Promise<Signal[]>`. */
|
|
15
|
+
localPath: string;
|
|
16
|
+
/**
|
|
17
|
+
* Phantom marker for the `data` type the script's signals carry. A script runs in the
|
|
18
|
+
* sandbox, so its data type can't be inferred from the file — set it explicitly with
|
|
19
|
+
* `sensors.script<MyData>(path)` to type the paired actuator. Never populated at runtime.
|
|
20
|
+
*/
|
|
21
|
+
readonly __data?: TData;
|
|
22
|
+
}
|
|
23
|
+
export type Sensor<TData = unknown> = SensorFn<TData> | SensorScript<TData>;
|
|
24
|
+
export { script } from "./script";
|
|
25
|
+
export { grep, type GrepOptions } from "./grep";
|
|
26
|
+
export { astGrep, type AstGrepOptions, type AstGrepMatch } from "./ast-grep";
|
|
27
|
+
export { reactDoctor, type ReactDoctorOptions, type ReactDoctorDiagnostic } from "./react-doctor";
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var sensors_exports = {};
|
|
20
|
+
__export(sensors_exports, {
|
|
21
|
+
astGrep: () => import_ast_grep.astGrep,
|
|
22
|
+
grep: () => import_grep.grep,
|
|
23
|
+
reactDoctor: () => import_react_doctor.reactDoctor,
|
|
24
|
+
script: () => import_script.script
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(sensors_exports);
|
|
27
|
+
var import_script = require("./script");
|
|
28
|
+
var import_grep = require("./grep");
|
|
29
|
+
var import_ast_grep = require("./ast-grep");
|
|
30
|
+
var import_react_doctor = require("./react-doctor");
|
|
31
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
32
|
+
0 && (module.exports = {
|
|
33
|
+
astGrep,
|
|
34
|
+
grep,
|
|
35
|
+
reactDoctor,
|
|
36
|
+
script
|
|
37
|
+
});
|
|
38
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sensors/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { RepoHandle } from \"../repo-handle\";\nimport type { Signal } from \"../types\";\n\n/**\n * A sensor that runs on the host but reads the repo through a {@link RepoHandle}\n * proxying into the sandbox. Closures and imports work normally.\n */\nexport type SensorFn<TData = unknown> = (repo: RepoHandle) => Promise<Signal<TData>[]>;\n\n/**\n * A sensor that is a standalone script uploaded into the sandbox and executed there\n * with the repo root as cwd. Create one with `sensors.script`.\n */\nexport interface SensorScript<TData = unknown> {\n kind: \"sensor-script\";\n /** Path on the host to a script whose default export is `() => Promise<Signal[]>`. */\n localPath: string;\n /**\n * Phantom marker for the `data` type the script's signals carry. A script runs in the\n * sandbox, so its data type can't be inferred from the file \u2014 set it explicitly with\n * `sensors.script<MyData>(path)` to type the paired actuator. Never populated at runtime.\n */\n readonly __data?: TData;\n}\n\nexport type Sensor<TData = unknown> = SensorFn<TData> | SensorScript<TData>;\n\nexport { script } from \"./script\";\nexport { grep, type GrepOptions } from \"./grep\";\nexport { astGrep, type AstGrepOptions, type AstGrepMatch } from \"./ast-grep\";\nexport { reactDoctor, type ReactDoctorOptions, type ReactDoctorDiagnostic } from \"./react-doctor\";\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BA,oBAAuB;AACvB,kBAAuC;AACvC,sBAAgE;AAChE,0BAAiF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { SensorFn } from "./index";
|
|
2
|
+
/** One react-doctor finding, as emitted by `react-doctor --json`. */
|
|
3
|
+
export interface ReactDoctorDiagnostic {
|
|
4
|
+
filePath: string;
|
|
5
|
+
plugin: string;
|
|
6
|
+
rule: string;
|
|
7
|
+
severity: string;
|
|
8
|
+
title?: string;
|
|
9
|
+
message: string;
|
|
10
|
+
help?: string;
|
|
11
|
+
/** 1-based; 0 means the finding is file-level. */
|
|
12
|
+
line: number;
|
|
13
|
+
column?: number;
|
|
14
|
+
category?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ReactDoctorOptions {
|
|
17
|
+
/**
|
|
18
|
+
* Directory (relative to the repo root) to scan. Required — scoping the scan keeps
|
|
19
|
+
* the signal set intentional; pass "." to explicitly scan the whole repo.
|
|
20
|
+
*/
|
|
21
|
+
path: string;
|
|
22
|
+
/** Only report these categories (e.g. ["Performance", "Bugs"]). Default: all. */
|
|
23
|
+
categories?: string[];
|
|
24
|
+
/**
|
|
25
|
+
* Builds the signal message for a diagnostic. Signals are matched across runs by
|
|
26
|
+
* file + message, so keep it stable for a given anomaly. The default combines the
|
|
27
|
+
* rule with react-doctor's message.
|
|
28
|
+
*/
|
|
29
|
+
message?: (diagnostic: ReactDoctorDiagnostic) => string;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Sensor that runs [react-doctor](https://react.doctor) inside the sandbox and turns
|
|
33
|
+
* every diagnostic into a signal. Severity maps to priority (error -> high,
|
|
34
|
+
* warning -> medium, else low); the full diagnostic (including react-doctor's `help`
|
|
35
|
+
* text, useful in an actuator's instructions) rides along in `data`.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* sensor: sensors.reactDoctor({
|
|
39
|
+
* path: "packages/frontend",
|
|
40
|
+
* categories: ["Performance", "Bugs"],
|
|
41
|
+
* })
|
|
42
|
+
*/
|
|
43
|
+
export declare const reactDoctor: (options: ReactDoctorOptions) => SensorFn<ReactDoctorDiagnostic>;
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var react_doctor_exports = {};
|
|
20
|
+
__export(react_doctor_exports, {
|
|
21
|
+
reactDoctor: () => reactDoctor
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(react_doctor_exports);
|
|
24
|
+
const reactDoctor = (options) => {
|
|
25
|
+
return async (repo) => {
|
|
26
|
+
const output = "/tmp/react-doctor.json";
|
|
27
|
+
const categories = (options.categories ?? []).map((category) => ` --category ${shellQuote(category)}`).join("");
|
|
28
|
+
const run = await repo.exec(
|
|
29
|
+
`bunx -y react-doctor@latest ${shellQuote(options.path)} --json --no-analytics${categories} > ${output} 2>/dev/null; cat ${output}`,
|
|
30
|
+
{ timeoutSec: 900 }
|
|
31
|
+
);
|
|
32
|
+
let report;
|
|
33
|
+
try {
|
|
34
|
+
report = JSON.parse(run.output);
|
|
35
|
+
} catch {
|
|
36
|
+
throw new Error(`react-doctor produced unparseable output: ${run.output.slice(0, 500)}`);
|
|
37
|
+
}
|
|
38
|
+
if (!report.ok) {
|
|
39
|
+
throw new Error(`react-doctor failed: ${run.output.slice(0, 500)}`);
|
|
40
|
+
}
|
|
41
|
+
return (report.projects ?? []).flatMap((project) => {
|
|
42
|
+
const projectRelative = project.directory.slice(report.directory.length);
|
|
43
|
+
const prefix = joinPaths(options.path, projectRelative);
|
|
44
|
+
return (project.diagnostics ?? []).map((diagnostic) => ({
|
|
45
|
+
location: {
|
|
46
|
+
file: joinPaths(prefix, diagnostic.filePath),
|
|
47
|
+
line: diagnostic.line > 0 ? diagnostic.line : void 0
|
|
48
|
+
},
|
|
49
|
+
message: options.message?.(diagnostic) ?? `react-doctor ${diagnostic.rule}: ${diagnostic.message}`,
|
|
50
|
+
priority: reactDoctorPriority(diagnostic.severity),
|
|
51
|
+
data: diagnostic
|
|
52
|
+
}));
|
|
53
|
+
});
|
|
54
|
+
};
|
|
55
|
+
};
|
|
56
|
+
function reactDoctorPriority(severity) {
|
|
57
|
+
if (severity === "error") return "high";
|
|
58
|
+
if (severity === "warning") return "medium";
|
|
59
|
+
return "low";
|
|
60
|
+
}
|
|
61
|
+
function joinPaths(...parts) {
|
|
62
|
+
return parts.map((part) => part.replace(/^[./]+|\/+$/g, "")).filter((part) => part !== "").join("/");
|
|
63
|
+
}
|
|
64
|
+
function shellQuote(value) {
|
|
65
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
66
|
+
}
|
|
67
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
68
|
+
0 && (module.exports = {
|
|
69
|
+
reactDoctor
|
|
70
|
+
});
|
|
71
|
+
//# sourceMappingURL=react-doctor.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sensors/react-doctor.ts"],
|
|
4
|
+
"sourcesContent": ["import type { SignalPriority } from \"../types\";\nimport type { SensorFn } from \"./index\";\n\n/** One react-doctor finding, as emitted by `react-doctor --json`. */\nexport interface ReactDoctorDiagnostic {\n filePath: string;\n plugin: string;\n rule: string;\n severity: string;\n title?: string;\n message: string;\n help?: string;\n /** 1-based; 0 means the finding is file-level. */\n line: number;\n column?: number;\n category?: string;\n}\n\nexport interface ReactDoctorOptions {\n /**\n * Directory (relative to the repo root) to scan. Required \u2014 scoping the scan keeps\n * the signal set intentional; pass \".\" to explicitly scan the whole repo.\n */\n path: string;\n /** Only report these categories (e.g. [\"Performance\", \"Bugs\"]). Default: all. */\n categories?: string[];\n /**\n * Builds the signal message for a diagnostic. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default combines the\n * rule with react-doctor's message.\n */\n message?: (diagnostic: ReactDoctorDiagnostic) => string;\n}\n\n/**\n * Sensor that runs [react-doctor](https://react.doctor) inside the sandbox and turns\n * every diagnostic into a signal. Severity maps to priority (error -> high,\n * warning -> medium, else low); the full diagnostic (including react-doctor's `help`\n * text, useful in an actuator's instructions) rides along in `data`.\n *\n * @example\n * sensor: sensors.reactDoctor({\n * path: \"packages/frontend\",\n * categories: [\"Performance\", \"Bugs\"],\n * })\n */\nexport const reactDoctor = (options: ReactDoctorOptions): SensorFn<ReactDoctorDiagnostic> => {\n return async (repo) => {\n const output = \"/tmp/react-doctor.json\";\n const categories = (options.categories ?? [])\n .map((category) => ` --category ${shellQuote(category)}`)\n .join(\"\");\n // react-doctor exits 1 whenever it finds issues, so success is judged from the\n // JSON (`ok`), not the exit code. stdout goes through a file to keep it parseable\n // even if bunx prints installation noise.\n const run = await repo.exec(\n `bunx -y react-doctor@latest ${shellQuote(options.path)} --json --no-analytics${categories} > ${output} 2>/dev/null; cat ${output}`,\n { timeoutSec: 900 },\n );\n\n type Report = {\n ok: boolean;\n directory: string;\n projects?: Array<{ directory: string; diagnostics?: ReactDoctorDiagnostic[] }>;\n };\n let report: Report;\n try {\n report = JSON.parse(run.output) as Report;\n } catch {\n throw new Error(`react-doctor produced unparseable output: ${run.output.slice(0, 500)}`);\n }\n if (!report.ok) {\n throw new Error(`react-doctor failed: ${run.output.slice(0, 500)}`);\n }\n\n return (report.projects ?? []).flatMap((project) => {\n // filePath is relative to the project directory; rebuild a repo-relative path\n // (scan path + project dir within the scan root + filePath) for the signal's file.\n const projectRelative = project.directory.slice(report.directory.length);\n const prefix = joinPaths(options.path, projectRelative);\n return (project.diagnostics ?? []).map((diagnostic) => ({\n location: {\n file: joinPaths(prefix, diagnostic.filePath),\n line: diagnostic.line > 0 ? diagnostic.line : undefined,\n },\n message:\n options.message?.(diagnostic) ??\n `react-doctor ${diagnostic.rule}: ${diagnostic.message}`,\n priority: reactDoctorPriority(diagnostic.severity),\n data: diagnostic,\n }));\n });\n };\n};\n\nfunction reactDoctorPriority(severity: string): SignalPriority {\n if (severity === \"error\") return \"high\";\n if (severity === \"warning\") return \"medium\";\n return \"low\";\n}\n\nfunction joinPaths(...parts: string[]): string {\n return parts\n .map((part) => part.replace(/^[./]+|\\/+$/g, \"\"))\n .filter((part) => part !== \"\")\n .join(\"/\");\n}\n\n/** Single-quote for the shell; category names and paths are arbitrary user text. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA8CO,MAAM,cAAc,CAAC,YAAiE;AAC3F,SAAO,OAAO,SAAS;AACrB,UAAM,SAAS;AACf,UAAM,cAAc,QAAQ,cAAc,CAAC,GACxC,IAAI,CAAC,aAAa,eAAe,WAAW,QAAQ,CAAC,EAAE,EACvD,KAAK,EAAE;AAIV,UAAM,MAAM,MAAM,KAAK;AAAA,MACrB,+BAA+B,WAAW,QAAQ,IAAI,CAAC,yBAAyB,UAAU,MAAM,MAAM,qBAAqB,MAAM;AAAA,MACjI,EAAE,YAAY,IAAI;AAAA,IACpB;AAOA,QAAI;AACJ,QAAI;AACF,eAAS,KAAK,MAAM,IAAI,MAAM;AAAA,IAChC,QAAQ;AACN,YAAM,IAAI,MAAM,6CAA6C,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,IACzF;AACA,QAAI,CAAC,OAAO,IAAI;AACd,YAAM,IAAI,MAAM,wBAAwB,IAAI,OAAO,MAAM,GAAG,GAAG,CAAC,EAAE;AAAA,IACpE;AAEA,YAAQ,OAAO,YAAY,CAAC,GAAG,QAAQ,CAAC,YAAY;AAGlD,YAAM,kBAAkB,QAAQ,UAAU,MAAM,OAAO,UAAU,MAAM;AACvE,YAAM,SAAS,UAAU,QAAQ,MAAM,eAAe;AACtD,cAAQ,QAAQ,eAAe,CAAC,GAAG,IAAI,CAAC,gBAAgB;AAAA,QACtD,UAAU;AAAA,UACR,MAAM,UAAU,QAAQ,WAAW,QAAQ;AAAA,UAC3C,MAAM,WAAW,OAAO,IAAI,WAAW,OAAO;AAAA,QAChD;AAAA,QACA,SACE,QAAQ,UAAU,UAAU,KAC5B,gBAAgB,WAAW,IAAI,KAAK,WAAW,OAAO;AAAA,QACxD,UAAU,oBAAoB,WAAW,QAAQ;AAAA,QACjD,MAAM;AAAA,MACR,EAAE;AAAA,IACJ,CAAC;AAAA,EACH;AACF;AAEA,SAAS,oBAAoB,UAAkC;AAC7D,MAAI,aAAa,QAAS,QAAO;AACjC,MAAI,aAAa,UAAW,QAAO;AACnC,SAAO;AACT;AAEA,SAAS,aAAa,OAAyB;AAC7C,SAAO,MACJ,IAAI,CAAC,SAAS,KAAK,QAAQ,gBAAgB,EAAE,CAAC,EAC9C,OAAO,CAAC,SAAS,SAAS,EAAE,EAC5B,KAAK,GAAG;AACb;AAGA,SAAS,WAAW,OAAuB;AACzC,SAAO,IAAI,MAAM,QAAQ,MAAM,OAAO,CAAC;AACzC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { SensorScript } from "./index";
|
|
2
|
+
/**
|
|
3
|
+
* Sensor that is a standalone local script uploaded into the sandbox and executed
|
|
4
|
+
* there with the repo root as cwd. The file's default export must be
|
|
5
|
+
* `() => Promise<Signal[]>`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const script: <TData = unknown>(localPath: string) => SensorScript<TData>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var script_exports = {};
|
|
20
|
+
__export(script_exports, {
|
|
21
|
+
script: () => script
|
|
22
|
+
});
|
|
23
|
+
module.exports = __toCommonJS(script_exports);
|
|
24
|
+
const script = (localPath) => ({
|
|
25
|
+
kind: "sensor-script",
|
|
26
|
+
localPath
|
|
27
|
+
});
|
|
28
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
29
|
+
0 && (module.exports = {
|
|
30
|
+
script
|
|
31
|
+
});
|
|
32
|
+
//# sourceMappingURL=script.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/sensors/script.ts"],
|
|
4
|
+
"sourcesContent": ["import type { SensorScript } from \"./index\";\n\n/**\n * Sensor that is a standalone local script uploaded into the sandbox and executed\n * there with the repo root as cwd. The file's default export must be\n * `() => Promise<Signal[]>`.\n */\nexport const script = <TData = unknown>(localPath: string): SensorScript<TData> => ({\n kind: \"sensor-script\",\n localPath,\n});\n"],
|
|
5
|
+
"mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAOO,MAAM,SAAS,CAAkB,eAA4C;AAAA,EAClF,MAAM;AAAA,EACN;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import type { GrepMatch } from "./repo-handle";
|
|
2
|
+
import type { SensorFn, SensorScript, SignalPriority } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Sensor that is a standalone local script uploaded into the sandbox and executed
|
|
5
|
+
* there with the repo root as cwd. The file's default export must be
|
|
6
|
+
* `() => Promise<Signal[]>`.
|
|
7
|
+
*/
|
|
8
|
+
export declare const script: <TData = unknown>(localPath: string) => SensorScript<TData>;
|
|
9
|
+
export interface GrepOptions {
|
|
10
|
+
/** Text/regex pattern searched in file contents. */
|
|
11
|
+
pattern: string;
|
|
12
|
+
/**
|
|
13
|
+
* Paths (relative to the repo root) to search. Required — scoping the search keeps
|
|
14
|
+
* the signal set intentional; pass ["."] to explicitly search the whole repo.
|
|
15
|
+
*/
|
|
16
|
+
paths: string[];
|
|
17
|
+
/** Applied to every signal. Default "medium". */
|
|
18
|
+
priority?: SignalPriority;
|
|
19
|
+
/**
|
|
20
|
+
* Builds the signal message for a match. Signals are matched across runs by
|
|
21
|
+
* file + message, so keep it stable for a given anomaly. The default includes the
|
|
22
|
+
* matched line, which naturally disappears once the anomaly is fixed.
|
|
23
|
+
*/
|
|
24
|
+
message?: (match: GrepMatch) => string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Sensor that searches file contents and turns every matching line into a signal.
|
|
28
|
+
*
|
|
29
|
+
* @example
|
|
30
|
+
* sensor: sensors.grep({
|
|
31
|
+
* pattern: "TODO:",
|
|
32
|
+
* paths: ["src/"],
|
|
33
|
+
* message: (match) => `unresolved TODO: ${match.content.trim()}`,
|
|
34
|
+
* })
|
|
35
|
+
*/
|
|
36
|
+
export declare const grep: (options: GrepOptions) => SensorFn<GrepMatch>;
|
|
37
|
+
/** One react-doctor finding, as emitted by `react-doctor --json`. */
|
|
38
|
+
export interface ReactDoctorDiagnostic {
|
|
39
|
+
filePath: string;
|
|
40
|
+
plugin: string;
|
|
41
|
+
rule: string;
|
|
42
|
+
severity: string;
|
|
43
|
+
title?: string;
|
|
44
|
+
message: string;
|
|
45
|
+
help?: string;
|
|
46
|
+
/** 1-based; 0 means the finding is file-level. */
|
|
47
|
+
line: number;
|
|
48
|
+
column?: number;
|
|
49
|
+
category?: string;
|
|
50
|
+
}
|
|
51
|
+
export interface ReactDoctorOptions {
|
|
52
|
+
/**
|
|
53
|
+
* Directory (relative to the repo root) to scan. Required — scoping the scan keeps
|
|
54
|
+
* the signal set intentional; pass "." to explicitly scan the whole repo.
|
|
55
|
+
*/
|
|
56
|
+
path: string;
|
|
57
|
+
/** Only report these categories (e.g. ["Performance", "Bugs"]). Default: all. */
|
|
58
|
+
categories?: string[];
|
|
59
|
+
/**
|
|
60
|
+
* Builds the signal message for a diagnostic. Signals are matched across runs by
|
|
61
|
+
* file + message, so keep it stable for a given anomaly. The default combines the
|
|
62
|
+
* rule with react-doctor's message.
|
|
63
|
+
*/
|
|
64
|
+
message?: (diagnostic: ReactDoctorDiagnostic) => string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Sensor that runs [react-doctor](https://react.doctor) inside the sandbox and turns
|
|
68
|
+
* every diagnostic into a signal. Severity maps to priority (error -> high,
|
|
69
|
+
* warning -> medium, else low); the full diagnostic (including react-doctor's `help`
|
|
70
|
+
* text, useful in an actuator's instructions) rides along in `data`.
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* sensor: sensors.reactDoctor({
|
|
74
|
+
* path: "packages/frontend",
|
|
75
|
+
* categories: ["Performance", "Bugs"],
|
|
76
|
+
* })
|
|
77
|
+
*/
|
|
78
|
+
export declare const reactDoctor: (options: ReactDoctorOptions) => SensorFn<ReactDoctorDiagnostic>;
|
|
79
|
+
export interface AstGrepMatch {
|
|
80
|
+
file: string;
|
|
81
|
+
/** 1-based, like Signal.location.line. */
|
|
82
|
+
line: number;
|
|
83
|
+
/** Source text of the matched node. */
|
|
84
|
+
text: string;
|
|
85
|
+
}
|
|
86
|
+
export interface AstGrepOptions {
|
|
87
|
+
/** ast-grep pattern, e.g. "console.log($$$ARGS)". */
|
|
88
|
+
pattern: string;
|
|
89
|
+
/** Language to parse, e.g. "typescript" or "tsx". Omit to let ast-grep infer it per file. */
|
|
90
|
+
language?: string;
|
|
91
|
+
/** Applied to every signal. Default "medium". */
|
|
92
|
+
priority?: SignalPriority;
|
|
93
|
+
/**
|
|
94
|
+
* Builds the signal message for a match. Signals are matched across runs by
|
|
95
|
+
* file + message, so keep it stable for a given anomaly. The default includes the
|
|
96
|
+
* matched text, which naturally disappears once the anomaly is fixed.
|
|
97
|
+
*/
|
|
98
|
+
message?: (match: AstGrepMatch) => string;
|
|
99
|
+
/**
|
|
100
|
+
* Paths (relative to the repo root) to scan. Required — scoping the scan keeps the
|
|
101
|
+
* signal set intentional; pass ["."] to explicitly scan the whole repo.
|
|
102
|
+
*/
|
|
103
|
+
paths: string[];
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Sensor that runs [ast-grep](https://ast-grep.github.io) inside the sandbox and turns
|
|
107
|
+
* every structural match into a signal. The CLI is installed on first use if the
|
|
108
|
+
* sandbox image doesn't have it (pre-bake `@ast-grep/cli` in a snapshot to skip that).
|
|
109
|
+
*
|
|
110
|
+
* @example
|
|
111
|
+
* sensor: sensors.astGrep({
|
|
112
|
+
* pattern: "throw new Error($$$)",
|
|
113
|
+
* language: "ts",
|
|
114
|
+
* paths: ["packages/backend/src/trpc/routers/"],
|
|
115
|
+
* })
|
|
116
|
+
*/
|
|
117
|
+
export declare const astGrep: (options: AstGrepOptions) => SensorFn<AstGrepMatch>;
|
package/dist/sensors.js
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
var sensors_exports = {};
|
|
20
|
+
__export(sensors_exports, {
|
|
21
|
+
astGrep: () => astGrep,
|
|
22
|
+
grep: () => grep,
|
|
23
|
+
reactDoctor: () => reactDoctor,
|
|
24
|
+
script: () => script
|
|
25
|
+
});
|
|
26
|
+
module.exports = __toCommonJS(sensors_exports);
|
|
27
|
+
const script = (localPath) => ({
|
|
28
|
+
kind: "sensor-script",
|
|
29
|
+
localPath
|
|
30
|
+
});
|
|
31
|
+
const grep = (options) => {
|
|
32
|
+
return async (repo) => {
|
|
33
|
+
const matches = await repo.grep(options.pattern, options.paths);
|
|
34
|
+
return matches.map((match) => ({
|
|
35
|
+
location: { file: match.file, line: match.line },
|
|
36
|
+
message: options.message?.(match) ?? `matches "${options.pattern}": ${match.content.trim().slice(0, 120)}`,
|
|
37
|
+
priority: options.priority ?? "medium",
|
|
38
|
+
data: match
|
|
39
|
+
}));
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
const reactDoctor = (options) => {
|
|
43
|
+
return async (repo) => {
|
|
44
|
+
const output = "/tmp/react-doctor.json";
|
|
45
|
+
const categories = (options.categories ?? []).map((category) => ` --category ${shellQuote(category)}`).join("");
|
|
46
|
+
const run = await repo.exec(
|
|
47
|
+
`bunx -y react-doctor@latest ${shellQuote(options.path)} --json --no-analytics${categories} > ${output} 2>/dev/null; cat ${output}`,
|
|
48
|
+
{ timeoutSec: 900 }
|
|
49
|
+
);
|
|
50
|
+
let report;
|
|
51
|
+
try {
|
|
52
|
+
report = JSON.parse(run.output);
|
|
53
|
+
} catch {
|
|
54
|
+
throw new Error(`react-doctor produced unparseable output: ${run.output.slice(0, 500)}`);
|
|
55
|
+
}
|
|
56
|
+
if (!report.ok) {
|
|
57
|
+
throw new Error(`react-doctor failed: ${run.output.slice(0, 500)}`);
|
|
58
|
+
}
|
|
59
|
+
return (report.projects ?? []).flatMap((project) => {
|
|
60
|
+
const projectRelative = project.directory.slice(report.directory.length);
|
|
61
|
+
const prefix = joinPaths(options.path, projectRelative);
|
|
62
|
+
return (project.diagnostics ?? []).map((diagnostic) => ({
|
|
63
|
+
location: {
|
|
64
|
+
file: joinPaths(prefix, diagnostic.filePath),
|
|
65
|
+
line: diagnostic.line > 0 ? diagnostic.line : void 0
|
|
66
|
+
},
|
|
67
|
+
message: options.message?.(diagnostic) ?? `react-doctor ${diagnostic.rule}: ${diagnostic.message}`,
|
|
68
|
+
priority: reactDoctorPriority(diagnostic.severity),
|
|
69
|
+
data: diagnostic
|
|
70
|
+
}));
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
function reactDoctorPriority(severity) {
|
|
75
|
+
if (severity === "error") return "high";
|
|
76
|
+
if (severity === "warning") return "medium";
|
|
77
|
+
return "low";
|
|
78
|
+
}
|
|
79
|
+
function joinPaths(...parts) {
|
|
80
|
+
return parts.map((part) => part.replace(/^[./]+|\/+$/g, "")).filter((part) => part !== "").join("/");
|
|
81
|
+
}
|
|
82
|
+
const astGrep = (options) => {
|
|
83
|
+
return async (repo) => {
|
|
84
|
+
const install = await repo.exec("command -v ast-grep || npm install -g @ast-grep/cli", { timeoutSec: 600 });
|
|
85
|
+
if (install.exitCode !== 0) {
|
|
86
|
+
throw new Error(`failed to install ast-grep: ${install.output}`);
|
|
87
|
+
}
|
|
88
|
+
const lang = options.language ? ` --lang ${options.language}` : "";
|
|
89
|
+
const paths = ` ${options.paths.map(shellQuote).join(" ")}`;
|
|
90
|
+
const run = await repo.exec(`ast-grep run --pattern ${shellQuote(options.pattern)}${lang} --json${paths}`, {
|
|
91
|
+
timeoutSec: 600
|
|
92
|
+
});
|
|
93
|
+
if (run.exitCode !== 0) {
|
|
94
|
+
throw new Error(`ast-grep exited ${run.exitCode}: ${run.output}`);
|
|
95
|
+
}
|
|
96
|
+
const results = JSON.parse(run.output);
|
|
97
|
+
return results.map((result) => {
|
|
98
|
+
const match = {
|
|
99
|
+
file: result.file,
|
|
100
|
+
line: result.range.start.line + 1,
|
|
101
|
+
text: result.text
|
|
102
|
+
};
|
|
103
|
+
return {
|
|
104
|
+
location: { file: match.file, line: match.line },
|
|
105
|
+
message: options.message?.(match) ?? defaultMessage(options.pattern, match),
|
|
106
|
+
priority: options.priority ?? "medium",
|
|
107
|
+
data: match
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
};
|
|
111
|
+
};
|
|
112
|
+
function defaultMessage(pattern, match) {
|
|
113
|
+
const text = match.text.split("\n")[0].slice(0, 120);
|
|
114
|
+
return `matches pattern "${pattern}": ${text}`;
|
|
115
|
+
}
|
|
116
|
+
function shellQuote(value) {
|
|
117
|
+
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
118
|
+
}
|
|
119
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
120
|
+
0 && (module.exports = {
|
|
121
|
+
astGrep,
|
|
122
|
+
grep,
|
|
123
|
+
reactDoctor,
|
|
124
|
+
script
|
|
125
|
+
});
|
|
126
|
+
//# sourceMappingURL=sensors.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../src/sensors.ts"],
|
|
4
|
+
"sourcesContent": ["import type { GrepMatch } from \"./repo-handle\";\nimport type { SensorFn, SensorScript, SignalPriority } from \"./types\";\n\n/**\n * Sensor that is a standalone local script uploaded into the sandbox and executed\n * there with the repo root as cwd. The file's default export must be\n * `() => Promise<Signal[]>`.\n */\nexport const script = <TData = unknown>(localPath: string): SensorScript<TData> => ({\n kind: \"sensor-script\",\n localPath,\n});\n\nexport interface GrepOptions {\n /** Text/regex pattern searched in file contents. */\n pattern: string;\n /**\n * Paths (relative to the repo root) to search. Required \u2014 scoping the search keeps\n * the signal set intentional; pass [\".\"] to explicitly search the whole repo.\n */\n paths: string[];\n /** Applied to every signal. Default \"medium\". */\n priority?: SignalPriority;\n /**\n * Builds the signal message for a match. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default includes the\n * matched line, which naturally disappears once the anomaly is fixed.\n */\n message?: (match: GrepMatch) => string;\n}\n\n/**\n * Sensor that searches file contents and turns every matching line into a signal.\n *\n * @example\n * sensor: sensors.grep({\n * pattern: \"TODO:\",\n * paths: [\"src/\"],\n * message: (match) => `unresolved TODO: ${match.content.trim()}`,\n * })\n */\nexport const grep = (options: GrepOptions): SensorFn<GrepMatch> => {\n return async (repo) => {\n const matches = await repo.grep(options.pattern, options.paths);\n return matches.map((match) => ({\n location: { file: match.file, line: match.line },\n message: options.message?.(match) ?? `matches \"${options.pattern}\": ${match.content.trim().slice(0, 120)}`,\n priority: options.priority ?? \"medium\",\n data: match,\n }));\n };\n};\n\n/** One react-doctor finding, as emitted by `react-doctor --json`. */\nexport interface ReactDoctorDiagnostic {\n filePath: string;\n plugin: string;\n rule: string;\n severity: string;\n title?: string;\n message: string;\n help?: string;\n /** 1-based; 0 means the finding is file-level. */\n line: number;\n column?: number;\n category?: string;\n}\n\nexport interface ReactDoctorOptions {\n /**\n * Directory (relative to the repo root) to scan. Required \u2014 scoping the scan keeps\n * the signal set intentional; pass \".\" to explicitly scan the whole repo.\n */\n path: string;\n /** Only report these categories (e.g. [\"Performance\", \"Bugs\"]). Default: all. */\n categories?: string[];\n /**\n * Builds the signal message for a diagnostic. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default combines the\n * rule with react-doctor's message.\n */\n message?: (diagnostic: ReactDoctorDiagnostic) => string;\n}\n\n/**\n * Sensor that runs [react-doctor](https://react.doctor) inside the sandbox and turns\n * every diagnostic into a signal. Severity maps to priority (error -> high,\n * warning -> medium, else low); the full diagnostic (including react-doctor's `help`\n * text, useful in an actuator's instructions) rides along in `data`.\n *\n * @example\n * sensor: sensors.reactDoctor({\n * path: \"packages/frontend\",\n * categories: [\"Performance\", \"Bugs\"],\n * })\n */\nexport const reactDoctor = (options: ReactDoctorOptions): SensorFn<ReactDoctorDiagnostic> => {\n return async (repo) => {\n const output = \"/tmp/react-doctor.json\";\n const categories = (options.categories ?? [])\n .map((category) => ` --category ${shellQuote(category)}`)\n .join(\"\");\n // react-doctor exits 1 whenever it finds issues, so success is judged from the\n // JSON (`ok`), not the exit code. stdout goes through a file to keep it parseable\n // even if bunx prints installation noise.\n const run = await repo.exec(\n `bunx -y react-doctor@latest ${shellQuote(options.path)} --json --no-analytics${categories} > ${output} 2>/dev/null; cat ${output}`,\n { timeoutSec: 900 },\n );\n\n type Report = {\n ok: boolean;\n directory: string;\n projects?: Array<{ directory: string; diagnostics?: ReactDoctorDiagnostic[] }>;\n };\n let report: Report;\n try {\n report = JSON.parse(run.output) as Report;\n } catch {\n throw new Error(`react-doctor produced unparseable output: ${run.output.slice(0, 500)}`);\n }\n if (!report.ok) {\n throw new Error(`react-doctor failed: ${run.output.slice(0, 500)}`);\n }\n\n return (report.projects ?? []).flatMap((project) => {\n // filePath is relative to the project directory; rebuild a repo-relative path\n // (scan path + project dir within the scan root + filePath) for the signal's file.\n const projectRelative = project.directory.slice(report.directory.length);\n const prefix = joinPaths(options.path, projectRelative);\n return (project.diagnostics ?? []).map((diagnostic) => ({\n location: {\n file: joinPaths(prefix, diagnostic.filePath),\n line: diagnostic.line > 0 ? diagnostic.line : undefined,\n },\n message:\n options.message?.(diagnostic) ??\n `react-doctor ${diagnostic.rule}: ${diagnostic.message}`,\n priority: reactDoctorPriority(diagnostic.severity),\n data: diagnostic,\n }));\n });\n };\n};\n\nfunction reactDoctorPriority(severity: string): SignalPriority {\n if (severity === \"error\") return \"high\";\n if (severity === \"warning\") return \"medium\";\n return \"low\";\n}\n\nfunction joinPaths(...parts: string[]): string {\n return parts\n .map((part) => part.replace(/^[./]+|\\/+$/g, \"\"))\n .filter((part) => part !== \"\")\n .join(\"/\");\n}\n\nexport interface AstGrepMatch {\n file: string;\n /** 1-based, like Signal.location.line. */\n line: number;\n /** Source text of the matched node. */\n text: string;\n}\n\nexport interface AstGrepOptions {\n /** ast-grep pattern, e.g. \"console.log($$$ARGS)\". */\n pattern: string;\n /** Language to parse, e.g. \"typescript\" or \"tsx\". Omit to let ast-grep infer it per file. */\n language?: string;\n /** Applied to every signal. Default \"medium\". */\n priority?: SignalPriority;\n /**\n * Builds the signal message for a match. Signals are matched across runs by\n * file + message, so keep it stable for a given anomaly. The default includes the\n * matched text, which naturally disappears once the anomaly is fixed.\n */\n message?: (match: AstGrepMatch) => string;\n /**\n * Paths (relative to the repo root) to scan. Required \u2014 scoping the scan keeps the\n * signal set intentional; pass [\".\"] to explicitly scan the whole repo.\n */\n paths: string[];\n}\n\n/**\n * Sensor that runs [ast-grep](https://ast-grep.github.io) inside the sandbox and turns\n * every structural match into a signal. The CLI is installed on first use if the\n * sandbox image doesn't have it (pre-bake `@ast-grep/cli` in a snapshot to skip that).\n *\n * @example\n * sensor: sensors.astGrep({\n * pattern: \"throw new Error($$$)\",\n * language: \"ts\",\n * paths: [\"packages/backend/src/trpc/routers/\"],\n * })\n */\nexport const astGrep = (options: AstGrepOptions): SensorFn<AstGrepMatch> => {\n return async (repo) => {\n const install = await repo.exec(\"command -v ast-grep || npm install -g @ast-grep/cli\", { timeoutSec: 600 });\n if (install.exitCode !== 0) {\n throw new Error(`failed to install ast-grep: ${install.output}`);\n }\n\n const lang = options.language ? ` --lang ${options.language}` : \"\";\n const paths = ` ${options.paths.map(shellQuote).join(\" \")}`;\n const run = await repo.exec(`ast-grep run --pattern ${shellQuote(options.pattern)}${lang} --json${paths}`, {\n timeoutSec: 600,\n });\n if (run.exitCode !== 0) {\n throw new Error(`ast-grep exited ${run.exitCode}: ${run.output}`);\n }\n\n const results = JSON.parse(run.output) as Array<{\n file: string;\n text: string;\n range: { start: { line: number } };\n }>;\n\n return results.map((result) => {\n const match: AstGrepMatch = {\n file: result.file,\n line: result.range.start.line + 1,\n text: result.text,\n };\n return {\n location: { file: match.file, line: match.line },\n message: options.message?.(match) ?? defaultMessage(options.pattern, match),\n priority: options.priority ?? \"medium\",\n data: match,\n };\n });\n };\n};\n\nfunction defaultMessage(pattern: string, match: AstGrepMatch): string {\n const text = match.text.split(\"\\n\")[0]!.slice(0, 120);\n return `matches pattern \"${pattern}\": ${text}`;\n}\n\n/** Single-quote for the shell \u2014 ast-grep patterns are full of `$VAR`s the shell must not expand. */\nfunction shellQuote(value: string): string {\n return `'${value.replace(/'/g, `'\\\\''`)}'`;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import type { Agent } from "./agents";
|
|
2
|
+
import type { Actuator, CommentActuator } from "./actuators";
|
|
3
|
+
import type { Sensor } from "./sensors";
|
|
4
|
+
import type { Picker } from "./pickers";
|
|
5
|
+
import type { GitSource, PrComment } from "./github";
|
|
6
|
+
export type SignalPriority = "low" | "medium" | "high";
|
|
7
|
+
/**
|
|
8
|
+
* An anomaly detected by the sensor.
|
|
9
|
+
*
|
|
10
|
+
* Identity note: after a fix, line numbers shift, so a signal is considered "the same"
|
|
11
|
+
* across sensor runs when its `location.file` and `message` match — `location.line` is
|
|
12
|
+
* informational only. Keep `message` stable for a given anomaly (don't embed line numbers
|
|
13
|
+
* or timestamps in it).
|
|
14
|
+
*/
|
|
15
|
+
export interface Signal<TData = unknown> {
|
|
16
|
+
/**
|
|
17
|
+
* Where the anomaly lives. Optional — some anomalies are repo-wide or not tied to a
|
|
18
|
+
* single place. When present, `file` anchors the signal's identity (see note above).
|
|
19
|
+
*/
|
|
20
|
+
location?: {
|
|
21
|
+
file: string;
|
|
22
|
+
line?: number;
|
|
23
|
+
};
|
|
24
|
+
message: string;
|
|
25
|
+
/** Optional; treated as unset (rather than a default) wherever no priority is given. */
|
|
26
|
+
priority?: SignalPriority;
|
|
27
|
+
/**
|
|
28
|
+
* Anything the sensor wants to carry through to the picker and actuator untouched.
|
|
29
|
+
* `TData` is inferred from the sensor, so an actuator paired with it sees this exact
|
|
30
|
+
* type on `signal.data` (see {@link ControlLoopOptions}).
|
|
31
|
+
*/
|
|
32
|
+
data?: TData;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Constructor argument of ControlLoop: identity and static settings, behavior alongside them.
|
|
36
|
+
*
|
|
37
|
+
* `TData` is inferred from the `sensor` and flows to the `picker` and `actuator`, so the
|
|
38
|
+
* `data` a sensor attaches to its signals reaches the actuator with the same static type.
|
|
39
|
+
*/
|
|
40
|
+
export interface ControlLoopOptions<TData = unknown> {
|
|
41
|
+
/**
|
|
42
|
+
* Human-readable name of the loop. Its slug (e.g. "My Loop" -> "my-loop") is applied
|
|
43
|
+
* as a label on every PR the loop opens, and is what `config.maxOpenPrCount` counts
|
|
44
|
+
* against.
|
|
45
|
+
*/
|
|
46
|
+
label: string;
|
|
47
|
+
config: ControlLoopConfig;
|
|
48
|
+
sensor: Sensor<TData>;
|
|
49
|
+
/** Defaults to `pickers.count(1)`. */
|
|
50
|
+
picker?: Picker<TData>;
|
|
51
|
+
actuator: Actuator<TData>;
|
|
52
|
+
/** Customizes the instructions built from PR comments in `applyPrComments`. */
|
|
53
|
+
commentActuator?: CommentActuator;
|
|
54
|
+
}
|
|
55
|
+
export interface ControlLoopConfig {
|
|
56
|
+
/** Skip the run entirely if this many PRs with the loop's label are already open. */
|
|
57
|
+
maxOpenPrCount?: number;
|
|
58
|
+
/**
|
|
59
|
+
* Prefix of the branches the loop pushes, e.g. "bots/cleanup" ->
|
|
60
|
+
* "bots/cleanup/<label-slug>-<run-id>". Default "control-loop".
|
|
61
|
+
*/
|
|
62
|
+
branchPrefix?: string;
|
|
63
|
+
git: GitSource;
|
|
64
|
+
agent: Agent;
|
|
65
|
+
/** Shell commands the loop runs inside the sandbox, all from the repo root. */
|
|
66
|
+
hooks?: {
|
|
67
|
+
/**
|
|
68
|
+
* Run right after cloning, before the sensor and agent (e.g. "bun install" to set
|
|
69
|
+
* up dependencies). The run aborts if it exits non-zero.
|
|
70
|
+
*/
|
|
71
|
+
setup?: string;
|
|
72
|
+
/**
|
|
73
|
+
* Run after the agent finishes and before committing — typically the repo's
|
|
74
|
+
* auto-fix formatter (e.g. "bun run fix:format"). It normalizes everything the
|
|
75
|
+
* loop commits (agent edits and lib-generated files alike) so the PR passes repo
|
|
76
|
+
* formatting checks. The run aborts if it exits non-zero.
|
|
77
|
+
*/
|
|
78
|
+
preCommit?: string;
|
|
79
|
+
};
|
|
80
|
+
/** Environment variables set on the sandbox at creation. */
|
|
81
|
+
env?: Record<string, string>;
|
|
82
|
+
/** Max agent attempts per signal before the run is declared failed. Default 3. */
|
|
83
|
+
maxFixAttempts?: number;
|
|
84
|
+
sandbox?: {
|
|
85
|
+
/**
|
|
86
|
+
* Daytona snapshot to create the sandbox from. Pre-bake your toolchain and the
|
|
87
|
+
* agent's CLI into it to skip per-run installs. Defaults to Daytona's typescript
|
|
88
|
+
* sandbox, with the agent CLI installed at run start.
|
|
89
|
+
*/
|
|
90
|
+
snapshot?: string;
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
export type ControlLoopRunResult<TData = unknown> = {
|
|
94
|
+
status: "skipped";
|
|
95
|
+
reason: string;
|
|
96
|
+
} | {
|
|
97
|
+
status: "clean";
|
|
98
|
+
} | {
|
|
99
|
+
status: "fix-failed";
|
|
100
|
+
unresolved: Signal<TData>[];
|
|
101
|
+
} | {
|
|
102
|
+
status: "pr-opened";
|
|
103
|
+
prUrl: string;
|
|
104
|
+
fixed: Signal<TData>[];
|
|
105
|
+
};
|
|
106
|
+
export type ApplyCommentsResult =
|
|
107
|
+
/** Every comment predates the PR's head commit — nothing new to address. */
|
|
108
|
+
{
|
|
109
|
+
status: "no-new-comments";
|
|
110
|
+
}
|
|
111
|
+
/** The agent ran but left the working tree untouched, so nothing was pushed. */
|
|
112
|
+
| {
|
|
113
|
+
status: "no-changes";
|
|
114
|
+
comments: PrComment[];
|
|
115
|
+
} | {
|
|
116
|
+
status: "comments-applied";
|
|
117
|
+
branch: string;
|
|
118
|
+
comments: PrComment[];
|
|
119
|
+
};
|