jules-orchestrator-kit 0.29.0 → 0.31.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -89
- package/bin/agentctl.mjs +205 -6
- package/index.mjs +21 -1
- package/package.json +1 -1
- package/src/config.mjs +18 -2
- package/src/dag-engine.mjs +76 -0
- package/src/dashboard.mjs +2 -2
- package/src/engine.mjs +296 -37
- package/src/envelope.mjs +24 -2
- package/src/mcp.mjs +35 -1
- package/src/ops/checkpoint.mjs +167 -0
- package/src/ops/command-registry.mjs +274 -0
- package/src/ops/doctor-planner.mjs +214 -0
- package/src/ops/doctor-registry.mjs +442 -0
- package/src/ops/ide-scaffold.mjs +139 -0
- package/src/ops/receipts.mjs +171 -0
- package/src/ops/swarm-actions.mjs +156 -0
- package/src/ops/task-actions.mjs +214 -0
- package/src/ops/tdd-generator.mjs +115 -0
- package/src/ops/transaction.mjs +278 -0
- package/src/provider.mjs +164 -8
- package/src/security.mjs +15 -7
- package/src/stack-detector.mjs +15 -5
- package/src/state.mjs +44 -7
- package/src/task-optimizer.mjs +306 -0
- package/src/telemetry.mjs +35 -2
- package/src/tui.mjs +4 -1
- package/src/ux/capabilities.mjs +180 -0
- package/src/ux/diff-viewer.mjs +289 -0
- package/src/ux/key-decoder.mjs +281 -0
- package/src/ux/layout.mjs +195 -0
- package/src/ux/log-viewer.mjs +110 -0
- package/src/ux/palette.mjs +179 -0
- package/src/ux/queue-model.mjs +206 -0
- package/src/ux/renderer.mjs +188 -0
- package/src/ux/swarm-model.mjs +146 -0
- package/src/ux/terminal-session.mjs +215 -0
- package/src/ux/widgets.mjs +323 -0
- package/src/webhook.mjs +120 -0
- package/src/wizard-init.mjs +41 -11
- package/src/wizard-task.mjs +51 -15
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { execFileSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @typedef {"pass" | "warn" | "fail" | "skip" | "unknown"} DiagnosticStatus
|
|
8
|
+
* @typedef {"info" | "low" | "medium" | "high" | "critical"} DiagnosticSeverity
|
|
9
|
+
*
|
|
10
|
+
* @typedef {Object} DiagnosticEvidence
|
|
11
|
+
* @property {string} label
|
|
12
|
+
* @property {string | number | boolean} value
|
|
13
|
+
* @property {boolean} sensitive
|
|
14
|
+
*
|
|
15
|
+
* @typedef {Object} FixDescriptor
|
|
16
|
+
* @property {string} id
|
|
17
|
+
* @property {string} title
|
|
18
|
+
* @property {string} summary
|
|
19
|
+
* @property {"low" | "moderate" | "high" | "destructive"} risk
|
|
20
|
+
* @property {boolean} automatic
|
|
21
|
+
* @property {boolean} requiresProbe
|
|
22
|
+
*
|
|
23
|
+
* @typedef {Object} DiagnosticResult
|
|
24
|
+
* @property {"agentctl/diagnostic-result-v1"} schema
|
|
25
|
+
* @property {string} id
|
|
26
|
+
* @property {string} category
|
|
27
|
+
* @property {string} title
|
|
28
|
+
* @property {DiagnosticStatus} status
|
|
29
|
+
* @property {DiagnosticSeverity} severity
|
|
30
|
+
* @property {string} summary
|
|
31
|
+
* @property {number} durationMs
|
|
32
|
+
* @property {boolean} passive
|
|
33
|
+
* @property {DiagnosticEvidence[]} evidence
|
|
34
|
+
* @property {FixDescriptor[]} fixes
|
|
35
|
+
* @property {{ code: string, message: string }} [error]
|
|
36
|
+
*
|
|
37
|
+
* @typedef {Object} DoctorReport
|
|
38
|
+
* @property {"agentctl/doctor-report-v1"} schema
|
|
39
|
+
* @property {string} repository
|
|
40
|
+
* @property {string} [headSha]
|
|
41
|
+
* @property {string} generatedAt
|
|
42
|
+
* @property {boolean} activeProbe
|
|
43
|
+
* @property {Record<DiagnosticStatus, number>} summary
|
|
44
|
+
* @property {DiagnosticResult[]} results
|
|
45
|
+
* @property {string} reportHash
|
|
46
|
+
*/
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Compute SHA-256 report hash for DoctorReport.
|
|
50
|
+
* @param {Omit<DoctorReport, "reportHash">} report
|
|
51
|
+
* @returns {string}
|
|
52
|
+
*/
|
|
53
|
+
export function computeReportHash(report) {
|
|
54
|
+
const payload = JSON.stringify({
|
|
55
|
+
schema: report.schema,
|
|
56
|
+
repository: report.repository,
|
|
57
|
+
generatedAt: report.generatedAt,
|
|
58
|
+
summary: report.summary,
|
|
59
|
+
results: report.results.map((r) => ({ id: r.id, status: r.status, summary: r.summary })),
|
|
60
|
+
});
|
|
61
|
+
return "sha256:" + createHash("sha256").update(payload).digest("hex");
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Run diagnostic check suite across DAG.
|
|
66
|
+
* @param {Object} options
|
|
67
|
+
* @param {string} options.root
|
|
68
|
+
* @param {boolean} [options.activeProbe=false]
|
|
69
|
+
* @param {string[]} [options.selectedChecks]
|
|
70
|
+
* @returns {Promise<DoctorReport>}
|
|
71
|
+
*/
|
|
72
|
+
export async function runDoctorChecks(options = {}) {
|
|
73
|
+
const root = resolve(options.root || process.cwd());
|
|
74
|
+
const activeProbe = Boolean(options.activeProbe);
|
|
75
|
+
const _startTime = Date.now();
|
|
76
|
+
|
|
77
|
+
/** @type {DiagnosticResult[]} */
|
|
78
|
+
const results = [];
|
|
79
|
+
|
|
80
|
+
// Helper to append result
|
|
81
|
+
const addResult = (res) => {
|
|
82
|
+
results.push({
|
|
83
|
+
schema: "agentctl/diagnostic-result-v1",
|
|
84
|
+
durationMs: 0,
|
|
85
|
+
passive: !activeProbe,
|
|
86
|
+
evidence: [],
|
|
87
|
+
fixes: [],
|
|
88
|
+
...res,
|
|
89
|
+
});
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
// 1. System Checks
|
|
93
|
+
// runtime.node
|
|
94
|
+
const nodeVersion = process.version;
|
|
95
|
+
const nodeMajor = parseInt(nodeVersion.slice(1).split(".")[0], 10);
|
|
96
|
+
if (nodeMajor >= 20) {
|
|
97
|
+
addResult({
|
|
98
|
+
id: "runtime.node",
|
|
99
|
+
category: "System",
|
|
100
|
+
title: "Node.js Engine Version",
|
|
101
|
+
status: "pass",
|
|
102
|
+
severity: "info",
|
|
103
|
+
summary: `Node.js ${nodeVersion} satisfies runtime requirements (v20+)`,
|
|
104
|
+
evidence: [{ label: "nodeVersion", value: nodeVersion, sensitive: false }],
|
|
105
|
+
});
|
|
106
|
+
} else {
|
|
107
|
+
addResult({
|
|
108
|
+
id: "runtime.node",
|
|
109
|
+
category: "System",
|
|
110
|
+
title: "Node.js Engine Version",
|
|
111
|
+
status: "fail",
|
|
112
|
+
severity: "critical",
|
|
113
|
+
summary: `Node.js ${nodeVersion} is below required minimum v20.0.0`,
|
|
114
|
+
evidence: [{ label: "nodeVersion", value: nodeVersion, sensitive: false }],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// runtime.git
|
|
119
|
+
let gitPassed = false;
|
|
120
|
+
try {
|
|
121
|
+
const gitOut = execFileSync("git", ["--version"], { encoding: "utf-8", timeout: 5000 });
|
|
122
|
+
gitPassed = true;
|
|
123
|
+
addResult({
|
|
124
|
+
id: "runtime.git",
|
|
125
|
+
category: "System",
|
|
126
|
+
title: "Git Command Available",
|
|
127
|
+
status: "pass",
|
|
128
|
+
severity: "info",
|
|
129
|
+
summary: gitOut.trim(),
|
|
130
|
+
evidence: [{ label: "gitVersion", value: gitOut.trim(), sensitive: false }],
|
|
131
|
+
});
|
|
132
|
+
} catch (err) {
|
|
133
|
+
addResult({
|
|
134
|
+
id: "runtime.git",
|
|
135
|
+
category: "System",
|
|
136
|
+
title: "Git Command Available",
|
|
137
|
+
status: "fail",
|
|
138
|
+
severity: "critical",
|
|
139
|
+
summary: "Git executable not found or failed to run",
|
|
140
|
+
error: { code: "ENOENT", message: err.message },
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 2. Repository Checks
|
|
145
|
+
|
|
146
|
+
if (!gitPassed) {
|
|
147
|
+
addResult({
|
|
148
|
+
id: "repo.root",
|
|
149
|
+
category: "Repository",
|
|
150
|
+
title: "Repository Root Verification",
|
|
151
|
+
status: "skip",
|
|
152
|
+
severity: "info",
|
|
153
|
+
summary: "Skipped due to failed runtime.git check",
|
|
154
|
+
});
|
|
155
|
+
addResult({
|
|
156
|
+
id: "repo.dirty",
|
|
157
|
+
category: "Repository",
|
|
158
|
+
title: "Working Directory Cleanliness",
|
|
159
|
+
status: "skip",
|
|
160
|
+
severity: "info",
|
|
161
|
+
summary: "Skipped due to failed runtime.git check",
|
|
162
|
+
});
|
|
163
|
+
} else {
|
|
164
|
+
let isRepo = false;
|
|
165
|
+
let headSha = "";
|
|
166
|
+
try {
|
|
167
|
+
headSha = execFileSync("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf-8" }).trim();
|
|
168
|
+
isRepo = true;
|
|
169
|
+
addResult({
|
|
170
|
+
id: "repo.root",
|
|
171
|
+
category: "Repository",
|
|
172
|
+
title: "Repository Root Verification",
|
|
173
|
+
status: "pass",
|
|
174
|
+
severity: "info",
|
|
175
|
+
summary: `Valid repository at ${root} (HEAD: ${headSha.slice(0, 8)})`,
|
|
176
|
+
evidence: [{ label: "headSha", value: headSha, sensitive: false }],
|
|
177
|
+
});
|
|
178
|
+
} catch {
|
|
179
|
+
addResult({
|
|
180
|
+
id: "repo.root",
|
|
181
|
+
category: "Repository",
|
|
182
|
+
title: "Repository Root Verification",
|
|
183
|
+
status: "fail",
|
|
184
|
+
severity: "high",
|
|
185
|
+
summary: `Directory ${root} is not a valid Git repository`,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (isRepo) {
|
|
190
|
+
try {
|
|
191
|
+
const statusOut = execFileSync("git", ["status", "--porcelain"], { cwd: root, encoding: "utf-8" }).trim();
|
|
192
|
+
const modifiedCount = statusOut ? statusOut.split("\n").length : 0;
|
|
193
|
+
if (modifiedCount === 0) {
|
|
194
|
+
addResult({
|
|
195
|
+
id: "repo.dirty",
|
|
196
|
+
category: "Repository",
|
|
197
|
+
title: "Working Directory Cleanliness",
|
|
198
|
+
status: "pass",
|
|
199
|
+
severity: "info",
|
|
200
|
+
summary: "Working tree is clean",
|
|
201
|
+
evidence: [{ label: "uncommittedFiles", value: 0, sensitive: false }],
|
|
202
|
+
});
|
|
203
|
+
} else {
|
|
204
|
+
addResult({
|
|
205
|
+
id: "repo.dirty",
|
|
206
|
+
category: "Repository",
|
|
207
|
+
title: "Working Directory Cleanliness",
|
|
208
|
+
status: "warn",
|
|
209
|
+
severity: "low",
|
|
210
|
+
summary: `Working tree has ${modifiedCount} uncommitted changes`,
|
|
211
|
+
evidence: [{ label: "uncommittedFiles", value: modifiedCount, sensitive: false }],
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
} catch {
|
|
215
|
+
addResult({
|
|
216
|
+
id: "repo.dirty",
|
|
217
|
+
category: "Repository",
|
|
218
|
+
title: "Working Directory Cleanliness",
|
|
219
|
+
status: "warn",
|
|
220
|
+
severity: "low",
|
|
221
|
+
summary: "Could not query git status",
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// 3. Config Checks
|
|
228
|
+
const configCandidates = [
|
|
229
|
+
[".agent/config.yml", join(root, ".agent", "config.yml")],
|
|
230
|
+
[".agent/jules.yml", join(root, ".agent", "jules.yml")],
|
|
231
|
+
];
|
|
232
|
+
const existingConfig = configCandidates.find(([, path]) => existsSync(path));
|
|
233
|
+
if (existingConfig) {
|
|
234
|
+
addResult({
|
|
235
|
+
id: "config.present",
|
|
236
|
+
category: "Config",
|
|
237
|
+
title: "Agent Configuration File",
|
|
238
|
+
status: "pass",
|
|
239
|
+
severity: "info",
|
|
240
|
+
summary: `${existingConfig[0]} exists`,
|
|
241
|
+
evidence: [{ label: "configPath", value: existingConfig[0], sensitive: false }],
|
|
242
|
+
});
|
|
243
|
+
} else {
|
|
244
|
+
addResult({
|
|
245
|
+
id: "config.present",
|
|
246
|
+
category: "Config",
|
|
247
|
+
title: "Agent Configuration File",
|
|
248
|
+
status: "warn",
|
|
249
|
+
severity: "medium",
|
|
250
|
+
summary: "Neither .agent/config.yml nor .agent/jules.yml exists",
|
|
251
|
+
fixes: [
|
|
252
|
+
{
|
|
253
|
+
id: "config.create-default",
|
|
254
|
+
title: "Create default config",
|
|
255
|
+
summary: "Initialize standard .agent/config.yml manifest",
|
|
256
|
+
risk: "low",
|
|
257
|
+
automatic: true,
|
|
258
|
+
requiresProbe: false,
|
|
259
|
+
},
|
|
260
|
+
],
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
// 4. Verification Oracle Checks
|
|
265
|
+
const pkgPath = join(root, "package.json");
|
|
266
|
+
if (existsSync(pkgPath)) {
|
|
267
|
+
try {
|
|
268
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
269
|
+
const testScript = pkg.scripts && pkg.scripts.test;
|
|
270
|
+
if (testScript) {
|
|
271
|
+
addResult({
|
|
272
|
+
id: "oracle.test",
|
|
273
|
+
category: "Verification",
|
|
274
|
+
title: "Test Oracle Configuration",
|
|
275
|
+
status: "pass",
|
|
276
|
+
severity: "info",
|
|
277
|
+
summary: `Test oracle script found: "npm test" -> "${testScript}"`,
|
|
278
|
+
evidence: [{ label: "testScript", value: testScript, sensitive: false }],
|
|
279
|
+
});
|
|
280
|
+
} else {
|
|
281
|
+
addResult({
|
|
282
|
+
id: "oracle.test",
|
|
283
|
+
category: "Verification",
|
|
284
|
+
title: "Test Oracle Configuration",
|
|
285
|
+
status: "warn",
|
|
286
|
+
severity: "medium",
|
|
287
|
+
summary: "package.json missing test script entry",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
} catch {
|
|
291
|
+
addResult({
|
|
292
|
+
id: "oracle.test",
|
|
293
|
+
category: "Verification",
|
|
294
|
+
title: "Test Oracle Configuration",
|
|
295
|
+
status: "warn",
|
|
296
|
+
severity: "medium",
|
|
297
|
+
summary: "Malformed package.json file",
|
|
298
|
+
});
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
addResult({
|
|
302
|
+
id: "oracle.test",
|
|
303
|
+
category: "Verification",
|
|
304
|
+
title: "Test Oracle Configuration",
|
|
305
|
+
status: "pass",
|
|
306
|
+
severity: "info",
|
|
307
|
+
summary: "Non-Node workspace verified",
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// 5. State & Telemetry Checks
|
|
312
|
+
const telemetryDate = new Date().toISOString().split("T")[0];
|
|
313
|
+
const telemetryHeadPath = join(root, ".agent", "state", `telemetry-${telemetryDate}.head`);
|
|
314
|
+
if (existsSync(telemetryHeadPath)) {
|
|
315
|
+
addResult({
|
|
316
|
+
id: "state.telemetry",
|
|
317
|
+
category: "State",
|
|
318
|
+
title: "Telemetry Spine Head Integrity",
|
|
319
|
+
status: "pass",
|
|
320
|
+
severity: "info",
|
|
321
|
+
summary: "Telemetry .head file present"
|
|
322
|
+
});
|
|
323
|
+
} else {
|
|
324
|
+
addResult({
|
|
325
|
+
id: "state.telemetry",
|
|
326
|
+
category: "State",
|
|
327
|
+
title: "Telemetry Spine Head Integrity",
|
|
328
|
+
status: "warn",
|
|
329
|
+
severity: "low",
|
|
330
|
+
summary: "Telemetry .head file missing; can be rebuilt from tail log",
|
|
331
|
+
fixes: [
|
|
332
|
+
{
|
|
333
|
+
id: "state.rebuild-head",
|
|
334
|
+
title: "Rebuild telemetry head",
|
|
335
|
+
summary: "Recompute .head SHA-256 pointer from tail log",
|
|
336
|
+
risk: "low",
|
|
337
|
+
automatic: true,
|
|
338
|
+
requiresProbe: false,
|
|
339
|
+
},
|
|
340
|
+
],
|
|
341
|
+
});
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// 6. VFS Locks Check
|
|
345
|
+
const locksDir = join(root, ".agent", "state", "locks");
|
|
346
|
+
let activeLockCount = 0;
|
|
347
|
+
let lockInspectionFailed = false;
|
|
348
|
+
try {
|
|
349
|
+
if (existsSync(locksDir)) {
|
|
350
|
+
const lockFiles = readdirSync(locksDir).filter((f) => f.endsWith(".json"));
|
|
351
|
+
activeLockCount = lockFiles.length;
|
|
352
|
+
}
|
|
353
|
+
} catch (_) {
|
|
354
|
+
lockInspectionFailed = true;
|
|
355
|
+
addResult({
|
|
356
|
+
id: "locks.active",
|
|
357
|
+
category: "State",
|
|
358
|
+
title: "VFS Active Locks",
|
|
359
|
+
status: "unknown",
|
|
360
|
+
severity: "medium",
|
|
361
|
+
summary: "Could not inspect VFS lock directory",
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
if (!lockInspectionFailed && activeLockCount === 0) {
|
|
365
|
+
addResult({
|
|
366
|
+
id: "locks.active",
|
|
367
|
+
category: "State",
|
|
368
|
+
title: "VFS Active Locks",
|
|
369
|
+
status: "pass",
|
|
370
|
+
severity: "info",
|
|
371
|
+
summary: "No active VFS locks present",
|
|
372
|
+
});
|
|
373
|
+
} else if (!lockInspectionFailed) {
|
|
374
|
+
addResult({
|
|
375
|
+
id: "locks.active",
|
|
376
|
+
category: "State",
|
|
377
|
+
title: "VFS Active Locks",
|
|
378
|
+
status: "warn",
|
|
379
|
+
severity: "medium",
|
|
380
|
+
summary: `${activeLockCount} active VFS lock(s) held`,
|
|
381
|
+
evidence: [{ label: "lockCount", value: activeLockCount, sensitive: false }],
|
|
382
|
+
fixes: [
|
|
383
|
+
{
|
|
384
|
+
id: "locks.prune-stale",
|
|
385
|
+
title: "Prune stale locks",
|
|
386
|
+
summary: "Remove locks owned by dead processes",
|
|
387
|
+
risk: "low",
|
|
388
|
+
automatic: true,
|
|
389
|
+
requiresProbe: false,
|
|
390
|
+
},
|
|
391
|
+
],
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
// 7. Jules Provider Key Check
|
|
396
|
+
const hasApiKey = Boolean(process.env.JULES_API_KEY || process.env.GEMINI_API_KEY);
|
|
397
|
+
if (hasApiKey) {
|
|
398
|
+
addResult({
|
|
399
|
+
id: "provider.key",
|
|
400
|
+
category: "Jules",
|
|
401
|
+
title: "Jules Provider API Key",
|
|
402
|
+
status: "pass",
|
|
403
|
+
severity: "info",
|
|
404
|
+
summary: "API key environment variable detected",
|
|
405
|
+
evidence: [{ label: "keyConfigured", value: true, sensitive: false }],
|
|
406
|
+
});
|
|
407
|
+
} else {
|
|
408
|
+
addResult({
|
|
409
|
+
id: "provider.key",
|
|
410
|
+
category: "Jules",
|
|
411
|
+
title: "Jules Provider API Key",
|
|
412
|
+
status: "warn",
|
|
413
|
+
severity: "high",
|
|
414
|
+
summary: "Neither JULES_API_KEY nor GEMINI_API_KEY environment variable is set",
|
|
415
|
+
evidence: [{ label: "keyConfigured", value: false, sensitive: false }],
|
|
416
|
+
});
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Summarize count by status
|
|
420
|
+
const summary = { pass: 0, warn: 0, fail: 0, skip: 0, unknown: 0 };
|
|
421
|
+
for (const r of results) {
|
|
422
|
+
summary[r.status] = (summary[r.status] || 0) + 1;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const baseReport = {
|
|
426
|
+
schema: /** @type {const} */ ("agentctl/doctor-report-v1"),
|
|
427
|
+
repository: root,
|
|
428
|
+
generatedAt: new Date().toISOString(),
|
|
429
|
+
activeProbe,
|
|
430
|
+
summary,
|
|
431
|
+
results,
|
|
432
|
+
};
|
|
433
|
+
|
|
434
|
+
const reportHash = computeReportHash(baseReport);
|
|
435
|
+
/** @type {DoctorReport} */
|
|
436
|
+
const report = {
|
|
437
|
+
...baseReport,
|
|
438
|
+
reportHash,
|
|
439
|
+
};
|
|
440
|
+
|
|
441
|
+
return report;
|
|
442
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { writeFileSync, mkdirSync, existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { resolveRoot } from "../config.mjs";
|
|
4
|
+
|
|
5
|
+
export class IdeScaffoldError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = "IdeScaffoldError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Scaffolds IDE integration configuration files for Cursor, VS Code, and Claude Desktop.
|
|
14
|
+
* @param {string} target - 'cursor' | 'vscode' | 'claude' | 'all'
|
|
15
|
+
* @param {object} [options]
|
|
16
|
+
* @returns {object} Summary of scaffolded files
|
|
17
|
+
*/
|
|
18
|
+
export function scaffoldIdeConfig(target = "all", options = {}) {
|
|
19
|
+
const root = options.root || resolveRoot();
|
|
20
|
+
const validTargets = new Set(["cursor", "vscode", "claude", "all"]);
|
|
21
|
+
|
|
22
|
+
const normTarget = (target || "all").toLowerCase();
|
|
23
|
+
if (!validTargets.has(normTarget)) {
|
|
24
|
+
throw new IdeScaffoldError(`Invalid target '${target}'. Allowed targets: cursor, vscode, claude, all`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const results = [];
|
|
28
|
+
|
|
29
|
+
// Target: Cursor (.cursor/mcp.json)
|
|
30
|
+
if (normTarget === "cursor" || normTarget === "all") {
|
|
31
|
+
const cursorDir = join(root, ".cursor");
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(cursorDir, { recursive: true });
|
|
34
|
+
} catch (_) {}
|
|
35
|
+
|
|
36
|
+
const cursorConfigPath = join(cursorDir, "mcp.json");
|
|
37
|
+
let existingConfig = {};
|
|
38
|
+
if (existsSync(cursorConfigPath)) {
|
|
39
|
+
try {
|
|
40
|
+
existingConfig = JSON.parse(readFileSync(cursorConfigPath, "utf-8"));
|
|
41
|
+
} catch (_) {}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const updatedCursorConfig = {
|
|
45
|
+
...existingConfig,
|
|
46
|
+
mcpServers: {
|
|
47
|
+
...(existingConfig.mcpServers || {}),
|
|
48
|
+
"jules-orchestrator-kit": {
|
|
49
|
+
command: "npx",
|
|
50
|
+
args: ["-y", "jules-orchestrator-kit@latest", "mcp"],
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
writeFileSync(cursorConfigPath, JSON.stringify(updatedCursorConfig, null, 2), "utf-8");
|
|
56
|
+
results.push({ target: "cursor", file: ".cursor/mcp.json", ok: true });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Target: VS Code (.vscode/tasks.json)
|
|
60
|
+
if (normTarget === "vscode" || normTarget === "all") {
|
|
61
|
+
const vscodeDir = join(root, ".vscode");
|
|
62
|
+
try {
|
|
63
|
+
mkdirSync(vscodeDir, { recursive: true });
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
|
|
66
|
+
const vscodeTasksPath = join(vscodeDir, "tasks.json");
|
|
67
|
+
let existingConfig = { version: "2.0.0", tasks: [] };
|
|
68
|
+
if (existsSync(vscodeTasksPath)) {
|
|
69
|
+
try {
|
|
70
|
+
existingConfig = JSON.parse(readFileSync(vscodeTasksPath, "utf-8"));
|
|
71
|
+
} catch (_) {}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const defaultTasks = [
|
|
75
|
+
{
|
|
76
|
+
label: "Jules: Run System Doctor",
|
|
77
|
+
type: "shell",
|
|
78
|
+
command: "npx agentctl doctor",
|
|
79
|
+
group: "test",
|
|
80
|
+
problemMatcher: [],
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
label: "Jules: Create Task Envelope",
|
|
84
|
+
type: "shell",
|
|
85
|
+
command: "npx agentctl task create",
|
|
86
|
+
group: "build",
|
|
87
|
+
problemMatcher: [],
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
label: "Jules: 1-Click Rollback Checkpoint",
|
|
91
|
+
type: "shell",
|
|
92
|
+
command: "npx agentctl rollback --latest",
|
|
93
|
+
group: "none",
|
|
94
|
+
problemMatcher: [],
|
|
95
|
+
},
|
|
96
|
+
];
|
|
97
|
+
|
|
98
|
+
const currentTasks = existingConfig.tasks || [];
|
|
99
|
+
const updatedTasks = [...currentTasks];
|
|
100
|
+
|
|
101
|
+
for (const newTask of defaultTasks) {
|
|
102
|
+
if (!updatedTasks.some((t) => t.label === newTask.label)) {
|
|
103
|
+
updatedTasks.push(newTask);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const updatedVsCodeConfig = {
|
|
108
|
+
...existingConfig,
|
|
109
|
+
version: existingConfig.version || "2.0.0",
|
|
110
|
+
tasks: updatedTasks,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
writeFileSync(vscodeTasksPath, JSON.stringify(updatedVsCodeConfig, null, 2), "utf-8");
|
|
114
|
+
results.push({ target: "vscode", file: ".vscode/tasks.json", ok: true });
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Target: Claude Desktop (claude_desktop_config.json snippet helper)
|
|
118
|
+
if (normTarget === "claude" || normTarget === "all") {
|
|
119
|
+
const agentDir = join(root, ".agent");
|
|
120
|
+
try {
|
|
121
|
+
mkdirSync(agentDir, { recursive: true });
|
|
122
|
+
} catch (_) {}
|
|
123
|
+
|
|
124
|
+
const snippetPath = join(agentDir, "claude_desktop_config.snippet.json");
|
|
125
|
+
const claudeSnippet = {
|
|
126
|
+
mcpServers: {
|
|
127
|
+
"jules-orchestrator-kit": {
|
|
128
|
+
command: "npx",
|
|
129
|
+
args: ["-y", "jules-orchestrator-kit@latest", "mcp"],
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
writeFileSync(snippetPath, JSON.stringify(claudeSnippet, null, 2), "utf-8");
|
|
135
|
+
results.push({ target: "claude", file: ".agent/claude_desktop_config.snippet.json", ok: true });
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
return { ok: true, target: normTarget, results };
|
|
139
|
+
}
|