@ttsc/graph 0.16.4
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 +97 -0
- package/lib/bin.d.ts +2 -0
- package/lib/bin.js +9 -0
- package/lib/bin.js.map +1 -0
- package/lib/diagnostics.d.ts +23 -0
- package/lib/diagnostics.js +84 -0
- package/lib/diagnostics.js.map +1 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +157 -0
- package/lib/index.js.map +1 -0
- package/package.json +48 -0
- package/src/bin.ts +8 -0
- package/src/diagnostics.ts +93 -0
- package/src/index.ts +171 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jeongho Nam
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# `@ttsc/graph`
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+
|
|
5
|
+
[](https://github.com/samchon/ttsc/blob/master/LICENSE) [](https://www.npmjs.com/package/@ttsc/graph) [](https://www.npmjs.com/package/@ttsc/graph) [](https://github.com/samchon/ttsc/actions?query=workflow%3Atest) [](https://ttsc.dev/docs/graph) [](https://discord.gg/E94XhzrUCZ)
|
|
6
|
+
|
|
7
|
+
Gives your AI coding assistant a **map of your codebase**. It can answer "how does this work?" without opening file after file.
|
|
8
|
+
|
|
9
|
+
When you ask an AI agent (Claude Code, Codex, and the like) about your project, it pokes around. It opens a file, follows an import, opens another, and on and on.
|
|
10
|
+
|
|
11
|
+
`@ttsc/graph` hands it the map up front: what calls what, what depends on what, and where each thing lives.
|
|
12
|
+
|
|
13
|
+
The map comes from the real TypeScript compiler. So it is exact — not a guess from skimming text.
|
|
14
|
+
|
|
15
|
+
## What your agent gets
|
|
16
|
+
|
|
17
|
+
Say you ask: _"how does the editor draw a shape?"_
|
|
18
|
+
|
|
19
|
+
Your agent looks it up once instead of reading a dozen files, and gets back something like this:
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
ShapeRenderer — src/render/shape.ts (line 18)
|
|
23
|
+
→ calls rasterize(), new Canvas()
|
|
24
|
+
← used by Editor
|
|
25
|
+
change it and 9 other things are affected
|
|
26
|
+
18 export class ShapeRenderer {
|
|
27
|
+
19 constructor(private canvas: Canvas) {}
|
|
28
|
+
...
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
One step shows what this thing calls, what uses it, and its source.
|
|
32
|
+
|
|
33
|
+
The answer comes from the compiler, so it is precise. When one file just re-exports something from another, the map still points at the file that really defines it.
|
|
34
|
+
|
|
35
|
+
Anything from `node_modules` is left out, because that is not your code.
|
|
36
|
+
|
|
37
|
+
On a public benchmark, a Claude agent answered reading zero files, cutting its token use by **77% to 86%** and its tool calls by **94% to 95%**.
|
|
38
|
+
|
|
39
|
+
See the [benchmark](https://ttsc.dev/docs/benchmark#code-graph-mcp) for the full numbers.
|
|
40
|
+
|
|
41
|
+
## Install
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install -D ttsc @ttsc/graph typescript@rc
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Install `ttsc` alongside it. `@ttsc/graph` runs through `ttsc`, so the two go together — the same pair as `@ttsc/lint`.
|
|
48
|
+
|
|
49
|
+
There is nothing else to set up: no separate compiler, no Go.
|
|
50
|
+
|
|
51
|
+
## Connect it to your agent
|
|
52
|
+
|
|
53
|
+
Add this to your AI tool's config once. For Claude Code, that is a `.mcp.json` file in your project:
|
|
54
|
+
|
|
55
|
+
```json
|
|
56
|
+
{
|
|
57
|
+
"mcpServers": {
|
|
58
|
+
"ttsc-graph": {
|
|
59
|
+
"command": "npx",
|
|
60
|
+
"args": ["-y", "@ttsc/graph"]
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Start your agent from your project folder so it finds your `tsconfig.json`.
|
|
67
|
+
|
|
68
|
+
Your agent now has two new abilities:
|
|
69
|
+
|
|
70
|
+
| Ability | What it does |
|
|
71
|
+
| --- | --- |
|
|
72
|
+
| `graph_explore` | Look up a name or a file. Get back what it connects to — what it calls, what uses it, its types — and its source code. |
|
|
73
|
+
| `graph_diagnostics` | Get the errors in one file: TypeScript type errors, plus your project's `@ttsc/lint` and plugin findings, as `ttsc` reports them. |
|
|
74
|
+
|
|
75
|
+
You never call these yourself. Your agent uses them when it needs to.
|
|
76
|
+
|
|
77
|
+
Claude Code does this on its own. Codex is more cautious with third-party MCP tools and often explores with the shell instead, so tell it directly — add a line to your `AGENTS.md` asking it to call `graph_explore` first. See the [setup guide](https://ttsc.dev/docs/setup#codex-and-other-tool-conservative-agents).
|
|
78
|
+
|
|
79
|
+
## How it works
|
|
80
|
+
|
|
81
|
+
`ttsc` already type-checks your project with the real TypeScript compiler and keeps the result in memory.
|
|
82
|
+
|
|
83
|
+
`@ttsc/graph` reads the map straight out of that. Every connection it shows is the compiler's own answer, not a guess.
|
|
84
|
+
|
|
85
|
+
That is why it is exact, and why it is fast: nothing is recompiled to answer a question.
|
|
86
|
+
|
|
87
|
+
## References
|
|
88
|
+
|
|
89
|
+
`@ttsc/graph` is inspired by [codegraph](https://github.com/colbymchenry/codegraph), which gives agents a code graph over MCP. The [benchmark](https://ttsc.dev/docs/benchmark#code-graph-mcp) here is a faithful port of codegraph's.
|
|
90
|
+
|
|
91
|
+
The difference is how the map is built.
|
|
92
|
+
|
|
93
|
+
codegraph reads your code with tree-sitter, across many languages, and saves the result in a database it keeps in sync as files change. Its edges are a best guess from the syntax.
|
|
94
|
+
|
|
95
|
+
`@ttsc/graph` is TypeScript-only and keeps no database. It reads the graph live from the program `ttsc` already type-checked in memory.
|
|
96
|
+
|
|
97
|
+
So every edge is the compiler's own answer: a re-export is followed to the file that really defines the symbol, an external library is left out, and nothing is guessed.
|
package/lib/bin.d.ts
ADDED
package/lib/bin.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const index_1 = require("./index");
|
|
5
|
+
const code = (0, index_1.runGraph)(process.argv.slice(2));
|
|
6
|
+
if (typeof code === "number") {
|
|
7
|
+
process.exitCode = code;
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=bin.js.map
|
package/lib/bin.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"bin.js","sourceRoot":"","sources":["../src/bin.ts"],"names":[],"mappings":";;;AAEA,mCAAmC;AAEnC,MAAM,IAAI,GAAG,IAAA,gBAAQ,EAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7C,IAAI,OAAO,IAAI,KAAK,QAAQ,EAAE,CAAC;IAC7B,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;AAC1B,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background worker that computes the project's plugin diagnostics and writes
|
|
3
|
+
* them where the ttscgraph server reads them.
|
|
4
|
+
*
|
|
5
|
+
* The graph binary runs only the TypeScript semantic pass. Everything a plugin
|
|
6
|
+
* adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
|
|
7
|
+
* findings — comes from `ttsc`'s own check, which runs whatever plugins the
|
|
8
|
+
* project configured. This worker invokes that check through the public
|
|
9
|
+
* `TtscCompiler`, so nothing here is specific to any plugin: it forwards
|
|
10
|
+
* whatever diagnostics ttsc produces.
|
|
11
|
+
*
|
|
12
|
+
* A native plugin reports its findings with a string `code` (tsc uses numeric
|
|
13
|
+
* codes), so the string-coded diagnostics are exactly the plugin/lint set the
|
|
14
|
+
* graph does not already have. They are serialized with code 0 and the rule
|
|
15
|
+
* folded into the message, the shape ttscgraph's injected-diagnostics provider
|
|
16
|
+
* consumes.
|
|
17
|
+
*
|
|
18
|
+
* Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
|
|
19
|
+
* build a plugin, a project that does not compile — any of these simply leaves
|
|
20
|
+
* no file, and the graph shows its tsc diagnostics alone. The worker must never
|
|
21
|
+
* be able to break the server it feeds.
|
|
22
|
+
*/
|
|
23
|
+
export declare function runDiagnosticsWorker(argv?: readonly string[]): void;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.runDiagnosticsWorker = runDiagnosticsWorker;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_module_1 = require("node:module");
|
|
9
|
+
/**
|
|
10
|
+
* Background worker that computes the project's plugin diagnostics and writes
|
|
11
|
+
* them where the ttscgraph server reads them.
|
|
12
|
+
*
|
|
13
|
+
* The graph binary runs only the TypeScript semantic pass. Everything a plugin
|
|
14
|
+
* adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
|
|
15
|
+
* findings — comes from `ttsc`'s own check, which runs whatever plugins the
|
|
16
|
+
* project configured. This worker invokes that check through the public
|
|
17
|
+
* `TtscCompiler`, so nothing here is specific to any plugin: it forwards
|
|
18
|
+
* whatever diagnostics ttsc produces.
|
|
19
|
+
*
|
|
20
|
+
* A native plugin reports its findings with a string `code` (tsc uses numeric
|
|
21
|
+
* codes), so the string-coded diagnostics are exactly the plugin/lint set the
|
|
22
|
+
* graph does not already have. They are serialized with code 0 and the rule
|
|
23
|
+
* folded into the message, the shape ttscgraph's injected-diagnostics provider
|
|
24
|
+
* consumes.
|
|
25
|
+
*
|
|
26
|
+
* Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
|
|
27
|
+
* build a plugin, a project that does not compile — any of these simply leaves
|
|
28
|
+
* no file, and the graph shows its tsc diagnostics alone. The worker must never
|
|
29
|
+
* be able to break the server it feeds.
|
|
30
|
+
*/
|
|
31
|
+
function runDiagnosticsWorker(argv = process.argv.slice(2)) {
|
|
32
|
+
const [cwd, tsconfig, outPath] = argv;
|
|
33
|
+
if (!cwd || !outPath) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
try {
|
|
37
|
+
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
38
|
+
paths: [cwd],
|
|
39
|
+
});
|
|
40
|
+
const fromProject = (0, node_module_1.createRequire)(ttscPackageJson);
|
|
41
|
+
const { TtscCompiler } = fromProject("ttsc");
|
|
42
|
+
const result = new TtscCompiler({
|
|
43
|
+
cwd,
|
|
44
|
+
tsconfig: tsconfig || undefined,
|
|
45
|
+
}).compile();
|
|
46
|
+
const raw = result &&
|
|
47
|
+
typeof result === "object" &&
|
|
48
|
+
Array.isArray(result.diagnostics)
|
|
49
|
+
? (result.diagnostics)
|
|
50
|
+
: [];
|
|
51
|
+
const out = raw
|
|
52
|
+
.filter((d) => typeof d.file === "string" &&
|
|
53
|
+
(typeof d.line === "number" || typeof d.start === "number"))
|
|
54
|
+
.map((d) => ({
|
|
55
|
+
file: d.file,
|
|
56
|
+
// A byte offset when the structured lane gives one; otherwise null and
|
|
57
|
+
// the server attributes by line. @ttsc/lint and transform plugins reach
|
|
58
|
+
// the result through ttsc's text banner, which carries a line but no
|
|
59
|
+
// offset.
|
|
60
|
+
start: typeof d.start === "number" ? d.start : null,
|
|
61
|
+
line: typeof d.line === "number" ? d.line : 1,
|
|
62
|
+
column: typeof d.character === "number" ? d.character : 1,
|
|
63
|
+
// tsc diagnostics use numeric codes; @ttsc/lint and native plugins hash
|
|
64
|
+
// their rule to a code >= 9000. A rare string id is marked non-tsc (the
|
|
65
|
+
// server then drops the "TS" prefix); the rule name travels in the
|
|
66
|
+
// message regardless.
|
|
67
|
+
code: typeof d.code === "number" ? d.code : 9000,
|
|
68
|
+
message: String(d.messageText ?? ""),
|
|
69
|
+
}));
|
|
70
|
+
// Atomic publish: write to a sibling temp file and rename, so the server
|
|
71
|
+
// never reads a half-written file (a partial read would drop every finding
|
|
72
|
+
// for that query).
|
|
73
|
+
const tmp = `${outPath}.${process.pid}.tmp`;
|
|
74
|
+
node_fs_1.default.writeFileSync(tmp, JSON.stringify(out));
|
|
75
|
+
node_fs_1.default.renameSync(tmp, outPath);
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// Resilient by contract: no file means the graph shows tsc-only diagnostics.
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (require.main === module) {
|
|
82
|
+
runDiagnosticsWorker();
|
|
83
|
+
}
|
|
84
|
+
//# sourceMappingURL=diagnostics.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"diagnostics.js","sourceRoot":"","sources":["../src/diagnostics.ts"],"names":[],"mappings":";;;;;;AAAA,sDAAyB;AACzB,6CAA4C;AAE5C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,8BACE,IAAI,GAAsB,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC;IAE/C,MAAM,CAAC,GAAG,EAAE,QAAQ,EAAE,OAAO,CAAC,GAAG,IAAI,CAAC;IACtC,IAAI,CAAC,GAAG,IAAI,CAAC,OAAO,EAAE,CAAC;QACrB,OAAO;IACT,CAAC;IACD,IAAI,CAAC;QACH,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,CAAC,mBAAmB,EAAE;YAC3D,KAAK,EAAE,CAAC,GAAG,CAAC;SACb,CAAC,CAAC;QACH,MAAM,WAAW,GAAG,IAAA,2BAAa,EAAC,eAAe,CAAC,CAAC;QACnD,MAAM,EAAE,YAAY,EAAE,GAAG,WAAW,CAAC,MAAM,CAK1C,CAAC;QAEF,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;YAC9B,GAAG;YACH,QAAQ,EAAE,QAAQ,IAAI,SAAS;SAChC,CAAC,CAAC,OAAO,EAAE,CAAC;QAEb,MAAM,GAAG,GACP,MAAM;YACN,OAAO,MAAM,KAAK,QAAQ;YAC1B,KAAK,CAAC,OAAO,CAAE,MAAoC,CAAC,WAAW,CAAC;YAC9D,CAAC,CAAC,CAAE,MAAqD,CAAC,WAAW,CAAC;YACtE,CAAC,CAAC,EAAE,CAAC;QAET,MAAM,GAAG,GAAG,GAAG;aACZ,MAAM,CACL,CAAC,CAAC,EAAE,EAAE,CACJ,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ;YAC1B,CAAC,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAC9D;aACA,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,IAAc;YACtB,uEAAuE;YACvE,wEAAwE;YACxE,qEAAqE;YACrE,UAAU;YACV,KAAK,EAAE,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,KAAgB,CAAC,CAAC,CAAC,IAAI;YAC/D,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAe,CAAC,CAAC,CAAC,CAAC;YACzD,MAAM,EAAE,OAAO,CAAC,CAAC,SAAS,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,SAAoB,CAAC,CAAC,CAAC,CAAC;YACrE,wEAAwE;YACxE,wEAAwE;YACxE,mEAAmE;YACnE,sBAAsB;YACtB,IAAI,EAAE,OAAO,CAAC,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAE,CAAC,CAAC,IAAe,CAAC,CAAC,CAAC,IAAI;YAC5D,OAAO,EAAE,MAAM,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE,CAAC;SACrC,CAAC,CAAC,CAAC;QAEN,yEAAyE;QACzE,2EAA2E;QAC3E,mBAAmB;QACnB,MAAM,GAAG,GAAG,GAAG,OAAO,IAAI,OAAO,CAAC,GAAG,MAAM,CAAC;QAC5C,iBAAE,CAAC,aAAa,CAAC,GAAG,EAAE,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;QAC3C,iBAAE,CAAC,UAAU,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;IAC9B,CAAC;IAAC,MAAM,CAAC;QACP,6EAA6E;IAC/E,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC5B,oBAAoB,EAAE,CAAC;AACzB,CAAC"}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
|
|
3
|
+
* cannot be located.
|
|
4
|
+
*
|
|
5
|
+
* `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
|
|
6
|
+
* of this launcher), so resolution starts from the user's project, not from
|
|
7
|
+
* this package's own tree.
|
|
8
|
+
*
|
|
9
|
+
* Resolution order:
|
|
10
|
+
*
|
|
11
|
+
* 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
|
|
12
|
+
* 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
|
|
13
|
+
* That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
|
|
14
|
+
* an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
|
|
15
|
+
* location — found from `process.cwd()` (the project where the agent ran the
|
|
16
|
+
* server).
|
|
17
|
+
*/
|
|
18
|
+
export declare function resolveGraphBinary(env?: NodeJS.ProcessEnv, cwd?: string): string | null;
|
|
19
|
+
/**
|
|
20
|
+
* Spawn the resident MCP server, inheriting stdio so the agent's MCP client
|
|
21
|
+
* speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
|
|
22
|
+
* child's exit code.
|
|
23
|
+
*
|
|
24
|
+
* Before spawning, it kicks off the background diagnostics worker (except in
|
|
25
|
+
* `--connect` proxy mode) and points the server at its output file, so a
|
|
26
|
+
* plugin-using project's lint and transform-plugin diagnostics fuse onto the
|
|
27
|
+
* graph without blocking startup.
|
|
28
|
+
*/
|
|
29
|
+
export declare function runGraph(argv?: readonly string[]): number;
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.resolveGraphBinary = resolveGraphBinary;
|
|
7
|
+
exports.runGraph = runGraph;
|
|
8
|
+
const node_child_process_1 = require("node:child_process");
|
|
9
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
10
|
+
const node_module_1 = require("node:module");
|
|
11
|
+
const node_os_1 = __importDefault(require("node:os"));
|
|
12
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
|
|
15
|
+
* cannot be located.
|
|
16
|
+
*
|
|
17
|
+
* `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
|
|
18
|
+
* of this launcher), so resolution starts from the user's project, not from
|
|
19
|
+
* this package's own tree.
|
|
20
|
+
*
|
|
21
|
+
* Resolution order:
|
|
22
|
+
*
|
|
23
|
+
* 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
|
|
24
|
+
* 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
|
|
25
|
+
* That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
|
|
26
|
+
* an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
|
|
27
|
+
* location — found from `process.cwd()` (the project where the agent ran the
|
|
28
|
+
* server).
|
|
29
|
+
*/
|
|
30
|
+
function resolveGraphBinary(env = process.env, cwd = process.cwd()) {
|
|
31
|
+
if (env.TTSC_GRAPH_BINARY && node_path_1.default.isAbsolute(env.TTSC_GRAPH_BINARY)) {
|
|
32
|
+
return env.TTSC_GRAPH_BINARY;
|
|
33
|
+
}
|
|
34
|
+
const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
|
|
35
|
+
try {
|
|
36
|
+
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
37
|
+
paths: [cwd],
|
|
38
|
+
});
|
|
39
|
+
const fromTtsc = (0, node_module_1.createRequire)(ttscPackageJson);
|
|
40
|
+
return fromTtsc.resolve(`@ttsc/${process.platform}-${process.arch}/bin/${exe}`);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** The project root and tsconfig the server was pointed at, mirroring the
|
|
47
|
+
* `--cwd` / `--tsconfig` flags ttscgraph itself parses, so the background
|
|
48
|
+
* diagnostics worker checks the same project. */
|
|
49
|
+
function parseProjectArgs(argv) {
|
|
50
|
+
let cwd = process.cwd();
|
|
51
|
+
let tsconfig = "tsconfig.json";
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const arg = argv[i];
|
|
54
|
+
if (arg === "--cwd" && i + 1 < argv.length)
|
|
55
|
+
cwd = argv[++i];
|
|
56
|
+
else if (arg.startsWith("--cwd="))
|
|
57
|
+
cwd = arg.slice("--cwd=".length);
|
|
58
|
+
else if (arg === "--tsconfig" && i + 1 < argv.length)
|
|
59
|
+
tsconfig = argv[++i];
|
|
60
|
+
else if (arg.startsWith("--tsconfig="))
|
|
61
|
+
tsconfig = arg.slice("--tsconfig=".length);
|
|
62
|
+
}
|
|
63
|
+
return { cwd: node_path_1.default.resolve(cwd), tsconfig };
|
|
64
|
+
}
|
|
65
|
+
/** A `--connect` proxy pipes stdio to a running daemon and serves no graph of
|
|
66
|
+
* its own, so it needs no diagnostics computed locally. */
|
|
67
|
+
function isConnectProxy(argv) {
|
|
68
|
+
return argv.some((a) => a === "--connect" || a.startsWith("--connect="));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Start the background worker that computes the project's plugin diagnostics
|
|
72
|
+
* (`@ttsc/lint` and transform-plugin findings) and writes them to
|
|
73
|
+
* `diagnosticsFile`, where the server picks them up. Returns the child handle
|
|
74
|
+
* for cleanup, or `null` when it could not be spawned.
|
|
75
|
+
*
|
|
76
|
+
* It runs in parallel with the server so the MCP handshake is never blocked on
|
|
77
|
+
* a compile; the server shows its tsc diagnostics immediately and fuses the
|
|
78
|
+
* plugin findings once the file lands. Any failure is non-fatal — the graph
|
|
79
|
+
* simply stays tsc-only.
|
|
80
|
+
*/
|
|
81
|
+
function startDiagnosticsWorker(argv, diagnosticsFile) {
|
|
82
|
+
try {
|
|
83
|
+
const { cwd, tsconfig } = parseProjectArgs(argv);
|
|
84
|
+
const worker = (0, node_child_process_1.spawn)(process.execPath, [require.resolve("./diagnostics.js"), cwd, tsconfig, diagnosticsFile], { stdio: "ignore", windowsHide: true });
|
|
85
|
+
worker.on("error", () => {
|
|
86
|
+
/* resilient: a worker that cannot start leaves no file */
|
|
87
|
+
});
|
|
88
|
+
worker.unref();
|
|
89
|
+
return worker;
|
|
90
|
+
}
|
|
91
|
+
catch {
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Spawn the resident MCP server, inheriting stdio so the agent's MCP client
|
|
97
|
+
* speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
|
|
98
|
+
* child's exit code.
|
|
99
|
+
*
|
|
100
|
+
* Before spawning, it kicks off the background diagnostics worker (except in
|
|
101
|
+
* `--connect` proxy mode) and points the server at its output file, so a
|
|
102
|
+
* plugin-using project's lint and transform-plugin diagnostics fuse onto the
|
|
103
|
+
* graph without blocking startup.
|
|
104
|
+
*/
|
|
105
|
+
function runGraph(argv = process.argv.slice(2)) {
|
|
106
|
+
const binary = resolveGraphBinary();
|
|
107
|
+
if (binary === null) {
|
|
108
|
+
process.stderr.write("@ttsc/graph: could not resolve the ttscgraph binary. " +
|
|
109
|
+
"Install `ttsc` so its platform package is present, " +
|
|
110
|
+
"or set TTSC_GRAPH_BINARY to an absolute path.\n");
|
|
111
|
+
return 1;
|
|
112
|
+
}
|
|
113
|
+
const env = { ...process.env };
|
|
114
|
+
let diagnosticsFile = null;
|
|
115
|
+
let worker = null;
|
|
116
|
+
if (!isConnectProxy(argv)) {
|
|
117
|
+
diagnosticsFile = node_path_1.default.join(node_os_1.default.tmpdir(), `ttsc-graph-diagnostics-${process.pid}.json`);
|
|
118
|
+
env.TTSC_GRAPH_DIAGNOSTICS_FILE = diagnosticsFile;
|
|
119
|
+
worker = startDiagnosticsWorker(argv, diagnosticsFile);
|
|
120
|
+
}
|
|
121
|
+
const result = (0, node_child_process_1.spawnSync)(binary, [...argv], {
|
|
122
|
+
stdio: "inherit",
|
|
123
|
+
env,
|
|
124
|
+
windowsHide: true,
|
|
125
|
+
});
|
|
126
|
+
const workerPid = worker?.pid;
|
|
127
|
+
try {
|
|
128
|
+
worker?.kill();
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
/* ignore */
|
|
132
|
+
}
|
|
133
|
+
if (diagnosticsFile) {
|
|
134
|
+
try {
|
|
135
|
+
node_fs_1.default.rmSync(diagnosticsFile, { force: true });
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
/* ignore */
|
|
139
|
+
}
|
|
140
|
+
// The worker writes atomically through `<file>.<pid>.tmp`; remove a leftover
|
|
141
|
+
// if it was killed mid-write, so a churning daemon does not litter tmpdir.
|
|
142
|
+
if (workerPid !== undefined) {
|
|
143
|
+
try {
|
|
144
|
+
node_fs_1.default.rmSync(`${diagnosticsFile}.${workerPid}.tmp`, { force: true });
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
if (result.error) {
|
|
152
|
+
process.stderr.write(`@ttsc/graph: ${result.error.message}\n`);
|
|
153
|
+
return 1;
|
|
154
|
+
}
|
|
155
|
+
return result.status ?? 1;
|
|
156
|
+
}
|
|
157
|
+
//# sourceMappingURL=index.js.map
|
package/lib/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ttsc/graph",
|
|
3
|
+
"version": "0.16.4",
|
|
4
|
+
"description": "Checker-resolved code graph and diagnostics over MCP for coding agents, backed by ttsc's in-process TypeScript-Go compiler.",
|
|
5
|
+
"main": "lib/index.js",
|
|
6
|
+
"types": "lib/index.d.ts",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ttsc-graph": "lib/bin.js"
|
|
9
|
+
},
|
|
10
|
+
"exports": {
|
|
11
|
+
".": {
|
|
12
|
+
"types": "./lib/index.d.ts",
|
|
13
|
+
"default": "./lib/index.js"
|
|
14
|
+
},
|
|
15
|
+
"./package.json": "./package.json"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ttsc",
|
|
19
|
+
"mcp",
|
|
20
|
+
"code-graph",
|
|
21
|
+
"typescript",
|
|
22
|
+
"tsgo"
|
|
23
|
+
],
|
|
24
|
+
"files": [
|
|
25
|
+
"README.md",
|
|
26
|
+
"lib",
|
|
27
|
+
"src"
|
|
28
|
+
],
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^25.3.0",
|
|
31
|
+
"rimraf": "^6.1.2",
|
|
32
|
+
"typescript": "7.0.1-rc",
|
|
33
|
+
"ttsc": "0.16.4"
|
|
34
|
+
},
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/samchon/ttsc"
|
|
38
|
+
},
|
|
39
|
+
"author": "Jeongho Nam",
|
|
40
|
+
"license": "MIT",
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/samchon/ttsc/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://ttsc.dev",
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "rimraf lib && tsc"
|
|
47
|
+
}
|
|
48
|
+
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Background worker that computes the project's plugin diagnostics and writes
|
|
6
|
+
* them where the ttscgraph server reads them.
|
|
7
|
+
*
|
|
8
|
+
* The graph binary runs only the TypeScript semantic pass. Everything a plugin
|
|
9
|
+
* adds — `@ttsc/lint` rule violations and transform-plugin (typia, nestia, …)
|
|
10
|
+
* findings — comes from `ttsc`'s own check, which runs whatever plugins the
|
|
11
|
+
* project configured. This worker invokes that check through the public
|
|
12
|
+
* `TtscCompiler`, so nothing here is specific to any plugin: it forwards
|
|
13
|
+
* whatever diagnostics ttsc produces.
|
|
14
|
+
*
|
|
15
|
+
* A native plugin reports its findings with a string `code` (tsc uses numeric
|
|
16
|
+
* codes), so the string-coded diagnostics are exactly the plugin/lint set the
|
|
17
|
+
* graph does not already have. They are serialized with code 0 and the rule
|
|
18
|
+
* folded into the message, the shape ttscgraph's injected-diagnostics provider
|
|
19
|
+
* consumes.
|
|
20
|
+
*
|
|
21
|
+
* Every failure is swallowed: a missing `ttsc`, a go toolchain that cannot
|
|
22
|
+
* build a plugin, a project that does not compile — any of these simply leaves
|
|
23
|
+
* no file, and the graph shows its tsc diagnostics alone. The worker must never
|
|
24
|
+
* be able to break the server it feeds.
|
|
25
|
+
*/
|
|
26
|
+
export function runDiagnosticsWorker(
|
|
27
|
+
argv: readonly string[] = process.argv.slice(2),
|
|
28
|
+
): void {
|
|
29
|
+
const [cwd, tsconfig, outPath] = argv;
|
|
30
|
+
if (!cwd || !outPath) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
try {
|
|
34
|
+
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
35
|
+
paths: [cwd],
|
|
36
|
+
});
|
|
37
|
+
const fromProject = createRequire(ttscPackageJson);
|
|
38
|
+
const { TtscCompiler } = fromProject("ttsc") as {
|
|
39
|
+
TtscCompiler: new (context: {
|
|
40
|
+
cwd: string;
|
|
41
|
+
tsconfig?: string;
|
|
42
|
+
}) => { compile: () => unknown };
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const result = new TtscCompiler({
|
|
46
|
+
cwd,
|
|
47
|
+
tsconfig: tsconfig || undefined,
|
|
48
|
+
}).compile();
|
|
49
|
+
|
|
50
|
+
const raw: ReadonlyArray<Record<string, unknown>> =
|
|
51
|
+
result &&
|
|
52
|
+
typeof result === "object" &&
|
|
53
|
+
Array.isArray((result as { diagnostics?: unknown }).diagnostics)
|
|
54
|
+
? ((result as { diagnostics: Record<string, unknown>[] }).diagnostics)
|
|
55
|
+
: [];
|
|
56
|
+
|
|
57
|
+
const out = raw
|
|
58
|
+
.filter(
|
|
59
|
+
(d) =>
|
|
60
|
+
typeof d.file === "string" &&
|
|
61
|
+
(typeof d.line === "number" || typeof d.start === "number"),
|
|
62
|
+
)
|
|
63
|
+
.map((d) => ({
|
|
64
|
+
file: d.file as string,
|
|
65
|
+
// A byte offset when the structured lane gives one; otherwise null and
|
|
66
|
+
// the server attributes by line. @ttsc/lint and transform plugins reach
|
|
67
|
+
// the result through ttsc's text banner, which carries a line but no
|
|
68
|
+
// offset.
|
|
69
|
+
start: typeof d.start === "number" ? (d.start as number) : null,
|
|
70
|
+
line: typeof d.line === "number" ? (d.line as number) : 1,
|
|
71
|
+
column: typeof d.character === "number" ? (d.character as number) : 1,
|
|
72
|
+
// tsc diagnostics use numeric codes; @ttsc/lint and native plugins hash
|
|
73
|
+
// their rule to a code >= 9000. A rare string id is marked non-tsc (the
|
|
74
|
+
// server then drops the "TS" prefix); the rule name travels in the
|
|
75
|
+
// message regardless.
|
|
76
|
+
code: typeof d.code === "number" ? (d.code as number) : 9000,
|
|
77
|
+
message: String(d.messageText ?? ""),
|
|
78
|
+
}));
|
|
79
|
+
|
|
80
|
+
// Atomic publish: write to a sibling temp file and rename, so the server
|
|
81
|
+
// never reads a half-written file (a partial read would drop every finding
|
|
82
|
+
// for that query).
|
|
83
|
+
const tmp = `${outPath}.${process.pid}.tmp`;
|
|
84
|
+
fs.writeFileSync(tmp, JSON.stringify(out));
|
|
85
|
+
fs.renameSync(tmp, outPath);
|
|
86
|
+
} catch {
|
|
87
|
+
// Resilient by contract: no file means the graph shows tsc-only diagnostics.
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (require.main === module) {
|
|
92
|
+
runDiagnosticsWorker();
|
|
93
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
import os from "node:os";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Resolve the per-platform `ttscgraph` MCP server binary, or `null` when it
|
|
9
|
+
* cannot be located.
|
|
10
|
+
*
|
|
11
|
+
* `ttsc` is a peer the user installs alongside `@ttsc/graph` (not a dependency
|
|
12
|
+
* of this launcher), so resolution starts from the user's project, not from
|
|
13
|
+
* this package's own tree.
|
|
14
|
+
*
|
|
15
|
+
* Resolution order:
|
|
16
|
+
*
|
|
17
|
+
* 1. `TTSC_GRAPH_BINARY` env var, when set to an absolute path.
|
|
18
|
+
* 2. The per-platform npm package `@ttsc/<platform>-<arch>/bin/ttscgraph[.exe]`.
|
|
19
|
+
* That package carries `ttsc`, `ttscserver`, and `ttscgraph` together and is
|
|
20
|
+
* an `optionalDependency` of `ttsc`, so it is resolved from `ttsc`'s
|
|
21
|
+
* location — found from `process.cwd()` (the project where the agent ran the
|
|
22
|
+
* server).
|
|
23
|
+
*/
|
|
24
|
+
export function resolveGraphBinary(
|
|
25
|
+
env: NodeJS.ProcessEnv = process.env,
|
|
26
|
+
cwd: string = process.cwd(),
|
|
27
|
+
): string | null {
|
|
28
|
+
if (env.TTSC_GRAPH_BINARY && path.isAbsolute(env.TTSC_GRAPH_BINARY)) {
|
|
29
|
+
return env.TTSC_GRAPH_BINARY;
|
|
30
|
+
}
|
|
31
|
+
const exe = process.platform === "win32" ? "ttscgraph.exe" : "ttscgraph";
|
|
32
|
+
try {
|
|
33
|
+
const ttscPackageJson = require.resolve("ttsc/package.json", {
|
|
34
|
+
paths: [cwd],
|
|
35
|
+
});
|
|
36
|
+
const fromTtsc = createRequire(ttscPackageJson);
|
|
37
|
+
return fromTtsc.resolve(
|
|
38
|
+
`@ttsc/${process.platform}-${process.arch}/bin/${exe}`,
|
|
39
|
+
);
|
|
40
|
+
} catch {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The project root and tsconfig the server was pointed at, mirroring the
|
|
46
|
+
* `--cwd` / `--tsconfig` flags ttscgraph itself parses, so the background
|
|
47
|
+
* diagnostics worker checks the same project. */
|
|
48
|
+
function parseProjectArgs(argv: readonly string[]): {
|
|
49
|
+
cwd: string;
|
|
50
|
+
tsconfig: string;
|
|
51
|
+
} {
|
|
52
|
+
let cwd = process.cwd();
|
|
53
|
+
let tsconfig = "tsconfig.json";
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const arg = argv[i]!;
|
|
56
|
+
if (arg === "--cwd" && i + 1 < argv.length) cwd = argv[++i]!;
|
|
57
|
+
else if (arg.startsWith("--cwd=")) cwd = arg.slice("--cwd=".length);
|
|
58
|
+
else if (arg === "--tsconfig" && i + 1 < argv.length) tsconfig = argv[++i]!;
|
|
59
|
+
else if (arg.startsWith("--tsconfig=")) tsconfig = arg.slice("--tsconfig=".length);
|
|
60
|
+
}
|
|
61
|
+
return { cwd: path.resolve(cwd), tsconfig };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** A `--connect` proxy pipes stdio to a running daemon and serves no graph of
|
|
65
|
+
* its own, so it needs no diagnostics computed locally. */
|
|
66
|
+
function isConnectProxy(argv: readonly string[]): boolean {
|
|
67
|
+
return argv.some((a) => a === "--connect" || a.startsWith("--connect="));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Start the background worker that computes the project's plugin diagnostics
|
|
72
|
+
* (`@ttsc/lint` and transform-plugin findings) and writes them to
|
|
73
|
+
* `diagnosticsFile`, where the server picks them up. Returns the child handle
|
|
74
|
+
* for cleanup, or `null` when it could not be spawned.
|
|
75
|
+
*
|
|
76
|
+
* It runs in parallel with the server so the MCP handshake is never blocked on
|
|
77
|
+
* a compile; the server shows its tsc diagnostics immediately and fuses the
|
|
78
|
+
* plugin findings once the file lands. Any failure is non-fatal — the graph
|
|
79
|
+
* simply stays tsc-only.
|
|
80
|
+
*/
|
|
81
|
+
function startDiagnosticsWorker(
|
|
82
|
+
argv: readonly string[],
|
|
83
|
+
diagnosticsFile: string,
|
|
84
|
+
): ReturnType<typeof spawn> | null {
|
|
85
|
+
try {
|
|
86
|
+
const { cwd, tsconfig } = parseProjectArgs(argv);
|
|
87
|
+
const worker = spawn(
|
|
88
|
+
process.execPath,
|
|
89
|
+
[require.resolve("./diagnostics.js"), cwd, tsconfig, diagnosticsFile],
|
|
90
|
+
{ stdio: "ignore", windowsHide: true },
|
|
91
|
+
);
|
|
92
|
+
worker.on("error", () => {
|
|
93
|
+
/* resilient: a worker that cannot start leaves no file */
|
|
94
|
+
});
|
|
95
|
+
worker.unref();
|
|
96
|
+
return worker;
|
|
97
|
+
} catch {
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Spawn the resident MCP server, inheriting stdio so the agent's MCP client
|
|
104
|
+
* speaks JSON-RPC to it directly over this process's stdin/stdout. Returns the
|
|
105
|
+
* child's exit code.
|
|
106
|
+
*
|
|
107
|
+
* Before spawning, it kicks off the background diagnostics worker (except in
|
|
108
|
+
* `--connect` proxy mode) and points the server at its output file, so a
|
|
109
|
+
* plugin-using project's lint and transform-plugin diagnostics fuse onto the
|
|
110
|
+
* graph without blocking startup.
|
|
111
|
+
*/
|
|
112
|
+
export function runGraph(
|
|
113
|
+
argv: readonly string[] = process.argv.slice(2),
|
|
114
|
+
): number {
|
|
115
|
+
const binary = resolveGraphBinary();
|
|
116
|
+
if (binary === null) {
|
|
117
|
+
process.stderr.write(
|
|
118
|
+
"@ttsc/graph: could not resolve the ttscgraph binary. " +
|
|
119
|
+
"Install `ttsc` so its platform package is present, " +
|
|
120
|
+
"or set TTSC_GRAPH_BINARY to an absolute path.\n",
|
|
121
|
+
);
|
|
122
|
+
return 1;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const env: NodeJS.ProcessEnv = { ...process.env };
|
|
126
|
+
let diagnosticsFile: string | null = null;
|
|
127
|
+
let worker: ReturnType<typeof spawn> | null = null;
|
|
128
|
+
if (!isConnectProxy(argv)) {
|
|
129
|
+
diagnosticsFile = path.join(
|
|
130
|
+
os.tmpdir(),
|
|
131
|
+
`ttsc-graph-diagnostics-${process.pid}.json`,
|
|
132
|
+
);
|
|
133
|
+
env.TTSC_GRAPH_DIAGNOSTICS_FILE = diagnosticsFile;
|
|
134
|
+
worker = startDiagnosticsWorker(argv, diagnosticsFile);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const result = spawnSync(binary, [...argv], {
|
|
138
|
+
stdio: "inherit",
|
|
139
|
+
env,
|
|
140
|
+
windowsHide: true,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
const workerPid = worker?.pid;
|
|
144
|
+
try {
|
|
145
|
+
worker?.kill();
|
|
146
|
+
} catch {
|
|
147
|
+
/* ignore */
|
|
148
|
+
}
|
|
149
|
+
if (diagnosticsFile) {
|
|
150
|
+
try {
|
|
151
|
+
fs.rmSync(diagnosticsFile, { force: true });
|
|
152
|
+
} catch {
|
|
153
|
+
/* ignore */
|
|
154
|
+
}
|
|
155
|
+
// The worker writes atomically through `<file>.<pid>.tmp`; remove a leftover
|
|
156
|
+
// if it was killed mid-write, so a churning daemon does not litter tmpdir.
|
|
157
|
+
if (workerPid !== undefined) {
|
|
158
|
+
try {
|
|
159
|
+
fs.rmSync(`${diagnosticsFile}.${workerPid}.tmp`, { force: true });
|
|
160
|
+
} catch {
|
|
161
|
+
/* ignore */
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (result.error) {
|
|
167
|
+
process.stderr.write(`@ttsc/graph: ${result.error.message}\n`);
|
|
168
|
+
return 1;
|
|
169
|
+
}
|
|
170
|
+
return result.status ?? 1;
|
|
171
|
+
}
|