jules-orchestrator-kit 0.22.8 ā 0.22.9
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/bin/agentctl.mjs +3 -3
- package/package.json +1 -1
- package/src/dag-engine.mjs +50 -14
- package/src/engine.mjs +1 -1
- package/src/mcp.mjs +1 -1
- package/src/merge-blocks.mjs +238 -0
- package/src/merge-verify.mjs +63 -0
package/bin/agentctl.mjs
CHANGED
|
@@ -14,7 +14,7 @@ const command = args[0];
|
|
|
14
14
|
|
|
15
15
|
function printHelp() {
|
|
16
16
|
console.log(`
|
|
17
|
-
š agentctl v0.22.
|
|
17
|
+
š agentctl v0.22.9 ā Universal Agent Orchestrator & Safety Gatekeeper
|
|
18
18
|
|
|
19
19
|
Usage: agentctl <command> [options]
|
|
20
20
|
|
|
@@ -44,7 +44,7 @@ async function main() {
|
|
|
44
44
|
}
|
|
45
45
|
|
|
46
46
|
if (command === "version" || command === "--version" || command === "-v") {
|
|
47
|
-
console.log("agentctl v0.22.
|
|
47
|
+
console.log("agentctl v0.22.9");
|
|
48
48
|
process.exit(0);
|
|
49
49
|
}
|
|
50
50
|
|
|
@@ -226,7 +226,7 @@ async function main() {
|
|
|
226
226
|
}
|
|
227
227
|
|
|
228
228
|
case "doctor": {
|
|
229
|
-
console.log(`\nš agentctl System Diagnostics (v0.22.
|
|
229
|
+
console.log(`\nš agentctl System Diagnostics (v0.22.9)`);
|
|
230
230
|
console.log(`--------------------------------------------------`);
|
|
231
231
|
console.log(` Project Root : ${root}`);
|
|
232
232
|
console.log(` Config File : ${config._file || "None (Using defaults)"}`);
|
package/package.json
CHANGED
package/src/dag-engine.mjs
CHANGED
|
@@ -14,23 +14,50 @@ export class DagCycleError extends Error {
|
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
|
-
*
|
|
17
|
+
* Wraps an async runner function with a timeout (default 15 minutes = 900,000 ms).
|
|
18
|
+
* @param {Function} runner
|
|
19
|
+
* @param {number} [timeoutMs=900000]
|
|
20
|
+
* @returns {Promise<any>}
|
|
21
|
+
*/
|
|
22
|
+
export async function withTaskTimeout(runner, timeoutMs = 15 * 60 * 1000) {
|
|
23
|
+
let timer;
|
|
24
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
25
|
+
timer = setTimeout(() => {
|
|
26
|
+
reject(new Error(`Task execution timed out after ${timeoutMs}ms`));
|
|
27
|
+
}, timeoutMs);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
return await Promise.race([runner(), timeoutPromise]);
|
|
32
|
+
} finally {
|
|
33
|
+
if (timer) clearTimeout(timer);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Zero-Dependency Task DAG Executor with cycle detection, interface fingerprinting, and execution freezing.
|
|
18
39
|
*/
|
|
19
40
|
export class DagExecutor {
|
|
20
|
-
constructor({ concurrency = 4 } = {}) {
|
|
41
|
+
constructor({ concurrency = 4, taskTimeout = 15 * 60 * 1000 } = {}) {
|
|
21
42
|
this.concurrency = concurrency;
|
|
43
|
+
this.taskTimeout = taskTimeout;
|
|
22
44
|
this.tasks = new Map();
|
|
45
|
+
this.isExecuting = false;
|
|
23
46
|
}
|
|
24
47
|
|
|
25
48
|
/**
|
|
26
|
-
* Registers a task into the DAG.
|
|
49
|
+
* Registers a task into the DAG. Throws if called after execution has started.
|
|
27
50
|
* @param {Object} task
|
|
28
51
|
* @param {string} task.id
|
|
29
52
|
* @param {string[]} [task.dependsOn=[]]
|
|
30
53
|
* @param {string[]} [task.outputs=[]]
|
|
31
54
|
* @param {Function} [task.runner]
|
|
55
|
+
* @param {number} [task.timeout]
|
|
32
56
|
*/
|
|
33
|
-
addTask({ id, dependsOn = [], outputs = [], runner }) {
|
|
57
|
+
addTask({ id, dependsOn = [], outputs = [], runner, timeout }) {
|
|
58
|
+
if (this.isExecuting) {
|
|
59
|
+
throw new Error("Cannot add task after execution has started");
|
|
60
|
+
}
|
|
34
61
|
if (!id || typeof id !== "string") {
|
|
35
62
|
throw new Error("Task id must be a non-empty string");
|
|
36
63
|
}
|
|
@@ -42,6 +69,7 @@ export class DagExecutor {
|
|
|
42
69
|
dependsOn: Array.isArray(dependsOn) ? [...dependsOn] : [],
|
|
43
70
|
outputs: Array.isArray(outputs) ? [...outputs] : [],
|
|
44
71
|
runner: runner || (async () => {}),
|
|
72
|
+
timeout,
|
|
45
73
|
});
|
|
46
74
|
}
|
|
47
75
|
|
|
@@ -171,8 +199,10 @@ export class DagExecutor {
|
|
|
171
199
|
* Executes tasks in topological order with parallel concurrency and interface fingerprinting.
|
|
172
200
|
* @param {Object} [options]
|
|
173
201
|
* @param {number} [options.concurrency]
|
|
202
|
+
* @param {number} [options.taskTimeout]
|
|
174
203
|
*/
|
|
175
|
-
async execute({ concurrency = this.concurrency } = {}) {
|
|
204
|
+
async execute({ concurrency = this.concurrency, taskTimeout = this.taskTimeout } = {}) {
|
|
205
|
+
this.isExecuting = true;
|
|
176
206
|
const { dependents } = this.validateDag();
|
|
177
207
|
|
|
178
208
|
const completed = new Set();
|
|
@@ -221,7 +251,7 @@ export class DagExecutor {
|
|
|
221
251
|
for (const depId of task.dependsOn) {
|
|
222
252
|
const depTask = this.tasks.get(depId);
|
|
223
253
|
for (const outputFile of depTask.outputs) {
|
|
224
|
-
const expectedHash = fingerprints.get(outputFile);
|
|
254
|
+
const expectedHash = fingerprints.get(`${depId}:${outputFile}`) ?? fingerprints.get(outputFile);
|
|
225
255
|
const actualHash = this._computeFileHash(outputFile);
|
|
226
256
|
if (expectedHash !== actualHash) {
|
|
227
257
|
throw new Error(
|
|
@@ -239,16 +269,22 @@ export class DagExecutor {
|
|
|
239
269
|
(async () => {
|
|
240
270
|
try {
|
|
241
271
|
executionHistory.push(taskId);
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
272
|
+
const timeoutMs = task.timeout || taskTimeout;
|
|
273
|
+
await withTaskTimeout(
|
|
274
|
+
() =>
|
|
275
|
+
task.runner({
|
|
276
|
+
id: task.id,
|
|
277
|
+
dependsOn: task.dependsOn,
|
|
278
|
+
outputs: task.outputs,
|
|
279
|
+
fingerprints,
|
|
280
|
+
}),
|
|
281
|
+
timeoutMs
|
|
282
|
+
);
|
|
283
|
+
|
|
284
|
+
// Post-task Interface Fingerprinting (keyed by taskId:filePath and filePath alias)
|
|
250
285
|
for (const outputFile of task.outputs) {
|
|
251
286
|
const hash = this._computeFileHash(outputFile);
|
|
287
|
+
fingerprints.set(`${taskId}:${outputFile}`, hash);
|
|
252
288
|
fingerprints.set(outputFile, hash);
|
|
253
289
|
}
|
|
254
290
|
|
package/src/engine.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import { readdirSync, readFileSync, renameSync, existsSync } from "node:fs";
|
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { createHash } from "node:crypto";
|
|
11
11
|
|
|
12
|
-
export { recordVerifyRun, readVerifyRuns, flakyVerdict };
|
|
12
|
+
export { recordVerifyRun, readVerifyRuns, flakyVerdict, sanitizeUntrustedData };
|
|
13
13
|
|
|
14
14
|
/**
|
|
15
15
|
* Validates whether a file in .agent/jules-queue/ is a task file.
|
package/src/mcp.mjs
CHANGED
|
@@ -7,7 +7,7 @@ import { reapOrphanedIntents, reapStaleMutexDirs } from "./journal.mjs";
|
|
|
7
7
|
|
|
8
8
|
export const MCP_SERVER_INFO = {
|
|
9
9
|
name: "jules-orchestrator-kit",
|
|
10
|
-
version: "0.22.
|
|
10
|
+
version: "0.22.9",
|
|
11
11
|
};
|
|
12
12
|
|
|
13
13
|
export const MAX_MCP_FRAME_SIZE = 4 * 1024 * 1024; // 4 MB memory safety ceiling
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Computes SHA-1 hash of a string.
|
|
5
|
+
* @param {string} content
|
|
6
|
+
* @returns {string} SHA-1 hex string
|
|
7
|
+
*/
|
|
8
|
+
function sha1(content) {
|
|
9
|
+
return createHash("sha1").update(content).digest("hex");
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Chunks source code text into declaration blocks bounded by column-0 declaration boundaries.
|
|
14
|
+
* @param {string} text
|
|
15
|
+
* @param {string} [lang="js"]
|
|
16
|
+
* @returns {Array<Object>} Array of block descriptors
|
|
17
|
+
*/
|
|
18
|
+
export function chunkBlocks(text = "", lang = "js") {
|
|
19
|
+
if (typeof text !== "string") {
|
|
20
|
+
text = String(text || "");
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const lines = text.split(/\r?\n/);
|
|
24
|
+
const blocks = [];
|
|
25
|
+
const nameCounts = new Map();
|
|
26
|
+
|
|
27
|
+
const isPython = lang === "py" || lang === "python";
|
|
28
|
+
|
|
29
|
+
// Declaration regex matching column 0
|
|
30
|
+
const declRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum)\s+([a-zA-Z0-9_$]+)/;
|
|
31
|
+
const declAnyRegex = /^(?:export\s+)?(?:async\s+)?(function\*?|class|const|let|var|def|type|interface|enum)\b/;
|
|
32
|
+
|
|
33
|
+
let currentLines = [];
|
|
34
|
+
let currentType = "prelude";
|
|
35
|
+
let currentName = null;
|
|
36
|
+
let inBlock = false;
|
|
37
|
+
let braceDepth = 0;
|
|
38
|
+
let blockIndentLevel = 0;
|
|
39
|
+
|
|
40
|
+
function flushBlock() {
|
|
41
|
+
if (currentLines.length === 0) return;
|
|
42
|
+
const content = currentLines.join("\n");
|
|
43
|
+
if (currentType === "prelude" && content.trim().length === 0) {
|
|
44
|
+
currentLines = [];
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
let name = currentName;
|
|
48
|
+
if (!name) {
|
|
49
|
+
name = currentType === "prelude" ? `prelude_${blocks.length}` : `block_${blocks.length}`;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Ensure unique block ID if duplicate names exist
|
|
53
|
+
const count = (nameCounts.get(name) || 0) + 1;
|
|
54
|
+
nameCounts.set(name, count);
|
|
55
|
+
const uniqueId = count > 1 ? `${currentType}:${name}:${count}` : `${currentType}:${name}`;
|
|
56
|
+
|
|
57
|
+
blocks.push({
|
|
58
|
+
id: uniqueId,
|
|
59
|
+
name,
|
|
60
|
+
type: currentType,
|
|
61
|
+
lines: [...currentLines],
|
|
62
|
+
content,
|
|
63
|
+
hash: sha1(content),
|
|
64
|
+
indentation: blockIndentLevel,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
currentLines = [];
|
|
68
|
+
currentType = "prelude";
|
|
69
|
+
currentName = null;
|
|
70
|
+
inBlock = false;
|
|
71
|
+
braceDepth = 0;
|
|
72
|
+
blockIndentLevel = 0;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
for (let i = 0; i < lines.length; i++) {
|
|
76
|
+
const line = lines[i];
|
|
77
|
+
const isColumnZero = line.length > 0 && line[0] !== " " && line[0] !== "\t";
|
|
78
|
+
|
|
79
|
+
if (isPython) {
|
|
80
|
+
// Python block logic
|
|
81
|
+
if (isColumnZero && declAnyRegex.test(line)) {
|
|
82
|
+
flushBlock();
|
|
83
|
+
const match = line.match(declRegex);
|
|
84
|
+
currentType = match ? match[1] : "def";
|
|
85
|
+
currentName = match ? match[2] : null;
|
|
86
|
+
inBlock = true;
|
|
87
|
+
blockIndentLevel = 0;
|
|
88
|
+
} else if (inBlock && isColumnZero && line.trim().length > 0 && !line.trim().startsWith("#")) {
|
|
89
|
+
// Dedented back to column 0 in Python
|
|
90
|
+
flushBlock();
|
|
91
|
+
}
|
|
92
|
+
} else {
|
|
93
|
+
// JS/TS block logic
|
|
94
|
+
if (isColumnZero && declAnyRegex.test(line)) {
|
|
95
|
+
flushBlock();
|
|
96
|
+
const match = line.match(declRegex);
|
|
97
|
+
currentType = match ? match[1] : "decl";
|
|
98
|
+
currentName = match ? match[2] : null;
|
|
99
|
+
inBlock = true;
|
|
100
|
+
blockIndentLevel = 0;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
currentLines.push(line);
|
|
105
|
+
|
|
106
|
+
if (!isPython && inBlock) {
|
|
107
|
+
// Track curly brace balance for JS/TS
|
|
108
|
+
for (const char of line) {
|
|
109
|
+
if (char === "{") braceDepth++;
|
|
110
|
+
if (char === "}") braceDepth--;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check if block ends on this line
|
|
114
|
+
const isClosingBraceAtColZero = isColumnZero && line.trim() === "}";
|
|
115
|
+
const isSingleLineStatement = braceDepth <= 0 && (line.trim().endsWith(";") || isClosingBraceAtColZero);
|
|
116
|
+
|
|
117
|
+
if (braceDepth <= 0 && isSingleLineStatement) {
|
|
118
|
+
flushBlock();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
flushBlock();
|
|
124
|
+
return blocks;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Performs a 3-way block structural merge between base, ours, and theirs.
|
|
129
|
+
* @param {string} baseText
|
|
130
|
+
* @param {string} oursText
|
|
131
|
+
* @param {string} theirsText
|
|
132
|
+
* @param {string} [lang="js"]
|
|
133
|
+
* @returns {Object} { mergedText, classifications, conflicts, toString() }
|
|
134
|
+
*/
|
|
135
|
+
export function mergeBlocks3Way(baseText = "", oursText = "", theirsText = "", lang = "js") {
|
|
136
|
+
const baseBlocks = chunkBlocks(baseText, lang);
|
|
137
|
+
const oursBlocks = chunkBlocks(oursText, lang);
|
|
138
|
+
const theirsBlocks = chunkBlocks(theirsText, lang);
|
|
139
|
+
|
|
140
|
+
const baseMap = new Map(baseBlocks.map((b) => [b.id, b]));
|
|
141
|
+
const theirsMap = new Map(theirsBlocks.map((b) => [b.id, b]));
|
|
142
|
+
|
|
143
|
+
const classifications = [];
|
|
144
|
+
const processed = new Set();
|
|
145
|
+
const outputBlocks = [];
|
|
146
|
+
let conflicts = 0;
|
|
147
|
+
|
|
148
|
+
// Process oursBlocks first
|
|
149
|
+
for (const oBlock of oursBlocks) {
|
|
150
|
+
const id = oBlock.id;
|
|
151
|
+
processed.add(id);
|
|
152
|
+
|
|
153
|
+
const bBlock = baseMap.get(id);
|
|
154
|
+
const tBlock = theirsMap.get(id);
|
|
155
|
+
|
|
156
|
+
if (tBlock && bBlock) {
|
|
157
|
+
if (oBlock.hash === tBlock.hash) {
|
|
158
|
+
// Both match exactly
|
|
159
|
+
classifications.push({ id, type: "IDENTICAL" });
|
|
160
|
+
outputBlocks.push(oBlock.content);
|
|
161
|
+
} else if (oBlock.hash === bBlock.hash) {
|
|
162
|
+
// Ours didn't change, theirs changed
|
|
163
|
+
classifications.push({ id, type: "ONLY_THEIRS" });
|
|
164
|
+
outputBlocks.push(tBlock.content);
|
|
165
|
+
} else if (tBlock.hash === bBlock.hash) {
|
|
166
|
+
// Theirs didn't change, ours changed
|
|
167
|
+
classifications.push({ id, type: "ONLY_OURS" });
|
|
168
|
+
outputBlocks.push(oBlock.content);
|
|
169
|
+
} else {
|
|
170
|
+
// Both changed differently
|
|
171
|
+
conflicts++;
|
|
172
|
+
classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
|
|
173
|
+
outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
|
|
174
|
+
}
|
|
175
|
+
} else if (tBlock && !bBlock) {
|
|
176
|
+
// Added in both ours and theirs
|
|
177
|
+
if (oBlock.hash === tBlock.hash) {
|
|
178
|
+
classifications.push({ id, type: "IDENTICAL" });
|
|
179
|
+
outputBlocks.push(oBlock.content);
|
|
180
|
+
} else {
|
|
181
|
+
conflicts++;
|
|
182
|
+
classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
|
|
183
|
+
outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
|
|
184
|
+
}
|
|
185
|
+
} else if (!tBlock && bBlock) {
|
|
186
|
+
// Present in base and ours, missing in theirs
|
|
187
|
+
if (oBlock.hash === bBlock.hash) {
|
|
188
|
+
// Deleted by theirs, unchanged by ours
|
|
189
|
+
classifications.push({ id, type: "DELETED" });
|
|
190
|
+
} else {
|
|
191
|
+
// Modified by ours, deleted by theirs -> CONFLICT
|
|
192
|
+
conflicts++;
|
|
193
|
+
classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
|
|
194
|
+
outputBlocks.push(`<<<<<<< OURS\n${oBlock.content}\n=======\n>>>>>>> THEIRS`);
|
|
195
|
+
}
|
|
196
|
+
} else {
|
|
197
|
+
// !tBlock && !bBlock -> ADDED_OURS
|
|
198
|
+
classifications.push({ id, type: "ADDED_OURS" });
|
|
199
|
+
outputBlocks.push(oBlock.content);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// Handle blocks in theirs that were not in ours
|
|
204
|
+
for (const tBlock of theirsBlocks) {
|
|
205
|
+
const id = tBlock.id;
|
|
206
|
+
if (processed.has(id)) continue;
|
|
207
|
+
processed.add(id);
|
|
208
|
+
|
|
209
|
+
const bBlock = baseMap.get(id);
|
|
210
|
+
if (!bBlock) {
|
|
211
|
+
// Added in theirs
|
|
212
|
+
classifications.push({ id, type: "ADDED_THEIRS" });
|
|
213
|
+
outputBlocks.push(tBlock.content);
|
|
214
|
+
} else {
|
|
215
|
+
// Present in base and theirs, deleted by ours
|
|
216
|
+
if (tBlock.hash === bBlock.hash) {
|
|
217
|
+
// Deleted by ours, unchanged by theirs
|
|
218
|
+
classifications.push({ id, type: "DELETED" });
|
|
219
|
+
} else {
|
|
220
|
+
// Modified by theirs, deleted by ours -> CONFLICT
|
|
221
|
+
conflicts++;
|
|
222
|
+
classifications.push({ id, type: "CONFLICT_EDIT_EDIT" });
|
|
223
|
+
outputBlocks.push(`<<<<<<< OURS\n=======\n${tBlock.content}\n>>>>>>> THEIRS`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const mergedText = outputBlocks.join("\n\n");
|
|
229
|
+
|
|
230
|
+
return {
|
|
231
|
+
mergedText,
|
|
232
|
+
classifications,
|
|
233
|
+
conflicts,
|
|
234
|
+
toString() {
|
|
235
|
+
return this.mergedText;
|
|
236
|
+
},
|
|
237
|
+
};
|
|
238
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, rmSync, existsSync } from "node:fs";
|
|
2
|
+
import { join, basename, extname } from "node:path";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Syntax Verification Chain validating merged output via target runtime tools.
|
|
8
|
+
* @param {string} mergedText
|
|
9
|
+
* @param {string} fileName
|
|
10
|
+
* @param {string} [root=process.cwd()]
|
|
11
|
+
* @returns {{ ok: boolean, tool: string, error?: string }}
|
|
12
|
+
*/
|
|
13
|
+
export function mergeVerifyChain(mergedText, fileName, root = process.cwd()) {
|
|
14
|
+
const ext = extname(fileName).toLowerCase();
|
|
15
|
+
const base = basename(fileName) || `temp_file${ext}`;
|
|
16
|
+
const tempDir = join(tmpdir(), `merge-verify-${Date.now()}-${Math.random().toString(36).slice(2)}`);
|
|
17
|
+
|
|
18
|
+
mkdirSync(tempDir, { recursive: true });
|
|
19
|
+
const tempFilePath = join(tempDir, base);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
writeFileSync(tempFilePath, mergedText, "utf8");
|
|
23
|
+
|
|
24
|
+
let cmd = "";
|
|
25
|
+
let args = [];
|
|
26
|
+
let tool = "";
|
|
27
|
+
|
|
28
|
+
if (ext === ".py") {
|
|
29
|
+
cmd = "python3";
|
|
30
|
+
args = ["-m", "py_compile", tempFilePath];
|
|
31
|
+
tool = "python3 -m py_compile";
|
|
32
|
+
} else if (ext === ".ts" || ext === ".tsx" || ext === ".mts" || ext === ".cts") {
|
|
33
|
+
const hasTsConfig = existsSync(join(root, "tsconfig.json"));
|
|
34
|
+
if (hasTsConfig) {
|
|
35
|
+
cmd = "npx";
|
|
36
|
+
args = ["tsc", "--noEmit", tempFilePath];
|
|
37
|
+
tool = "tsc --noEmit";
|
|
38
|
+
} else {
|
|
39
|
+
cmd = "node";
|
|
40
|
+
args = ["--check", tempFilePath];
|
|
41
|
+
tool = "node --check";
|
|
42
|
+
}
|
|
43
|
+
} else {
|
|
44
|
+
// JS, MJS, CJS or default
|
|
45
|
+
cmd = "node";
|
|
46
|
+
args = ["--check", tempFilePath];
|
|
47
|
+
tool = "node --check";
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const res = spawnSync(cmd, args, { encoding: "utf8" });
|
|
51
|
+
const ok = res.status === 0;
|
|
52
|
+
|
|
53
|
+
return {
|
|
54
|
+
ok,
|
|
55
|
+
tool,
|
|
56
|
+
...(ok ? {} : { error: res.stderr || res.stdout || `Process exited with code ${res.status}` }),
|
|
57
|
+
};
|
|
58
|
+
} finally {
|
|
59
|
+
if (existsSync(tempDir)) {
|
|
60
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|