@illuminis/comprism 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.
Files changed (80) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +281 -0
  3. package/out/agent/command.d.ts +86 -0
  4. package/out/agent/command.js +259 -0
  5. package/out/agent/render.d.ts +97 -0
  6. package/out/agent/render.js +255 -0
  7. package/out/agent/session.d.ts +175 -0
  8. package/out/agent/session.js +573 -0
  9. package/out/commands/ask.d.ts +1 -0
  10. package/out/commands/ask.js +146 -0
  11. package/out/commands/codemap.d.ts +2 -0
  12. package/out/commands/codemap.js +151 -0
  13. package/out/commands/commands-thin.d.ts +39 -0
  14. package/out/commands/commands-thin.js +182 -0
  15. package/out/commands/install.d.ts +163 -0
  16. package/out/commands/install.js +543 -0
  17. package/out/commands/keys.d.ts +55 -0
  18. package/out/commands/keys.js +344 -0
  19. package/out/commands/login.d.ts +9 -0
  20. package/out/commands/login.js +384 -0
  21. package/out/commands/repl.d.ts +1 -0
  22. package/out/commands/repl.js +752 -0
  23. package/out/commands/settings.d.ts +21 -0
  24. package/out/commands/settings.js +244 -0
  25. package/out/commands/welcome.d.ts +1 -0
  26. package/out/commands/welcome.js +196 -0
  27. package/out/executor/documents.d.ts +40 -0
  28. package/out/executor/documents.js +170 -0
  29. package/out/executor/files.d.ts +2 -0
  30. package/out/executor/files.js +360 -0
  31. package/out/executor/git.d.ts +48 -0
  32. package/out/executor/git.js +132 -0
  33. package/out/executor/hooks.d.ts +67 -0
  34. package/out/executor/hooks.js +247 -0
  35. package/out/executor/index.d.ts +29 -0
  36. package/out/executor/index.js +221 -0
  37. package/out/executor/notebook.d.ts +2 -0
  38. package/out/executor/notebook.js +147 -0
  39. package/out/executor/paths.d.ts +15 -0
  40. package/out/executor/paths.js +126 -0
  41. package/out/executor/shell.d.ts +41 -0
  42. package/out/executor/shell.js +336 -0
  43. package/out/graph/build.d.ts +45 -0
  44. package/out/graph/build.js +91 -0
  45. package/out/graph/facts.d.ts +47 -0
  46. package/out/graph/facts.js +12 -0
  47. package/out/graph/files.d.ts +45 -0
  48. package/out/graph/files.js +207 -0
  49. package/out/graph/read-locales.d.ts +29 -0
  50. package/out/graph/read-locales.js +246 -0
  51. package/out/graph/read-python.d.ts +11 -0
  52. package/out/graph/read-python.js +115 -0
  53. package/out/graph/read-typescript.d.ts +16 -0
  54. package/out/graph/read-typescript.js +292 -0
  55. package/out/graph/sync.d.ts +66 -0
  56. package/out/graph/sync.js +242 -0
  57. package/out/lib/attach.d.ts +62 -0
  58. package/out/lib/attach.js +228 -0
  59. package/out/lib/config.d.ts +93 -0
  60. package/out/lib/config.js +198 -0
  61. package/out/lib/connection.d.ts +73 -0
  62. package/out/lib/connection.js +188 -0
  63. package/out/lib/gateway.d.ts +239 -0
  64. package/out/lib/gateway.js +171 -0
  65. package/out/lib/prompt.d.ts +34 -0
  66. package/out/lib/prompt.js +108 -0
  67. package/out/lib/types.d.ts +417 -0
  68. package/out/lib/types.js +21 -0
  69. package/out/lib/ui.d.ts +114 -0
  70. package/out/lib/ui.js +265 -0
  71. package/out/lib/version.d.ts +24 -0
  72. package/out/lib/version.js +27 -0
  73. package/out/lib/voice.d.ts +50 -0
  74. package/out/lib/voice.js +218 -0
  75. package/out/postinstall.d.ts +2 -0
  76. package/out/postinstall.js +92 -0
  77. package/out/thin.d.ts +2 -0
  78. package/out/thin.js +259 -0
  79. package/package.json +101 -0
  80. package/scripts/read_python.py +270 -0
@@ -0,0 +1,126 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.OutsideWorkspace = void 0;
37
+ exports.isSecret = isSecret;
38
+ exports.resolveInside = resolveInside;
39
+ /**
40
+ * The folder boundary, on a real filesystem.
41
+ *
42
+ * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md §8, register
43
+ * items F1 (the folder is a boundary) and F2 (shortcuts do not defeat it).
44
+ *
45
+ * The browser executor gets this property free: it walks directory handles, and
46
+ * a handle it was never given simply cannot be reached. A real filesystem has no
47
+ * such protection, so here it has to be enforced, and enforced in the one order
48
+ * that works.
49
+ *
50
+ * **Resolve first, then compare.** Checking the string somebody sent and then
51
+ * resolving it is the standard way out: `a/../../etc/passwd` passes a check for
52
+ * ".." at the start and becomes `/etc/passwd` afterwards. And on a real
53
+ * filesystem there is a second door the browser does not have, which is a
54
+ * symbolic link inside the project pointing outside it. `realpath` follows it;
55
+ * a string comparison does not.
56
+ *
57
+ * The server checks this too. Two checks is not belt and braces: the server
58
+ * cannot see the filesystem, so it can only reason about the path it was given,
59
+ * and this can see the filesystem but runs on a machine we do not control.
60
+ * Neither alone is the boundary.
61
+ */
62
+ const fs = __importStar(require("fs"));
63
+ const path = __importStar(require("path"));
64
+ class OutsideWorkspace extends Error {
65
+ attempted;
66
+ constructor(attempted) {
67
+ super(`Refused: ${attempted} is outside this project. The agent works only ` +
68
+ `inside the folder that was opened.`);
69
+ this.attempted = attempted;
70
+ this.name = "OutsideWorkspace";
71
+ }
72
+ }
73
+ exports.OutsideWorkspace = OutsideWorkspace;
74
+ /** Files never read, whatever the mode. Mirrors the server's own list. */
75
+ const SECRET = /^(\.env(\..*)?|.*\.pem|.*\.key|.*\.p12|.*\.pfx|id_rsa.*|id_ed25519.*|.*\.keystore|credentials|\.npmrc|\.netrc|\.git-credentials)$/i;
76
+ function isSecret(p) {
77
+ return SECRET.test(path.basename(p));
78
+ }
79
+ /**
80
+ * An absolute path inside the workspace, or a refusal.
81
+ *
82
+ * `mustExist` is false when the caller is about to CREATE the thing. A file that
83
+ * does not exist yet cannot be resolved, so the check is applied to the deepest
84
+ * parent that does exist: creating `src/new/deep/file.ts` is inside the project
85
+ * exactly when `src` is, and demanding the leaf exist first would make it
86
+ * impossible to write a new file at all.
87
+ */
88
+ function resolveInside(root, rel, mustExist = false) {
89
+ const realRoot = fs.realpathSync(root);
90
+ const asked = String(rel ?? "");
91
+ // An ABSOLUTE path is resolved as written, and refused if it lands outside.
92
+ // It used to have its leading slash stripped and be treated as relative to
93
+ // the project, which was safe and quietly wrong: asking to write
94
+ // `/tmp/notes.txt` created `<project>/tmp/notes.txt` and reported the path the
95
+ // caller gave. Nothing escaped, but the confirmation named a file that did not
96
+ // exist, so the next read failed and the model spent steps on a mystery. An
97
+ // absolute path inside the project still works, which is the only case anybody
98
+ // actually means.
99
+ const joined = path.isAbsolute(asked)
100
+ ? path.resolve(asked)
101
+ : path.resolve(realRoot, asked);
102
+ let probe = joined;
103
+ if (!mustExist) {
104
+ // Walk up to the deepest part that exists, so a path we are about to create
105
+ // is still checked against where it would actually land.
106
+ while (!fs.existsSync(probe) && path.dirname(probe) !== probe) {
107
+ probe = path.dirname(probe);
108
+ }
109
+ }
110
+ let real;
111
+ try {
112
+ real = fs.realpathSync(probe);
113
+ }
114
+ catch {
115
+ throw new OutsideWorkspace(rel);
116
+ }
117
+ // The comparison, with a separator on the end. Without it `/work/project-two`
118
+ // passes as being inside `/work/project`, because one string starts with the
119
+ // other.
120
+ if (real !== realRoot && !real.startsWith(realRoot + path.sep)) {
121
+ throw new OutsideWorkspace(rel);
122
+ }
123
+ // Return the requested path, not the probe: the caller wants to write to the
124
+ // file it named, and the probe may be an ancestor of it.
125
+ return mustExist ? real : joined;
126
+ }
@@ -0,0 +1,41 @@
1
+ export interface RunResult {
2
+ content: string;
3
+ isError?: boolean;
4
+ summary?: string;
5
+ }
6
+ export declare function safeEnvironment(from?: NodeJS.ProcessEnv): NodeJS.ProcessEnv;
7
+ export interface ShellOptions {
8
+ root: string;
9
+ cwd?: string;
10
+ timeoutMs?: number;
11
+ onOutput?: (chunk: string) => void;
12
+ /** Extra variables for this one command.
13
+ *
14
+ * Used by hooks, so a project's own script can read which file the agent
15
+ * just touched without the agent having to rewrite the script's arguments.
16
+ * Still filtered by `safeEnvironment`, so a hook cannot be handed a
17
+ * credential this way either. */
18
+ extraEnv?: Record<string, string>;
19
+ }
20
+ /** Run a command and wait for it.
21
+ *
22
+ * Never throws for an ordinary failure. A command that exits non-zero is a
23
+ * RESULT: it is usually the most useful thing that can happen, because it is
24
+ * how the agent learns what is wrong.
25
+ */
26
+ export declare function runCommand(command: string, opts: ShellOptions): Promise<RunResult>;
27
+ export declare function startBackground(command: string, opts: ShellOptions): RunResult;
28
+ export declare function readOutput(id: string): RunResult;
29
+ export declare function stopProcess(id: string): RunResult;
30
+ /** Everything still running, stopped. Called when a session ends, so a job that
31
+ * started a dev server does not leave it holding a port after the person has
32
+ * closed the terminal. */
33
+ export declare function stopEverything(): void;
34
+ export declare function guessTestCommand(root: string): string | null;
35
+ export declare function runTests(opts: ShellOptions & {
36
+ command?: string;
37
+ path?: string;
38
+ }): Promise<RunResult & {
39
+ passed?: boolean;
40
+ command?: string;
41
+ }>;
@@ -0,0 +1,336 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.safeEnvironment = safeEnvironment;
37
+ exports.runCommand = runCommand;
38
+ exports.startBackground = startBackground;
39
+ exports.readOutput = readOutput;
40
+ exports.stopProcess = stopProcess;
41
+ exports.stopEverything = stopEverything;
42
+ exports.guessTestCommand = guessTestCommand;
43
+ exports.runTests = runTests;
44
+ /**
45
+ * Running things on a real machine.
46
+ *
47
+ * Specification: docs/modules/CODING_AGENT_BUILD_SPECIFICATION.md, register
48
+ * items D17 (run a command), D18 (run the tests), D19 to D21 (long running
49
+ * processes), D22 (install a dependency) and F12 (a restricted environment).
50
+ *
51
+ * ## Why the tests are their own action
52
+ *
53
+ * `run_tests` could be a shell command and deliberately is not. Its result is
54
+ * the grading signal the whole product is built on: it is how a job knows it is
55
+ * finished, and how the routing engine eventually learns whether a cheaper
56
+ * model actually finished the work. A result buried inside the output of a
57
+ * generic command is a result nobody can count.
58
+ *
59
+ * ## Why the environment is trimmed
60
+ *
61
+ * A command inherits the environment of whatever started it, and on a
62
+ * developer's machine that includes cloud credentials, deployment tokens and
63
+ * signing keys. None of that is needed to run a test suite, and all of it is
64
+ * available to anything the command chooses to run. The agent is not the risk
65
+ * here; a package in the project's own dependency tree is.
66
+ */
67
+ const child_process_1 = require("child_process");
68
+ const fs = __importStar(require("fs"));
69
+ const path = __importStar(require("path"));
70
+ const paths_1 = require("./paths");
71
+ /** What a command may print back into the conversation. Beyond this it is cut,
72
+ * and the cut is stated: a model that believes it saw the whole output, and
73
+ * did not, draws a confident conclusion from half of it. */
74
+ const MAX_OUTPUT_CHARS = 30_000;
75
+ /** Environment variables never passed to a command.
76
+ *
77
+ * Matched by pattern rather than listed by name, because the list is
78
+ * unknowable: every cloud provider, CI system and package registry invents its
79
+ * own, and a list of the ones we happened to think of is a list that is out of
80
+ * date the day it is written. */
81
+ const SECRET_ENV = /(^|_)(KEY|SECRET|TOKEN|PASSWORD|PASSWD|CREDENTIAL|CREDENTIALS|AUTH|PRIVATE|SESSION|COOKIE)(_|$)/i;
82
+ /** Kept regardless, because a command that cannot find its own tools is a
83
+ * command that fails for a reason nobody can diagnose. */
84
+ const ALWAYS_KEEP = new Set([
85
+ "PATH", "HOME", "USER", "SHELL", "LANG", "LC_ALL", "TERM", "TMPDIR", "TZ",
86
+ "NODE_ENV", "PWD", "PYTHONPATH", "VIRTUAL_ENV", "NVM_DIR", "JAVA_HOME",
87
+ "GOPATH", "GOROOT", "CARGO_HOME", "RUSTUP_HOME",
88
+ ]);
89
+ function safeEnvironment(from = process.env) {
90
+ const out = {};
91
+ for (const [k, v] of Object.entries(from)) {
92
+ if (ALWAYS_KEEP.has(k) || !SECRET_ENV.test(k))
93
+ out[k] = v;
94
+ }
95
+ // Told, not hidden. A command that behaves differently under the agent should
96
+ // be able to say so, and a person debugging one should be able to see why.
97
+ out.ILLUMINIS_AGENT = "1";
98
+ return out;
99
+ }
100
+ function clip(text) {
101
+ if (text.length <= MAX_OUTPUT_CHARS)
102
+ return text;
103
+ const cut = text.length - MAX_OUTPUT_CHARS;
104
+ return (`${text.slice(0, MAX_OUTPUT_CHARS)}\n\n[... ${cut.toLocaleString()} more ` +
105
+ `characters. Narrow the command, or write the output to a file and read part of it.]`);
106
+ }
107
+ /** Run a command and wait for it.
108
+ *
109
+ * Never throws for an ordinary failure. A command that exits non-zero is a
110
+ * RESULT: it is usually the most useful thing that can happen, because it is
111
+ * how the agent learns what is wrong.
112
+ */
113
+ function runCommand(command, opts) {
114
+ const cwd = opts.cwd ? (0, paths_1.resolveInside)(opts.root, opts.cwd, true) : fs.realpathSync(opts.root);
115
+ const timeoutMs = opts.timeoutMs ?? 300_000;
116
+ return new Promise((resolve) => {
117
+ let out = "";
118
+ let done = false;
119
+ // Through a shell on purpose: pipes, redirection and `&&` are how people
120
+ // actually write commands, and an agent told it cannot use them writes three
121
+ // commands where a person would write one.
122
+ //
123
+ // Running a command IS this function. It is the agent's shell tool, and a
124
+ // shell tool that cannot reach a shell is not a tool. What stands between a
125
+ // command and the machine is the approval list every command is matched
126
+ // against before it reaches here, plus the working directory being resolved
127
+ // inside the workspace root. Removing the shell would not add safety; it
128
+ // would move the same commands somewhere with fewer checks in front of them.
129
+ // The marker has to be the LAST line before the call, not the first line of
130
+ // the explanation: semgrep only reads the line immediately above.
131
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
132
+ const child = (0, child_process_1.spawn)(command, {
133
+ cwd, shell: true,
134
+ env: { ...safeEnvironment(), ...(opts.extraEnv ?? {}) },
135
+ stdio: ["ignore", "pipe", "pipe"],
136
+ });
137
+ const collect = (chunk) => {
138
+ const text = chunk.toString();
139
+ out += text;
140
+ opts.onOutput?.(text);
141
+ };
142
+ child.stdout?.on("data", collect);
143
+ child.stderr?.on("data", collect);
144
+ const timer = setTimeout(() => {
145
+ if (done)
146
+ return;
147
+ done = true;
148
+ // The whole process group, not just the shell. Killing the shell leaves
149
+ // whatever it started running, which is how a stuck job leaves a server
150
+ // holding a port after everybody has gone home.
151
+ try {
152
+ process.kill(-child.pid, "SIGKILL");
153
+ }
154
+ catch {
155
+ child.kill("SIGKILL");
156
+ }
157
+ resolve({
158
+ isError: true,
159
+ content: clip(out) +
160
+ `\n\n[Stopped after ${Math.round(timeoutMs / 1000)}s. If this command ` +
161
+ `waits for input it will never finish; run it in a way that does not.]`,
162
+ summary: `timed out after ${Math.round(timeoutMs / 1000)}s`,
163
+ });
164
+ }, timeoutMs);
165
+ child.on("error", (err) => {
166
+ if (done)
167
+ return;
168
+ done = true;
169
+ clearTimeout(timer);
170
+ resolve({ isError: true, content: `Could not run it: ${err.message}` });
171
+ });
172
+ child.on("close", (code) => {
173
+ if (done)
174
+ return;
175
+ done = true;
176
+ clearTimeout(timer);
177
+ const body = clip(out).trim() || "(no output)";
178
+ resolve({
179
+ // Non-zero is not an error in the sense that matters here. It is
180
+ // information, and it is usually the most useful information available.
181
+ isError: false,
182
+ content: `exit ${code}\n\n${body}`,
183
+ summary: code === 0 ? "exit 0" : `exit ${code}`,
184
+ });
185
+ });
186
+ });
187
+ }
188
+ const running = new Map();
189
+ let counter = 0;
190
+ function startBackground(command, opts) {
191
+ const cwd = opts.cwd ? (0, paths_1.resolveInside)(opts.root, opts.cwd, true) : fs.realpathSync(opts.root);
192
+ const id = `p${++counter}`;
193
+ // The background half of the same shell tool, with the same approval list and
194
+ // the same workspace root in front of it. See `runCommand` above.
195
+ // nosemgrep: javascript.lang.security.detect-child-process.detect-child-process
196
+ const child = (0, child_process_1.spawn)(command, {
197
+ cwd, shell: true, env: safeEnvironment(), stdio: ["ignore", "pipe", "pipe"],
198
+ });
199
+ const entry = { child, buffer: "", read: 0, command };
200
+ const collect = (chunk) => {
201
+ entry.buffer += chunk.toString();
202
+ // Bounded. A watcher left running for an hour would otherwise hold its whole
203
+ // output in memory, and nobody is going to read the first hour of it.
204
+ if (entry.buffer.length > MAX_OUTPUT_CHARS * 4) {
205
+ entry.buffer = entry.buffer.slice(-MAX_OUTPUT_CHARS * 2);
206
+ entry.read = 0;
207
+ }
208
+ };
209
+ child.stdout?.on("data", collect);
210
+ child.stderr?.on("data", collect);
211
+ running.set(id, entry);
212
+ return {
213
+ content: `Started as ${id}: ${command}\nUse read_output with ${id} to see what it prints, and stop_process to end it.`,
214
+ summary: `started ${id}`,
215
+ };
216
+ }
217
+ function readOutput(id) {
218
+ const entry = running.get(id);
219
+ if (!entry)
220
+ return { isError: true, content: `No process called ${id}.` };
221
+ const fresh = entry.buffer.slice(entry.read);
222
+ entry.read = entry.buffer.length;
223
+ const alive = entry.child.exitCode === null && !entry.child.killed;
224
+ return {
225
+ content: (fresh.trim() || "(nothing new)") +
226
+ `\n\n[${id} is ${alive ? "still running" : `finished, exit ${entry.child.exitCode}`}.]`,
227
+ summary: alive ? `${id} running` : `${id} exited`,
228
+ };
229
+ }
230
+ function stopProcess(id) {
231
+ const entry = running.get(id);
232
+ if (!entry)
233
+ return { isError: true, content: `No process called ${id}.` };
234
+ try {
235
+ process.kill(-entry.child.pid, "SIGTERM");
236
+ }
237
+ catch {
238
+ entry.child.kill("SIGTERM");
239
+ }
240
+ running.delete(id);
241
+ return { content: `Stopped ${id}.`, summary: `stopped ${id}` };
242
+ }
243
+ /** Everything still running, stopped. Called when a session ends, so a job that
244
+ * started a dev server does not leave it holding a port after the person has
245
+ * closed the terminal. */
246
+ function stopEverything() {
247
+ for (const id of Array.from(running.keys()))
248
+ stopProcess(id);
249
+ }
250
+ // ── the tests ───────────────────────────────────────────────────────────────
251
+ /** How this project runs its tests, worked out from what is in it.
252
+ *
253
+ * Ordered by how specific the evidence is. A `test` script in package.json is
254
+ * a statement by the project's own authors; the presence of a pytest.ini is an
255
+ * inference. Guessing wrong is cheap here because the result says what it ran.
256
+ */
257
+ /** Whether this folder holds Python tests, by their conventional names.
258
+ *
259
+ * `test_x.py` and `x_test.py` are the two names pytest collects by default, so
260
+ * a folder containing either is a folder pytest can run. Deliberately shallow:
261
+ * one directory read, no walk, because this is a guess made before every test
262
+ * run and a recursive scan of somebody's repository is not.
263
+ */
264
+ function looksLikePython(root) {
265
+ try {
266
+ return fs.readdirSync(root).some((name) => /^test_.*\.py$/.test(name) || /.*_test\.py$/.test(name));
267
+ }
268
+ catch {
269
+ // An unreadable project directory is a different problem, and it will be
270
+ // reported by whatever tries to read a file next.
271
+ return false;
272
+ }
273
+ }
274
+ function guessTestCommand(root) {
275
+ const has = (f) => fs.existsSync(path.join(root, f));
276
+ const pkgPath = path.join(root, "package.json");
277
+ if (has("package.json")) {
278
+ try {
279
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
280
+ if (pkg.scripts?.test) {
281
+ return has("pnpm-lock.yaml") ? "pnpm test"
282
+ : has("yarn.lock") ? "yarn test" : "npm test";
283
+ }
284
+ }
285
+ catch { /* a malformed package.json is not our problem to report here */ }
286
+ }
287
+ if (has("pytest.ini") || has("pyproject.toml") || has("tests") || has("setup.cfg")) {
288
+ return "python -m pytest -q";
289
+ }
290
+ // A project identified by its TEST FILES rather than by a configuration file.
291
+ // A plain folder of `test_*.py` with no `pyproject.toml` is an entirely normal
292
+ // Python project and was not recognized, so `run_tests` refused and the model
293
+ // had to work out the command itself. It did, and it cost a step and an
294
+ // approval on a job that was otherwise clean. Found by watching a real run in
295
+ // the desktop app.
296
+ //
297
+ // One shallow directory read, and only when nothing above matched.
298
+ if (looksLikePython(root))
299
+ return "python -m pytest -q";
300
+ if (has("Cargo.toml"))
301
+ return "cargo test";
302
+ if (has("go.mod"))
303
+ return "go test ./...";
304
+ if (has("Gemfile"))
305
+ return "bundle exec rspec";
306
+ if (has("pom.xml"))
307
+ return "mvn -q test";
308
+ if (has("build.gradle") || has("build.gradle.kts"))
309
+ return "gradle test";
310
+ return null;
311
+ }
312
+ async function runTests(opts) {
313
+ const root = fs.realpathSync(opts.root);
314
+ let command = opts.command?.trim() || guessTestCommand(root) || "";
315
+ if (!command) {
316
+ return {
317
+ isError: true,
318
+ content: "I could not work out how this project runs its tests. Tell me the " +
319
+ "command and I will use it from now on.",
320
+ };
321
+ }
322
+ if (opts.path)
323
+ command = `${command} ${(0, paths_1.resolveInside)(root, opts.path, false)}`;
324
+ const result = await runCommand(command, { ...opts, timeoutMs: opts.timeoutMs ?? 600_000 });
325
+ const passed = /^exit 0\b/.test(result.content);
326
+ return {
327
+ ...result,
328
+ passed,
329
+ command,
330
+ // The verdict first, in words, before the output. This result is read by a
331
+ // model deciding whether it has finished, and burying "exit 1" under two
332
+ // hundred lines of test output is how a job declares success on a red suite.
333
+ content: `${passed ? "TESTS PASSED" : "TESTS FAILED"} (${command})\n\n${result.content}`,
334
+ summary: `${passed ? "tests passed" : "tests failed"}: ${command}`,
335
+ };
336
+ }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Turn a folder of source files into code map facts, on the machine that holds
3
+ * them.
4
+ *
5
+ * Reading is the only part of the map that happens here. The joining, the
6
+ * storing and the answering all happen in the service, because a client that
7
+ * joined the map would be a client holding an opinion, and because a map that
8
+ * lived on one laptop could not be read by the same person's editor an hour
9
+ * later.
10
+ */
11
+ import type { FactSet } from "./facts";
12
+ import { IndexedFile } from "./files";
13
+ export interface ReadResult {
14
+ /** One entry per file successfully read. */
15
+ facts: Record<string, FactSet>;
16
+ failures: {
17
+ file: string;
18
+ reason: string;
19
+ }[];
20
+ /** Files present in the project that no reader on this machine can read. */
21
+ unread: {
22
+ python: number;
23
+ typescript: number;
24
+ };
25
+ seconds: number;
26
+ }
27
+ /** What this machine can read at all, said plainly and once. */
28
+ export declare function readers(): {
29
+ python: boolean;
30
+ typescript: boolean;
31
+ };
32
+ /** Read the named files. Everything when `rels` is absent. */
33
+ export declare function readFacts(root: string, rels?: string[], knownNames?: string[]): ReadResult;
34
+ export interface ProjectState {
35
+ files: IndexedFile[];
36
+ stamps: Record<string, string>;
37
+ fingerprint: {
38
+ hash: string;
39
+ files: number;
40
+ };
41
+ commit: string;
42
+ }
43
+ /** What the project looks like right now: which files, and one short string
44
+ * that changes the moment any of them does. Costs about ten milliseconds. */
45
+ export declare function projectState(root: string): ProjectState;
@@ -0,0 +1,91 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readers = readers;
4
+ exports.readFacts = readFacts;
5
+ exports.projectState = projectState;
6
+ const files_1 = require("./files");
7
+ const read_locales_1 = require("./read-locales");
8
+ const read_python_1 = require("./read-python");
9
+ const read_typescript_1 = require("./read-typescript");
10
+ /** What this machine can read at all, said plainly and once. */
11
+ function readers() {
12
+ return {
13
+ python: Boolean(require("./read-python").findPython()),
14
+ typescript: Boolean((0, read_typescript_1.loadCompiler)()),
15
+ };
16
+ }
17
+ /**
18
+ * Drop the calls that could never resolve to anything in this project.
19
+ *
20
+ * Two thirds of the raw facts read from a real 1,816 file project were calls to
21
+ * names the project does not define: `len`, `push`, `map`, and every library
22
+ * function anybody imported. The service discards every one of them when it
23
+ * joins the map, so sending and storing them is pure waste, measured at 17.8
24
+ * megabytes of 26.6.
25
+ *
26
+ * `known` is what the project already defines, which the service sends back
27
+ * with the file stamps, plus whatever the files just read define themselves.
28
+ * Nothing is dropped that the service would have kept: the join applies the same
29
+ * rule, only later. Correctness never depends on this, only size.
30
+ */
31
+ function prune(facts, known) {
32
+ const droppable = new Set(["calls", "references"]);
33
+ for (const rel of Object.keys(facts)) {
34
+ const entry = facts[rel];
35
+ if (!entry)
36
+ continue;
37
+ entry.links = entry.links.filter((link) => {
38
+ if (!droppable.has(link.kind))
39
+ return true;
40
+ const leaf = String(link.to_name ?? "").split(".").pop() ?? "";
41
+ return known.has(leaf);
42
+ });
43
+ }
44
+ }
45
+ /** Read the named files. Everything when `rels` is absent. */
46
+ function readFacts(root, rels, knownNames) {
47
+ const started = Date.now();
48
+ const all = (0, files_1.indexedFiles)(root);
49
+ const wanted = rels ? new Set(rels) : null;
50
+ const chosen = wanted ? all.filter((f) => wanted.has(f.rel)) : all;
51
+ const py = chosen.filter((f) => f.lang === "python").map((f) => f.rel);
52
+ const ts = chosen.filter((f) => f.lang === "typescript").map((f) => f.rel);
53
+ const locales = chosen.filter((f) => f.lang === "translations").map((f) => f.rel);
54
+ const fromPython = (0, read_python_1.readPython)(root, py);
55
+ const fromTs = (0, read_typescript_1.readTypeScript)(root, ts);
56
+ // Translation files need no compiler and no interpreter, so they are always
57
+ // read: a machine missing Python still gets its labels mapped.
58
+ const fromLocales = (0, read_locales_1.readLocales)(root, locales);
59
+ const facts = { ...fromPython.facts, ...fromTs.facts, ...fromLocales.facts };
60
+ // Everything the files just read define, plus everything the service already
61
+ // knows this project defines. A name on neither list cannot resolve.
62
+ const known = new Set(knownNames ?? []);
63
+ for (const entry of Object.values(facts)) {
64
+ for (const object of entry.objects) {
65
+ if (object.kind === "file" || object.kind === "route" || object.kind === "table")
66
+ continue;
67
+ const leaf = String(object.name).split(".").pop();
68
+ if (leaf)
69
+ known.add(leaf);
70
+ }
71
+ }
72
+ prune(facts, known);
73
+ return {
74
+ facts,
75
+ failures: [...fromPython.failures, ...fromTs.failures, ...fromLocales.failures],
76
+ unread: {
77
+ python: fromPython.available ? 0 : py.length,
78
+ typescript: fromTs.available ? 0 : ts.length,
79
+ },
80
+ seconds: Math.round((Date.now() - started) / 100) / 10,
81
+ };
82
+ }
83
+ /** What the project looks like right now: which files, and one short string
84
+ * that changes the moment any of them does. Costs about ten milliseconds. */
85
+ function projectState(root) {
86
+ const files = (0, files_1.indexedFiles)(root);
87
+ const stamps = {};
88
+ for (const file of files)
89
+ stamps[file.rel] = (0, files_1.stampOf)(file);
90
+ return { files, stamps, fingerprint: (0, files_1.fingerprint)(files), commit: (0, files_1.headCommit)(root) };
91
+ }