jules-orchestrator-kit 0.51.0 → 0.52.1
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/README.md +5 -2
- package/bin/agentctl.mjs +176 -6
- package/index.mjs +10 -0
- package/package.json +1 -1
- package/scripts/jules-scan-todos.mjs +2 -2
- package/src/mcp.mjs +277 -0
- package/src/ops/command-registry.mjs +204 -7
- package/src/ops/next-step.mjs +2 -1
- package/src/provider.mjs +134 -5
- package/src/session-ops.mjs +312 -0
- package/src/stack-detector.mjs +31 -9
- package/src/wizard-oracle.mjs +42 -8
- package/src/wizard-task.mjs +4 -3
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { writeFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { createProvider } from "./provider.mjs";
|
|
5
|
+
import { loadConfig, resolveRoot } from "./config.mjs";
|
|
6
|
+
import { redactSecrets } from "./security.mjs";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Parses human-readable duration strings (e.g. "7d", "24h", "30m", "60s") into milliseconds.
|
|
10
|
+
*
|
|
11
|
+
* @param {string|number} duration
|
|
12
|
+
* @returns {number} Milliseconds
|
|
13
|
+
*/
|
|
14
|
+
export function parseAgeDuration(duration) {
|
|
15
|
+
if (typeof duration === "number" && Number.isFinite(duration)) return duration;
|
|
16
|
+
if (!duration || typeof duration !== "string") return 0;
|
|
17
|
+
const match = duration.trim().match(/^(\d+(?:\.\d+)?)\s*([smhdw])$/i);
|
|
18
|
+
if (!match) {
|
|
19
|
+
const num = Number(duration);
|
|
20
|
+
return Number.isFinite(num) ? num : 0;
|
|
21
|
+
}
|
|
22
|
+
const val = parseFloat(match[1]);
|
|
23
|
+
const unit = match[2].toLowerCase();
|
|
24
|
+
switch (unit) {
|
|
25
|
+
case "s": return val * 1000;
|
|
26
|
+
case "m": return val * 60 * 1000;
|
|
27
|
+
case "h": return val * 60 * 60 * 1000;
|
|
28
|
+
case "d": return val * 24 * 60 * 60 * 1000;
|
|
29
|
+
case "w": return val * 7 * 24 * 60 * 60 * 1000;
|
|
30
|
+
default: return 0;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Extracts git diff patch, PR details, and modified files from a Jules session.
|
|
36
|
+
*
|
|
37
|
+
* @param {string} sessionId
|
|
38
|
+
* @param {object} opts
|
|
39
|
+
* @returns {Promise<{ ok: boolean, id: string, patch: string, pr: object|null, files: string[], error?: string }>}
|
|
40
|
+
*/
|
|
41
|
+
export async function extractSessionPatch(sessionId, opts = {}) {
|
|
42
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
43
|
+
throw new TypeError("extractSessionPatch requires a valid sessionId string");
|
|
44
|
+
}
|
|
45
|
+
const root = opts.root || resolveRoot();
|
|
46
|
+
const config = opts.config || loadConfig(root);
|
|
47
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
48
|
+
|
|
49
|
+
const session = await provider.getSession(sessionId, opts);
|
|
50
|
+
const activitiesRes = await provider.listActivities(sessionId, opts).catch(() => ({ activities: [] }));
|
|
51
|
+
const activities = activitiesRes.activities || [];
|
|
52
|
+
|
|
53
|
+
let patch = "";
|
|
54
|
+
let pr = null;
|
|
55
|
+
const files = new Set();
|
|
56
|
+
|
|
57
|
+
// 1. Inspect session outputs
|
|
58
|
+
const outputs = session?.raw?.outputs || session?.outputs || [];
|
|
59
|
+
for (const out of outputs) {
|
|
60
|
+
if (out.pullRequest) {
|
|
61
|
+
pr = out.pullRequest;
|
|
62
|
+
}
|
|
63
|
+
if (out.gitPatch || out.patch || out.diff) {
|
|
64
|
+
patch = out.gitPatch || out.patch || out.diff;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// 2. Inspect activity artifacts and logs if patch wasn't in top-level output
|
|
69
|
+
for (const act of activities) {
|
|
70
|
+
if (act.pullRequest) pr = act.pullRequest;
|
|
71
|
+
if (act.gitPatch || act.patch || act.diff) {
|
|
72
|
+
patch = patch || act.gitPatch || act.patch || act.diff;
|
|
73
|
+
}
|
|
74
|
+
if (act.artifacts && Array.isArray(act.artifacts)) {
|
|
75
|
+
for (const art of act.artifacts) {
|
|
76
|
+
if (art.patch || art.diff || (typeof art.content === "string" && art.content.startsWith("diff --git"))) {
|
|
77
|
+
patch = patch || art.patch || art.diff || art.content;
|
|
78
|
+
}
|
|
79
|
+
if (art.path || art.filename) {
|
|
80
|
+
files.add(art.path || art.filename);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (act.changedFiles && Array.isArray(act.changedFiles)) {
|
|
85
|
+
act.changedFiles.forEach((f) => files.add(f));
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Extract changed files from patch headers if files set is empty
|
|
90
|
+
if (patch && files.size === 0) {
|
|
91
|
+
const fileMatches = patch.matchAll(/^diff --git a\/(.+?) b\/(.+?)$/gm);
|
|
92
|
+
for (const m of fileMatches) {
|
|
93
|
+
files.add(m[2]);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return {
|
|
98
|
+
ok: Boolean(patch || pr),
|
|
99
|
+
id: sessionId,
|
|
100
|
+
patch: patch ? (patch.endsWith("\n") ? patch : patch + "\n") : "",
|
|
101
|
+
pr,
|
|
102
|
+
files: Array.from(files),
|
|
103
|
+
rawSession: session.raw,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Applies a Jules session's patch to the local working tree with git apply safety checks.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} sessionId
|
|
111
|
+
* @param {object} opts
|
|
112
|
+
* @returns {Promise<{ ok: boolean, patchApplied: boolean, checkPassed: boolean, patch: string, files: string[], error?: string }>}
|
|
113
|
+
*/
|
|
114
|
+
export async function applySessionPatch(sessionId, opts = {}) {
|
|
115
|
+
const root = opts.root || resolveRoot();
|
|
116
|
+
const res = await extractSessionPatch(sessionId, { ...opts, root });
|
|
117
|
+
|
|
118
|
+
if (!res.patch) {
|
|
119
|
+
return {
|
|
120
|
+
ok: false,
|
|
121
|
+
patchApplied: false,
|
|
122
|
+
checkPassed: false,
|
|
123
|
+
patch: "",
|
|
124
|
+
files: res.files || [],
|
|
125
|
+
error: "No git patch found in session output or activities.",
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (opts.save) {
|
|
130
|
+
const savePath = resolve(root, opts.save);
|
|
131
|
+
writeFileSync(savePath, res.patch, "utf-8");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// 1. Dry run: git apply --check
|
|
135
|
+
const checkRes = spawnSync("git", ["apply", "--check", "-"], {
|
|
136
|
+
cwd: root,
|
|
137
|
+
input: res.patch,
|
|
138
|
+
encoding: "utf-8",
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (checkRes.status !== 0) {
|
|
142
|
+
const stderr = (checkRes.stderr || "").trim();
|
|
143
|
+
return {
|
|
144
|
+
ok: false,
|
|
145
|
+
patchApplied: false,
|
|
146
|
+
checkPassed: false,
|
|
147
|
+
patch: res.patch,
|
|
148
|
+
files: res.files,
|
|
149
|
+
error: `git apply --check failed: ${stderr || "Patch does not cleanly apply to working directory"}`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 2. If apply requested, execute git apply
|
|
154
|
+
if (opts.apply) {
|
|
155
|
+
const applyRes = spawnSync("git", ["apply", "-"], {
|
|
156
|
+
cwd: root,
|
|
157
|
+
input: res.patch,
|
|
158
|
+
encoding: "utf-8",
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
if (applyRes.status !== 0) {
|
|
162
|
+
const stderr = (applyRes.stderr || "").trim();
|
|
163
|
+
return {
|
|
164
|
+
ok: false,
|
|
165
|
+
patchApplied: false,
|
|
166
|
+
checkPassed: true,
|
|
167
|
+
patch: res.patch,
|
|
168
|
+
files: res.files,
|
|
169
|
+
error: `git apply failed: ${stderr}`,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
ok: true,
|
|
175
|
+
patchApplied: true,
|
|
176
|
+
checkPassed: true,
|
|
177
|
+
patch: res.patch,
|
|
178
|
+
files: res.files,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
ok: true,
|
|
184
|
+
patchApplied: false,
|
|
185
|
+
checkPassed: true,
|
|
186
|
+
patch: res.patch,
|
|
187
|
+
files: res.files,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Gathers failure context and error output from a failed session and synthesizes a new OODA retry session.
|
|
193
|
+
*
|
|
194
|
+
* @param {string} sessionId
|
|
195
|
+
* @param {object} opts
|
|
196
|
+
* @returns {Promise<{ ok: boolean, originalSessionId: string, newSession: object, failureReason: string }>}
|
|
197
|
+
*/
|
|
198
|
+
export async function retrySession(sessionId, opts = {}) {
|
|
199
|
+
if (!sessionId || typeof sessionId !== "string") {
|
|
200
|
+
throw new TypeError("retrySession requires a valid sessionId string");
|
|
201
|
+
}
|
|
202
|
+
const root = opts.root || resolveRoot();
|
|
203
|
+
const config = opts.config || loadConfig(root);
|
|
204
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
205
|
+
|
|
206
|
+
const session = await provider.getSession(sessionId, opts);
|
|
207
|
+
const activitiesRes = await provider.listActivities(sessionId, opts).catch(() => ({ activities: [] }));
|
|
208
|
+
const activities = activitiesRes.activities || [];
|
|
209
|
+
|
|
210
|
+
// Extract failure diagnostics
|
|
211
|
+
const errors = [];
|
|
212
|
+
for (const act of activities) {
|
|
213
|
+
if (act.error) errors.push(typeof act.error === "string" ? act.error : JSON.stringify(act.error));
|
|
214
|
+
if (act.executionOutput && (act.exitCode !== 0 || act.status === "FAILED")) {
|
|
215
|
+
errors.push(act.executionOutput);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const rawPrompt = session?.raw?.prompt || session?.prompt || "";
|
|
220
|
+
const title = session?.raw?.title || `Retry of Session ${sessionId}`;
|
|
221
|
+
const failureReason = errors.join("\n").slice(0, 4000) || "Previous session did not complete cleanly.";
|
|
222
|
+
|
|
223
|
+
let synthesizedPrompt = rawPrompt;
|
|
224
|
+
if (opts.withFailure !== false && failureReason) {
|
|
225
|
+
synthesizedPrompt = `${rawPrompt}\n\n[PREVIOUS_ATTEMPT_FAILURE_DIAGNOSTIC]\nThe previous automated session failed with the following error output:\n\`\`\`\n${redactSecrets(failureReason)}\n\`\`\`\nPlease analyze this failure, fix the root cause, and ensure all tests pass.\n`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const source = session?.raw?.sourceContext?.source || opts.source;
|
|
229
|
+
const branch = session?.raw?.sourceContext?.githubRepoContext?.startingBranch || opts.branch || "main";
|
|
230
|
+
|
|
231
|
+
const dispatchRes = await provider.dispatch(
|
|
232
|
+
{
|
|
233
|
+
title: opts.title || title,
|
|
234
|
+
prompt: synthesizedPrompt,
|
|
235
|
+
role: opts.role,
|
|
236
|
+
source,
|
|
237
|
+
branch,
|
|
238
|
+
},
|
|
239
|
+
opts
|
|
240
|
+
);
|
|
241
|
+
|
|
242
|
+
return {
|
|
243
|
+
ok: true,
|
|
244
|
+
originalSessionId: sessionId,
|
|
245
|
+
newSession: dispatchRes,
|
|
246
|
+
failureReason,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Queries and batch-archives old or completed Jules sessions based on age and state filters.
|
|
252
|
+
*
|
|
253
|
+
* @param {object} opts
|
|
254
|
+
* @returns {Promise<{ ok: boolean, matchedCount: number, archivedCount: number, sessions: Array<{ id: string, state: string, createTime: string, action: string }> }>}
|
|
255
|
+
*/
|
|
256
|
+
export async function pruneSessions(opts = {}) {
|
|
257
|
+
const root = opts.root || resolveRoot();
|
|
258
|
+
const config = opts.config || loadConfig(root);
|
|
259
|
+
const provider = opts.provider || createProvider(config.provider || "jules", config);
|
|
260
|
+
|
|
261
|
+
const listRes = await provider.listSessions({ ...opts, pageSize: opts.pageSize || 100 });
|
|
262
|
+
const allSessions = listRes.sessions || [];
|
|
263
|
+
|
|
264
|
+
const maxAgeMs = opts.age ? parseAgeDuration(opts.age) : 0;
|
|
265
|
+
const now = Date.now();
|
|
266
|
+
const targetState = opts.state ? String(opts.state).toUpperCase() : null;
|
|
267
|
+
|
|
268
|
+
const matched = [];
|
|
269
|
+
for (const s of allSessions) {
|
|
270
|
+
const state = (s.state || s.status || "UNKNOWN").toUpperCase();
|
|
271
|
+
if (targetState && state !== targetState) continue;
|
|
272
|
+
|
|
273
|
+
if (maxAgeMs > 0) {
|
|
274
|
+
const timeStr = s.createTime || s.updateTime;
|
|
275
|
+
const sessionTime = timeStr ? new Date(timeStr).getTime() : 0;
|
|
276
|
+
if (sessionTime > 0 && now - sessionTime < maxAgeMs) {
|
|
277
|
+
continue; // Session is newer than the cutoff age
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
matched.push(s);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const results = [];
|
|
285
|
+
for (const s of matched) {
|
|
286
|
+
const id = s.id || (s.name ? s.name.split("/").pop() : "");
|
|
287
|
+
if (!id) continue;
|
|
288
|
+
|
|
289
|
+
if (opts.dryRun) {
|
|
290
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "DRY_RUN_SKIP" });
|
|
291
|
+
} else {
|
|
292
|
+
try {
|
|
293
|
+
if (opts.delete) {
|
|
294
|
+
await provider.deleteSession(id, opts);
|
|
295
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "DELETED" });
|
|
296
|
+
} else {
|
|
297
|
+
await provider.archiveSession(id, opts);
|
|
298
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: "ARCHIVED" });
|
|
299
|
+
}
|
|
300
|
+
} catch (err) {
|
|
301
|
+
results.push({ id, state: s.state || s.status, createTime: s.createTime, action: `ERROR: ${err.message}` });
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
ok: true,
|
|
308
|
+
matchedCount: matched.length,
|
|
309
|
+
archivedCount: results.filter((r) => r.action === "ARCHIVED" || r.action === "DELETED").length,
|
|
310
|
+
sessions: results,
|
|
311
|
+
};
|
|
312
|
+
}
|
package/src/stack-detector.mjs
CHANGED
|
@@ -210,8 +210,12 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
210
210
|
setupCmd = "npx drizzle-kit push";
|
|
211
211
|
}
|
|
212
212
|
|
|
213
|
-
if (existsSync(join(projectRoot, "bunfig.toml")) || existsSync(join(projectRoot, "bun.lockb"))) {
|
|
214
|
-
const triggerFile = existsSync(join(projectRoot, "bunfig.toml"))
|
|
213
|
+
if (existsSync(join(projectRoot, "bunfig.toml")) || existsSync(join(projectRoot, "bun.lockb")) || existsSync(join(projectRoot, "bun.lock"))) {
|
|
214
|
+
const triggerFile = existsSync(join(projectRoot, "bunfig.toml"))
|
|
215
|
+
? "bunfig.toml"
|
|
216
|
+
: existsSync(join(projectRoot, "bun.lockb"))
|
|
217
|
+
? "bun.lockb"
|
|
218
|
+
: "bun.lock";
|
|
215
219
|
return { ...container, stack: "bun", setupCmd, testCmd: "bun test", buildCmd: "bun run build", triggerFile };
|
|
216
220
|
}
|
|
217
221
|
if (existsSync(join(projectRoot, "deno.json")) || existsSync(join(projectRoot, "deno.jsonc"))) {
|
|
@@ -219,20 +223,38 @@ export function detectPolyglotStack(projectRoot = process.cwd()) {
|
|
|
219
223
|
return { ...container, stack: "deno", setupCmd, testCmd: "deno test", buildCmd: "deno task build", triggerFile };
|
|
220
224
|
}
|
|
221
225
|
if (existsSync(join(projectRoot, "package.json"))) {
|
|
222
|
-
|
|
223
|
-
|
|
226
|
+
const hasPnpm = existsSync(join(projectRoot, "pnpm-lock.yaml"));
|
|
227
|
+
const hasYarn = existsSync(join(projectRoot, "yarn.lock"));
|
|
228
|
+
const hasBun = existsSync(join(projectRoot, "bun.lock")) || existsSync(join(projectRoot, "bun.lockb"));
|
|
229
|
+
const pm = hasPnpm ? "pnpm" : hasYarn ? "yarn" : hasBun ? "bun" : "npm";
|
|
230
|
+
const triggerFile = hasPnpm
|
|
231
|
+
? "pnpm-lock.yaml"
|
|
232
|
+
: hasYarn
|
|
233
|
+
? "yarn.lock"
|
|
234
|
+
: hasBun
|
|
235
|
+
? (existsSync(join(projectRoot, "bun.lock")) ? "bun.lock" : "bun.lockb")
|
|
236
|
+
: "package.json";
|
|
237
|
+
|
|
238
|
+
let testScript = "";
|
|
239
|
+
let buildScript = "";
|
|
224
240
|
try {
|
|
225
241
|
const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
|
|
226
242
|
if (pkg && typeof pkg === "object") {
|
|
227
243
|
const scripts = pkg.scripts || {};
|
|
228
|
-
|
|
229
|
-
|
|
244
|
+
if (scripts.test) {
|
|
245
|
+
testScript = pm === "yarn" ? "yarn test" : `${pm} test`;
|
|
246
|
+
} else if (pm === "bun") {
|
|
247
|
+
testScript = "bun test";
|
|
248
|
+
}
|
|
249
|
+
if (scripts.build) {
|
|
250
|
+
buildScript = pm === "yarn" ? "yarn build" : `${pm} run build`;
|
|
251
|
+
}
|
|
230
252
|
}
|
|
231
253
|
} catch (_) {
|
|
232
|
-
testScript = "
|
|
233
|
-
buildScript = "
|
|
254
|
+
testScript = "";
|
|
255
|
+
buildScript = "";
|
|
234
256
|
}
|
|
235
|
-
return { ...container, stack: "node", setupCmd, testCmd: testScript, buildCmd: buildScript, triggerFile
|
|
257
|
+
return { ...container, stack: pm === "npm" ? "node" : pm, setupCmd, testCmd: testScript, buildCmd: buildScript, triggerFile };
|
|
236
258
|
}
|
|
237
259
|
|
|
238
260
|
// 7. .NET / C# / F# / PHP / Python root file extension fallback
|
package/src/wizard-oracle.mjs
CHANGED
|
@@ -1,8 +1,28 @@
|
|
|
1
1
|
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
|
+
import { spawnSync } from "node:child_process";
|
|
3
4
|
import { detectStack, parseYaml } from "./config.mjs";
|
|
4
5
|
import { runCmd } from "./git.mjs";
|
|
5
6
|
|
|
7
|
+
/**
|
|
8
|
+
* Check if a command-line binary is available on the system PATH.
|
|
9
|
+
* @param {string} binName
|
|
10
|
+
* @returns {boolean}
|
|
11
|
+
*/
|
|
12
|
+
export function hasBinary(binName) {
|
|
13
|
+
if (!binName || typeof binName !== "string") return false;
|
|
14
|
+
const bin = binName.trim().split(/\s+/)[0];
|
|
15
|
+
if (!bin) return false;
|
|
16
|
+
try {
|
|
17
|
+
const isWin = process.platform === "win32";
|
|
18
|
+
const checker = isWin ? "where" : "which";
|
|
19
|
+
const res = spawnSync(checker, [bin], { stdio: "ignore" });
|
|
20
|
+
return res.status === 0;
|
|
21
|
+
} catch (_) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
6
26
|
/**
|
|
7
27
|
* Inspect repository topology, workspace structure, and manifests to infer candidate verification oracles.
|
|
8
28
|
* @param {string} [root=process.cwd()]
|
|
@@ -37,18 +57,24 @@ export function detectStackOracles(root = process.cwd()) {
|
|
|
37
57
|
|
|
38
58
|
const packages = [];
|
|
39
59
|
|
|
40
|
-
// Check Node.
|
|
60
|
+
// Check Node.js package.json scripts
|
|
41
61
|
const pkgPath = join(root, "package.json");
|
|
42
62
|
if (existsSync(pkgPath)) {
|
|
43
63
|
try {
|
|
44
64
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
45
65
|
const scripts = pkg.scripts || {};
|
|
66
|
+
const hasPnpm = existsSync(join(root, "pnpm-lock.yaml")) || existsSync(join(root, "pnpm-workspace.yaml"));
|
|
67
|
+
const hasYarn = existsSync(join(root, "yarn.lock"));
|
|
68
|
+
const hasBun = existsSync(join(root, "bun.lock")) || existsSync(join(root, "bun.lockb")) || existsSync(join(root, "bunfig.toml"));
|
|
69
|
+
const pm = hasPnpm ? "pnpm" : hasYarn ? "yarn" : hasBun ? "bun" : "npm";
|
|
46
70
|
|
|
47
|
-
if (scripts.test) candidates.testCmd = candidates.testCmd || "
|
|
48
|
-
if (scripts.build) candidates.buildCmd = candidates.buildCmd || "
|
|
49
|
-
if (scripts.lint) candidates.lintCmd = "
|
|
71
|
+
if (scripts.test) candidates.testCmd = candidates.testCmd || (pm === "yarn" ? "yarn test" : `${pm} test`);
|
|
72
|
+
if (scripts.build) candidates.buildCmd = candidates.buildCmd || (pm === "yarn" ? "yarn build" : `${pm} run build`);
|
|
73
|
+
if (scripts.lint) candidates.lintCmd = pm === "yarn" ? "yarn lint" : `${pm} run lint`;
|
|
50
74
|
if (scripts.typecheck || scripts["type-check"]) {
|
|
51
|
-
candidates.typecheckCmd = scripts.typecheck
|
|
75
|
+
candidates.typecheckCmd = scripts.typecheck
|
|
76
|
+
? (pm === "yarn" ? "yarn typecheck" : `${pm} run typecheck`)
|
|
77
|
+
: (pm === "yarn" ? "yarn type-check" : `${pm} run type-check`);
|
|
52
78
|
} else if (existsSync(join(root, "tsconfig.json"))) {
|
|
53
79
|
candidates.typecheckCmd = "npx tsc --noEmit";
|
|
54
80
|
}
|
|
@@ -69,7 +95,9 @@ export function detectStackOracles(root = process.cwd()) {
|
|
|
69
95
|
if (stack.includes("go") || existsSync(join(root, "go.mod"))) {
|
|
70
96
|
candidates.testCmd = candidates.testCmd || "go test ./...";
|
|
71
97
|
candidates.buildCmd = candidates.buildCmd || "go build ./...";
|
|
72
|
-
|
|
98
|
+
if (hasBinary("golangci-lint")) {
|
|
99
|
+
candidates.lintCmd = candidates.lintCmd || "golangci-lint run";
|
|
100
|
+
}
|
|
73
101
|
candidates.typecheckCmd = candidates.typecheckCmd || "go vet ./...";
|
|
74
102
|
}
|
|
75
103
|
|
|
@@ -77,8 +105,14 @@ export function detectStackOracles(root = process.cwd()) {
|
|
|
77
105
|
if (stack.includes("pytest") || existsSync(join(root, "pyproject.toml")) || existsSync(join(root, "requirements.txt"))) {
|
|
78
106
|
candidates.testCmd = candidates.testCmd || "pytest";
|
|
79
107
|
candidates.buildCmd = candidates.buildCmd || "";
|
|
80
|
-
|
|
81
|
-
|
|
108
|
+
if (hasBinary("flake8")) {
|
|
109
|
+
candidates.lintCmd = candidates.lintCmd || "flake8 .";
|
|
110
|
+
} else if (hasBinary("ruff")) {
|
|
111
|
+
candidates.lintCmd = candidates.lintCmd || "ruff check .";
|
|
112
|
+
}
|
|
113
|
+
if (hasBinary("mypy")) {
|
|
114
|
+
candidates.typecheckCmd = candidates.typecheckCmd || "mypy .";
|
|
115
|
+
}
|
|
82
116
|
}
|
|
83
117
|
|
|
84
118
|
// Discover subpackages if monorepo
|
package/src/wizard-task.mjs
CHANGED
|
@@ -280,14 +280,15 @@ export async function runTaskCreateWizard(root = process.cwd(), options = {}) {
|
|
|
280
280
|
if (useTodo) {
|
|
281
281
|
const selectedTodo = await select(
|
|
282
282
|
todos.slice(0, 10).map((t) => ({
|
|
283
|
-
label: `[${t.type}] ${t.text} (${t.file}:${t.line})`,
|
|
283
|
+
label: `[${t.type || t.tag || "TODO"}] ${t.text} (${t.file}:${t.line})`,
|
|
284
284
|
value: t,
|
|
285
285
|
})),
|
|
286
286
|
"Select TODO Candidate",
|
|
287
287
|
options
|
|
288
288
|
);
|
|
289
|
-
|
|
290
|
-
|
|
289
|
+
const todoKind = selectedTodo.type || selectedTodo.tag || "TODO";
|
|
290
|
+
title = `Fix ${todoKind}: ${selectedTodo.text.slice(0, 40)}`;
|
|
291
|
+
promptText = `Resolve ${todoKind} in ${selectedTodo.file} at line ${selectedTodo.line}: ${selectedTodo.text}`;
|
|
291
292
|
}
|
|
292
293
|
}
|
|
293
294
|
|