rush-ai 0.2.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/LICENSE +21 -0
- package/README.md +136 -0
- package/dist/chunk-7H2T3KCJ.js +870 -0
- package/dist/chunk-7H2T3KCJ.js.map +1 -0
- package/dist/chunk-ISJJIX7U.js +148 -0
- package/dist/chunk-ISJJIX7U.js.map +1 -0
- package/dist/client-2GSYT7IS.js +10 -0
- package/dist/client-2GSYT7IS.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +3496 -0
- package/dist/index.js.map +1 -0
- package/dist/plugin-assets/SKILL.md +89 -0
- package/dist/plugin-assets/commands/commands.test.ts +140 -0
- package/dist/plugin-assets/commands/rush-agent.md +18 -0
- package/dist/plugin-assets/commands/rush-files.md +19 -0
- package/dist/plugin-assets/commands/rush-task.md +15 -0
- package/dist/plugin-assets/rules/rush-agent.md +24 -0
- package/dist/server-PVZOTJZA.js +499 -0
- package/dist/server-PVZOTJZA.js.map +1 -0
- package/package.json +52 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,3496 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
consumeSSEStream,
|
|
4
|
+
consumeSSEStreamWithReconnect
|
|
5
|
+
} from "./chunk-ISJJIX7U.js";
|
|
6
|
+
import {
|
|
7
|
+
ApiError,
|
|
8
|
+
AuthError,
|
|
9
|
+
RushError,
|
|
10
|
+
TaskFailedError,
|
|
11
|
+
VERSION,
|
|
12
|
+
clearAuthConfig,
|
|
13
|
+
createClient,
|
|
14
|
+
createProfile,
|
|
15
|
+
deleteProfile,
|
|
16
|
+
getActiveProfile,
|
|
17
|
+
getAuthConfig,
|
|
18
|
+
getAuthMethod,
|
|
19
|
+
getAuthToken,
|
|
20
|
+
getConfigDir,
|
|
21
|
+
getGlobalConfig,
|
|
22
|
+
getProfileAuth,
|
|
23
|
+
getProfileConfig,
|
|
24
|
+
isLoggedIn,
|
|
25
|
+
isRushError,
|
|
26
|
+
listProfiles,
|
|
27
|
+
output,
|
|
28
|
+
revokeCurrentSession,
|
|
29
|
+
setActiveProfile,
|
|
30
|
+
setAuthConfig,
|
|
31
|
+
setGlobalConfig,
|
|
32
|
+
setVerbosity,
|
|
33
|
+
verifyCurrentAuthSession
|
|
34
|
+
} from "./chunk-7H2T3KCJ.js";
|
|
35
|
+
|
|
36
|
+
// src/index.ts
|
|
37
|
+
import chalk6 from "chalk";
|
|
38
|
+
import { Command } from "commander";
|
|
39
|
+
|
|
40
|
+
// src/output/formatters/csv.ts
|
|
41
|
+
function escapeField(value) {
|
|
42
|
+
if (value.includes(",") || value.includes('"') || value.includes("\n") || value.includes("\r") || value !== value.trim()) {
|
|
43
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
44
|
+
}
|
|
45
|
+
return value;
|
|
46
|
+
}
|
|
47
|
+
function formatCsv(rows) {
|
|
48
|
+
if (rows.length === 0) return "";
|
|
49
|
+
const keys = Object.keys(rows[0]);
|
|
50
|
+
const header = keys.map(escapeField).join(",");
|
|
51
|
+
const lines = rows.map(
|
|
52
|
+
(row) => keys.map((k) => escapeField(row[k] ?? "")).join(",")
|
|
53
|
+
);
|
|
54
|
+
return [header, ...lines].join("\n");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// src/output/formatters/table.ts
|
|
58
|
+
import chalk from "chalk";
|
|
59
|
+
var STATUS_COLORS = {
|
|
60
|
+
completed: chalk.green,
|
|
61
|
+
active: chalk.green,
|
|
62
|
+
installed: chalk.green,
|
|
63
|
+
running: chalk.yellow,
|
|
64
|
+
pending: chalk.yellow,
|
|
65
|
+
available: chalk.dim,
|
|
66
|
+
failed: chalk.red,
|
|
67
|
+
cancelled: chalk.red,
|
|
68
|
+
error: chalk.red
|
|
69
|
+
};
|
|
70
|
+
function colorizeStatus(value) {
|
|
71
|
+
const colorFn = STATUS_COLORS[value.toLowerCase()];
|
|
72
|
+
return colorFn ? colorFn(value) : value;
|
|
73
|
+
}
|
|
74
|
+
function truncate(str, max) {
|
|
75
|
+
if (str.length <= max) return str;
|
|
76
|
+
return `${str.slice(0, max - 3)}...`;
|
|
77
|
+
}
|
|
78
|
+
function formatTableEnhanced(rows, options) {
|
|
79
|
+
if (rows.length === 0) return "";
|
|
80
|
+
const keys = Object.keys(rows[0]);
|
|
81
|
+
const colOpts = options?.columns ?? {};
|
|
82
|
+
const widths = {};
|
|
83
|
+
for (const key of keys) {
|
|
84
|
+
widths[key] = key.length;
|
|
85
|
+
}
|
|
86
|
+
for (const row of rows) {
|
|
87
|
+
for (const key of keys) {
|
|
88
|
+
const val = row[key] ?? "";
|
|
89
|
+
const maxW = colOpts[key]?.maxWidth;
|
|
90
|
+
const display = maxW ? truncate(val, maxW) : val;
|
|
91
|
+
if (display.length > widths[key]) {
|
|
92
|
+
widths[key] = display.length;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const header = keys.map((k) => chalk.bold(k.padEnd(widths[k]))).join(" ");
|
|
97
|
+
const separator = keys.map((k) => "-".repeat(widths[k])).join(" ");
|
|
98
|
+
const lines = rows.map(
|
|
99
|
+
(row) => keys.map((k) => {
|
|
100
|
+
const raw = row[k] ?? "-";
|
|
101
|
+
const display = raw === "" ? "-" : raw;
|
|
102
|
+
const maxW = colOpts[k]?.maxWidth;
|
|
103
|
+
const truncated = maxW ? truncate(display, maxW) : display;
|
|
104
|
+
const align = colOpts[k]?.align ?? "left";
|
|
105
|
+
const padded = align === "right" ? truncated.padStart(widths[k]) : truncated.padEnd(widths[k]);
|
|
106
|
+
if (k.toLowerCase() === "status" || k.toLowerCase() === "state") {
|
|
107
|
+
return colorizeStatus(padded);
|
|
108
|
+
}
|
|
109
|
+
return padded;
|
|
110
|
+
}).join(" ")
|
|
111
|
+
);
|
|
112
|
+
return [header, separator, ...lines].join("\n");
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// src/output/formatters/index.ts
|
|
116
|
+
var VALID_FORMATS = ["table", "json", "csv"];
|
|
117
|
+
function resolveFormat(opts) {
|
|
118
|
+
const hasJson = opts.json === true;
|
|
119
|
+
const hasFormat = opts.format != null && opts.format !== void 0;
|
|
120
|
+
if (hasFormat && !VALID_FORMATS.includes(opts.format)) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`Invalid format: "${opts.format}". Supported formats: ${VALID_FORMATS.join(", ")}`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
if (hasJson && hasFormat && opts.format !== "json") {
|
|
126
|
+
throw new Error(
|
|
127
|
+
`Conflicting options: --json cannot be used with --format ${opts.format}. Use one or the other.`
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
if (hasJson) return "json";
|
|
131
|
+
if (hasFormat) return opts.format;
|
|
132
|
+
return "table";
|
|
133
|
+
}
|
|
134
|
+
function formatOutput(rows, format, options) {
|
|
135
|
+
switch (format) {
|
|
136
|
+
case "csv":
|
|
137
|
+
return formatCsv(rows);
|
|
138
|
+
case "json":
|
|
139
|
+
return JSON.stringify(rows, null, 2);
|
|
140
|
+
default:
|
|
141
|
+
return formatTableEnhanced(rows, options);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/util/require-auth.ts
|
|
146
|
+
function requireAuth() {
|
|
147
|
+
const token = getAuthToken();
|
|
148
|
+
if (!token) {
|
|
149
|
+
throw new AuthError();
|
|
150
|
+
}
|
|
151
|
+
if (!process.env.RUSH_API_KEY) {
|
|
152
|
+
const auth = getAuthConfig();
|
|
153
|
+
if (auth.expiresAt && Date.now() > auth.expiresAt) {
|
|
154
|
+
if (auth.method === "cas" && auth.refreshToken) {
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
throw new AuthError(
|
|
158
|
+
"Token expired. Run `rush-ai auth login` to re-authenticate."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// src/commands/agent/index.ts
|
|
165
|
+
function registerAgentCommand(program) {
|
|
166
|
+
const agent = program.command("agent").description("Manage and inspect agents");
|
|
167
|
+
agent.command("list").alias("ls").description("List available agents").action(async () => {
|
|
168
|
+
requireAuth();
|
|
169
|
+
const format = resolveFormat(program.opts());
|
|
170
|
+
const client = createClient();
|
|
171
|
+
const { data } = await client.get("/api/agents");
|
|
172
|
+
if (format === "json") {
|
|
173
|
+
output.log(JSON.stringify(data, null, 2));
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (data.agents.length === 0) {
|
|
177
|
+
output.info("No agents found.");
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
const total = data.pagination?.total ?? data.agents.length;
|
|
181
|
+
output.log(output.bold(`Agents (${total} total):`));
|
|
182
|
+
output.newline();
|
|
183
|
+
const rows = data.agents.map((a) => ({
|
|
184
|
+
Name: a.name,
|
|
185
|
+
Description: truncate2(a.description ?? "", 50),
|
|
186
|
+
Status: a.status,
|
|
187
|
+
Skills: String(a.skills?.length ?? 0),
|
|
188
|
+
MCP: String(a.mcp_servers?.length ?? 0)
|
|
189
|
+
}));
|
|
190
|
+
output.log(
|
|
191
|
+
formatOutput(rows, format, {
|
|
192
|
+
columns: { Description: { maxWidth: 50 } }
|
|
193
|
+
})
|
|
194
|
+
);
|
|
195
|
+
});
|
|
196
|
+
agent.command("info").description("Show agent details").argument("<name>", "Agent name or ID").action(async (nameOrId) => {
|
|
197
|
+
requireAuth();
|
|
198
|
+
const format = resolveFormat(program.opts());
|
|
199
|
+
const client = createClient();
|
|
200
|
+
let data;
|
|
201
|
+
try {
|
|
202
|
+
({ data } = await client.get(
|
|
203
|
+
`/api/agents/${encodeURIComponent(nameOrId)}`
|
|
204
|
+
));
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err instanceof ApiError && err.status === 409) {
|
|
207
|
+
let parsed;
|
|
208
|
+
try {
|
|
209
|
+
parsed = JSON.parse(err.meta.body);
|
|
210
|
+
} catch {
|
|
211
|
+
}
|
|
212
|
+
const candidates = parsed?.candidates;
|
|
213
|
+
const isValid = Array.isArray(candidates) && candidates.length > 0 && candidates.every(
|
|
214
|
+
(c) => typeof c.id === "string" && typeof c.name === "string"
|
|
215
|
+
);
|
|
216
|
+
if (isValid) {
|
|
217
|
+
if (format === "json") {
|
|
218
|
+
output.log(JSON.stringify(parsed, null, 2));
|
|
219
|
+
process.exitCode = 1;
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
output.error(`Multiple agents found with name "${nameOrId}":`);
|
|
223
|
+
output.newline();
|
|
224
|
+
for (const c of candidates) {
|
|
225
|
+
const date = c.created_at?.split("T")[0] ?? "";
|
|
226
|
+
output.dim(` ${c.id} ${c.name} ${c.status ?? ""} ${date}`);
|
|
227
|
+
}
|
|
228
|
+
output.newline();
|
|
229
|
+
output.info(`Use agent ID to specify: rush agent info <id>`);
|
|
230
|
+
process.exitCode = 1;
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
throw err;
|
|
235
|
+
}
|
|
236
|
+
const a = data.agent;
|
|
237
|
+
if (format === "json") {
|
|
238
|
+
output.log(JSON.stringify(a, null, 2));
|
|
239
|
+
return;
|
|
240
|
+
}
|
|
241
|
+
output.log(output.bold(a.name));
|
|
242
|
+
output.log(` ID: ${a.id}`);
|
|
243
|
+
output.log(` Description: ${a.description ?? "(none)"}`);
|
|
244
|
+
output.log(` Status: ${a.status}`);
|
|
245
|
+
output.log(` Visibility: ${a.visibility}`);
|
|
246
|
+
output.log(
|
|
247
|
+
` Provider: ${a.provider_type}${a.provider_model ? ` (${a.provider_model})` : ""}`
|
|
248
|
+
);
|
|
249
|
+
output.log(` Capabilities: ${a.capabilities?.join(", ") || "none"}`);
|
|
250
|
+
output.newline();
|
|
251
|
+
if (a.skills && a.skills.length > 0) {
|
|
252
|
+
output.log(output.bold(" Skills:"));
|
|
253
|
+
for (const skill of a.skills) {
|
|
254
|
+
output.log(` - ${skill}`);
|
|
255
|
+
}
|
|
256
|
+
output.newline();
|
|
257
|
+
}
|
|
258
|
+
if (a.mcp_servers && a.mcp_servers.length > 0) {
|
|
259
|
+
output.log(output.bold(` MCP Servers (${a.mcp_servers.length}):`));
|
|
260
|
+
for (const mcp of a.mcp_servers) {
|
|
261
|
+
if (typeof mcp === "object" && mcp !== null) {
|
|
262
|
+
const m = mcp;
|
|
263
|
+
const name = String(m.serverName ?? m.name ?? "unknown");
|
|
264
|
+
const disabled = m.disabled ? " (disabled)" : "";
|
|
265
|
+
output.log(` - ${name}${disabled}`);
|
|
266
|
+
} else {
|
|
267
|
+
output.log(` - ${JSON.stringify(mcp)}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
output.newline();
|
|
271
|
+
}
|
|
272
|
+
output.dim(` Updated: ${new Date(a.updated_at).toLocaleString()}`);
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
function truncate2(str, max) {
|
|
276
|
+
if (str.length <= max) return str;
|
|
277
|
+
return `${str.slice(0, max - 3)}...`;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/commands/auth/browser-login.ts
|
|
281
|
+
import { randomBytes } from "crypto";
|
|
282
|
+
import { createServer } from "http";
|
|
283
|
+
import open from "open";
|
|
284
|
+
function getApiBaseUrl() {
|
|
285
|
+
return process.env.RUSH_API_URL ?? getGlobalConfig().api;
|
|
286
|
+
}
|
|
287
|
+
async function loginViaBrowser(jsonMode) {
|
|
288
|
+
const baseUrl = getApiBaseUrl();
|
|
289
|
+
const state = randomBytes(16).toString("hex");
|
|
290
|
+
return new Promise((resolve7, reject) => {
|
|
291
|
+
const server = createServer(async (req, res) => {
|
|
292
|
+
try {
|
|
293
|
+
const url = new URL(req.url ?? "/", "http://localhost");
|
|
294
|
+
if (url.pathname !== "/callback") {
|
|
295
|
+
res.writeHead(404);
|
|
296
|
+
res.end("Not found");
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const code = url.searchParams.get("code");
|
|
300
|
+
const returnedState = url.searchParams.get("state");
|
|
301
|
+
const error = url.searchParams.get("error");
|
|
302
|
+
if (error) {
|
|
303
|
+
const desc = url.searchParams.get("error_description") ?? "Authentication failed";
|
|
304
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
305
|
+
res.end(
|
|
306
|
+
getHtmlPage(
|
|
307
|
+
"Authentication Failed",
|
|
308
|
+
`Error: ${desc}. You can close this window.`
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
server.close();
|
|
312
|
+
reject(new AuthError(`Browser authentication failed: ${desc}`));
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
if (!code || returnedState !== state) {
|
|
316
|
+
res.writeHead(400, { "Content-Type": "text/html" });
|
|
317
|
+
res.end(
|
|
318
|
+
getHtmlPage(
|
|
319
|
+
"Authentication Failed",
|
|
320
|
+
"Invalid callback parameters. You can close this window."
|
|
321
|
+
)
|
|
322
|
+
);
|
|
323
|
+
server.close();
|
|
324
|
+
reject(
|
|
325
|
+
new AuthError("Invalid callback: missing code or state mismatch")
|
|
326
|
+
);
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
const tokenUrl = `${baseUrl}/api/cli/auth/token`;
|
|
330
|
+
const tokenResponse = await fetch(tokenUrl, {
|
|
331
|
+
method: "POST",
|
|
332
|
+
headers: { "Content-Type": "application/json" },
|
|
333
|
+
body: JSON.stringify({ code, state })
|
|
334
|
+
});
|
|
335
|
+
if (!tokenResponse.ok) {
|
|
336
|
+
const errorText = await tokenResponse.text().catch(() => "Unknown error");
|
|
337
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
338
|
+
res.end(
|
|
339
|
+
getHtmlPage(
|
|
340
|
+
"Authentication Failed",
|
|
341
|
+
"Token exchange failed. You can close this window."
|
|
342
|
+
)
|
|
343
|
+
);
|
|
344
|
+
server.close();
|
|
345
|
+
reject(new AuthError(`Token exchange failed: ${errorText}`));
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
const tokenData = await tokenResponse.json();
|
|
349
|
+
const expiresAt = tokenData.expires_at ? new Date(tokenData.expires_at).getTime() : null;
|
|
350
|
+
setAuthConfig({
|
|
351
|
+
token: tokenData.token,
|
|
352
|
+
expiresAt,
|
|
353
|
+
refreshToken: null,
|
|
354
|
+
method: "platform_token",
|
|
355
|
+
tokenId: tokenData.token_id
|
|
356
|
+
});
|
|
357
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
358
|
+
res.end(
|
|
359
|
+
getHtmlPage(
|
|
360
|
+
"Authentication Successful",
|
|
361
|
+
"You are now logged in. You can close this window and return to the terminal."
|
|
362
|
+
)
|
|
363
|
+
);
|
|
364
|
+
server.close();
|
|
365
|
+
if (jsonMode) {
|
|
366
|
+
output.log(
|
|
367
|
+
JSON.stringify({
|
|
368
|
+
status: "authenticated",
|
|
369
|
+
method: "platform_token",
|
|
370
|
+
expiresAt
|
|
371
|
+
})
|
|
372
|
+
);
|
|
373
|
+
} else {
|
|
374
|
+
output.success("Logged in successfully.");
|
|
375
|
+
if (expiresAt) {
|
|
376
|
+
output.dim(
|
|
377
|
+
`Token expires: ${new Date(expiresAt).toLocaleString()}`
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
resolve7();
|
|
382
|
+
} catch (err) {
|
|
383
|
+
server.close();
|
|
384
|
+
reject(
|
|
385
|
+
err instanceof AuthError ? err : new AuthError(
|
|
386
|
+
`Authentication error: ${err instanceof Error ? err.message : "Unknown"}`
|
|
387
|
+
)
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
});
|
|
391
|
+
server.listen(0, "127.0.0.1", () => {
|
|
392
|
+
const { port } = server.address();
|
|
393
|
+
const authUrl = `${baseUrl}/api/cli/auth/authorize?callback_port=${port}&state=${encodeURIComponent(state)}`;
|
|
394
|
+
if (!jsonMode) {
|
|
395
|
+
output.log("Opening browser for authentication...");
|
|
396
|
+
output.dim("If the browser doesn't open, visit:");
|
|
397
|
+
output.dim(authUrl);
|
|
398
|
+
output.newline();
|
|
399
|
+
output.dim("Waiting for authentication...");
|
|
400
|
+
}
|
|
401
|
+
open(authUrl).catch(() => {
|
|
402
|
+
if (!jsonMode) {
|
|
403
|
+
output.warn(
|
|
404
|
+
"Could not open browser. Please visit the URL above manually."
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
const timeoutId = setTimeout(() => {
|
|
410
|
+
server.close();
|
|
411
|
+
reject(new AuthError("Authentication timed out after 5 minutes"));
|
|
412
|
+
}, 3e5);
|
|
413
|
+
server.on("close", () => clearTimeout(timeoutId));
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
function escapeHtml(text) {
|
|
417
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
418
|
+
}
|
|
419
|
+
function getHtmlPage(title, message) {
|
|
420
|
+
return `<!DOCTYPE html>
|
|
421
|
+
<html>
|
|
422
|
+
<head><title>Rush CLI - ${escapeHtml(title)}</title>
|
|
423
|
+
<style>
|
|
424
|
+
body { font-family: -apple-system, BlinkMacSystemFont, sans-serif; display: flex; align-items: center; justify-content: center; height: 100vh; margin: 0; background: #f5f5f5; }
|
|
425
|
+
.card { background: white; border-radius: 12px; padding: 40px; box-shadow: 0 2px 10px rgba(0,0,0,0.1); text-align: center; max-width: 400px; }
|
|
426
|
+
h1 { color: #333; font-size: 1.5em; }
|
|
427
|
+
p { color: #666; line-height: 1.6; }
|
|
428
|
+
</style>
|
|
429
|
+
</head>
|
|
430
|
+
<body>
|
|
431
|
+
<div class="card">
|
|
432
|
+
<h1>${escapeHtml(title)}</h1>
|
|
433
|
+
<p>${escapeHtml(message)}</p>
|
|
434
|
+
</div>
|
|
435
|
+
</body>
|
|
436
|
+
</html>`;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
// src/commands/auth/index.ts
|
|
440
|
+
function registerAuthCommand(program) {
|
|
441
|
+
const auth = program.command("auth").description("Manage authentication");
|
|
442
|
+
auth.command("login").description("Log in to the Rush platform").option("--api-key <key>", "Authenticate with an API key instead of CAS").option("--no-verify", "Skip online validation for API key login").action(async (options) => {
|
|
443
|
+
const jsonMode = program.opts().json;
|
|
444
|
+
if (options.apiKey) {
|
|
445
|
+
setAuthConfig({
|
|
446
|
+
token: options.apiKey,
|
|
447
|
+
expiresAt: null,
|
|
448
|
+
refreshToken: null,
|
|
449
|
+
method: "api_key"
|
|
450
|
+
});
|
|
451
|
+
let verification;
|
|
452
|
+
if (options.verify !== false) {
|
|
453
|
+
verification = await verifyCurrentAuthSession();
|
|
454
|
+
if (!verification.valid) {
|
|
455
|
+
clearAuthConfig();
|
|
456
|
+
throw new RushError(
|
|
457
|
+
`API key validation failed: ${verification.message ?? "invalid credentials"}`,
|
|
458
|
+
{},
|
|
459
|
+
"AUTH_ERROR"
|
|
460
|
+
);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
if (jsonMode) {
|
|
464
|
+
output.log(
|
|
465
|
+
JSON.stringify({
|
|
466
|
+
status: "authenticated",
|
|
467
|
+
method: "api_key",
|
|
468
|
+
verified: options.verify !== false,
|
|
469
|
+
...verification ? { serverStatus: verification.status } : {}
|
|
470
|
+
})
|
|
471
|
+
);
|
|
472
|
+
} else {
|
|
473
|
+
output.success("Authenticated with API key.");
|
|
474
|
+
if (options.verify !== false) {
|
|
475
|
+
output.dim("Server validation: OK");
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
if (isLoggedIn()) {
|
|
481
|
+
if (jsonMode) {
|
|
482
|
+
output.log(JSON.stringify({ status: "already_authenticated" }));
|
|
483
|
+
} else {
|
|
484
|
+
output.warn("You are already logged in.");
|
|
485
|
+
output.dim("Run `rush-ai auth logout` first to log out.");
|
|
486
|
+
}
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
await loginViaBrowser(jsonMode);
|
|
490
|
+
});
|
|
491
|
+
auth.command("status").description("Show the current authentication status").option("--verify", "Validate the current credential against the server").action(async (options) => {
|
|
492
|
+
const jsonMode = program.opts().json;
|
|
493
|
+
const method = getAuthMethod();
|
|
494
|
+
const authConfig = getAuthConfig();
|
|
495
|
+
const loggedIn = method !== null && (method === "api_key" || isLoggedIn());
|
|
496
|
+
const verification = loggedIn && options.verify ? await verifyCurrentAuthSession() : null;
|
|
497
|
+
if (jsonMode) {
|
|
498
|
+
output.log(
|
|
499
|
+
JSON.stringify({
|
|
500
|
+
authenticated: loggedIn,
|
|
501
|
+
method: method ?? null,
|
|
502
|
+
expiresAt: authConfig.expiresAt,
|
|
503
|
+
hasRefreshToken: Boolean(authConfig.refreshToken),
|
|
504
|
+
configDir: getConfigDir(),
|
|
505
|
+
...verification ? {
|
|
506
|
+
serverAuthenticated: verification.valid,
|
|
507
|
+
serverStatus: verification.status,
|
|
508
|
+
serverMessage: verification.message
|
|
509
|
+
} : {}
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
} else {
|
|
513
|
+
if (loggedIn) {
|
|
514
|
+
output.success(`Authenticated via ${method ?? "token"}`);
|
|
515
|
+
if (authConfig.expiresAt) {
|
|
516
|
+
const expiresDate = new Date(authConfig.expiresAt).toLocaleString();
|
|
517
|
+
output.dim(`Token expires: ${expiresDate}`);
|
|
518
|
+
}
|
|
519
|
+
if (authConfig.refreshToken) {
|
|
520
|
+
output.dim("Refresh token: available");
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
output.warn("Not authenticated.");
|
|
524
|
+
output.dim("Run `rush-ai auth login` to log in.");
|
|
525
|
+
}
|
|
526
|
+
if (verification) {
|
|
527
|
+
if (verification.valid) {
|
|
528
|
+
output.dim("Server validation: OK");
|
|
529
|
+
} else {
|
|
530
|
+
output.warn(
|
|
531
|
+
`Server validation failed: ${verification.message ?? "token rejected"}`
|
|
532
|
+
);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
output.dim(`Config: ${getConfigDir()}`);
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
auth.command("logout").description("Log out and clear stored credentials").option("--no-revoke", "Skip best-effort CAS revoke request").action(async (options) => {
|
|
539
|
+
const jsonMode = program.opts().json;
|
|
540
|
+
const authConfig = getAuthConfig();
|
|
541
|
+
const hasToken = authConfig.token !== null;
|
|
542
|
+
if (!hasToken) {
|
|
543
|
+
if (jsonMode) {
|
|
544
|
+
output.log(JSON.stringify({ status: "not_authenticated" }));
|
|
545
|
+
} else {
|
|
546
|
+
output.warn("You are not currently logged in.");
|
|
547
|
+
}
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
const shouldRevoke = options.revoke !== false && (authConfig.method === "cas" || authConfig.method === "platform_token");
|
|
551
|
+
const revoked = shouldRevoke ? await revokeCurrentSession() : false;
|
|
552
|
+
clearAuthConfig();
|
|
553
|
+
if (jsonMode) {
|
|
554
|
+
output.log(
|
|
555
|
+
JSON.stringify({
|
|
556
|
+
status: "logged_out",
|
|
557
|
+
...shouldRevoke ? { revoked } : {}
|
|
558
|
+
})
|
|
559
|
+
);
|
|
560
|
+
} else {
|
|
561
|
+
output.success("Logged out successfully.");
|
|
562
|
+
if (shouldRevoke) {
|
|
563
|
+
if (revoked) {
|
|
564
|
+
output.dim("Remote token revoked.");
|
|
565
|
+
} else {
|
|
566
|
+
output.warn(
|
|
567
|
+
"Could not confirm remote revoke. Local credentials were cleared."
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// src/commands/completion/index.ts
|
|
576
|
+
import { existsSync } from "fs";
|
|
577
|
+
import { appendFile, mkdir, readFile, writeFile } from "fs/promises";
|
|
578
|
+
import { homedir } from "os";
|
|
579
|
+
import { basename, dirname, join } from "path";
|
|
580
|
+
var MARKER_BEGIN = "###-begin-rush-ai-completion-###";
|
|
581
|
+
var MARKER_END = "###-end-rush-ai-completion-###";
|
|
582
|
+
var COMPLETION_SCRIPTS = {
|
|
583
|
+
bash: `
|
|
584
|
+
${MARKER_BEGIN}
|
|
585
|
+
_rush_cli_completion() {
|
|
586
|
+
local cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
587
|
+
local prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
588
|
+
local cmds="auth agent task mcp plugin completion config doctor"
|
|
589
|
+
|
|
590
|
+
case "$prev" in
|
|
591
|
+
rush-ai) COMPREPLY=( $(compgen -W "$cmds" -- "$cur") ) ;;
|
|
592
|
+
auth) COMPREPLY=( $(compgen -W "login logout status" -- "$cur") ) ;;
|
|
593
|
+
agent) COMPREPLY=( $(compgen -W "list info" -- "$cur") ) ;;
|
|
594
|
+
task) COMPREPLY=( $(compgen -W "run create status result list watch cancel" -- "$cur") ) ;;
|
|
595
|
+
plugin) COMPREPLY=( $(compgen -W "install list status update uninstall" -- "$cur") ) ;;
|
|
596
|
+
completion) COMPREPLY=( $(compgen -W "install bash zsh fish" -- "$cur") ) ;;
|
|
597
|
+
config) COMPREPLY=( $(compgen -W "show set use create delete list" -- "$cur") ) ;;
|
|
598
|
+
esac
|
|
599
|
+
}
|
|
600
|
+
complete -F _rush_cli_completion rush-ai
|
|
601
|
+
${MARKER_END}`.trimStart(),
|
|
602
|
+
zsh: `
|
|
603
|
+
${MARKER_BEGIN}
|
|
604
|
+
_rush_cli_completion() {
|
|
605
|
+
local -a cmds
|
|
606
|
+
cmds=(
|
|
607
|
+
'auth:Authentication commands'
|
|
608
|
+
'agent:Agent management'
|
|
609
|
+
'task:Task operations'
|
|
610
|
+
'mcp:MCP server'
|
|
611
|
+
'plugin:Plugin management'
|
|
612
|
+
'completion:Shell completion'
|
|
613
|
+
'config:Configuration management'
|
|
614
|
+
'doctor:System diagnostics'
|
|
615
|
+
)
|
|
616
|
+
_describe 'command' cmds
|
|
617
|
+
}
|
|
618
|
+
compdef _rush_cli_completion rush-ai
|
|
619
|
+
${MARKER_END}`.trimStart(),
|
|
620
|
+
fish: `
|
|
621
|
+
${MARKER_BEGIN}
|
|
622
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a auth -d 'Authentication'
|
|
623
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a agent -d 'Agent management'
|
|
624
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a task -d 'Task operations'
|
|
625
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a mcp -d 'MCP server'
|
|
626
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a plugin -d 'Plugin management'
|
|
627
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a completion -d 'Shell completion'
|
|
628
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a config -d 'Configuration management'
|
|
629
|
+
complete -c rush-ai -n '__fish_use_subcommand' -a doctor -d 'System diagnostics'
|
|
630
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from auth' -a 'login logout status'
|
|
631
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from agent' -a 'list info'
|
|
632
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from task' -a 'run create status result list watch cancel'
|
|
633
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from plugin' -a 'install list status update uninstall'
|
|
634
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from completion' -a 'install bash zsh fish'
|
|
635
|
+
complete -c rush-ai -n '__fish_seen_subcommand_from config' -a 'show set use create delete list'
|
|
636
|
+
${MARKER_END}`.trimStart()
|
|
637
|
+
};
|
|
638
|
+
function detectShell() {
|
|
639
|
+
const shell = process.env.SHELL;
|
|
640
|
+
if (!shell) return null;
|
|
641
|
+
const name = basename(shell);
|
|
642
|
+
return COMPLETION_SCRIPTS[name] ? name : null;
|
|
643
|
+
}
|
|
644
|
+
function getShellRcFile(shell) {
|
|
645
|
+
const home = homedir();
|
|
646
|
+
switch (shell) {
|
|
647
|
+
case "bash":
|
|
648
|
+
return existsSync(join(home, ".bashrc")) ? join(home, ".bashrc") : join(home, ".bash_profile");
|
|
649
|
+
case "zsh":
|
|
650
|
+
return join(home, ".zshrc");
|
|
651
|
+
case "fish":
|
|
652
|
+
return join(home, ".config", "fish", "completions", "rush-ai.fish");
|
|
653
|
+
default:
|
|
654
|
+
throw new Error(`Unsupported shell: ${shell}`);
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
function registerCompletionCommand(program) {
|
|
658
|
+
const completion = program.command("completion").description("Shell completion utilities");
|
|
659
|
+
completion.command("bash").description("Print bash completion script").action(() => {
|
|
660
|
+
output.log(COMPLETION_SCRIPTS.bash);
|
|
661
|
+
});
|
|
662
|
+
completion.command("zsh").description("Print zsh completion script").action(() => {
|
|
663
|
+
output.log(COMPLETION_SCRIPTS.zsh);
|
|
664
|
+
});
|
|
665
|
+
completion.command("fish").description("Print fish completion script").action(() => {
|
|
666
|
+
output.log(COMPLETION_SCRIPTS.fish);
|
|
667
|
+
});
|
|
668
|
+
completion.command("install").description("Auto-install shell completion to your shell rc file").option("--shell <shell>", "Shell type (auto-detect if omitted)").action(async (opts) => {
|
|
669
|
+
const shell = opts.shell ?? detectShell();
|
|
670
|
+
if (!shell) {
|
|
671
|
+
output.error("Could not detect shell. Use --shell <bash|zsh|fish>.");
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
if (!COMPLETION_SCRIPTS[shell]) {
|
|
675
|
+
output.error(
|
|
676
|
+
`Unsupported shell: ${shell}. Supported: bash, zsh, fish.`
|
|
677
|
+
);
|
|
678
|
+
process.exit(1);
|
|
679
|
+
}
|
|
680
|
+
const script = COMPLETION_SCRIPTS[shell];
|
|
681
|
+
const rcFile = getShellRcFile(shell);
|
|
682
|
+
if (shell === "fish") {
|
|
683
|
+
try {
|
|
684
|
+
await mkdir(dirname(rcFile), { recursive: true });
|
|
685
|
+
await writeFile(rcFile, script + "\n");
|
|
686
|
+
output.success(`Fish completion installed to ${rcFile}`);
|
|
687
|
+
} catch {
|
|
688
|
+
output.error(`Failed to write to ${rcFile}`);
|
|
689
|
+
output.dim(
|
|
690
|
+
" You can manually add the following to your fish completions:"
|
|
691
|
+
);
|
|
692
|
+
output.log(script);
|
|
693
|
+
}
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
try {
|
|
697
|
+
const existing = await readFile(rcFile, "utf-8");
|
|
698
|
+
if (existing.includes(MARKER_BEGIN)) {
|
|
699
|
+
output.info(`Completion already installed in ${rcFile}`);
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
} catch {
|
|
703
|
+
}
|
|
704
|
+
try {
|
|
705
|
+
await appendFile(rcFile, "\n" + script + "\n");
|
|
706
|
+
output.success(`Completion installed to ${rcFile}`);
|
|
707
|
+
output.dim(
|
|
708
|
+
` Run \`source ${rcFile}\` or restart your shell to activate.`
|
|
709
|
+
);
|
|
710
|
+
} catch {
|
|
711
|
+
output.error(`Failed to write to ${rcFile}`);
|
|
712
|
+
output.dim(
|
|
713
|
+
" You can manually add the following to your shell rc file:"
|
|
714
|
+
);
|
|
715
|
+
output.log(script);
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/commands/config/index.ts
|
|
721
|
+
var SETTABLE_KEYS = ["api", "collectMetrics", "currentTeam"];
|
|
722
|
+
function maskToken(token) {
|
|
723
|
+
if (!token) return "(none)";
|
|
724
|
+
if (token.length <= 8) return "****";
|
|
725
|
+
return `${token.slice(0, 4)}...****`;
|
|
726
|
+
}
|
|
727
|
+
function registerConfigCommand(program) {
|
|
728
|
+
const config = program.command("config").description("Manage CLI configuration and profiles");
|
|
729
|
+
config.command("show").description("Show current configuration").action(() => {
|
|
730
|
+
const jsonMode = program.opts().json;
|
|
731
|
+
const profile = getActiveProfile();
|
|
732
|
+
const globalConf = getGlobalConfig();
|
|
733
|
+
const authConf = getAuthConfig();
|
|
734
|
+
const configDir = getConfigDir();
|
|
735
|
+
const apiOverride = process.env.RUSH_API_URL;
|
|
736
|
+
const profileOverride = process.env.RUSH_PROFILE;
|
|
737
|
+
if (jsonMode) {
|
|
738
|
+
output.log(
|
|
739
|
+
JSON.stringify(
|
|
740
|
+
{
|
|
741
|
+
profile,
|
|
742
|
+
api: apiOverride ?? globalConf.api,
|
|
743
|
+
apiOverridden: Boolean(apiOverride),
|
|
744
|
+
profileOverridden: Boolean(profileOverride),
|
|
745
|
+
authMethod: authConf.method,
|
|
746
|
+
collectMetrics: globalConf.collectMetrics,
|
|
747
|
+
currentTeam: globalConf.currentTeam,
|
|
748
|
+
configDir
|
|
749
|
+
},
|
|
750
|
+
null,
|
|
751
|
+
2
|
|
752
|
+
)
|
|
753
|
+
);
|
|
754
|
+
return;
|
|
755
|
+
}
|
|
756
|
+
output.log(output.bold("Configuration"));
|
|
757
|
+
output.newline();
|
|
758
|
+
const profileLabel = profileOverride ? `${profile} (overridden by RUSH_PROFILE env)` : profile;
|
|
759
|
+
output.log(` Active profile: ${profileLabel}`);
|
|
760
|
+
const apiLabel = apiOverride ? `${apiOverride} (overridden by RUSH_API_URL env)` : globalConf.api;
|
|
761
|
+
output.log(` API: ${apiLabel}`);
|
|
762
|
+
output.log(
|
|
763
|
+
` Auth method: ${authConf.method ?? "(not authenticated)"}`
|
|
764
|
+
);
|
|
765
|
+
output.log(` Token: ${maskToken(authConf.token)}`);
|
|
766
|
+
output.log(
|
|
767
|
+
` Metrics: ${globalConf.collectMetrics ? "enabled" : "disabled"}`
|
|
768
|
+
);
|
|
769
|
+
output.log(` Config dir: ${configDir}`);
|
|
770
|
+
});
|
|
771
|
+
config.command("set").description("Set a configuration value").argument("<key>", `Config key (${SETTABLE_KEYS.join(", ")})`).argument("<value>", "Config value").action((key, value) => {
|
|
772
|
+
const jsonMode = program.opts().json;
|
|
773
|
+
const profile = getActiveProfile();
|
|
774
|
+
if (!SETTABLE_KEYS.includes(key)) {
|
|
775
|
+
output.error(
|
|
776
|
+
`Unknown config key '${key}'. Valid keys: ${SETTABLE_KEYS.join(", ")}`
|
|
777
|
+
);
|
|
778
|
+
process.exit(1);
|
|
779
|
+
}
|
|
780
|
+
let typedValue = value;
|
|
781
|
+
if (key === "collectMetrics") {
|
|
782
|
+
if (value !== "true" && value !== "false") {
|
|
783
|
+
output.error("'collectMetrics' must be 'true' or 'false'.");
|
|
784
|
+
process.exit(1);
|
|
785
|
+
}
|
|
786
|
+
typedValue = value === "true";
|
|
787
|
+
}
|
|
788
|
+
if (key === "currentTeam" && value === "null") {
|
|
789
|
+
typedValue = null;
|
|
790
|
+
}
|
|
791
|
+
setGlobalConfig({ [key]: typedValue });
|
|
792
|
+
if (jsonMode) {
|
|
793
|
+
output.log(JSON.stringify({ key, value: typedValue, profile }));
|
|
794
|
+
} else {
|
|
795
|
+
output.success(
|
|
796
|
+
`Updated '${key}' to '${String(typedValue)}' (profile: ${profile})`
|
|
797
|
+
);
|
|
798
|
+
}
|
|
799
|
+
});
|
|
800
|
+
config.command("use").description("Switch active profile").argument("<profile>", "Profile name to switch to").action((profileName) => {
|
|
801
|
+
const jsonMode = program.opts().json;
|
|
802
|
+
try {
|
|
803
|
+
setActiveProfile(profileName);
|
|
804
|
+
} catch (err) {
|
|
805
|
+
output.error(
|
|
806
|
+
err instanceof Error ? err.message : "Failed to switch profile"
|
|
807
|
+
);
|
|
808
|
+
const available = listProfiles();
|
|
809
|
+
if (!jsonMode) {
|
|
810
|
+
output.dim(`Available profiles: ${available.join(", ")}`);
|
|
811
|
+
output.dim(
|
|
812
|
+
`Run \`rush-ai config create ${profileName}\` to create it.`
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
process.exit(1);
|
|
816
|
+
}
|
|
817
|
+
const globalConf = getGlobalConfig();
|
|
818
|
+
if (jsonMode) {
|
|
819
|
+
output.log(
|
|
820
|
+
JSON.stringify({
|
|
821
|
+
profile: profileName,
|
|
822
|
+
api: globalConf.api
|
|
823
|
+
})
|
|
824
|
+
);
|
|
825
|
+
} else {
|
|
826
|
+
output.success(`Switched to profile '${profileName}'`);
|
|
827
|
+
output.dim(`API: ${globalConf.api}`);
|
|
828
|
+
}
|
|
829
|
+
});
|
|
830
|
+
config.command("create").description("Create a new profile").argument("<profile>", "Profile name").option("--api <url>", "API base URL").action((profileName, options) => {
|
|
831
|
+
const jsonMode = program.opts().json;
|
|
832
|
+
try {
|
|
833
|
+
createProfile(
|
|
834
|
+
profileName,
|
|
835
|
+
options.api ? { api: options.api } : void 0
|
|
836
|
+
);
|
|
837
|
+
} catch (err) {
|
|
838
|
+
output.error(
|
|
839
|
+
err instanceof Error ? err.message : "Failed to create profile"
|
|
840
|
+
);
|
|
841
|
+
process.exit(1);
|
|
842
|
+
}
|
|
843
|
+
if (jsonMode) {
|
|
844
|
+
output.log(JSON.stringify({ profile: profileName, created: true }));
|
|
845
|
+
} else {
|
|
846
|
+
output.success(`Created profile '${profileName}'`);
|
|
847
|
+
output.dim(
|
|
848
|
+
`Run \`rush-ai config use ${profileName}\` to switch to it.`
|
|
849
|
+
);
|
|
850
|
+
}
|
|
851
|
+
});
|
|
852
|
+
config.command("delete").description("Delete a profile").argument("<profile>", "Profile name to delete").action((profileName) => {
|
|
853
|
+
const jsonMode = program.opts().json;
|
|
854
|
+
try {
|
|
855
|
+
deleteProfile(profileName);
|
|
856
|
+
} catch (err) {
|
|
857
|
+
output.error(
|
|
858
|
+
err instanceof Error ? err.message : "Failed to delete profile"
|
|
859
|
+
);
|
|
860
|
+
process.exit(1);
|
|
861
|
+
}
|
|
862
|
+
if (jsonMode) {
|
|
863
|
+
output.log(JSON.stringify({ profile: profileName, deleted: true }));
|
|
864
|
+
} else {
|
|
865
|
+
output.success(`Deleted profile '${profileName}'`);
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
config.command("list").alias("ls").description("List all profiles").action(() => {
|
|
869
|
+
const jsonMode = program.opts().json;
|
|
870
|
+
const profiles = listProfiles();
|
|
871
|
+
const active = getActiveProfile();
|
|
872
|
+
if (jsonMode) {
|
|
873
|
+
const data = profiles.map((name) => {
|
|
874
|
+
const conf = getProfileConfig(name);
|
|
875
|
+
const auth = getProfileAuth(name);
|
|
876
|
+
return {
|
|
877
|
+
name,
|
|
878
|
+
active: name === active,
|
|
879
|
+
api: conf?.api ?? "",
|
|
880
|
+
authMethod: auth.method,
|
|
881
|
+
loggedIn: Boolean(auth.token)
|
|
882
|
+
};
|
|
883
|
+
});
|
|
884
|
+
output.log(JSON.stringify(data, null, 2));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (profiles.length === 0) {
|
|
888
|
+
output.info("No profiles configured.");
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
const rows = profiles.map((name) => {
|
|
892
|
+
const conf = getProfileConfig(name);
|
|
893
|
+
const auth = getProfileAuth(name);
|
|
894
|
+
const marker = name === active ? "*" : " ";
|
|
895
|
+
const authStatus = auth.token ? `${auth.method ?? "unknown"} (logged in)` : "not authenticated";
|
|
896
|
+
return {
|
|
897
|
+
" ": marker,
|
|
898
|
+
Profile: name,
|
|
899
|
+
API: conf?.api ?? "",
|
|
900
|
+
Auth: authStatus
|
|
901
|
+
};
|
|
902
|
+
});
|
|
903
|
+
output.log(formatTableEnhanced(rows));
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
|
|
907
|
+
// src/commands/doctor/index.ts
|
|
908
|
+
import chalk2 from "chalk";
|
|
909
|
+
|
|
910
|
+
// src/commands/doctor/checks/auth.ts
|
|
911
|
+
var checkAuth = async () => {
|
|
912
|
+
const checks = [];
|
|
913
|
+
const method = getAuthMethod();
|
|
914
|
+
if (method === "platform_token") {
|
|
915
|
+
checks.push({
|
|
916
|
+
name: "auth_method",
|
|
917
|
+
group: "Authentication",
|
|
918
|
+
status: "pass",
|
|
919
|
+
label: "Auth method",
|
|
920
|
+
value: "Platform Token (browser login)"
|
|
921
|
+
});
|
|
922
|
+
} else if (method === "cas") {
|
|
923
|
+
checks.push({
|
|
924
|
+
name: "auth_method",
|
|
925
|
+
group: "Authentication",
|
|
926
|
+
status: "pass",
|
|
927
|
+
label: "Auth method",
|
|
928
|
+
value: "CAS PKCE"
|
|
929
|
+
});
|
|
930
|
+
} else if (method === "api_key") {
|
|
931
|
+
checks.push({
|
|
932
|
+
name: "auth_method",
|
|
933
|
+
group: "Authentication",
|
|
934
|
+
status: "pass",
|
|
935
|
+
label: "Auth method",
|
|
936
|
+
value: "API Key"
|
|
937
|
+
});
|
|
938
|
+
} else {
|
|
939
|
+
checks.push({
|
|
940
|
+
name: "auth_method",
|
|
941
|
+
group: "Authentication",
|
|
942
|
+
status: "warn",
|
|
943
|
+
label: "Auth method",
|
|
944
|
+
value: "Not authenticated",
|
|
945
|
+
fix: "Run `rush-ai auth login` to authenticate"
|
|
946
|
+
});
|
|
947
|
+
}
|
|
948
|
+
if (method === "api_key") {
|
|
949
|
+
checks.push({
|
|
950
|
+
name: "token_status",
|
|
951
|
+
group: "Authentication",
|
|
952
|
+
status: "info",
|
|
953
|
+
label: "Token status",
|
|
954
|
+
value: "Using API key auth"
|
|
955
|
+
});
|
|
956
|
+
} else if (method === "cas" || method === "platform_token") {
|
|
957
|
+
const auth = getAuthConfig();
|
|
958
|
+
if (auth.token && auth.expiresAt) {
|
|
959
|
+
const remaining = auth.expiresAt - Date.now();
|
|
960
|
+
if (remaining > 0) {
|
|
961
|
+
const hours = Math.floor(remaining / (1e3 * 60 * 60));
|
|
962
|
+
const minutes = Math.floor(
|
|
963
|
+
remaining % (1e3 * 60 * 60) / (1e3 * 60)
|
|
964
|
+
);
|
|
965
|
+
const timeStr = hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
966
|
+
checks.push({
|
|
967
|
+
name: "token_status",
|
|
968
|
+
group: "Authentication",
|
|
969
|
+
status: "pass",
|
|
970
|
+
label: "Token status",
|
|
971
|
+
value: `Valid (expires in ${timeStr})`
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
checks.push({
|
|
975
|
+
name: "token_status",
|
|
976
|
+
group: "Authentication",
|
|
977
|
+
status: "warn",
|
|
978
|
+
label: "Token status",
|
|
979
|
+
value: "Expired",
|
|
980
|
+
fix: "Run `rush-ai auth login` to re-authenticate"
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
} else if (auth.token) {
|
|
984
|
+
checks.push({
|
|
985
|
+
name: "token_status",
|
|
986
|
+
group: "Authentication",
|
|
987
|
+
status: "pass",
|
|
988
|
+
label: "Token status",
|
|
989
|
+
value: "Valid (no expiry set)"
|
|
990
|
+
});
|
|
991
|
+
} else {
|
|
992
|
+
checks.push({
|
|
993
|
+
name: "token_status",
|
|
994
|
+
group: "Authentication",
|
|
995
|
+
status: "warn",
|
|
996
|
+
label: "Token status",
|
|
997
|
+
value: "No token found",
|
|
998
|
+
fix: "Run `rush-ai auth login` to authenticate"
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
} else {
|
|
1002
|
+
checks.push({
|
|
1003
|
+
name: "token_status",
|
|
1004
|
+
group: "Authentication",
|
|
1005
|
+
status: "warn",
|
|
1006
|
+
label: "Token status",
|
|
1007
|
+
value: "Not authenticated",
|
|
1008
|
+
fix: "Run `rush-ai auth login` to authenticate"
|
|
1009
|
+
});
|
|
1010
|
+
}
|
|
1011
|
+
const apiKey = process.env.RUSH_API_KEY;
|
|
1012
|
+
checks.push({
|
|
1013
|
+
name: "api_key",
|
|
1014
|
+
group: "Authentication",
|
|
1015
|
+
status: "info",
|
|
1016
|
+
label: "API Key",
|
|
1017
|
+
value: apiKey ? "Set (RUSH_API_KEY)" : "Not set"
|
|
1018
|
+
});
|
|
1019
|
+
return checks;
|
|
1020
|
+
};
|
|
1021
|
+
|
|
1022
|
+
// src/commands/doctor/checks/config.ts
|
|
1023
|
+
import { accessSync, constants, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1024
|
+
var checkConfig = async () => {
|
|
1025
|
+
const checks = [];
|
|
1026
|
+
const configDir = getConfigDir();
|
|
1027
|
+
const dirExists = existsSync2(configDir);
|
|
1028
|
+
let writable = false;
|
|
1029
|
+
if (dirExists) {
|
|
1030
|
+
try {
|
|
1031
|
+
accessSync(configDir, constants.W_OK);
|
|
1032
|
+
writable = true;
|
|
1033
|
+
} catch {
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
if (dirExists && writable) {
|
|
1037
|
+
checks.push({
|
|
1038
|
+
name: "config_dir",
|
|
1039
|
+
group: "Configuration",
|
|
1040
|
+
status: "pass",
|
|
1041
|
+
label: "Config directory",
|
|
1042
|
+
value: configDir
|
|
1043
|
+
});
|
|
1044
|
+
} else if (dirExists && !writable) {
|
|
1045
|
+
checks.push({
|
|
1046
|
+
name: "config_dir",
|
|
1047
|
+
group: "Configuration",
|
|
1048
|
+
status: "fail",
|
|
1049
|
+
label: "Config directory",
|
|
1050
|
+
value: `${configDir} (not writable)`,
|
|
1051
|
+
fix: `Check permissions on ${configDir}`
|
|
1052
|
+
});
|
|
1053
|
+
} else {
|
|
1054
|
+
checks.push({
|
|
1055
|
+
name: "config_dir",
|
|
1056
|
+
group: "Configuration",
|
|
1057
|
+
status: "fail",
|
|
1058
|
+
label: "Config directory",
|
|
1059
|
+
value: `${configDir} (not found)`,
|
|
1060
|
+
fix: `Run \`rush-ai doctor --fix\` to create it`,
|
|
1061
|
+
autoFix: async () => {
|
|
1062
|
+
mkdirSync(configDir, { recursive: true });
|
|
1063
|
+
}
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
try {
|
|
1067
|
+
getGlobalConfig();
|
|
1068
|
+
getAuthConfig();
|
|
1069
|
+
checks.push({
|
|
1070
|
+
name: "config_validity",
|
|
1071
|
+
group: "Configuration",
|
|
1072
|
+
status: "pass",
|
|
1073
|
+
label: "Config files",
|
|
1074
|
+
value: "Valid"
|
|
1075
|
+
});
|
|
1076
|
+
} catch (err) {
|
|
1077
|
+
checks.push({
|
|
1078
|
+
name: "config_validity",
|
|
1079
|
+
group: "Configuration",
|
|
1080
|
+
status: "fail",
|
|
1081
|
+
label: "Config files",
|
|
1082
|
+
value: `Error: ${err instanceof Error ? err.message : "unknown"}`,
|
|
1083
|
+
fix: "Check config files in " + configDir
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
try {
|
|
1087
|
+
const config = getGlobalConfig();
|
|
1088
|
+
checks.push({
|
|
1089
|
+
name: "api_url",
|
|
1090
|
+
group: "Configuration",
|
|
1091
|
+
status: "info",
|
|
1092
|
+
label: "API URL",
|
|
1093
|
+
value: config.api
|
|
1094
|
+
});
|
|
1095
|
+
} catch {
|
|
1096
|
+
checks.push({
|
|
1097
|
+
name: "api_url",
|
|
1098
|
+
group: "Configuration",
|
|
1099
|
+
status: "warn",
|
|
1100
|
+
label: "API URL",
|
|
1101
|
+
value: "Unable to read"
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
return checks;
|
|
1105
|
+
};
|
|
1106
|
+
|
|
1107
|
+
// src/commands/doctor/checks/connectivity.ts
|
|
1108
|
+
var checkConnectivity = async () => {
|
|
1109
|
+
const checks = [];
|
|
1110
|
+
const config = getGlobalConfig();
|
|
1111
|
+
const apiUrl = process.env.RUSH_API_URL ?? config.api;
|
|
1112
|
+
try {
|
|
1113
|
+
const start = Date.now();
|
|
1114
|
+
const response = await fetch(apiUrl, {
|
|
1115
|
+
method: "HEAD",
|
|
1116
|
+
signal: AbortSignal.timeout(5e3)
|
|
1117
|
+
});
|
|
1118
|
+
const rtt = Date.now() - start;
|
|
1119
|
+
checks.push({
|
|
1120
|
+
name: "api_endpoint",
|
|
1121
|
+
group: "Connectivity",
|
|
1122
|
+
status: "pass",
|
|
1123
|
+
label: "API endpoint",
|
|
1124
|
+
value: `${apiUrl} (${rtt}ms, HTTP ${response.status})`
|
|
1125
|
+
});
|
|
1126
|
+
} catch (err) {
|
|
1127
|
+
const message = err instanceof Error ? err.message : "Unknown error";
|
|
1128
|
+
const isTimeout = message.includes("timeout") || message.includes("abort");
|
|
1129
|
+
checks.push({
|
|
1130
|
+
name: "api_endpoint",
|
|
1131
|
+
group: "Connectivity",
|
|
1132
|
+
status: "fail",
|
|
1133
|
+
label: "API endpoint",
|
|
1134
|
+
value: isTimeout ? `${apiUrl} (timeout after 5s)` : `${apiUrl} (${message})`,
|
|
1135
|
+
fix: "Check your network connection and API URL in config"
|
|
1136
|
+
});
|
|
1137
|
+
}
|
|
1138
|
+
const httpProxy = process.env.HTTP_PROXY || process.env.http_proxy;
|
|
1139
|
+
const httpsProxy = process.env.HTTPS_PROXY || process.env.https_proxy;
|
|
1140
|
+
if (httpProxy || httpsProxy) {
|
|
1141
|
+
const parts = [];
|
|
1142
|
+
if (httpProxy) parts.push(`HTTP_PROXY=${maskUrl(httpProxy)}`);
|
|
1143
|
+
if (httpsProxy) parts.push(`HTTPS_PROXY=${maskUrl(httpsProxy)}`);
|
|
1144
|
+
checks.push({
|
|
1145
|
+
name: "proxy",
|
|
1146
|
+
group: "Connectivity",
|
|
1147
|
+
status: "info",
|
|
1148
|
+
label: "Proxy",
|
|
1149
|
+
value: parts.join(", ")
|
|
1150
|
+
});
|
|
1151
|
+
} else {
|
|
1152
|
+
checks.push({
|
|
1153
|
+
name: "proxy",
|
|
1154
|
+
group: "Connectivity",
|
|
1155
|
+
status: "info",
|
|
1156
|
+
label: "Proxy",
|
|
1157
|
+
value: "Not configured"
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
return checks;
|
|
1161
|
+
};
|
|
1162
|
+
function maskUrl(url) {
|
|
1163
|
+
try {
|
|
1164
|
+
const parsed = new URL(url);
|
|
1165
|
+
if (parsed.username || parsed.password) {
|
|
1166
|
+
parsed.username = "***";
|
|
1167
|
+
parsed.password = "";
|
|
1168
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1169
|
+
}
|
|
1170
|
+
if (url.includes("@")) {
|
|
1171
|
+
return url.replace(/[^/@]+@/, "***@");
|
|
1172
|
+
}
|
|
1173
|
+
return parsed.toString().replace(/\/$/, "");
|
|
1174
|
+
} catch {
|
|
1175
|
+
if (url.includes("@")) {
|
|
1176
|
+
return url.replace(/[^/@]+@/, "***@");
|
|
1177
|
+
}
|
|
1178
|
+
return url;
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// src/commands/doctor/checks/environment.ts
|
|
1183
|
+
import { execFileSync } from "child_process";
|
|
1184
|
+
import { arch, platform, release, type } from "os";
|
|
1185
|
+
function formatOS() {
|
|
1186
|
+
const p = platform();
|
|
1187
|
+
const r = release();
|
|
1188
|
+
const a = arch();
|
|
1189
|
+
let osName;
|
|
1190
|
+
switch (p) {
|
|
1191
|
+
case "darwin":
|
|
1192
|
+
osName = "macOS";
|
|
1193
|
+
break;
|
|
1194
|
+
case "win32":
|
|
1195
|
+
osName = "Windows";
|
|
1196
|
+
break;
|
|
1197
|
+
case "linux":
|
|
1198
|
+
osName = "Linux";
|
|
1199
|
+
break;
|
|
1200
|
+
default:
|
|
1201
|
+
osName = type();
|
|
1202
|
+
}
|
|
1203
|
+
return `${osName} ${r} (${p} ${a})`;
|
|
1204
|
+
}
|
|
1205
|
+
var KNOWN_SHELLS = ["bash", "zsh", "fish", "sh", "dash", "ksh", "tcsh"];
|
|
1206
|
+
function detectShell2() {
|
|
1207
|
+
const shell = process.env.SHELL;
|
|
1208
|
+
if (!shell) {
|
|
1209
|
+
return { name: "unknown", version: null };
|
|
1210
|
+
}
|
|
1211
|
+
const name = shell.split("/").pop() ?? shell;
|
|
1212
|
+
if (!KNOWN_SHELLS.includes(name)) {
|
|
1213
|
+
return { name, version: null };
|
|
1214
|
+
}
|
|
1215
|
+
let version = null;
|
|
1216
|
+
try {
|
|
1217
|
+
const raw = execFileSync(shell, ["--version"], {
|
|
1218
|
+
timeout: 3e3,
|
|
1219
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1220
|
+
encoding: "utf-8"
|
|
1221
|
+
});
|
|
1222
|
+
const firstLine = raw.split("\n")[0] ?? "";
|
|
1223
|
+
const match = firstLine.match(/(\d+\.\d+[\d.]*)/);
|
|
1224
|
+
version = match?.[1] ?? null;
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
return { name, version };
|
|
1228
|
+
}
|
|
1229
|
+
var checkEnvironment = async () => {
|
|
1230
|
+
const checks = [];
|
|
1231
|
+
checks.push({
|
|
1232
|
+
name: "cli_version",
|
|
1233
|
+
group: "Environment",
|
|
1234
|
+
status: "info",
|
|
1235
|
+
label: "CLI version",
|
|
1236
|
+
value: VERSION
|
|
1237
|
+
});
|
|
1238
|
+
const nodeVersion = process.version;
|
|
1239
|
+
const major = parseInt(nodeVersion.slice(1), 10);
|
|
1240
|
+
const nodeOk = major >= 18;
|
|
1241
|
+
checks.push({
|
|
1242
|
+
name: "node_version",
|
|
1243
|
+
group: "Environment",
|
|
1244
|
+
status: nodeOk ? "pass" : "fail",
|
|
1245
|
+
label: "Node.js version",
|
|
1246
|
+
value: `${nodeVersion}${nodeOk ? "" : " (>=18.0.0 required)"}`,
|
|
1247
|
+
fix: nodeOk ? void 0 : "Upgrade Node.js to version 18 or higher"
|
|
1248
|
+
});
|
|
1249
|
+
checks.push({
|
|
1250
|
+
name: "os",
|
|
1251
|
+
group: "Environment",
|
|
1252
|
+
status: "info",
|
|
1253
|
+
label: "OS",
|
|
1254
|
+
value: formatOS()
|
|
1255
|
+
});
|
|
1256
|
+
const shell = detectShell2();
|
|
1257
|
+
const shellValue = shell.name === "unknown" ? "Unknown" : shell.version ? `${shell.name} ${shell.version}` : shell.name;
|
|
1258
|
+
checks.push({
|
|
1259
|
+
name: "shell",
|
|
1260
|
+
group: "Environment",
|
|
1261
|
+
status: shell.name === "unknown" ? "warn" : "pass",
|
|
1262
|
+
label: "Shell",
|
|
1263
|
+
value: shellValue
|
|
1264
|
+
});
|
|
1265
|
+
const terminal = process.env.TERM_PROGRAM ?? "Unknown";
|
|
1266
|
+
checks.push({
|
|
1267
|
+
name: "terminal",
|
|
1268
|
+
group: "Environment",
|
|
1269
|
+
status: "info",
|
|
1270
|
+
label: "Terminal",
|
|
1271
|
+
value: terminal
|
|
1272
|
+
});
|
|
1273
|
+
return checks;
|
|
1274
|
+
};
|
|
1275
|
+
|
|
1276
|
+
// src/commands/doctor/checks/plugins.ts
|
|
1277
|
+
import { existsSync as existsSync3, readFileSync, writeFileSync } from "fs";
|
|
1278
|
+
import { homedir as homedir2 } from "os";
|
|
1279
|
+
import { resolve } from "path";
|
|
1280
|
+
var PLUGINS_FILE = resolve(homedir2(), ".rush", "plugins", "installed.json");
|
|
1281
|
+
var MCP_CONFIG_PATHS = {
|
|
1282
|
+
"claude-code": resolve(homedir2(), ".claude", "settings.json"),
|
|
1283
|
+
cursor: resolve(homedir2(), ".cursor", "mcp.json")
|
|
1284
|
+
};
|
|
1285
|
+
function safeReadJson(filePath, fallback) {
|
|
1286
|
+
if (!existsSync3(filePath)) return fallback;
|
|
1287
|
+
try {
|
|
1288
|
+
return JSON.parse(readFileSync(filePath, "utf-8"));
|
|
1289
|
+
} catch {
|
|
1290
|
+
return fallback;
|
|
1291
|
+
}
|
|
1292
|
+
}
|
|
1293
|
+
function injectMcpConfig(configPath) {
|
|
1294
|
+
const config = safeReadJson(configPath, {});
|
|
1295
|
+
const mcpServers = config.mcpServers ?? {};
|
|
1296
|
+
const globalConfig = getGlobalConfig();
|
|
1297
|
+
mcpServers.rush = {
|
|
1298
|
+
command: "rush-ai",
|
|
1299
|
+
args: ["mcp", "serve"],
|
|
1300
|
+
env: {
|
|
1301
|
+
RUSH_API_URL: globalConfig.api
|
|
1302
|
+
}
|
|
1303
|
+
};
|
|
1304
|
+
config.mcpServers = mcpServers;
|
|
1305
|
+
writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8");
|
|
1306
|
+
}
|
|
1307
|
+
function checkMcpForPlugin(name, manifest) {
|
|
1308
|
+
const configPath = MCP_CONFIG_PATHS[manifest.type];
|
|
1309
|
+
if (!configPath) {
|
|
1310
|
+
return {
|
|
1311
|
+
name: `plugin_${name}`,
|
|
1312
|
+
group: "Plugins",
|
|
1313
|
+
status: "warn",
|
|
1314
|
+
label: name,
|
|
1315
|
+
value: `Installed (v${manifest.version}), unknown type: ${manifest.type}`
|
|
1316
|
+
};
|
|
1317
|
+
}
|
|
1318
|
+
if (!existsSync3(configPath)) {
|
|
1319
|
+
return {
|
|
1320
|
+
name: `plugin_${name}`,
|
|
1321
|
+
group: "Plugins",
|
|
1322
|
+
status: "warn",
|
|
1323
|
+
label: name,
|
|
1324
|
+
value: `Installed, MCP config missing (${configPath})`,
|
|
1325
|
+
fix: `Run \`rush-ai plugin install ${name}\``,
|
|
1326
|
+
autoFix: async () => {
|
|
1327
|
+
injectMcpConfig(configPath);
|
|
1328
|
+
}
|
|
1329
|
+
};
|
|
1330
|
+
}
|
|
1331
|
+
try {
|
|
1332
|
+
const config = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
1333
|
+
const servers = config.mcpServers;
|
|
1334
|
+
if (!servers || !("rush" in servers)) {
|
|
1335
|
+
return {
|
|
1336
|
+
name: `plugin_${name}`,
|
|
1337
|
+
group: "Plugins",
|
|
1338
|
+
status: "warn",
|
|
1339
|
+
label: name,
|
|
1340
|
+
value: "Installed, MCP server not configured",
|
|
1341
|
+
fix: `Run \`rush-ai doctor --fix\` to inject MCP config`,
|
|
1342
|
+
autoFix: async () => {
|
|
1343
|
+
injectMcpConfig(configPath);
|
|
1344
|
+
}
|
|
1345
|
+
};
|
|
1346
|
+
}
|
|
1347
|
+
const rushServer = servers.rush;
|
|
1348
|
+
const command = rushServer?.command;
|
|
1349
|
+
if (command !== "rush-ai") {
|
|
1350
|
+
return {
|
|
1351
|
+
name: `plugin_${name}`,
|
|
1352
|
+
group: "Plugins",
|
|
1353
|
+
status: "warn",
|
|
1354
|
+
label: name,
|
|
1355
|
+
value: `Installed, MCP command is "${command}" (expected "rush-ai")`,
|
|
1356
|
+
fix: `Run \`rush-ai plugin update ${name}\``
|
|
1357
|
+
};
|
|
1358
|
+
}
|
|
1359
|
+
return {
|
|
1360
|
+
name: `plugin_${name}`,
|
|
1361
|
+
group: "Plugins",
|
|
1362
|
+
status: "pass",
|
|
1363
|
+
label: name,
|
|
1364
|
+
value: `Installed (v${manifest.version}), MCP config valid`
|
|
1365
|
+
};
|
|
1366
|
+
} catch {
|
|
1367
|
+
return {
|
|
1368
|
+
name: `plugin_${name}`,
|
|
1369
|
+
group: "Plugins",
|
|
1370
|
+
status: "warn",
|
|
1371
|
+
label: name,
|
|
1372
|
+
value: `Installed, failed to parse MCP config`,
|
|
1373
|
+
fix: `Check ${configPath} for JSON syntax errors`
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
}
|
|
1377
|
+
var checkPlugins = async () => {
|
|
1378
|
+
const data = safeReadJson(PLUGINS_FILE, { plugins: {} });
|
|
1379
|
+
const plugins = data && typeof data === "object" && data.plugins && typeof data.plugins === "object" ? data.plugins : {};
|
|
1380
|
+
const entries = Object.entries(plugins);
|
|
1381
|
+
if (entries.length === 0) {
|
|
1382
|
+
return [
|
|
1383
|
+
{
|
|
1384
|
+
name: "plugins_none",
|
|
1385
|
+
group: "Plugins",
|
|
1386
|
+
status: "info",
|
|
1387
|
+
label: "Plugins",
|
|
1388
|
+
value: "No plugins installed"
|
|
1389
|
+
}
|
|
1390
|
+
];
|
|
1391
|
+
}
|
|
1392
|
+
return entries.map(([name, manifest]) => checkMcpForPlugin(name, manifest));
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
// src/commands/doctor/index.ts
|
|
1396
|
+
var CHECK_RUNNERS = [
|
|
1397
|
+
checkEnvironment,
|
|
1398
|
+
checkAuth,
|
|
1399
|
+
checkConnectivity,
|
|
1400
|
+
checkConfig,
|
|
1401
|
+
checkPlugins
|
|
1402
|
+
];
|
|
1403
|
+
async function runAllChecks() {
|
|
1404
|
+
const checks = [];
|
|
1405
|
+
for (const runner of CHECK_RUNNERS) {
|
|
1406
|
+
try {
|
|
1407
|
+
const results = await runner();
|
|
1408
|
+
checks.push(...results);
|
|
1409
|
+
} catch (err) {
|
|
1410
|
+
checks.push({
|
|
1411
|
+
name: "runner_error",
|
|
1412
|
+
group: "Environment",
|
|
1413
|
+
status: "fail",
|
|
1414
|
+
label: "Check error",
|
|
1415
|
+
value: `A check group failed: ${err instanceof Error ? err.message : "unknown error"}`
|
|
1416
|
+
});
|
|
1417
|
+
}
|
|
1418
|
+
}
|
|
1419
|
+
return checks;
|
|
1420
|
+
}
|
|
1421
|
+
function getSummary(checks) {
|
|
1422
|
+
let pass = 0;
|
|
1423
|
+
let warn = 0;
|
|
1424
|
+
let fail = 0;
|
|
1425
|
+
for (const c of checks) {
|
|
1426
|
+
if (c.status === "pass") pass++;
|
|
1427
|
+
else if (c.status === "warn") warn++;
|
|
1428
|
+
else if (c.status === "fail") fail++;
|
|
1429
|
+
}
|
|
1430
|
+
return { pass, warn, fail };
|
|
1431
|
+
}
|
|
1432
|
+
function statusIcon(status) {
|
|
1433
|
+
switch (status) {
|
|
1434
|
+
case "pass":
|
|
1435
|
+
return chalk2.green("\u2713");
|
|
1436
|
+
case "warn":
|
|
1437
|
+
return chalk2.yellow("!");
|
|
1438
|
+
case "fail":
|
|
1439
|
+
return chalk2.red("\u2717");
|
|
1440
|
+
case "info":
|
|
1441
|
+
return chalk2.dim("\xB7");
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
var LABEL_WIDTH = 20;
|
|
1445
|
+
function displayResults(checks) {
|
|
1446
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1447
|
+
for (const check of checks) {
|
|
1448
|
+
const existing = groups.get(check.group) ?? [];
|
|
1449
|
+
existing.push(check);
|
|
1450
|
+
groups.set(check.group, existing);
|
|
1451
|
+
}
|
|
1452
|
+
output.newline();
|
|
1453
|
+
for (const [groupName, groupChecks] of groups) {
|
|
1454
|
+
output.log(` ${chalk2.bold(groupName)}`);
|
|
1455
|
+
for (const check of groupChecks) {
|
|
1456
|
+
const icon = statusIcon(check.status);
|
|
1457
|
+
const label = check.label.padEnd(LABEL_WIDTH);
|
|
1458
|
+
output.log(` ${icon} ${label} ${check.value}`);
|
|
1459
|
+
}
|
|
1460
|
+
output.log("");
|
|
1461
|
+
}
|
|
1462
|
+
const summary = getSummary(checks);
|
|
1463
|
+
const parts = [];
|
|
1464
|
+
parts.push(chalk2.green(`${summary.pass} passed`));
|
|
1465
|
+
if (summary.warn > 0) parts.push(chalk2.yellow(`${summary.warn} warnings`));
|
|
1466
|
+
if (summary.fail > 0) parts.push(chalk2.red(`${summary.fail} failed`));
|
|
1467
|
+
if (summary.warn === 0 && summary.fail === 0) {
|
|
1468
|
+
parts.push("0 warnings");
|
|
1469
|
+
parts.push("0 failed");
|
|
1470
|
+
}
|
|
1471
|
+
output.log(` ${chalk2.bold("Summary:")} ${parts.join(", ")}`);
|
|
1472
|
+
if (summary.fail > 0 || summary.warn > 0) {
|
|
1473
|
+
const fixable = checks.filter(
|
|
1474
|
+
(c) => c.status !== "pass" && c.status !== "info" && c.autoFix
|
|
1475
|
+
);
|
|
1476
|
+
if (fixable.length > 0) {
|
|
1477
|
+
output.log(
|
|
1478
|
+
chalk2.dim(` Run \`rush-ai doctor --fix\` to attempt auto-fixes.`)
|
|
1479
|
+
);
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
output.log("");
|
|
1483
|
+
}
|
|
1484
|
+
function outputJson(checks) {
|
|
1485
|
+
const summary = getSummary(checks);
|
|
1486
|
+
const result = {
|
|
1487
|
+
checks: checks.map((c) => ({
|
|
1488
|
+
name: c.name,
|
|
1489
|
+
group: c.group,
|
|
1490
|
+
status: c.status,
|
|
1491
|
+
label: c.label,
|
|
1492
|
+
value: c.value,
|
|
1493
|
+
...c.fix ? { fix: c.fix } : {}
|
|
1494
|
+
})),
|
|
1495
|
+
summary
|
|
1496
|
+
};
|
|
1497
|
+
output.log(JSON.stringify(result, null, 2));
|
|
1498
|
+
}
|
|
1499
|
+
async function runAutoFixes(checks) {
|
|
1500
|
+
const fixable = checks.filter(
|
|
1501
|
+
(c) => c.status !== "pass" && c.status !== "info" && c.autoFix
|
|
1502
|
+
);
|
|
1503
|
+
if (fixable.length === 0) {
|
|
1504
|
+
output.info("No auto-fixable issues found.");
|
|
1505
|
+
return;
|
|
1506
|
+
}
|
|
1507
|
+
output.newline();
|
|
1508
|
+
for (const check of fixable) {
|
|
1509
|
+
output.info(`Fixing: ${check.label}...`);
|
|
1510
|
+
try {
|
|
1511
|
+
await check.autoFix();
|
|
1512
|
+
output.success(`Fixed: ${check.label}`);
|
|
1513
|
+
} catch (err) {
|
|
1514
|
+
output.error(
|
|
1515
|
+
`Failed to fix ${check.label}: ${err instanceof Error ? err.message : "unknown error"}`
|
|
1516
|
+
);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
output.newline();
|
|
1520
|
+
}
|
|
1521
|
+
function registerDoctorCommand(program) {
|
|
1522
|
+
program.command("doctor").description("Diagnose environment, auth, and connectivity issues").option("--fix", "Attempt to auto-fix detected issues").action(async (opts) => {
|
|
1523
|
+
const jsonMode = program.opts().json;
|
|
1524
|
+
let checks = await runAllChecks();
|
|
1525
|
+
if (opts.fix) {
|
|
1526
|
+
await runAutoFixes(checks);
|
|
1527
|
+
checks = await runAllChecks();
|
|
1528
|
+
}
|
|
1529
|
+
if (jsonMode) {
|
|
1530
|
+
outputJson(checks);
|
|
1531
|
+
} else {
|
|
1532
|
+
displayResults(checks);
|
|
1533
|
+
}
|
|
1534
|
+
const hasFail = checks.some((c) => c.status === "fail");
|
|
1535
|
+
if (hasFail) {
|
|
1536
|
+
process.exit(1);
|
|
1537
|
+
}
|
|
1538
|
+
});
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// src/commands/mcp/index.ts
|
|
1542
|
+
function formatValidationStatus(vs) {
|
|
1543
|
+
if (!vs) return "-";
|
|
1544
|
+
switch (vs.status) {
|
|
1545
|
+
case "verified":
|
|
1546
|
+
return "\u2713 verified";
|
|
1547
|
+
case "pending":
|
|
1548
|
+
return "\u2026 pending";
|
|
1549
|
+
case "credential_required":
|
|
1550
|
+
return "\u26A0 credential required";
|
|
1551
|
+
case "failed":
|
|
1552
|
+
return `\u2717 ${vs.errorCode || "failed"}`;
|
|
1553
|
+
default:
|
|
1554
|
+
return vs.status;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
function truncate3(str, max) {
|
|
1558
|
+
if (str.length <= max) return str;
|
|
1559
|
+
return `${str.slice(0, max - 3)}...`;
|
|
1560
|
+
}
|
|
1561
|
+
function registerMcpCommand(program) {
|
|
1562
|
+
const mcp = program.command("mcp").description("MCP server and platform MCP discovery");
|
|
1563
|
+
mcp.command("serve").description("Start the MCP stdio server").action(async () => {
|
|
1564
|
+
const { startMcpServer } = await import("./server-PVZOTJZA.js");
|
|
1565
|
+
await startMcpServer();
|
|
1566
|
+
});
|
|
1567
|
+
mcp.command("list").alias("ls").description("List available MCP servers on the platform").option("-c, --category <category>", "Filter by category").option("-t, --tag <tag>", "Filter by tag").option("-s, --search <query>", "Search by name or description").option("--transport <type>", "Filter by transport type (stdio|sse|http)").option(
|
|
1568
|
+
"-l, --limit <number>",
|
|
1569
|
+
"Max results per page (default: 50, max: 100)",
|
|
1570
|
+
"50"
|
|
1571
|
+
).option("-p, --page <number>", "Page number (default: 1)", "1").action(async (opts) => {
|
|
1572
|
+
requireAuth();
|
|
1573
|
+
const format = resolveFormat(program.opts());
|
|
1574
|
+
const client = createClient();
|
|
1575
|
+
const params = new URLSearchParams();
|
|
1576
|
+
if (opts.category) params.set("category", opts.category);
|
|
1577
|
+
if (opts.tag) params.set("tag", opts.tag);
|
|
1578
|
+
if (opts.search) params.set("search", opts.search);
|
|
1579
|
+
if (opts.transport) params.set("transport", opts.transport);
|
|
1580
|
+
const parsedLimit = parseInt(opts.limit, 10);
|
|
1581
|
+
params.set(
|
|
1582
|
+
"limit",
|
|
1583
|
+
String(
|
|
1584
|
+
Math.min(
|
|
1585
|
+
Math.max(Number.isFinite(parsedLimit) ? parsedLimit : 50, 1),
|
|
1586
|
+
100
|
|
1587
|
+
)
|
|
1588
|
+
)
|
|
1589
|
+
);
|
|
1590
|
+
const parsedPage = parseInt(opts.page, 10);
|
|
1591
|
+
params.set(
|
|
1592
|
+
"page",
|
|
1593
|
+
String(Math.max(Number.isFinite(parsedPage) ? parsedPage : 1, 1))
|
|
1594
|
+
);
|
|
1595
|
+
const { data } = await client.get(
|
|
1596
|
+
`/api/mcp-registry?${params.toString()}`
|
|
1597
|
+
);
|
|
1598
|
+
const servers = data.data;
|
|
1599
|
+
const pagination = data.meta?.pagination;
|
|
1600
|
+
if (format === "json") {
|
|
1601
|
+
output.log(JSON.stringify(data, null, 2));
|
|
1602
|
+
return;
|
|
1603
|
+
}
|
|
1604
|
+
if (servers.length === 0) {
|
|
1605
|
+
output.info("No MCP servers found.");
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
const totalItems = pagination?.totalItems ?? servers.length;
|
|
1609
|
+
output.log(output.bold(`MCP Servers (${totalItems} total):`));
|
|
1610
|
+
output.newline();
|
|
1611
|
+
const rows = servers.map((m) => ({
|
|
1612
|
+
ID: m.id,
|
|
1613
|
+
"Display Name": truncate3(m.displayName, 30),
|
|
1614
|
+
Category: m.category || "-",
|
|
1615
|
+
Transport: m.transportType || "-",
|
|
1616
|
+
Tools: String(m.tools?.length ?? 0),
|
|
1617
|
+
Status: formatValidationStatus(m.validationStatus)
|
|
1618
|
+
}));
|
|
1619
|
+
output.log(
|
|
1620
|
+
formatOutput(rows, format, {
|
|
1621
|
+
columns: { "Display Name": { maxWidth: 30 } }
|
|
1622
|
+
})
|
|
1623
|
+
);
|
|
1624
|
+
if (pagination && pagination.hasNext) {
|
|
1625
|
+
output.newline();
|
|
1626
|
+
output.dim(
|
|
1627
|
+
`Page ${pagination.currentPage} of ${pagination.totalPages}. Use --page ${pagination.currentPage + 1} to see more.`
|
|
1628
|
+
);
|
|
1629
|
+
}
|
|
1630
|
+
});
|
|
1631
|
+
mcp.command("list-tools <server-id>").description("List tools provided by an MCP server").action(async (serverId) => {
|
|
1632
|
+
requireAuth();
|
|
1633
|
+
const format = resolveFormat(program.opts());
|
|
1634
|
+
const client = createClient();
|
|
1635
|
+
let data;
|
|
1636
|
+
try {
|
|
1637
|
+
({ data } = await client.get(
|
|
1638
|
+
`/api/mcp-registry/${encodeURIComponent(serverId)}`
|
|
1639
|
+
));
|
|
1640
|
+
} catch (err) {
|
|
1641
|
+
if (err instanceof ApiError && err.status === 404) {
|
|
1642
|
+
output.error(`MCP server '${serverId}' not found.`);
|
|
1643
|
+
process.exitCode = 1;
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
throw err;
|
|
1647
|
+
}
|
|
1648
|
+
const server = data.data;
|
|
1649
|
+
if (format === "json") {
|
|
1650
|
+
output.log(JSON.stringify(server, null, 2));
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
output.log(output.bold(`${server.displayName} (${server.id})`));
|
|
1654
|
+
output.log(` Category: ${server.category || "-"}`);
|
|
1655
|
+
output.log(` Transport: ${server.transportType || "-"}`);
|
|
1656
|
+
output.log(` Author: ${server.author || "-"}`);
|
|
1657
|
+
output.log(
|
|
1658
|
+
` Status: ${formatValidationStatus(server.validationStatus)}`
|
|
1659
|
+
);
|
|
1660
|
+
output.newline();
|
|
1661
|
+
const tools = server.tools ?? [];
|
|
1662
|
+
if (tools.length === 0) {
|
|
1663
|
+
output.info("No tools defined for this MCP server.");
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
output.log(output.bold(`Tools (${tools.length}):`));
|
|
1667
|
+
output.newline();
|
|
1668
|
+
const rows = tools.map((t) => ({
|
|
1669
|
+
"Tool Name": t.name || "-",
|
|
1670
|
+
Description: truncate3(t.description || "", 60)
|
|
1671
|
+
}));
|
|
1672
|
+
output.log(
|
|
1673
|
+
formatOutput(rows, format, {
|
|
1674
|
+
columns: { Description: { maxWidth: 60 } }
|
|
1675
|
+
})
|
|
1676
|
+
);
|
|
1677
|
+
});
|
|
1678
|
+
}
|
|
1679
|
+
|
|
1680
|
+
// src/commands/plugin/index.ts
|
|
1681
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
1682
|
+
import { homedir as homedir8 } from "os";
|
|
1683
|
+
import { resolve as resolve6 } from "path";
|
|
1684
|
+
import chalk4 from "chalk";
|
|
1685
|
+
|
|
1686
|
+
// src/commands/plugin/adapters/claude-code.ts
|
|
1687
|
+
import { existsSync as existsSync5 } from "fs";
|
|
1688
|
+
import { homedir as homedir3 } from "os";
|
|
1689
|
+
import { resolve as resolve3 } from "path";
|
|
1690
|
+
|
|
1691
|
+
// src/commands/plugin/adapters/base.ts
|
|
1692
|
+
import {
|
|
1693
|
+
copyFileSync,
|
|
1694
|
+
existsSync as existsSync4,
|
|
1695
|
+
mkdirSync as mkdirSync2,
|
|
1696
|
+
readFileSync as readFileSync2,
|
|
1697
|
+
writeFileSync as writeFileSync2
|
|
1698
|
+
} from "fs";
|
|
1699
|
+
import { resolve as resolve2 } from "path";
|
|
1700
|
+
var BaseJsonAdapter = class {
|
|
1701
|
+
detect() {
|
|
1702
|
+
return existsSync4(this.configDir);
|
|
1703
|
+
}
|
|
1704
|
+
readConfig() {
|
|
1705
|
+
const file = this.configPath();
|
|
1706
|
+
if (!existsSync4(file)) return {};
|
|
1707
|
+
try {
|
|
1708
|
+
return JSON.parse(readFileSync2(file, "utf-8"));
|
|
1709
|
+
} catch (err) {
|
|
1710
|
+
throw new Error(
|
|
1711
|
+
`Failed to parse ${file}: ${err instanceof Error ? err.message : "invalid JSON"}`
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
validateConfig(config) {
|
|
1716
|
+
return config !== null && typeof config === "object" && !Array.isArray(config);
|
|
1717
|
+
}
|
|
1718
|
+
injectMcpConfig(config, apiUrl) {
|
|
1719
|
+
const mcpServers = config.mcpServers ?? {};
|
|
1720
|
+
mcpServers.rush = {
|
|
1721
|
+
command: "rush-ai",
|
|
1722
|
+
args: ["mcp", "serve"],
|
|
1723
|
+
env: {
|
|
1724
|
+
RUSH_API_URL: apiUrl
|
|
1725
|
+
}
|
|
1726
|
+
};
|
|
1727
|
+
return { ...config, mcpServers };
|
|
1728
|
+
}
|
|
1729
|
+
removeMcpConfig(config) {
|
|
1730
|
+
const mcpServers = config.mcpServers;
|
|
1731
|
+
if (mcpServers && "rush" in mcpServers) {
|
|
1732
|
+
delete mcpServers.rush;
|
|
1733
|
+
}
|
|
1734
|
+
return config;
|
|
1735
|
+
}
|
|
1736
|
+
writeConfig(config) {
|
|
1737
|
+
const file = this.configPath();
|
|
1738
|
+
if (!existsSync4(this.configDir)) {
|
|
1739
|
+
mkdirSync2(this.configDir, { recursive: true });
|
|
1740
|
+
}
|
|
1741
|
+
if (existsSync4(file)) {
|
|
1742
|
+
copyFileSync(file, `${file}.bak`);
|
|
1743
|
+
}
|
|
1744
|
+
writeFileSync2(file, JSON.stringify(config, null, 2), "utf-8");
|
|
1745
|
+
}
|
|
1746
|
+
configPath() {
|
|
1747
|
+
return resolve2(this.configDir, this.configFile);
|
|
1748
|
+
}
|
|
1749
|
+
};
|
|
1750
|
+
|
|
1751
|
+
// src/commands/plugin/adapters/claude-code.ts
|
|
1752
|
+
var ClaudeCodeAdapter = class extends BaseJsonAdapter {
|
|
1753
|
+
name = "claude-code";
|
|
1754
|
+
description = "Claude Code integration (MCP config + SKILL.md + commands)";
|
|
1755
|
+
version = "0.2.0";
|
|
1756
|
+
configDir = resolve3(homedir3(), ".claude");
|
|
1757
|
+
configFile = "settings.json";
|
|
1758
|
+
detect() {
|
|
1759
|
+
return existsSync5(this.configDir);
|
|
1760
|
+
}
|
|
1761
|
+
};
|
|
1762
|
+
|
|
1763
|
+
// src/commands/plugin/adapters/cursor.ts
|
|
1764
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1765
|
+
import { homedir as homedir4 } from "os";
|
|
1766
|
+
import { resolve as resolve4 } from "path";
|
|
1767
|
+
var CursorAdapter = class extends BaseJsonAdapter {
|
|
1768
|
+
name = "cursor";
|
|
1769
|
+
description = "Cursor integration (MCP config)";
|
|
1770
|
+
version = "0.2.0";
|
|
1771
|
+
configDir = resolve4(homedir4(), ".cursor");
|
|
1772
|
+
configFile = "mcp.json";
|
|
1773
|
+
detect() {
|
|
1774
|
+
return existsSync6(this.configDir);
|
|
1775
|
+
}
|
|
1776
|
+
};
|
|
1777
|
+
|
|
1778
|
+
// src/commands/plugin/adapters/index.ts
|
|
1779
|
+
var ADAPTER_REGISTRY = {
|
|
1780
|
+
"claude-code": () => new ClaudeCodeAdapter(),
|
|
1781
|
+
cursor: () => new CursorAdapter()
|
|
1782
|
+
};
|
|
1783
|
+
function getAdapter(name) {
|
|
1784
|
+
const factory = ADAPTER_REGISTRY[name];
|
|
1785
|
+
return factory ? factory() : null;
|
|
1786
|
+
}
|
|
1787
|
+
function getAvailableAdapters() {
|
|
1788
|
+
return Object.keys(ADAPTER_REGISTRY);
|
|
1789
|
+
}
|
|
1790
|
+
function getAdapterDescriptions() {
|
|
1791
|
+
return Object.entries(ADAPTER_REGISTRY).map(([name, factory]) => {
|
|
1792
|
+
const adapter = factory();
|
|
1793
|
+
return {
|
|
1794
|
+
name,
|
|
1795
|
+
description: adapter.description,
|
|
1796
|
+
version: adapter.version
|
|
1797
|
+
};
|
|
1798
|
+
});
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
// src/commands/plugin/assets.ts
|
|
1802
|
+
import { createHash } from "crypto";
|
|
1803
|
+
import {
|
|
1804
|
+
copyFileSync as copyFileSync2,
|
|
1805
|
+
existsSync as existsSync7,
|
|
1806
|
+
lstatSync,
|
|
1807
|
+
mkdirSync as mkdirSync3,
|
|
1808
|
+
readdirSync,
|
|
1809
|
+
readFileSync as readFileSync3,
|
|
1810
|
+
readlinkSync,
|
|
1811
|
+
rmSync,
|
|
1812
|
+
symlinkSync,
|
|
1813
|
+
unlinkSync,
|
|
1814
|
+
writeFileSync as writeFileSync3
|
|
1815
|
+
} from "fs";
|
|
1816
|
+
import { homedir as homedir5 } from "os";
|
|
1817
|
+
import { dirname as dirname2, join as join2, relative, resolve as resolve5 } from "path";
|
|
1818
|
+
var ASSET_SCHEMA_VERSION = 1;
|
|
1819
|
+
var PLUGINS_BASE = resolve5(homedir5(), ".rush", "plugins");
|
|
1820
|
+
function getAssetManifestPath(pluginName) {
|
|
1821
|
+
return resolve5(PLUGINS_BASE, pluginName, "asset-manifest.json");
|
|
1822
|
+
}
|
|
1823
|
+
function getAssetStorePath(pluginName) {
|
|
1824
|
+
return resolve5(PLUGINS_BASE, pluginName, "assets");
|
|
1825
|
+
}
|
|
1826
|
+
function computeChecksum(filePath) {
|
|
1827
|
+
const content = readFileSync3(filePath);
|
|
1828
|
+
return createHash("sha256").update(content).digest("hex");
|
|
1829
|
+
}
|
|
1830
|
+
function loadAssetManifest(pluginName) {
|
|
1831
|
+
const manifestPath = getAssetManifestPath(pluginName);
|
|
1832
|
+
if (!existsSync7(manifestPath)) return null;
|
|
1833
|
+
try {
|
|
1834
|
+
return JSON.parse(readFileSync3(manifestPath, "utf-8"));
|
|
1835
|
+
} catch {
|
|
1836
|
+
return null;
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
function installAssets(options) {
|
|
1840
|
+
const { pluginName, sourceDir, cliVersion, force = false } = options;
|
|
1841
|
+
const storePath = getAssetStorePath(pluginName);
|
|
1842
|
+
const manifestPath = getAssetManifestPath(pluginName);
|
|
1843
|
+
mkdirSync3(storePath, { recursive: true });
|
|
1844
|
+
const files = [];
|
|
1845
|
+
const existing = loadAssetManifest(pluginName);
|
|
1846
|
+
const sourceFiles = collectFiles(sourceDir);
|
|
1847
|
+
for (const relPath of sourceFiles) {
|
|
1848
|
+
const srcFile = resolve5(sourceDir, relPath);
|
|
1849
|
+
const destFile = resolve5(storePath, relPath);
|
|
1850
|
+
const newChecksum = computeChecksum(srcFile);
|
|
1851
|
+
if (existsSync7(destFile) && !force && existing) {
|
|
1852
|
+
const existingEntry = existing.files.find((f) => f.path === relPath);
|
|
1853
|
+
if (existingEntry) {
|
|
1854
|
+
const currentChecksum = computeChecksum(destFile);
|
|
1855
|
+
if (currentChecksum !== existingEntry.checksum && currentChecksum !== newChecksum) {
|
|
1856
|
+
output.warn(
|
|
1857
|
+
` Skipping ${relPath}: modified by user (use --force to overwrite)`
|
|
1858
|
+
);
|
|
1859
|
+
files.push({
|
|
1860
|
+
...existingEntry,
|
|
1861
|
+
checksum: currentChecksum
|
|
1862
|
+
});
|
|
1863
|
+
continue;
|
|
1864
|
+
}
|
|
1865
|
+
}
|
|
1866
|
+
}
|
|
1867
|
+
mkdirSync3(dirname2(destFile), { recursive: true });
|
|
1868
|
+
copyFileSync2(srcFile, destFile);
|
|
1869
|
+
files.push({
|
|
1870
|
+
path: relPath,
|
|
1871
|
+
checksum: newChecksum,
|
|
1872
|
+
target: destFile,
|
|
1873
|
+
type: "file"
|
|
1874
|
+
});
|
|
1875
|
+
}
|
|
1876
|
+
const skillTarget = resolve5(homedir5(), ".claude", "skills", "rush-task");
|
|
1877
|
+
if (!existsSync7(skillTarget)) {
|
|
1878
|
+
mkdirSync3(dirname2(skillTarget), { recursive: true });
|
|
1879
|
+
try {
|
|
1880
|
+
symlinkSync(storePath, skillTarget, "dir");
|
|
1881
|
+
files.push({
|
|
1882
|
+
path: "_symlink_skill",
|
|
1883
|
+
checksum: "",
|
|
1884
|
+
target: skillTarget,
|
|
1885
|
+
type: "symlink"
|
|
1886
|
+
});
|
|
1887
|
+
} catch {
|
|
1888
|
+
output.dim(
|
|
1889
|
+
` Note: Could not create symlink at ${skillTarget}. You may need to manually link the skill.`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
} else {
|
|
1893
|
+
try {
|
|
1894
|
+
const stat = lstatSync(skillTarget);
|
|
1895
|
+
if (stat.isSymbolicLink()) {
|
|
1896
|
+
const linkTarget = readlinkSync(skillTarget);
|
|
1897
|
+
if (linkTarget === storePath || resolve5(linkTarget) === storePath) {
|
|
1898
|
+
files.push({
|
|
1899
|
+
path: "_symlink_skill",
|
|
1900
|
+
checksum: "",
|
|
1901
|
+
target: skillTarget,
|
|
1902
|
+
type: "symlink"
|
|
1903
|
+
});
|
|
1904
|
+
} else {
|
|
1905
|
+
output.warn(
|
|
1906
|
+
` Skipping symlink at ${skillTarget}: already points to a different location (${linkTarget})`
|
|
1907
|
+
);
|
|
1908
|
+
}
|
|
1909
|
+
} else {
|
|
1910
|
+
output.warn(
|
|
1911
|
+
` Skipping symlink at ${skillTarget}: path already exists and is not a symlink`
|
|
1912
|
+
);
|
|
1913
|
+
}
|
|
1914
|
+
} catch {
|
|
1915
|
+
}
|
|
1916
|
+
}
|
|
1917
|
+
const manifest = {
|
|
1918
|
+
schema_version: ASSET_SCHEMA_VERSION,
|
|
1919
|
+
version: options.cliVersion,
|
|
1920
|
+
cli_version: cliVersion,
|
|
1921
|
+
owner: "rush-ai",
|
|
1922
|
+
files,
|
|
1923
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1924
|
+
};
|
|
1925
|
+
writeFileSync3(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
1926
|
+
return manifest;
|
|
1927
|
+
}
|
|
1928
|
+
var ALLOWED_DELETE_PREFIXES = [
|
|
1929
|
+
resolve5(homedir5(), ".rush", "plugins"),
|
|
1930
|
+
resolve5(homedir5(), ".claude", "skills")
|
|
1931
|
+
];
|
|
1932
|
+
function isPathAllowed(targetPath) {
|
|
1933
|
+
const resolved = resolve5(targetPath);
|
|
1934
|
+
return ALLOWED_DELETE_PREFIXES.some(
|
|
1935
|
+
(prefix) => resolved.startsWith(`${prefix}/`)
|
|
1936
|
+
);
|
|
1937
|
+
}
|
|
1938
|
+
function uninstallAssets(pluginName) {
|
|
1939
|
+
const manifest = loadAssetManifest(pluginName);
|
|
1940
|
+
if (!manifest) return;
|
|
1941
|
+
if (manifest.owner !== "rush-ai") {
|
|
1942
|
+
output.warn(
|
|
1943
|
+
` Skipping asset removal: manifest owner "${manifest.owner}" is not "rush-ai"`
|
|
1944
|
+
);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
for (const entry of manifest.files) {
|
|
1948
|
+
if (!existsSync7(entry.target)) continue;
|
|
1949
|
+
if (!isPathAllowed(entry.target)) {
|
|
1950
|
+
output.warn(
|
|
1951
|
+
` Refusing to delete ${entry.target}: outside allowed directories`
|
|
1952
|
+
);
|
|
1953
|
+
continue;
|
|
1954
|
+
}
|
|
1955
|
+
if (entry.type === "symlink") {
|
|
1956
|
+
try {
|
|
1957
|
+
const stat = lstatSync(entry.target);
|
|
1958
|
+
if (stat.isSymbolicLink()) {
|
|
1959
|
+
unlinkSync(entry.target);
|
|
1960
|
+
}
|
|
1961
|
+
} catch {
|
|
1962
|
+
}
|
|
1963
|
+
} else if (entry.type === "file") {
|
|
1964
|
+
try {
|
|
1965
|
+
const currentChecksum = computeChecksum(entry.target);
|
|
1966
|
+
if (currentChecksum === entry.checksum) {
|
|
1967
|
+
rmSync(entry.target);
|
|
1968
|
+
} else {
|
|
1969
|
+
output.warn(
|
|
1970
|
+
` Skipping ${entry.path}: file was modified after installation`
|
|
1971
|
+
);
|
|
1972
|
+
}
|
|
1973
|
+
} catch {
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
const manifestPath = getAssetManifestPath(pluginName);
|
|
1978
|
+
if (existsSync7(manifestPath)) {
|
|
1979
|
+
rmSync(manifestPath);
|
|
1980
|
+
}
|
|
1981
|
+
const storePath = getAssetStorePath(pluginName);
|
|
1982
|
+
try {
|
|
1983
|
+
const remaining = readdirSync(storePath);
|
|
1984
|
+
if (remaining.length === 0) {
|
|
1985
|
+
rmSync(storePath, { recursive: true });
|
|
1986
|
+
}
|
|
1987
|
+
} catch {
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
function collectFiles(dir, base) {
|
|
1991
|
+
const files = [];
|
|
1992
|
+
const baseDir = base ?? dir;
|
|
1993
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1994
|
+
const fullPath = join2(dir, entry.name);
|
|
1995
|
+
if (entry.isDirectory()) {
|
|
1996
|
+
files.push(...collectFiles(fullPath, baseDir));
|
|
1997
|
+
} else if (entry.isFile()) {
|
|
1998
|
+
files.push(relative(baseDir, fullPath));
|
|
1999
|
+
}
|
|
2000
|
+
}
|
|
2001
|
+
return files;
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
// src/commands/plugin/doctor.ts
|
|
2005
|
+
import { existsSync as existsSync8, readFileSync as readFileSync4 } from "fs";
|
|
2006
|
+
import { homedir as homedir6 } from "os";
|
|
2007
|
+
import path from "path";
|
|
2008
|
+
function findCliInPath() {
|
|
2009
|
+
const pathDirs = (process.env.PATH || "").split(path.delimiter);
|
|
2010
|
+
for (const dir of pathDirs) {
|
|
2011
|
+
const candidate = path.join(dir, "rush-ai");
|
|
2012
|
+
if (existsSync8(candidate)) return candidate;
|
|
2013
|
+
}
|
|
2014
|
+
return null;
|
|
2015
|
+
}
|
|
2016
|
+
function checkCliInPath() {
|
|
2017
|
+
const found = findCliInPath();
|
|
2018
|
+
if (found) {
|
|
2019
|
+
return {
|
|
2020
|
+
name: "cli_in_path",
|
|
2021
|
+
label: "CLI in PATH",
|
|
2022
|
+
status: "pass",
|
|
2023
|
+
detail: `rush-ai found at ${found}`
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
return {
|
|
2027
|
+
name: "cli_in_path",
|
|
2028
|
+
label: "CLI in PATH",
|
|
2029
|
+
status: "warn",
|
|
2030
|
+
detail: "rush-ai not found in PATH",
|
|
2031
|
+
fix: "Ensure rush-ai is installed globally or add its directory to PATH"
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
function checkAuth2() {
|
|
2035
|
+
try {
|
|
2036
|
+
const token = getAuthToken();
|
|
2037
|
+
if (token) {
|
|
2038
|
+
return {
|
|
2039
|
+
name: "auth_status",
|
|
2040
|
+
label: "Authentication",
|
|
2041
|
+
status: "pass",
|
|
2042
|
+
detail: "Authenticated"
|
|
2043
|
+
};
|
|
2044
|
+
}
|
|
2045
|
+
} catch {
|
|
2046
|
+
}
|
|
2047
|
+
return {
|
|
2048
|
+
name: "auth_status",
|
|
2049
|
+
label: "Authentication",
|
|
2050
|
+
status: "warn",
|
|
2051
|
+
detail: "Not authenticated",
|
|
2052
|
+
fix: "Run `rush-ai auth login` to authenticate"
|
|
2053
|
+
};
|
|
2054
|
+
}
|
|
2055
|
+
async function checkApiConnectivity() {
|
|
2056
|
+
try {
|
|
2057
|
+
const client = createClient();
|
|
2058
|
+
await client.get("/api/agents?limit=1");
|
|
2059
|
+
return {
|
|
2060
|
+
name: "api_connectivity",
|
|
2061
|
+
label: "API connectivity",
|
|
2062
|
+
status: "pass",
|
|
2063
|
+
detail: "API is reachable"
|
|
2064
|
+
};
|
|
2065
|
+
} catch {
|
|
2066
|
+
return {
|
|
2067
|
+
name: "api_connectivity",
|
|
2068
|
+
label: "API connectivity",
|
|
2069
|
+
status: "warn",
|
|
2070
|
+
detail: "API is unreachable",
|
|
2071
|
+
fix: "Check your network connection and API URL in ~/.rush/config.json"
|
|
2072
|
+
};
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
function checkPluginInstalled(ctx) {
|
|
2076
|
+
if (ctx.manifest) {
|
|
2077
|
+
return {
|
|
2078
|
+
name: "plugin_installed",
|
|
2079
|
+
label: "Plugin installed",
|
|
2080
|
+
status: "pass",
|
|
2081
|
+
detail: `v${ctx.manifest.version} (installed ${new Date(ctx.manifest.installedAt).toLocaleDateString()})`
|
|
2082
|
+
};
|
|
2083
|
+
}
|
|
2084
|
+
return {
|
|
2085
|
+
name: "plugin_installed",
|
|
2086
|
+
label: "Plugin installed",
|
|
2087
|
+
status: "fail",
|
|
2088
|
+
detail: "Not installed",
|
|
2089
|
+
fix: `Run \`rush-ai plugin install ${ctx.pluginName}\``
|
|
2090
|
+
};
|
|
2091
|
+
}
|
|
2092
|
+
function checkMcpConfig(ctx) {
|
|
2093
|
+
const type2 = ctx.manifest?.type ?? ctx.pluginInfo?.types?.[0];
|
|
2094
|
+
let configPath;
|
|
2095
|
+
if (type2 === "claude-code") {
|
|
2096
|
+
configPath = path.resolve(homedir6(), ".claude", "settings.json");
|
|
2097
|
+
} else if (type2 === "cursor") {
|
|
2098
|
+
configPath = path.resolve(homedir6(), ".cursor", "mcp.json");
|
|
2099
|
+
} else {
|
|
2100
|
+
return {
|
|
2101
|
+
name: "mcp_config",
|
|
2102
|
+
label: "MCP config",
|
|
2103
|
+
status: "warn",
|
|
2104
|
+
detail: `Unknown plugin type: ${type2}`
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
if (!existsSync8(configPath)) {
|
|
2108
|
+
return {
|
|
2109
|
+
name: "mcp_config",
|
|
2110
|
+
label: "MCP config",
|
|
2111
|
+
status: "fail",
|
|
2112
|
+
detail: `Config file not found: ${configPath}`,
|
|
2113
|
+
fix: `Run \`rush-ai plugin install ${ctx.pluginName}\``
|
|
2114
|
+
};
|
|
2115
|
+
}
|
|
2116
|
+
try {
|
|
2117
|
+
const config = JSON.parse(readFileSync4(configPath, "utf-8"));
|
|
2118
|
+
const servers = config.mcpServers;
|
|
2119
|
+
if (servers && "rush" in servers) {
|
|
2120
|
+
const rushServer = servers.rush;
|
|
2121
|
+
const command = rushServer?.command;
|
|
2122
|
+
if (command === "rush-ai") {
|
|
2123
|
+
return {
|
|
2124
|
+
name: "mcp_config",
|
|
2125
|
+
label: "MCP config",
|
|
2126
|
+
status: "pass",
|
|
2127
|
+
detail: "rush MCP server configured correctly"
|
|
2128
|
+
};
|
|
2129
|
+
}
|
|
2130
|
+
return {
|
|
2131
|
+
name: "mcp_config",
|
|
2132
|
+
label: "MCP config",
|
|
2133
|
+
status: "warn",
|
|
2134
|
+
detail: `rush server configured, but command is "${command}" (expected "rush-ai")`,
|
|
2135
|
+
fix: `Run \`rush-ai plugin update ${ctx.pluginName}\``
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
return {
|
|
2139
|
+
name: "mcp_config",
|
|
2140
|
+
label: "MCP config",
|
|
2141
|
+
status: "fail",
|
|
2142
|
+
detail: "rush MCP server not found in config",
|
|
2143
|
+
fix: `Run \`rush-ai plugin install ${ctx.pluginName}\``
|
|
2144
|
+
};
|
|
2145
|
+
} catch {
|
|
2146
|
+
return {
|
|
2147
|
+
name: "mcp_config",
|
|
2148
|
+
label: "MCP config",
|
|
2149
|
+
status: "fail",
|
|
2150
|
+
detail: `Failed to parse config: ${configPath}`,
|
|
2151
|
+
fix: "Check file for JSON syntax errors"
|
|
2152
|
+
};
|
|
2153
|
+
}
|
|
2154
|
+
}
|
|
2155
|
+
function checkVersionMatch(ctx) {
|
|
2156
|
+
if (!ctx.manifest || !ctx.pluginInfo) {
|
|
2157
|
+
return {
|
|
2158
|
+
name: "version_match",
|
|
2159
|
+
label: "Version",
|
|
2160
|
+
status: "warn",
|
|
2161
|
+
detail: "Cannot check version (plugin not installed or unknown)"
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
if (ctx.manifest.version === ctx.pluginInfo.version) {
|
|
2165
|
+
return {
|
|
2166
|
+
name: "version_match",
|
|
2167
|
+
label: "Version",
|
|
2168
|
+
status: "pass",
|
|
2169
|
+
detail: `Up to date (v${ctx.manifest.version})`
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
return {
|
|
2173
|
+
name: "version_match",
|
|
2174
|
+
label: "Version",
|
|
2175
|
+
status: "warn",
|
|
2176
|
+
detail: `Installed v${ctx.manifest.version}, latest v${ctx.pluginInfo.version}`,
|
|
2177
|
+
fix: `Run \`rush-ai plugin update ${ctx.pluginName}\``
|
|
2178
|
+
};
|
|
2179
|
+
}
|
|
2180
|
+
async function runDoctorChecks(ctx) {
|
|
2181
|
+
const results = [];
|
|
2182
|
+
results.push(checkCliInPath());
|
|
2183
|
+
results.push(checkAuth2());
|
|
2184
|
+
results.push(await checkApiConnectivity());
|
|
2185
|
+
results.push(checkPluginInstalled(ctx));
|
|
2186
|
+
results.push(checkMcpConfig(ctx));
|
|
2187
|
+
results.push(checkVersionMatch(ctx));
|
|
2188
|
+
return results;
|
|
2189
|
+
}
|
|
2190
|
+
|
|
2191
|
+
// src/commands/plugin/preflight.ts
|
|
2192
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync9, mkdirSync as mkdirSync4 } from "fs";
|
|
2193
|
+
import { homedir as homedir7 } from "os";
|
|
2194
|
+
import path2 from "path";
|
|
2195
|
+
import chalk3 from "chalk";
|
|
2196
|
+
async function runPreflightChecks(options) {
|
|
2197
|
+
const results = [];
|
|
2198
|
+
try {
|
|
2199
|
+
const token = getAuthToken();
|
|
2200
|
+
if (token) {
|
|
2201
|
+
results.push({
|
|
2202
|
+
name: "Authentication",
|
|
2203
|
+
status: "pass",
|
|
2204
|
+
detail: "Authenticated"
|
|
2205
|
+
});
|
|
2206
|
+
} else {
|
|
2207
|
+
results.push({
|
|
2208
|
+
name: "Authentication",
|
|
2209
|
+
status: options.strict ? "fail" : "warn",
|
|
2210
|
+
detail: "Not authenticated. MCP server may not work until you login."
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
} catch {
|
|
2214
|
+
results.push({
|
|
2215
|
+
name: "Authentication",
|
|
2216
|
+
status: options.strict ? "fail" : "warn",
|
|
2217
|
+
detail: "Not authenticated. MCP server may not work until you login."
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
try {
|
|
2221
|
+
const client = createClient();
|
|
2222
|
+
await client.get("/api/agents?limit=1");
|
|
2223
|
+
results.push({
|
|
2224
|
+
name: "API connectivity",
|
|
2225
|
+
status: "pass",
|
|
2226
|
+
detail: "API is reachable"
|
|
2227
|
+
});
|
|
2228
|
+
} catch {
|
|
2229
|
+
results.push({
|
|
2230
|
+
name: "API connectivity",
|
|
2231
|
+
status: options.strict ? "fail" : "warn",
|
|
2232
|
+
detail: "API is unreachable. MCP server may not work until API is reachable."
|
|
2233
|
+
});
|
|
2234
|
+
}
|
|
2235
|
+
const rushDir = path2.resolve(homedir7(), ".rush");
|
|
2236
|
+
try {
|
|
2237
|
+
if (!existsSync9(rushDir)) {
|
|
2238
|
+
mkdirSync4(rushDir, { recursive: true });
|
|
2239
|
+
}
|
|
2240
|
+
accessSync2(rushDir, constants2.W_OK);
|
|
2241
|
+
results.push({
|
|
2242
|
+
name: "Config directory",
|
|
2243
|
+
status: "pass",
|
|
2244
|
+
detail: `Config directory is writable: ${rushDir}`
|
|
2245
|
+
});
|
|
2246
|
+
} catch {
|
|
2247
|
+
results.push({
|
|
2248
|
+
name: "Config directory",
|
|
2249
|
+
status: "fail",
|
|
2250
|
+
detail: `Config directory is not writable: ${rushDir}`
|
|
2251
|
+
});
|
|
2252
|
+
}
|
|
2253
|
+
if (options.json) {
|
|
2254
|
+
output.log(JSON.stringify({ preflight: results }, null, 2));
|
|
2255
|
+
} else {
|
|
2256
|
+
output.info("Preflight checks...");
|
|
2257
|
+
for (const r of results) {
|
|
2258
|
+
const icon = r.status === "pass" ? chalk3.green("[PASS]") : r.status === "warn" ? chalk3.yellow("[WARN]") : chalk3.red("[FAIL]");
|
|
2259
|
+
output.log(` ${icon} ${r.name}: ${r.detail}`);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
return !results.some((r) => r.status === "fail");
|
|
2263
|
+
}
|
|
2264
|
+
|
|
2265
|
+
// src/commands/plugin/verify.ts
|
|
2266
|
+
import { execFileSync as execFileSync2 } from "child_process";
|
|
2267
|
+
import { existsSync as existsSync10 } from "fs";
|
|
2268
|
+
async function runVerification(options) {
|
|
2269
|
+
const results = [];
|
|
2270
|
+
results.push(checkConfigFile(options.adapter));
|
|
2271
|
+
const mcpResults = await verifyMcpConnectivity(options.mode);
|
|
2272
|
+
results.push(...mcpResults);
|
|
2273
|
+
results.push(checkAuthStatus());
|
|
2274
|
+
return results;
|
|
2275
|
+
}
|
|
2276
|
+
function checkConfigFile(adapter) {
|
|
2277
|
+
const configFile = adapter.configPath();
|
|
2278
|
+
if (!existsSync10(configFile)) {
|
|
2279
|
+
return {
|
|
2280
|
+
name: "config_file",
|
|
2281
|
+
label: "IDE config",
|
|
2282
|
+
stage: "config",
|
|
2283
|
+
status: "warn",
|
|
2284
|
+
detail: `Config file not found: ${configFile}`,
|
|
2285
|
+
fix: `Run \`rush-ai plugin install ${adapter.name}\``
|
|
2286
|
+
};
|
|
2287
|
+
}
|
|
2288
|
+
try {
|
|
2289
|
+
const config = adapter.readConfig();
|
|
2290
|
+
if (!adapter.validateConfig(config)) {
|
|
2291
|
+
return {
|
|
2292
|
+
name: "config_file",
|
|
2293
|
+
label: "IDE config",
|
|
2294
|
+
stage: "config",
|
|
2295
|
+
status: "fail",
|
|
2296
|
+
detail: `Invalid config format: ${configFile}`,
|
|
2297
|
+
fix: "Check file for JSON syntax errors"
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
const mcpServers = config.mcpServers;
|
|
2301
|
+
if (mcpServers && "rush" in mcpServers) {
|
|
2302
|
+
return {
|
|
2303
|
+
name: "config_file",
|
|
2304
|
+
label: "IDE config",
|
|
2305
|
+
stage: "config",
|
|
2306
|
+
status: "pass",
|
|
2307
|
+
detail: "Rush MCP server configured"
|
|
2308
|
+
};
|
|
2309
|
+
}
|
|
2310
|
+
return {
|
|
2311
|
+
name: "config_file",
|
|
2312
|
+
label: "IDE config",
|
|
2313
|
+
stage: "config",
|
|
2314
|
+
status: "fail",
|
|
2315
|
+
detail: "Rush MCP server not found in config",
|
|
2316
|
+
fix: `Run \`rush-ai plugin install ${adapter.name}\``
|
|
2317
|
+
};
|
|
2318
|
+
} catch {
|
|
2319
|
+
return {
|
|
2320
|
+
name: "config_file",
|
|
2321
|
+
label: "IDE config",
|
|
2322
|
+
stage: "config",
|
|
2323
|
+
status: "fail",
|
|
2324
|
+
detail: `Failed to read config: ${configFile}`,
|
|
2325
|
+
fix: "Check file for JSON syntax errors"
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
async function verifyMcpConnectivity(mode) {
|
|
2330
|
+
const results = [];
|
|
2331
|
+
try {
|
|
2332
|
+
execFileSync2("rush-ai", ["--version"], {
|
|
2333
|
+
timeout: 5e3,
|
|
2334
|
+
stdio: "pipe"
|
|
2335
|
+
});
|
|
2336
|
+
results.push({
|
|
2337
|
+
name: "mcp_cli_exec",
|
|
2338
|
+
label: "CLI executable",
|
|
2339
|
+
stage: "cli",
|
|
2340
|
+
status: "pass",
|
|
2341
|
+
detail: "rush-ai executable responds"
|
|
2342
|
+
});
|
|
2343
|
+
} catch (err) {
|
|
2344
|
+
results.push({
|
|
2345
|
+
name: "mcp_cli_exec",
|
|
2346
|
+
label: "CLI executable",
|
|
2347
|
+
stage: "cli",
|
|
2348
|
+
status: "fail",
|
|
2349
|
+
detail: `rush-ai failed to execute: ${err instanceof Error ? err.message : "unknown error"}`,
|
|
2350
|
+
fix: "Ensure rush-ai is installed and accessible in PATH"
|
|
2351
|
+
});
|
|
2352
|
+
return results;
|
|
2353
|
+
}
|
|
2354
|
+
try {
|
|
2355
|
+
const { createClient: createClient2 } = await import("./client-2GSYT7IS.js");
|
|
2356
|
+
const client = createClient2();
|
|
2357
|
+
await client.get("/api/agents?limit=1");
|
|
2358
|
+
results.push({
|
|
2359
|
+
name: "mcp_api",
|
|
2360
|
+
label: "API connectivity",
|
|
2361
|
+
stage: "api",
|
|
2362
|
+
status: "pass",
|
|
2363
|
+
detail: "Rush API is reachable"
|
|
2364
|
+
});
|
|
2365
|
+
} catch {
|
|
2366
|
+
results.push({
|
|
2367
|
+
name: "mcp_api",
|
|
2368
|
+
label: "API connectivity",
|
|
2369
|
+
stage: "api",
|
|
2370
|
+
status: mode === "install" ? "warn" : "fail",
|
|
2371
|
+
detail: mode === "install" ? "API unreachable (install will continue, but MCP tools may not work until resolved)" : "API unreachable",
|
|
2372
|
+
fix: "Check network connection or run `rush-ai auth login`"
|
|
2373
|
+
});
|
|
2374
|
+
}
|
|
2375
|
+
return results;
|
|
2376
|
+
}
|
|
2377
|
+
function checkAuthStatus() {
|
|
2378
|
+
try {
|
|
2379
|
+
const token = getAuthToken();
|
|
2380
|
+
if (token) {
|
|
2381
|
+
return {
|
|
2382
|
+
name: "auth_status",
|
|
2383
|
+
label: "Authentication",
|
|
2384
|
+
stage: "auth",
|
|
2385
|
+
status: "pass",
|
|
2386
|
+
detail: "Authenticated"
|
|
2387
|
+
};
|
|
2388
|
+
}
|
|
2389
|
+
} catch {
|
|
2390
|
+
}
|
|
2391
|
+
return {
|
|
2392
|
+
name: "auth_status",
|
|
2393
|
+
label: "Authentication",
|
|
2394
|
+
stage: "auth",
|
|
2395
|
+
status: "warn",
|
|
2396
|
+
detail: "Not authenticated",
|
|
2397
|
+
fix: "Run `rush-ai auth login` to authenticate"
|
|
2398
|
+
};
|
|
2399
|
+
}
|
|
2400
|
+
|
|
2401
|
+
// src/commands/plugin/index.ts
|
|
2402
|
+
var CURRENT_SCHEMA_VERSION = 2;
|
|
2403
|
+
var PLUGINS_DIR = resolve6(homedir8(), ".rush", "plugins");
|
|
2404
|
+
var PLUGINS_FILE2 = resolve6(PLUGINS_DIR, "installed.json");
|
|
2405
|
+
function safeReadJson2(filePath, fallback) {
|
|
2406
|
+
if (!existsSync11(filePath)) return fallback;
|
|
2407
|
+
try {
|
|
2408
|
+
return JSON.parse(readFileSync5(filePath, "utf-8"));
|
|
2409
|
+
} catch {
|
|
2410
|
+
return fallback;
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
function loadInstalledPlugins(autoMigrate = true) {
|
|
2414
|
+
const raw = safeReadJson2(PLUGINS_FILE2, {
|
|
2415
|
+
plugins: {}
|
|
2416
|
+
});
|
|
2417
|
+
const schemaVersion = raw.schema_version ?? 0;
|
|
2418
|
+
if (schemaVersion < CURRENT_SCHEMA_VERSION) {
|
|
2419
|
+
const migrated = migrateInstalledPlugins(raw);
|
|
2420
|
+
if (autoMigrate) {
|
|
2421
|
+
saveInstalledPlugins(migrated);
|
|
2422
|
+
}
|
|
2423
|
+
return migrated;
|
|
2424
|
+
}
|
|
2425
|
+
return raw;
|
|
2426
|
+
}
|
|
2427
|
+
function saveInstalledPlugins(data) {
|
|
2428
|
+
if (!existsSync11(PLUGINS_DIR)) {
|
|
2429
|
+
mkdirSync5(PLUGINS_DIR, { recursive: true });
|
|
2430
|
+
}
|
|
2431
|
+
writeFileSync4(PLUGINS_FILE2, JSON.stringify(data, null, 2), "utf-8");
|
|
2432
|
+
}
|
|
2433
|
+
function migrateInstalledPlugins(data) {
|
|
2434
|
+
const result = {
|
|
2435
|
+
schema_version: CURRENT_SCHEMA_VERSION,
|
|
2436
|
+
plugins: {}
|
|
2437
|
+
};
|
|
2438
|
+
const oldPlugins = data.plugins ?? {};
|
|
2439
|
+
for (const [name, manifest] of Object.entries(oldPlugins)) {
|
|
2440
|
+
result.plugins[name] = {
|
|
2441
|
+
name: manifest.name ?? name,
|
|
2442
|
+
version: manifest.version ?? "0.1.0",
|
|
2443
|
+
type: manifest.type ?? name,
|
|
2444
|
+
installedAt: manifest.installedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
2445
|
+
cli_version: manifest.cli_version ?? "unknown",
|
|
2446
|
+
adapter: manifest.adapter ?? (manifest.type === "claude-code" ? "claude-code" : manifest.type ?? name)
|
|
2447
|
+
};
|
|
2448
|
+
}
|
|
2449
|
+
return result;
|
|
2450
|
+
}
|
|
2451
|
+
function resolvePluginAssetsDir() {
|
|
2452
|
+
const baseDir = import.meta.dirname ?? __dirname;
|
|
2453
|
+
if (!baseDir) {
|
|
2454
|
+
output.dim(
|
|
2455
|
+
" Warning: Could not resolve plugin assets directory (import.meta.dirname and __dirname are both unavailable)"
|
|
2456
|
+
);
|
|
2457
|
+
return null;
|
|
2458
|
+
}
|
|
2459
|
+
const bundled = resolve6(baseDir, "plugin-assets");
|
|
2460
|
+
if (existsSync11(bundled)) return bundled;
|
|
2461
|
+
const monorepo = resolve6(baseDir, "..", "..", "..", "..", "rush-plugin");
|
|
2462
|
+
if (existsSync11(monorepo)) return monorepo;
|
|
2463
|
+
return null;
|
|
2464
|
+
}
|
|
2465
|
+
function getCliVersion() {
|
|
2466
|
+
try {
|
|
2467
|
+
const pkgPath = resolve6(
|
|
2468
|
+
import.meta.dirname ?? __dirname,
|
|
2469
|
+
"..",
|
|
2470
|
+
"..",
|
|
2471
|
+
"package.json"
|
|
2472
|
+
);
|
|
2473
|
+
const pkg = JSON.parse(readFileSync5(pkgPath, "utf-8"));
|
|
2474
|
+
return pkg.version ?? "0.2.0";
|
|
2475
|
+
} catch {
|
|
2476
|
+
return "0.2.0";
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
function registerPluginCommand(program) {
|
|
2480
|
+
const plugin = program.command("plugin").description("Manage IDE plugins");
|
|
2481
|
+
plugin.command("list").alias("ls").description("List available and installed plugins").action(() => {
|
|
2482
|
+
const format = resolveFormat(program.opts());
|
|
2483
|
+
const installed = loadInstalledPlugins(false);
|
|
2484
|
+
const adapters = getAdapterDescriptions();
|
|
2485
|
+
const plugins = adapters.map((a) => ({
|
|
2486
|
+
name: a.name,
|
|
2487
|
+
description: a.description,
|
|
2488
|
+
version: a.version,
|
|
2489
|
+
installed: a.name in installed.plugins,
|
|
2490
|
+
installedVersion: installed.plugins[a.name]?.version ?? null
|
|
2491
|
+
}));
|
|
2492
|
+
if (format === "json") {
|
|
2493
|
+
output.log(JSON.stringify({ plugins }, null, 2));
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
output.log(output.bold("Available plugins:"));
|
|
2497
|
+
output.newline();
|
|
2498
|
+
const rows = plugins.map((p) => ({
|
|
2499
|
+
Name: p.name,
|
|
2500
|
+
Version: p.version,
|
|
2501
|
+
Status: p.installed ? "installed" : "available",
|
|
2502
|
+
Description: p.description
|
|
2503
|
+
}));
|
|
2504
|
+
output.log(
|
|
2505
|
+
formatOutput(rows, format, {
|
|
2506
|
+
columns: { Description: { maxWidth: 60 } }
|
|
2507
|
+
})
|
|
2508
|
+
);
|
|
2509
|
+
});
|
|
2510
|
+
plugin.command("install").description("Install a plugin").argument("<name>", `Plugin name (${getAvailableAdapters().join(" | ")})`).option("--force", "Skip preflight checks and overwrite modified assets").option("--strict-preflight", "Fail on preflight warnings (for CI)").action(
|
|
2511
|
+
async (name, options) => {
|
|
2512
|
+
const format = resolveFormat(program.opts());
|
|
2513
|
+
const adapter = getAdapter(name);
|
|
2514
|
+
if (!adapter) {
|
|
2515
|
+
throw new RushError(
|
|
2516
|
+
`Unknown plugin: ${name}. Available: ${getAvailableAdapters().join(", ")}`
|
|
2517
|
+
);
|
|
2518
|
+
}
|
|
2519
|
+
const installed = loadInstalledPlugins();
|
|
2520
|
+
if (installed.plugins[name]) {
|
|
2521
|
+
if (format === "json") {
|
|
2522
|
+
output.log(JSON.stringify({ status: "already_installed", name }));
|
|
2523
|
+
} else {
|
|
2524
|
+
output.warn(`Plugin "${name}" is already installed.`);
|
|
2525
|
+
output.dim("Use `rush-ai plugin update` to update.");
|
|
2526
|
+
}
|
|
2527
|
+
return;
|
|
2528
|
+
}
|
|
2529
|
+
if (!options.force) {
|
|
2530
|
+
const preflightOk = await runPreflightChecks({
|
|
2531
|
+
strict: options.strictPreflight ?? false,
|
|
2532
|
+
json: format === "json"
|
|
2533
|
+
});
|
|
2534
|
+
if (!preflightOk) {
|
|
2535
|
+
output.newline();
|
|
2536
|
+
output.dim("Use --force to skip preflight checks.");
|
|
2537
|
+
process.exit(1);
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
const config = getGlobalConfig();
|
|
2541
|
+
const existing = adapter.readConfig();
|
|
2542
|
+
const updated = adapter.injectMcpConfig(existing, config.api);
|
|
2543
|
+
adapter.writeConfig(updated);
|
|
2544
|
+
output.dim(` MCP config injected into ${adapter.configPath()}`);
|
|
2545
|
+
const cliVersion = getCliVersion();
|
|
2546
|
+
if (name === "claude-code") {
|
|
2547
|
+
const assetsDir = resolvePluginAssetsDir();
|
|
2548
|
+
if (assetsDir) {
|
|
2549
|
+
installAssets({
|
|
2550
|
+
pluginName: name,
|
|
2551
|
+
sourceDir: assetsDir,
|
|
2552
|
+
cliVersion,
|
|
2553
|
+
force: options.force
|
|
2554
|
+
});
|
|
2555
|
+
output.dim(" Plugin assets installed");
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
installed.plugins[name] = {
|
|
2559
|
+
name,
|
|
2560
|
+
version: adapter.version,
|
|
2561
|
+
type: name,
|
|
2562
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2563
|
+
cli_version: cliVersion,
|
|
2564
|
+
adapter: adapter.name
|
|
2565
|
+
};
|
|
2566
|
+
saveInstalledPlugins(installed);
|
|
2567
|
+
if (format !== "json") {
|
|
2568
|
+
output.newline();
|
|
2569
|
+
output.info("Post-install verification...");
|
|
2570
|
+
const verifyResults = await runVerification({
|
|
2571
|
+
mode: "install",
|
|
2572
|
+
pluginName: name,
|
|
2573
|
+
adapter
|
|
2574
|
+
});
|
|
2575
|
+
printVerifyResults(verifyResults);
|
|
2576
|
+
const hasFail = verifyResults.some((r) => r.status === "fail");
|
|
2577
|
+
if (hasFail) {
|
|
2578
|
+
output.newline();
|
|
2579
|
+
output.warn(
|
|
2580
|
+
"Some checks failed. Plugin is installed but may not work correctly."
|
|
2581
|
+
);
|
|
2582
|
+
output.dim(
|
|
2583
|
+
"Run `rush-ai plugin doctor` for detailed diagnostics."
|
|
2584
|
+
);
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
if (format === "json") {
|
|
2588
|
+
output.log(
|
|
2589
|
+
JSON.stringify({
|
|
2590
|
+
status: "installed",
|
|
2591
|
+
name,
|
|
2592
|
+
version: adapter.version
|
|
2593
|
+
})
|
|
2594
|
+
);
|
|
2595
|
+
} else {
|
|
2596
|
+
output.newline();
|
|
2597
|
+
output.success(`Plugin "${name}" installed.`);
|
|
2598
|
+
}
|
|
2599
|
+
}
|
|
2600
|
+
);
|
|
2601
|
+
plugin.command("status").description("Show plugin status").argument("<name>", "Plugin name").action((name) => {
|
|
2602
|
+
const format = resolveFormat(program.opts());
|
|
2603
|
+
const installed = loadInstalledPlugins(false);
|
|
2604
|
+
const manifest = installed.plugins[name];
|
|
2605
|
+
if (format === "json") {
|
|
2606
|
+
output.log(
|
|
2607
|
+
JSON.stringify(
|
|
2608
|
+
manifest ? { ...manifest, installed: true } : { name, installed: false }
|
|
2609
|
+
)
|
|
2610
|
+
);
|
|
2611
|
+
} else if (manifest) {
|
|
2612
|
+
output.log(output.bold(name));
|
|
2613
|
+
output.log(` Version: ${manifest.version}`);
|
|
2614
|
+
output.log(` Type: ${manifest.type}`);
|
|
2615
|
+
output.log(` CLI version: ${manifest.cli_version}`);
|
|
2616
|
+
output.log(` Adapter: ${manifest.adapter}`);
|
|
2617
|
+
output.log(
|
|
2618
|
+
` Installed: ${new Date(manifest.installedAt).toLocaleString()}`
|
|
2619
|
+
);
|
|
2620
|
+
} else {
|
|
2621
|
+
output.warn(`Plugin "${name}" is not installed.`);
|
|
2622
|
+
}
|
|
2623
|
+
});
|
|
2624
|
+
plugin.command("update").description("Update a plugin").argument("<name>", "Plugin name").option("--force", "Overwrite modified assets").action(async (name, options) => {
|
|
2625
|
+
const format = resolveFormat(program.opts());
|
|
2626
|
+
const installed = loadInstalledPlugins();
|
|
2627
|
+
const manifest = installed.plugins[name];
|
|
2628
|
+
if (!manifest) {
|
|
2629
|
+
throw new RushError(`Plugin "${name}" is not installed.`);
|
|
2630
|
+
}
|
|
2631
|
+
const adapter = getAdapter(name);
|
|
2632
|
+
if (!adapter) {
|
|
2633
|
+
throw new RushError(`Unknown plugin: ${name}`);
|
|
2634
|
+
}
|
|
2635
|
+
const config = getGlobalConfig();
|
|
2636
|
+
const existing = adapter.readConfig();
|
|
2637
|
+
const updated = adapter.injectMcpConfig(existing, config.api);
|
|
2638
|
+
adapter.writeConfig(updated);
|
|
2639
|
+
const cliVersion = getCliVersion();
|
|
2640
|
+
if (name === "claude-code") {
|
|
2641
|
+
const assetsDir = resolvePluginAssetsDir();
|
|
2642
|
+
if (assetsDir) {
|
|
2643
|
+
installAssets({
|
|
2644
|
+
pluginName: name,
|
|
2645
|
+
sourceDir: assetsDir,
|
|
2646
|
+
cliVersion,
|
|
2647
|
+
force: options.force
|
|
2648
|
+
});
|
|
2649
|
+
}
|
|
2650
|
+
}
|
|
2651
|
+
installed.plugins[name] = {
|
|
2652
|
+
...manifest,
|
|
2653
|
+
version: adapter.version,
|
|
2654
|
+
installedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2655
|
+
cli_version: cliVersion,
|
|
2656
|
+
adapter: adapter.name
|
|
2657
|
+
};
|
|
2658
|
+
saveInstalledPlugins(installed);
|
|
2659
|
+
if (format === "json") {
|
|
2660
|
+
output.log(
|
|
2661
|
+
JSON.stringify({
|
|
2662
|
+
status: "updated",
|
|
2663
|
+
name,
|
|
2664
|
+
version: adapter.version
|
|
2665
|
+
})
|
|
2666
|
+
);
|
|
2667
|
+
} else {
|
|
2668
|
+
output.success(`Plugin "${name}" updated to v${adapter.version}.`);
|
|
2669
|
+
}
|
|
2670
|
+
});
|
|
2671
|
+
plugin.command("uninstall").description("Uninstall a plugin").argument("<name>", "Plugin name").action((name) => {
|
|
2672
|
+
const format = resolveFormat(program.opts());
|
|
2673
|
+
const installed = loadInstalledPlugins();
|
|
2674
|
+
if (!installed.plugins[name]) {
|
|
2675
|
+
if (format === "json") {
|
|
2676
|
+
output.log(JSON.stringify({ status: "not_installed", name }));
|
|
2677
|
+
} else {
|
|
2678
|
+
output.warn(`Plugin "${name}" is not installed.`);
|
|
2679
|
+
}
|
|
2680
|
+
return;
|
|
2681
|
+
}
|
|
2682
|
+
const adapter = getAdapter(name);
|
|
2683
|
+
if (adapter) {
|
|
2684
|
+
const config = adapter.readConfig();
|
|
2685
|
+
const cleaned = adapter.removeMcpConfig(config);
|
|
2686
|
+
adapter.writeConfig(cleaned);
|
|
2687
|
+
}
|
|
2688
|
+
uninstallAssets(name);
|
|
2689
|
+
delete installed.plugins[name];
|
|
2690
|
+
saveInstalledPlugins(installed);
|
|
2691
|
+
if (format === "json") {
|
|
2692
|
+
output.log(JSON.stringify({ status: "uninstalled", name }));
|
|
2693
|
+
} else {
|
|
2694
|
+
output.success(`Plugin "${name}" uninstalled.`);
|
|
2695
|
+
}
|
|
2696
|
+
});
|
|
2697
|
+
plugin.command("doctor").description("Diagnose plugin configuration issues").argument("[name]", "Plugin name (diagnose specific plugin)").action(async (name) => {
|
|
2698
|
+
const format = resolveFormat(program.opts());
|
|
2699
|
+
const installed = loadInstalledPlugins();
|
|
2700
|
+
const pluginNames = name ? [name] : Object.keys(installed.plugins).length > 0 ? Object.keys(installed.plugins) : getAvailableAdapters();
|
|
2701
|
+
if (format === "json") {
|
|
2702
|
+
const jsonOutput = [];
|
|
2703
|
+
for (const pluginName of pluginNames) {
|
|
2704
|
+
const adapter = getAdapter(pluginName);
|
|
2705
|
+
if (adapter) {
|
|
2706
|
+
const results = await runVerification({
|
|
2707
|
+
mode: "doctor",
|
|
2708
|
+
pluginName,
|
|
2709
|
+
adapter
|
|
2710
|
+
});
|
|
2711
|
+
const summary = { pass: 0, warn: 0, fail: 0 };
|
|
2712
|
+
for (const r of results) summary[r.status]++;
|
|
2713
|
+
jsonOutput.push({
|
|
2714
|
+
plugin: pluginName,
|
|
2715
|
+
checks: results.map((r) => ({
|
|
2716
|
+
name: r.name,
|
|
2717
|
+
stage: r.stage,
|
|
2718
|
+
status: r.status,
|
|
2719
|
+
detail: r.detail,
|
|
2720
|
+
...r.fix ? { fix: r.fix } : {}
|
|
2721
|
+
})),
|
|
2722
|
+
summary
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
}
|
|
2726
|
+
output.log(
|
|
2727
|
+
JSON.stringify(
|
|
2728
|
+
jsonOutput.length === 1 ? jsonOutput[0] : jsonOutput,
|
|
2729
|
+
null,
|
|
2730
|
+
2
|
|
2731
|
+
)
|
|
2732
|
+
);
|
|
2733
|
+
return;
|
|
2734
|
+
}
|
|
2735
|
+
for (const pluginName of pluginNames) {
|
|
2736
|
+
const manifest = installed.plugins[pluginName];
|
|
2737
|
+
const adapter = getAdapter(pluginName);
|
|
2738
|
+
output.log(output.bold(`Plugin Doctor \u2014 ${pluginName}`));
|
|
2739
|
+
output.newline();
|
|
2740
|
+
const checks = await runDoctorChecks({
|
|
2741
|
+
pluginName,
|
|
2742
|
+
manifest: manifest ?? null,
|
|
2743
|
+
pluginInfo: adapter ? {
|
|
2744
|
+
description: adapter.description,
|
|
2745
|
+
types: [adapter.name],
|
|
2746
|
+
version: adapter.version
|
|
2747
|
+
} : null
|
|
2748
|
+
});
|
|
2749
|
+
let pass = 0;
|
|
2750
|
+
let warn = 0;
|
|
2751
|
+
let fail = 0;
|
|
2752
|
+
for (const check of checks) {
|
|
2753
|
+
const icon = check.status === "pass" ? chalk4.green("[PASS]") : check.status === "warn" ? chalk4.yellow("[WARN]") : chalk4.red("[FAIL]");
|
|
2754
|
+
output.log(` ${icon} ${check.label}: ${check.detail}`);
|
|
2755
|
+
if (check.fix) {
|
|
2756
|
+
output.dim(` Fix: ${check.fix}`);
|
|
2757
|
+
}
|
|
2758
|
+
if (check.status === "pass") pass++;
|
|
2759
|
+
else if (check.status === "warn") warn++;
|
|
2760
|
+
else fail++;
|
|
2761
|
+
}
|
|
2762
|
+
output.newline();
|
|
2763
|
+
const parts = [];
|
|
2764
|
+
if (pass > 0) parts.push(`${pass} passed`);
|
|
2765
|
+
if (warn > 0) parts.push(`${warn} warning${warn > 1 ? "s" : ""}`);
|
|
2766
|
+
if (fail > 0) parts.push(`${fail} failed`);
|
|
2767
|
+
output.log(parts.join(", "));
|
|
2768
|
+
output.newline();
|
|
2769
|
+
}
|
|
2770
|
+
});
|
|
2771
|
+
}
|
|
2772
|
+
function printVerifyResults(results) {
|
|
2773
|
+
for (const r of results) {
|
|
2774
|
+
const icon = r.status === "pass" ? chalk4.green("[PASS]") : r.status === "warn" ? chalk4.yellow("[WARN]") : chalk4.red("[FAIL]");
|
|
2775
|
+
output.log(` ${icon} ${r.label}: ${r.detail}`);
|
|
2776
|
+
if (r.fix) {
|
|
2777
|
+
output.dim(` Fix: ${r.fix}`);
|
|
2778
|
+
}
|
|
2779
|
+
}
|
|
2780
|
+
}
|
|
2781
|
+
|
|
2782
|
+
// src/commands/task/index.ts
|
|
2783
|
+
import { createWriteStream } from "fs";
|
|
2784
|
+
import { mkdir as mkdir2 } from "fs/promises";
|
|
2785
|
+
import path3 from "path";
|
|
2786
|
+
import { Readable } from "stream";
|
|
2787
|
+
import { pipeline } from "stream/promises";
|
|
2788
|
+
|
|
2789
|
+
// src/output/diff.ts
|
|
2790
|
+
import chalk5 from "chalk";
|
|
2791
|
+
function containsDiff(text) {
|
|
2792
|
+
return /^@@\s+-\d+/m.test(text) || /^---\s+a\//m.test(text);
|
|
2793
|
+
}
|
|
2794
|
+
function colorizeDiff(text) {
|
|
2795
|
+
return text.split("\n").map((line) => {
|
|
2796
|
+
if (line.startsWith("+++") || line.startsWith("---"))
|
|
2797
|
+
return chalk5.bold(line);
|
|
2798
|
+
if (line.startsWith("+")) return chalk5.green(line);
|
|
2799
|
+
if (line.startsWith("-")) return chalk5.red(line);
|
|
2800
|
+
if (line.startsWith("@@")) return chalk5.cyan(line);
|
|
2801
|
+
return line;
|
|
2802
|
+
}).join("\n");
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2805
|
+
// src/util/ci.ts
|
|
2806
|
+
function isEnvTruthy(value) {
|
|
2807
|
+
if (!value) return false;
|
|
2808
|
+
return ["1", "true", "yes"].includes(value.trim().toLowerCase());
|
|
2809
|
+
}
|
|
2810
|
+
function isCIMode(programOpts) {
|
|
2811
|
+
if (process.argv.includes("--ci")) return true;
|
|
2812
|
+
if (programOpts?.ci) return true;
|
|
2813
|
+
if (isEnvTruthy(process.env.CI)) return true;
|
|
2814
|
+
if (isEnvTruthy(process.env.RUSH_CI)) return true;
|
|
2815
|
+
return false;
|
|
2816
|
+
}
|
|
2817
|
+
|
|
2818
|
+
// src/util/stdin.ts
|
|
2819
|
+
async function readStdinIfPiped() {
|
|
2820
|
+
if (process.stdin.isTTY) {
|
|
2821
|
+
return null;
|
|
2822
|
+
}
|
|
2823
|
+
return new Promise((resolve7, reject) => {
|
|
2824
|
+
const chunks = [];
|
|
2825
|
+
process.stdin.on("data", (chunk) => {
|
|
2826
|
+
chunks.push(chunk);
|
|
2827
|
+
});
|
|
2828
|
+
process.stdin.on("end", () => {
|
|
2829
|
+
resolve7(Buffer.concat(chunks).toString("utf-8").trim());
|
|
2830
|
+
});
|
|
2831
|
+
process.stdin.on("error", reject);
|
|
2832
|
+
});
|
|
2833
|
+
}
|
|
2834
|
+
|
|
2835
|
+
// src/commands/task/index.ts
|
|
2836
|
+
var VALID_CATEGORIES = ["code", "image", "document", "other"];
|
|
2837
|
+
function sanitizeFilePath(filePath, outputDir) {
|
|
2838
|
+
let stripped = filePath.replace(/^[/\\]+/, "").replace(/^[a-zA-Z]:/, "");
|
|
2839
|
+
stripped = stripped.split("/").filter((seg) => seg !== ".." && seg !== ".").join("/");
|
|
2840
|
+
if (!stripped) return null;
|
|
2841
|
+
const resolvedOutput = path3.resolve(outputDir);
|
|
2842
|
+
const target = path3.resolve(resolvedOutput, stripped);
|
|
2843
|
+
const rel = path3.relative(resolvedOutput, target);
|
|
2844
|
+
if (rel.startsWith("..") || path3.isAbsolute(rel)) {
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
return target;
|
|
2848
|
+
}
|
|
2849
|
+
async function downloadFile(url, destPath) {
|
|
2850
|
+
const response = await fetch(url);
|
|
2851
|
+
if (!response.ok) {
|
|
2852
|
+
throw new Error(
|
|
2853
|
+
`Download failed: ${response.status} ${response.statusText}`
|
|
2854
|
+
);
|
|
2855
|
+
}
|
|
2856
|
+
if (!response.body) {
|
|
2857
|
+
throw new Error("No response body");
|
|
2858
|
+
}
|
|
2859
|
+
await mkdir2(path3.dirname(destPath), { recursive: true });
|
|
2860
|
+
const nodeStream = Readable.fromWeb(
|
|
2861
|
+
response.body
|
|
2862
|
+
);
|
|
2863
|
+
const fileStream = createWriteStream(destPath);
|
|
2864
|
+
await pipeline(nodeStream, fileStream);
|
|
2865
|
+
}
|
|
2866
|
+
function buildCategorySummary(files) {
|
|
2867
|
+
const counts = {};
|
|
2868
|
+
for (const f of files) {
|
|
2869
|
+
counts[f.category] = (counts[f.category] || 0) + 1;
|
|
2870
|
+
}
|
|
2871
|
+
return Object.entries(counts).map(([cat, count]) => `${count} ${cat}`).join(", ");
|
|
2872
|
+
}
|
|
2873
|
+
function registerTaskCommand(program) {
|
|
2874
|
+
const task = program.command("task").description("Create and manage tasks");
|
|
2875
|
+
task.command("run").description("Run a task synchronously").requiredOption("-a, --agent <name>", "Agent name to execute the task").option("-p, --prompt <text>", "Task prompt").option("--skills <skills>", "Comma-separated skills to add", commaSplit).option("--mcp <servers>", "Comma-separated MCP servers to add", commaSplit).option("--stream", "Stream output in real-time").option("--timeout <seconds>", "Timeout in seconds", "300").action(
|
|
2876
|
+
async (options) => {
|
|
2877
|
+
requireAuth();
|
|
2878
|
+
const format = resolveFormat(program.opts());
|
|
2879
|
+
const client = createClient();
|
|
2880
|
+
const prompt = options.prompt ?? await readStdinIfPiped();
|
|
2881
|
+
if (!prompt) {
|
|
2882
|
+
throw new RushError(
|
|
2883
|
+
"No prompt provided. Use --prompt or pipe via stdin."
|
|
2884
|
+
);
|
|
2885
|
+
}
|
|
2886
|
+
if (options.stream) {
|
|
2887
|
+
const response = await client.fetchRaw("/api/agent/invoke", {
|
|
2888
|
+
method: "POST",
|
|
2889
|
+
body: JSON.stringify({
|
|
2890
|
+
agent: options.agent,
|
|
2891
|
+
prompt,
|
|
2892
|
+
skills: options.skills,
|
|
2893
|
+
mcpServers: options.mcp,
|
|
2894
|
+
stream: true
|
|
2895
|
+
})
|
|
2896
|
+
});
|
|
2897
|
+
if (!response.body) {
|
|
2898
|
+
throw new RushError("No response body for streaming");
|
|
2899
|
+
}
|
|
2900
|
+
let streamError = null;
|
|
2901
|
+
await consumeSSEStream(response.body, (event) => {
|
|
2902
|
+
if (format === "json") {
|
|
2903
|
+
output.log(JSON.stringify(event));
|
|
2904
|
+
} else if (event.type === "content") {
|
|
2905
|
+
process.stdout.write(event.data);
|
|
2906
|
+
} else if (event.type === "error") {
|
|
2907
|
+
streamError = event.data;
|
|
2908
|
+
output.error(event.data);
|
|
2909
|
+
} else if (event.type === "raw") {
|
|
2910
|
+
process.stdout.write(event.data);
|
|
2911
|
+
}
|
|
2912
|
+
});
|
|
2913
|
+
if (format !== "json") {
|
|
2914
|
+
output.newline();
|
|
2915
|
+
}
|
|
2916
|
+
if (streamError) {
|
|
2917
|
+
throw new TaskFailedError(`Task failed: ${streamError}`, {
|
|
2918
|
+
stream: true
|
|
2919
|
+
});
|
|
2920
|
+
}
|
|
2921
|
+
return;
|
|
2922
|
+
}
|
|
2923
|
+
const { data } = await client.post(
|
|
2924
|
+
"/api/agent/invoke",
|
|
2925
|
+
{
|
|
2926
|
+
agent: options.agent,
|
|
2927
|
+
prompt,
|
|
2928
|
+
skills: options.skills,
|
|
2929
|
+
mcpServers: options.mcp,
|
|
2930
|
+
timeout: Number(options.timeout)
|
|
2931
|
+
}
|
|
2932
|
+
);
|
|
2933
|
+
if (data.status === "failed") {
|
|
2934
|
+
throw new TaskFailedError(
|
|
2935
|
+
`Task ${data.id} failed${data.result?.content ? `: ${data.result.content}` : ""}`,
|
|
2936
|
+
{ taskId: data.id }
|
|
2937
|
+
);
|
|
2938
|
+
}
|
|
2939
|
+
if (format === "json") {
|
|
2940
|
+
output.log(JSON.stringify(data, null, 2));
|
|
2941
|
+
} else {
|
|
2942
|
+
output.success(`Task ${data.id} completed.`);
|
|
2943
|
+
if (data.duration) {
|
|
2944
|
+
output.dim(` Duration: ${(data.duration / 1e3).toFixed(1)}s`);
|
|
2945
|
+
}
|
|
2946
|
+
output.newline();
|
|
2947
|
+
if (data.result?.content) {
|
|
2948
|
+
const content = data.result.content;
|
|
2949
|
+
output.log(containsDiff(content) ? colorizeDiff(content) : content);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
);
|
|
2954
|
+
task.command("create").description("Create a task asynchronously").requiredOption("-a, --agent <name>", "Agent name to execute the task").option("-p, --prompt <text>", "Task prompt").option("--skills <skills>", "Comma-separated skills to add", commaSplit).option("--mcp <servers>", "Comma-separated MCP servers to add", commaSplit).action(
|
|
2955
|
+
async (options) => {
|
|
2956
|
+
requireAuth();
|
|
2957
|
+
const format = resolveFormat(program.opts());
|
|
2958
|
+
const client = createClient();
|
|
2959
|
+
const prompt = options.prompt ?? await readStdinIfPiped();
|
|
2960
|
+
if (!prompt) {
|
|
2961
|
+
throw new RushError(
|
|
2962
|
+
"No prompt provided. Use --prompt or pipe via stdin."
|
|
2963
|
+
);
|
|
2964
|
+
}
|
|
2965
|
+
const { data } = await client.post("/api/tasks", {
|
|
2966
|
+
agent: options.agent,
|
|
2967
|
+
prompt,
|
|
2968
|
+
skills: options.skills,
|
|
2969
|
+
mcpServers: options.mcp
|
|
2970
|
+
});
|
|
2971
|
+
if (format === "json") {
|
|
2972
|
+
output.log(JSON.stringify(data, null, 2));
|
|
2973
|
+
} else {
|
|
2974
|
+
output.success(`Task created: ${data.id}`);
|
|
2975
|
+
output.dim(`Status: ${data.status}`);
|
|
2976
|
+
output.dim(
|
|
2977
|
+
`Run \`rush-ai task status ${data.id}\` to check progress.`
|
|
2978
|
+
);
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
);
|
|
2982
|
+
task.command("status").description("Check task status").argument("<id>", "Task ID").action(async (id) => {
|
|
2983
|
+
requireAuth();
|
|
2984
|
+
const format = resolveFormat(program.opts());
|
|
2985
|
+
const client = createClient();
|
|
2986
|
+
const { data } = await client.get(
|
|
2987
|
+
`/api/tasks/${encodeURIComponent(id)}`
|
|
2988
|
+
);
|
|
2989
|
+
if (format === "json") {
|
|
2990
|
+
output.log(JSON.stringify(data, null, 2));
|
|
2991
|
+
} else {
|
|
2992
|
+
output.log(output.bold(`Task ${data.id}`));
|
|
2993
|
+
output.log(` Agent: ${data.agent}`);
|
|
2994
|
+
output.log(` Status: ${data.status}`);
|
|
2995
|
+
output.log(` Created: ${new Date(data.createdAt).toLocaleString()}`);
|
|
2996
|
+
output.log(` Updated: ${new Date(data.updatedAt).toLocaleString()}`);
|
|
2997
|
+
if (data.error) {
|
|
2998
|
+
output.error(` Error: ${data.error}`);
|
|
2999
|
+
}
|
|
3000
|
+
}
|
|
3001
|
+
if (isCIMode(program.opts()) && data.status === "failed") {
|
|
3002
|
+
throw new TaskFailedError(
|
|
3003
|
+
`Task ${id} failed: ${data.error ?? "unknown error"}`,
|
|
3004
|
+
{ taskId: id, status: data.status }
|
|
3005
|
+
);
|
|
3006
|
+
}
|
|
3007
|
+
});
|
|
3008
|
+
task.command("result").description("Get task result").argument("<id>", "Task ID").action(async (id) => {
|
|
3009
|
+
requireAuth();
|
|
3010
|
+
const format = resolveFormat(program.opts());
|
|
3011
|
+
const client = createClient();
|
|
3012
|
+
const { data } = await client.get(
|
|
3013
|
+
`/api/tasks/${encodeURIComponent(id)}/result`
|
|
3014
|
+
);
|
|
3015
|
+
let filesSummary = null;
|
|
3016
|
+
try {
|
|
3017
|
+
const { data: filesData } = await client.get(
|
|
3018
|
+
`/api/chat/${encodeURIComponent(id)}/files`
|
|
3019
|
+
);
|
|
3020
|
+
if (filesData.totalCount > 0) {
|
|
3021
|
+
filesSummary = {
|
|
3022
|
+
totalCount: filesData.totalCount,
|
|
3023
|
+
summary: buildCategorySummary(filesData.files)
|
|
3024
|
+
};
|
|
3025
|
+
}
|
|
3026
|
+
} catch {
|
|
3027
|
+
}
|
|
3028
|
+
if (format === "json") {
|
|
3029
|
+
output.log(
|
|
3030
|
+
JSON.stringify(
|
|
3031
|
+
{ ...data, filesSummary: filesSummary ?? void 0 },
|
|
3032
|
+
null,
|
|
3033
|
+
2
|
|
3034
|
+
)
|
|
3035
|
+
);
|
|
3036
|
+
} else {
|
|
3037
|
+
if (typeof data.result === "string") {
|
|
3038
|
+
output.log(
|
|
3039
|
+
containsDiff(data.result) ? colorizeDiff(data.result) : data.result
|
|
3040
|
+
);
|
|
3041
|
+
} else {
|
|
3042
|
+
output.log(JSON.stringify(data.result, null, 2));
|
|
3043
|
+
}
|
|
3044
|
+
if (filesSummary) {
|
|
3045
|
+
output.newline();
|
|
3046
|
+
output.dim(
|
|
3047
|
+
`Files: ${filesSummary.totalCount} files (${filesSummary.summary})`
|
|
3048
|
+
);
|
|
3049
|
+
output.dim(`Run \`rush-ai task files ${id}\` to view details.`);
|
|
3050
|
+
}
|
|
3051
|
+
}
|
|
3052
|
+
});
|
|
3053
|
+
task.command("files").description("List and download task artifact files").argument("<id>", "Task ID").option("--download <path>", "Download a specific file by its path").option("--download-all", "Download all files").option("-o, --output <dir>", "Output directory for downloads").option(
|
|
3054
|
+
"-c, --category <type>",
|
|
3055
|
+
"Filter by category: code, image, document, other"
|
|
3056
|
+
).action(
|
|
3057
|
+
async (id, options) => {
|
|
3058
|
+
requireAuth();
|
|
3059
|
+
const format = resolveFormat(program.opts());
|
|
3060
|
+
const client = createClient();
|
|
3061
|
+
const { data } = await client.get(
|
|
3062
|
+
`/api/chat/${encodeURIComponent(id)}/files`
|
|
3063
|
+
);
|
|
3064
|
+
let files = data.files;
|
|
3065
|
+
if (options.category && VALID_CATEGORIES.includes(options.category)) {
|
|
3066
|
+
files = files.filter((f) => f.category === options.category);
|
|
3067
|
+
} else if (options.category) {
|
|
3068
|
+
output.warn(
|
|
3069
|
+
`Unknown category "${options.category}". Showing all files.`
|
|
3070
|
+
);
|
|
3071
|
+
}
|
|
3072
|
+
if (options.download) {
|
|
3073
|
+
const file = files.find(
|
|
3074
|
+
(f) => f.filePath === options.download || f.fileName === options.download
|
|
3075
|
+
);
|
|
3076
|
+
if (!file) {
|
|
3077
|
+
output.error(`File not found: ${options.download}`);
|
|
3078
|
+
process.exit(1);
|
|
3079
|
+
}
|
|
3080
|
+
if (!file.ossUrl) {
|
|
3081
|
+
output.error(`File "${file.fileName}" has no download URL.`);
|
|
3082
|
+
process.exit(1);
|
|
3083
|
+
}
|
|
3084
|
+
const outputDir = options.output || ".";
|
|
3085
|
+
const destPath = sanitizeFilePath(file.filePath, outputDir);
|
|
3086
|
+
if (!destPath) {
|
|
3087
|
+
output.error(`Unsafe file path: ${file.filePath}`);
|
|
3088
|
+
process.exit(1);
|
|
3089
|
+
}
|
|
3090
|
+
await downloadFile(file.ossUrl, destPath);
|
|
3091
|
+
if (format === "json") {
|
|
3092
|
+
output.log(
|
|
3093
|
+
JSON.stringify(
|
|
3094
|
+
{ downloaded: file.filePath, dest: destPath },
|
|
3095
|
+
null,
|
|
3096
|
+
2
|
|
3097
|
+
)
|
|
3098
|
+
);
|
|
3099
|
+
} else {
|
|
3100
|
+
output.success(`Downloaded: ${file.filePath} -> ${destPath}`);
|
|
3101
|
+
}
|
|
3102
|
+
return;
|
|
3103
|
+
}
|
|
3104
|
+
if (options.downloadAll) {
|
|
3105
|
+
const outputDir = options.output || `task-files-${id.slice(0, 8)}`;
|
|
3106
|
+
let downloaded = 0;
|
|
3107
|
+
let skipped = 0;
|
|
3108
|
+
let failed = 0;
|
|
3109
|
+
for (const file of files) {
|
|
3110
|
+
if (!file.ossUrl) {
|
|
3111
|
+
skipped++;
|
|
3112
|
+
if (format !== "json") {
|
|
3113
|
+
output.dim(` Skipped (no URL): ${file.filePath}`);
|
|
3114
|
+
}
|
|
3115
|
+
continue;
|
|
3116
|
+
}
|
|
3117
|
+
const destPath = sanitizeFilePath(file.filePath, outputDir);
|
|
3118
|
+
if (!destPath) {
|
|
3119
|
+
skipped++;
|
|
3120
|
+
if (format !== "json") {
|
|
3121
|
+
output.warn(` Skipped (unsafe path): ${file.filePath}`);
|
|
3122
|
+
}
|
|
3123
|
+
continue;
|
|
3124
|
+
}
|
|
3125
|
+
try {
|
|
3126
|
+
await downloadFile(file.ossUrl, destPath);
|
|
3127
|
+
downloaded++;
|
|
3128
|
+
if (format !== "json") {
|
|
3129
|
+
output.dim(` Downloaded: ${file.filePath}`);
|
|
3130
|
+
}
|
|
3131
|
+
} catch {
|
|
3132
|
+
failed++;
|
|
3133
|
+
if (format !== "json") {
|
|
3134
|
+
output.warn(` Failed: ${file.filePath}`);
|
|
3135
|
+
}
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
if (format === "json") {
|
|
3139
|
+
output.log(
|
|
3140
|
+
JSON.stringify(
|
|
3141
|
+
{ outputDir, downloaded, skipped, failed, total: files.length },
|
|
3142
|
+
null,
|
|
3143
|
+
2
|
|
3144
|
+
)
|
|
3145
|
+
);
|
|
3146
|
+
} else {
|
|
3147
|
+
output.newline();
|
|
3148
|
+
output.success(
|
|
3149
|
+
`Downloaded ${downloaded}/${files.length} files to ${outputDir}`
|
|
3150
|
+
);
|
|
3151
|
+
if (skipped > 0) {
|
|
3152
|
+
output.dim(
|
|
3153
|
+
` ${skipped} skipped (no download URL or unsafe path)`
|
|
3154
|
+
);
|
|
3155
|
+
}
|
|
3156
|
+
if (failed > 0) {
|
|
3157
|
+
output.warn(` ${failed} failed`);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
return;
|
|
3161
|
+
}
|
|
3162
|
+
if (format === "json") {
|
|
3163
|
+
output.log(
|
|
3164
|
+
JSON.stringify({ files, totalCount: files.length }, null, 2)
|
|
3165
|
+
);
|
|
3166
|
+
return;
|
|
3167
|
+
}
|
|
3168
|
+
if (files.length === 0) {
|
|
3169
|
+
output.info("No files found.");
|
|
3170
|
+
return;
|
|
3171
|
+
}
|
|
3172
|
+
output.log(output.bold(`Files (${files.length} total):`));
|
|
3173
|
+
output.newline();
|
|
3174
|
+
const rows = files.map((f) => ({
|
|
3175
|
+
Name: f.fileName,
|
|
3176
|
+
Category: f.category,
|
|
3177
|
+
Source: f.source,
|
|
3178
|
+
Path: f.filePath
|
|
3179
|
+
}));
|
|
3180
|
+
output.log(formatOutput(rows, format));
|
|
3181
|
+
}
|
|
3182
|
+
);
|
|
3183
|
+
task.command("list").alias("ls").description("List tasks").option("-l, --limit <limit>", "Maximum number of tasks", "20").option("-s, --status <status>", "Filter by status").action(async (options) => {
|
|
3184
|
+
requireAuth();
|
|
3185
|
+
const format = resolveFormat(program.opts());
|
|
3186
|
+
const client = createClient();
|
|
3187
|
+
const params = new URLSearchParams();
|
|
3188
|
+
if (options.limit) params.set("limit", options.limit);
|
|
3189
|
+
if (options.status) params.set("status", options.status);
|
|
3190
|
+
const { data } = await client.get(
|
|
3191
|
+
`/api/tasks?${params.toString()}`
|
|
3192
|
+
);
|
|
3193
|
+
if (format === "json") {
|
|
3194
|
+
output.log(JSON.stringify(data, null, 2));
|
|
3195
|
+
return;
|
|
3196
|
+
}
|
|
3197
|
+
if (data.tasks.length === 0) {
|
|
3198
|
+
output.info("No tasks found.");
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
output.log(output.bold(`Tasks (${data.total} total):`));
|
|
3202
|
+
output.newline();
|
|
3203
|
+
const rows = data.tasks.map((t) => ({
|
|
3204
|
+
ID: t.id.slice(0, 8),
|
|
3205
|
+
Agent: t.agent,
|
|
3206
|
+
Status: t.status,
|
|
3207
|
+
Created: new Date(t.createdAt).toLocaleString()
|
|
3208
|
+
}));
|
|
3209
|
+
output.log(formatOutput(rows, format));
|
|
3210
|
+
});
|
|
3211
|
+
task.command("watch").description("Watch task execution in real-time").argument("<id>", "Task ID").action(async (id) => {
|
|
3212
|
+
requireAuth();
|
|
3213
|
+
const format = resolveFormat(program.opts());
|
|
3214
|
+
const client = createClient();
|
|
3215
|
+
if (format !== "json") {
|
|
3216
|
+
output.log(output.bold(`Watching task ${id}...`));
|
|
3217
|
+
output.newline();
|
|
3218
|
+
}
|
|
3219
|
+
const TERMINAL_STATUSES = ["completed", "failed", "cancelled"];
|
|
3220
|
+
const connectFn = async () => {
|
|
3221
|
+
const response = await client.fetchRaw(
|
|
3222
|
+
`/api/tasks/${encodeURIComponent(id)}/events`,
|
|
3223
|
+
{ method: "GET" }
|
|
3224
|
+
);
|
|
3225
|
+
if (!response.body) {
|
|
3226
|
+
throw new Error("No response body for SSE");
|
|
3227
|
+
}
|
|
3228
|
+
return response.body;
|
|
3229
|
+
};
|
|
3230
|
+
let finalStatus = "";
|
|
3231
|
+
await consumeSSEStreamWithReconnect(
|
|
3232
|
+
connectFn,
|
|
3233
|
+
(event) => {
|
|
3234
|
+
if (format === "json") {
|
|
3235
|
+
output.log(JSON.stringify(event));
|
|
3236
|
+
} else {
|
|
3237
|
+
renderTaskEvent(event);
|
|
3238
|
+
}
|
|
3239
|
+
if (event.type === "status") {
|
|
3240
|
+
finalStatus = event.data;
|
|
3241
|
+
}
|
|
3242
|
+
},
|
|
3243
|
+
{
|
|
3244
|
+
isTerminal: (event) => event.type === "status" && TERMINAL_STATUSES.includes(event.data),
|
|
3245
|
+
onReconnect: (attempt) => {
|
|
3246
|
+
if (format !== "json") {
|
|
3247
|
+
output.dim(
|
|
3248
|
+
`\u27F3 SSE connection lost. Reconnecting (${attempt}/5)...`
|
|
3249
|
+
);
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
);
|
|
3254
|
+
if (format !== "json") {
|
|
3255
|
+
output.newline();
|
|
3256
|
+
if (finalStatus === "completed") {
|
|
3257
|
+
output.success("Task completed.");
|
|
3258
|
+
} else if (finalStatus === "failed") {
|
|
3259
|
+
output.error("Task failed.");
|
|
3260
|
+
} else if (finalStatus === "cancelled") {
|
|
3261
|
+
output.info("Task cancelled.");
|
|
3262
|
+
}
|
|
3263
|
+
}
|
|
3264
|
+
});
|
|
3265
|
+
task.command("cancel").description("Cancel a running task").argument("<id>", "Task ID").action(async (id) => {
|
|
3266
|
+
requireAuth();
|
|
3267
|
+
const format = resolveFormat(program.opts());
|
|
3268
|
+
const client = createClient();
|
|
3269
|
+
await client.delete(`/api/tasks/${encodeURIComponent(id)}`);
|
|
3270
|
+
if (format === "json") {
|
|
3271
|
+
output.log(JSON.stringify({ id, status: "cancelled" }));
|
|
3272
|
+
} else {
|
|
3273
|
+
output.success(`Task ${id} cancelled.`);
|
|
3274
|
+
}
|
|
3275
|
+
});
|
|
3276
|
+
}
|
|
3277
|
+
function commaSplit(value) {
|
|
3278
|
+
return value.split(",").map((s) => s.trim()).filter(Boolean);
|
|
3279
|
+
}
|
|
3280
|
+
function renderTaskEvent(event) {
|
|
3281
|
+
switch (event.type) {
|
|
3282
|
+
case "status":
|
|
3283
|
+
output.info(`Status: ${event.data}`);
|
|
3284
|
+
break;
|
|
3285
|
+
case "content":
|
|
3286
|
+
process.stdout.write(event.data);
|
|
3287
|
+
break;
|
|
3288
|
+
case "error":
|
|
3289
|
+
output.error(event.data || "Unknown error");
|
|
3290
|
+
break;
|
|
3291
|
+
case "progress":
|
|
3292
|
+
output.dim(`Progress: ${event.data}`);
|
|
3293
|
+
break;
|
|
3294
|
+
case "raw":
|
|
3295
|
+
process.stdout.write(event.data);
|
|
3296
|
+
break;
|
|
3297
|
+
default:
|
|
3298
|
+
output.dim(JSON.stringify(event));
|
|
3299
|
+
}
|
|
3300
|
+
}
|
|
3301
|
+
|
|
3302
|
+
// src/commands/index.ts
|
|
3303
|
+
function registerCommands(program) {
|
|
3304
|
+
registerAuthCommand(program);
|
|
3305
|
+
registerAgentCommand(program);
|
|
3306
|
+
registerTaskCommand(program);
|
|
3307
|
+
registerMcpCommand(program);
|
|
3308
|
+
registerPluginCommand(program);
|
|
3309
|
+
registerCompletionCommand(program);
|
|
3310
|
+
registerConfigCommand(program);
|
|
3311
|
+
registerDoctorCommand(program);
|
|
3312
|
+
}
|
|
3313
|
+
|
|
3314
|
+
// src/util/update-check.ts
|
|
3315
|
+
import { mkdir as mkdir3, readFile as readFile2, writeFile as writeFile2 } from "fs/promises";
|
|
3316
|
+
import { homedir as homedir9 } from "os";
|
|
3317
|
+
import { dirname as dirname3, join as join3 } from "path";
|
|
3318
|
+
var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
|
|
3319
|
+
var FETCH_TIMEOUT_MS = 3e3;
|
|
3320
|
+
var REGISTRY_URL = "https://registry.npmjs.org/rush-ai/latest";
|
|
3321
|
+
function parseVersion(v) {
|
|
3322
|
+
const segments = v.replace(/^v/, "").split("-")[0].split(".").map(Number);
|
|
3323
|
+
if (segments.some((n) => Number.isNaN(n))) return null;
|
|
3324
|
+
return segments;
|
|
3325
|
+
}
|
|
3326
|
+
function isNewerVersion(current, latest) {
|
|
3327
|
+
const c = parseVersion(current);
|
|
3328
|
+
const l = parseVersion(latest);
|
|
3329
|
+
if (!c || !l) return false;
|
|
3330
|
+
for (let i = 0; i < Math.max(c.length, l.length); i++) {
|
|
3331
|
+
const cv = c[i] ?? 0;
|
|
3332
|
+
const lv = l[i] ?? 0;
|
|
3333
|
+
if (lv > cv) return true;
|
|
3334
|
+
if (lv < cv) return false;
|
|
3335
|
+
}
|
|
3336
|
+
return false;
|
|
3337
|
+
}
|
|
3338
|
+
async function writeLastCheck(checkFile) {
|
|
3339
|
+
try {
|
|
3340
|
+
await mkdir3(dirname3(checkFile), { recursive: true });
|
|
3341
|
+
await writeFile2(checkFile, JSON.stringify({ lastCheck: Date.now() }));
|
|
3342
|
+
} catch {
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
async function checkForUpdate(currentVersion) {
|
|
3346
|
+
const rushDir = join3(homedir9(), ".rush");
|
|
3347
|
+
const checkFile = join3(rushDir, "update-check.json");
|
|
3348
|
+
try {
|
|
3349
|
+
if (process.env.CI) return;
|
|
3350
|
+
let lastCheck = 0;
|
|
3351
|
+
try {
|
|
3352
|
+
const data = JSON.parse(await readFile2(checkFile, "utf-8"));
|
|
3353
|
+
lastCheck = data.lastCheck ?? 0;
|
|
3354
|
+
} catch {
|
|
3355
|
+
}
|
|
3356
|
+
if (Date.now() - lastCheck < CHECK_INTERVAL_MS) return;
|
|
3357
|
+
await writeLastCheck(checkFile);
|
|
3358
|
+
const response = await fetch(REGISTRY_URL, {
|
|
3359
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
3360
|
+
});
|
|
3361
|
+
if (!response.ok) return;
|
|
3362
|
+
const { version: latest } = await response.json();
|
|
3363
|
+
if (isNewerVersion(currentVersion, latest)) {
|
|
3364
|
+
output.newline();
|
|
3365
|
+
output.warn(
|
|
3366
|
+
`Update available: ${currentVersion} \u2192 ${latest}
|
|
3367
|
+
Run \`npm update -g rush-ai\` to update.`
|
|
3368
|
+
);
|
|
3369
|
+
}
|
|
3370
|
+
} catch {
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
// src/index.ts
|
|
3375
|
+
var BANNER = `
|
|
3376
|
+
${chalk6.bold.cyan("Rush CLI")} ${chalk6.dim(`v${VERSION}`)}
|
|
3377
|
+
${chalk6.dim("The command-line interface for the Rush AI platform")}
|
|
3378
|
+
`;
|
|
3379
|
+
function showWelcomeGuide() {
|
|
3380
|
+
const lines = [
|
|
3381
|
+
"",
|
|
3382
|
+
` ${chalk6.bold.cyan("Rush CLI")} ${chalk6.dim(`v${VERSION}`)}`,
|
|
3383
|
+
"",
|
|
3384
|
+
chalk6.dim(" Quick start:"),
|
|
3385
|
+
` ${chalk6.cyan("rush-ai auth login")} Log in to Rush platform`,
|
|
3386
|
+
` ${chalk6.cyan("rush-ai agent list")} Browse available agents`,
|
|
3387
|
+
` ${chalk6.cyan("rush-ai task run -a <agent>")} Run a task`,
|
|
3388
|
+
""
|
|
3389
|
+
];
|
|
3390
|
+
try {
|
|
3391
|
+
const method = getAuthMethod();
|
|
3392
|
+
if (method) {
|
|
3393
|
+
lines.push(chalk6.dim(" Status:"), ` Logged in via ${method}`, "");
|
|
3394
|
+
}
|
|
3395
|
+
} catch {
|
|
3396
|
+
}
|
|
3397
|
+
lines.push(chalk6.dim(" Run rush-ai --help for all commands."), "");
|
|
3398
|
+
console.error(lines.join("\n"));
|
|
3399
|
+
}
|
|
3400
|
+
function createProgram() {
|
|
3401
|
+
const program = new Command();
|
|
3402
|
+
program.name("rush-ai").version(VERSION, "-v, --version").description("Rush CLI - Command-line interface for the Rush AI platform").configureHelp({
|
|
3403
|
+
sortSubcommands: true
|
|
3404
|
+
}).addHelpText("before", BANNER).option("--no-color", "Disable color output").option("--json", "Output as JSON where supported").option("-f, --format <type>", "Output format (table, json, csv)").option(
|
|
3405
|
+
"--ci",
|
|
3406
|
+
"CI mode: JSON output, no interactive prompts, strict exit codes"
|
|
3407
|
+
).option("--verbose", "Show detailed output including HTTP requests").option("--debug", "Show debug-level output (implies --verbose)");
|
|
3408
|
+
program.hook("preAction", (thisCommand) => {
|
|
3409
|
+
const opts = thisCommand.optsWithGlobals();
|
|
3410
|
+
if (isCIMode(opts)) {
|
|
3411
|
+
if (!opts.json) {
|
|
3412
|
+
thisCommand.setOptionValue("json", true);
|
|
3413
|
+
}
|
|
3414
|
+
}
|
|
3415
|
+
setVerbosity(!!opts.verbose, !!opts.debug);
|
|
3416
|
+
});
|
|
3417
|
+
program.action(() => {
|
|
3418
|
+
if (process.stdout.isTTY) {
|
|
3419
|
+
showWelcomeGuide();
|
|
3420
|
+
} else {
|
|
3421
|
+
program.help();
|
|
3422
|
+
}
|
|
3423
|
+
});
|
|
3424
|
+
registerCommands(program);
|
|
3425
|
+
return program;
|
|
3426
|
+
}
|
|
3427
|
+
async function main() {
|
|
3428
|
+
if (!process.stdout.isTTY || process.argv.includes("--no-color") || isCIMode()) {
|
|
3429
|
+
process.env.NO_COLOR = "1";
|
|
3430
|
+
}
|
|
3431
|
+
const program = createProgram();
|
|
3432
|
+
try {
|
|
3433
|
+
await program.parseAsync(process.argv);
|
|
3434
|
+
} catch (error) {
|
|
3435
|
+
const ci = isCIMode(program.opts());
|
|
3436
|
+
if (isRushError(error)) {
|
|
3437
|
+
if (ci) {
|
|
3438
|
+
output.log(
|
|
3439
|
+
JSON.stringify({
|
|
3440
|
+
error: true,
|
|
3441
|
+
code: error.code,
|
|
3442
|
+
message: error.message,
|
|
3443
|
+
exitCode: error.exitCode
|
|
3444
|
+
})
|
|
3445
|
+
);
|
|
3446
|
+
} else {
|
|
3447
|
+
output.error(error.message);
|
|
3448
|
+
if (error.code === "AUTH_ERROR") {
|
|
3449
|
+
output.newline();
|
|
3450
|
+
output.dim("Run `rush-ai auth login` to authenticate.");
|
|
3451
|
+
}
|
|
3452
|
+
}
|
|
3453
|
+
process.exit(error.exitCode);
|
|
3454
|
+
}
|
|
3455
|
+
if (error instanceof Error) {
|
|
3456
|
+
const exitCode = 2;
|
|
3457
|
+
if (ci) {
|
|
3458
|
+
output.log(
|
|
3459
|
+
JSON.stringify({
|
|
3460
|
+
error: true,
|
|
3461
|
+
code: "UNKNOWN_ERROR",
|
|
3462
|
+
message: error.message,
|
|
3463
|
+
exitCode
|
|
3464
|
+
})
|
|
3465
|
+
);
|
|
3466
|
+
} else {
|
|
3467
|
+
output.error(error.message);
|
|
3468
|
+
if (process.env.DEBUG) {
|
|
3469
|
+
console.error(error.stack);
|
|
3470
|
+
}
|
|
3471
|
+
}
|
|
3472
|
+
process.exit(exitCode);
|
|
3473
|
+
}
|
|
3474
|
+
if (ci) {
|
|
3475
|
+
output.log(
|
|
3476
|
+
JSON.stringify({
|
|
3477
|
+
error: true,
|
|
3478
|
+
code: "UNKNOWN_ERROR",
|
|
3479
|
+
message: "An unexpected error occurred.",
|
|
3480
|
+
exitCode: 2
|
|
3481
|
+
})
|
|
3482
|
+
);
|
|
3483
|
+
} else {
|
|
3484
|
+
output.error("An unexpected error occurred.");
|
|
3485
|
+
}
|
|
3486
|
+
process.exit(2);
|
|
3487
|
+
}
|
|
3488
|
+
void checkForUpdate(VERSION);
|
|
3489
|
+
}
|
|
3490
|
+
if (!process.env.VITEST) {
|
|
3491
|
+
main();
|
|
3492
|
+
}
|
|
3493
|
+
export {
|
|
3494
|
+
createProgram
|
|
3495
|
+
};
|
|
3496
|
+
//# sourceMappingURL=index.js.map
|