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
|
@@ -0,0 +1,870 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/output/logger.ts
|
|
4
|
+
import chalk from "chalk";
|
|
5
|
+
var output = {
|
|
6
|
+
/** Content output → stdout. Use for data the user may pipe/redirect. */
|
|
7
|
+
log(message) {
|
|
8
|
+
console.log(message);
|
|
9
|
+
},
|
|
10
|
+
/** Status → stderr */
|
|
11
|
+
success(message) {
|
|
12
|
+
console.error(chalk.green("Success!"), message);
|
|
13
|
+
},
|
|
14
|
+
/** Error → stderr */
|
|
15
|
+
error(message) {
|
|
16
|
+
console.error(chalk.red("Error:"), message);
|
|
17
|
+
},
|
|
18
|
+
/** Warning → stderr */
|
|
19
|
+
warn(message) {
|
|
20
|
+
console.error(chalk.yellow("Warning:"), message);
|
|
21
|
+
},
|
|
22
|
+
/** Info → stderr */
|
|
23
|
+
info(message) {
|
|
24
|
+
console.error(chalk.cyan("Info:"), message);
|
|
25
|
+
},
|
|
26
|
+
/** Dim status → stderr */
|
|
27
|
+
dim(message) {
|
|
28
|
+
console.error(chalk.dim(message));
|
|
29
|
+
},
|
|
30
|
+
table(data) {
|
|
31
|
+
console.table(data);
|
|
32
|
+
},
|
|
33
|
+
/** Newline → stderr (status separator) */
|
|
34
|
+
newline() {
|
|
35
|
+
console.error();
|
|
36
|
+
},
|
|
37
|
+
link(text, url) {
|
|
38
|
+
return chalk.cyan.underline(url);
|
|
39
|
+
},
|
|
40
|
+
bold(text) {
|
|
41
|
+
return chalk.bold(text);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// src/version.ts
|
|
46
|
+
import { readFileSync } from "fs";
|
|
47
|
+
import { dirname, resolve } from "path";
|
|
48
|
+
import { fileURLToPath } from "url";
|
|
49
|
+
function loadVersion() {
|
|
50
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
51
|
+
const candidates = [
|
|
52
|
+
resolve(__dirname, "..", "package.json"),
|
|
53
|
+
resolve(__dirname, "..", "..", "package.json")
|
|
54
|
+
];
|
|
55
|
+
for (const candidate of candidates) {
|
|
56
|
+
try {
|
|
57
|
+
const pkg = JSON.parse(readFileSync(candidate, "utf-8"));
|
|
58
|
+
if (pkg.name === "rush-ai" && pkg.version) {
|
|
59
|
+
return pkg.version;
|
|
60
|
+
}
|
|
61
|
+
} catch {
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
console.error("[rush-ai] Warning: Could not read version from package.json");
|
|
65
|
+
return "0.0.0-unknown";
|
|
66
|
+
}
|
|
67
|
+
var VERSION = loadVersion();
|
|
68
|
+
|
|
69
|
+
// src/util/config.ts
|
|
70
|
+
import { homedir } from "os";
|
|
71
|
+
import { resolve as resolve2 } from "path";
|
|
72
|
+
import Conf from "conf";
|
|
73
|
+
var AUTH_CONFIG_DIR = resolve2(homedir(), ".rush");
|
|
74
|
+
var DEFAULT_API = "https://rush.zhenguanyu.com";
|
|
75
|
+
var AUTH_DEFAULTS = {
|
|
76
|
+
token: null,
|
|
77
|
+
expiresAt: null,
|
|
78
|
+
refreshToken: null,
|
|
79
|
+
method: null,
|
|
80
|
+
tokenId: null
|
|
81
|
+
};
|
|
82
|
+
var GLOBAL_DEFAULTS = {
|
|
83
|
+
currentTeam: null,
|
|
84
|
+
api: DEFAULT_API,
|
|
85
|
+
collectMetrics: true
|
|
86
|
+
};
|
|
87
|
+
var globalStore = new Conf({
|
|
88
|
+
projectName: "rush-ai",
|
|
89
|
+
cwd: AUTH_CONFIG_DIR,
|
|
90
|
+
configName: "config",
|
|
91
|
+
defaults: {}
|
|
92
|
+
});
|
|
93
|
+
var authStore = new Conf({
|
|
94
|
+
projectName: "rush-ai",
|
|
95
|
+
cwd: AUTH_CONFIG_DIR,
|
|
96
|
+
configName: "auth",
|
|
97
|
+
defaults: {}
|
|
98
|
+
});
|
|
99
|
+
function migrateGlobalStoreIfNeeded() {
|
|
100
|
+
const version = globalStore.get("_version");
|
|
101
|
+
if (version === 2) return;
|
|
102
|
+
const hasOldFormat = globalStore.has("api") && !globalStore.has("profiles");
|
|
103
|
+
const isEmpty = globalStore.size === 0;
|
|
104
|
+
if (hasOldFormat) {
|
|
105
|
+
const oldConfig = {
|
|
106
|
+
currentTeam: globalStore.get("currentTeam") ?? null,
|
|
107
|
+
api: globalStore.get("api") ?? DEFAULT_API,
|
|
108
|
+
collectMetrics: globalStore.get("collectMetrics") ?? true
|
|
109
|
+
};
|
|
110
|
+
globalStore.clear();
|
|
111
|
+
globalStore.set("_version", 2);
|
|
112
|
+
globalStore.set("activeProfile", "default");
|
|
113
|
+
globalStore.set("profiles", { default: oldConfig });
|
|
114
|
+
} else if (isEmpty || !globalStore.has("profiles")) {
|
|
115
|
+
globalStore.set("_version", 2);
|
|
116
|
+
if (!globalStore.has("activeProfile")) {
|
|
117
|
+
globalStore.set("activeProfile", "default");
|
|
118
|
+
}
|
|
119
|
+
if (!globalStore.has("profiles")) {
|
|
120
|
+
globalStore.set("profiles", { default: { ...GLOBAL_DEFAULTS } });
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function migrateAuthStoreIfNeeded() {
|
|
125
|
+
const version = authStore.get("_version");
|
|
126
|
+
if (version === 2) return;
|
|
127
|
+
const hasOldFormat = authStore.has("token") || authStore.has("method");
|
|
128
|
+
const isEmpty = authStore.size === 0;
|
|
129
|
+
if (hasOldFormat) {
|
|
130
|
+
const oldAuth = {
|
|
131
|
+
token: authStore.get("token") ?? null,
|
|
132
|
+
expiresAt: authStore.get("expiresAt") ?? null,
|
|
133
|
+
refreshToken: authStore.get("refreshToken") ?? null,
|
|
134
|
+
method: authStore.get("method") ?? null,
|
|
135
|
+
tokenId: authStore.get("tokenId") ?? null
|
|
136
|
+
};
|
|
137
|
+
authStore.clear();
|
|
138
|
+
authStore.set("_version", 2);
|
|
139
|
+
authStore.set("default", oldAuth);
|
|
140
|
+
} else if (isEmpty) {
|
|
141
|
+
authStore.set("_version", 2);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
migrateGlobalStoreIfNeeded();
|
|
145
|
+
migrateAuthStoreIfNeeded();
|
|
146
|
+
function getActiveProfile() {
|
|
147
|
+
return process.env.RUSH_PROFILE ?? globalStore.get("activeProfile") ?? "default";
|
|
148
|
+
}
|
|
149
|
+
function setActiveProfile(name) {
|
|
150
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
151
|
+
if (!profiles[name]) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Profile '${name}' does not exist. Available: ${Object.keys(profiles).join(", ")}`
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
globalStore.set("activeProfile", name);
|
|
157
|
+
}
|
|
158
|
+
function listProfiles() {
|
|
159
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
160
|
+
return Object.keys(profiles);
|
|
161
|
+
}
|
|
162
|
+
function createProfile(name, config) {
|
|
163
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
164
|
+
if (profiles[name]) {
|
|
165
|
+
throw new Error(`Profile '${name}' already exists.`);
|
|
166
|
+
}
|
|
167
|
+
const defaultConfig = profiles.default ?? GLOBAL_DEFAULTS;
|
|
168
|
+
profiles[name] = { ...defaultConfig, ...config };
|
|
169
|
+
globalStore.set("profiles", profiles);
|
|
170
|
+
}
|
|
171
|
+
function deleteProfile(name) {
|
|
172
|
+
if (name === "default") {
|
|
173
|
+
throw new Error("Cannot delete the 'default' profile.");
|
|
174
|
+
}
|
|
175
|
+
const active = getActiveProfile();
|
|
176
|
+
if (name === active) {
|
|
177
|
+
throw new Error(
|
|
178
|
+
`Cannot delete the active profile '${name}'. Switch to another profile first.`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
182
|
+
if (!profiles[name]) {
|
|
183
|
+
throw new Error(`Profile '${name}' does not exist.`);
|
|
184
|
+
}
|
|
185
|
+
delete profiles[name];
|
|
186
|
+
globalStore.set("profiles", profiles);
|
|
187
|
+
authStore.delete(name);
|
|
188
|
+
}
|
|
189
|
+
function getAuthConfig() {
|
|
190
|
+
const profile = getActiveProfile();
|
|
191
|
+
const profileAuth = authStore.get(profile);
|
|
192
|
+
return {
|
|
193
|
+
token: profileAuth?.token ?? null,
|
|
194
|
+
expiresAt: profileAuth?.expiresAt ?? null,
|
|
195
|
+
refreshToken: profileAuth?.refreshToken ?? null,
|
|
196
|
+
method: profileAuth?.method ?? null,
|
|
197
|
+
tokenId: profileAuth?.tokenId ?? null
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function setAuthConfig(config) {
|
|
201
|
+
const profile = getActiveProfile();
|
|
202
|
+
const current = authStore.get(profile) ?? {
|
|
203
|
+
...AUTH_DEFAULTS
|
|
204
|
+
};
|
|
205
|
+
authStore.set(profile, { ...current, ...config });
|
|
206
|
+
}
|
|
207
|
+
function clearAuthConfig() {
|
|
208
|
+
const profile = getActiveProfile();
|
|
209
|
+
authStore.set(profile, { ...AUTH_DEFAULTS });
|
|
210
|
+
}
|
|
211
|
+
function getGlobalConfig() {
|
|
212
|
+
const profile = getActiveProfile();
|
|
213
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
214
|
+
const profileConfig = profiles[profile] ?? GLOBAL_DEFAULTS;
|
|
215
|
+
return {
|
|
216
|
+
currentTeam: profileConfig.currentTeam ?? null,
|
|
217
|
+
api: profileConfig.api ?? DEFAULT_API,
|
|
218
|
+
collectMetrics: profileConfig.collectMetrics ?? true
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
function setGlobalConfig(config) {
|
|
222
|
+
const profile = getActiveProfile();
|
|
223
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
224
|
+
const current = profiles[profile] ?? { ...GLOBAL_DEFAULTS };
|
|
225
|
+
profiles[profile] = { ...current, ...config };
|
|
226
|
+
globalStore.set("profiles", profiles);
|
|
227
|
+
}
|
|
228
|
+
function getConfigDir() {
|
|
229
|
+
return AUTH_CONFIG_DIR;
|
|
230
|
+
}
|
|
231
|
+
function isLoggedIn() {
|
|
232
|
+
const auth = getAuthConfig();
|
|
233
|
+
if (!auth.token) return false;
|
|
234
|
+
if (auth.expiresAt && Date.now() > auth.expiresAt) {
|
|
235
|
+
return auth.method === "cas" && Boolean(auth.refreshToken);
|
|
236
|
+
}
|
|
237
|
+
return true;
|
|
238
|
+
}
|
|
239
|
+
function getProfileConfig(name) {
|
|
240
|
+
const profiles = globalStore.get("profiles") ?? {};
|
|
241
|
+
return profiles[name] ?? null;
|
|
242
|
+
}
|
|
243
|
+
function getProfileAuth(name) {
|
|
244
|
+
const profileAuth = authStore.get(name);
|
|
245
|
+
return {
|
|
246
|
+
token: profileAuth?.token ?? null,
|
|
247
|
+
expiresAt: profileAuth?.expiresAt ?? null,
|
|
248
|
+
refreshToken: profileAuth?.refreshToken ?? null,
|
|
249
|
+
method: profileAuth?.method ?? null,
|
|
250
|
+
tokenId: profileAuth?.tokenId ?? null
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/util/auth.ts
|
|
255
|
+
function getAuthMethod() {
|
|
256
|
+
if (process.env.RUSH_API_KEY) {
|
|
257
|
+
return "api_key";
|
|
258
|
+
}
|
|
259
|
+
const auth = getAuthConfig();
|
|
260
|
+
if (auth.token) {
|
|
261
|
+
if (auth.method === "api_key" || auth.token.startsWith("rush_sk_")) {
|
|
262
|
+
return "api_key";
|
|
263
|
+
}
|
|
264
|
+
if (auth.method === "platform_token") {
|
|
265
|
+
return "platform_token";
|
|
266
|
+
}
|
|
267
|
+
return auth.method ?? "cas";
|
|
268
|
+
}
|
|
269
|
+
return null;
|
|
270
|
+
}
|
|
271
|
+
function getAuthToken() {
|
|
272
|
+
if (process.env.RUSH_API_KEY) {
|
|
273
|
+
return process.env.RUSH_API_KEY;
|
|
274
|
+
}
|
|
275
|
+
const auth = getAuthConfig();
|
|
276
|
+
return auth.token;
|
|
277
|
+
}
|
|
278
|
+
function getCasConfig() {
|
|
279
|
+
const config = getGlobalConfig();
|
|
280
|
+
const baseUrl = process.env.RUSH_API_URL ?? config.api;
|
|
281
|
+
return {
|
|
282
|
+
authorizeEndpoint: process.env.RUSH_CAS_AUTHORIZE_ENDPOINT ?? `${baseUrl}/cas/oauth2/authorize`,
|
|
283
|
+
tokenEndpoint: process.env.RUSH_CAS_TOKEN_ENDPOINT ?? `${baseUrl}/cas/oauth2/token`,
|
|
284
|
+
revokeEndpoint: process.env.RUSH_CAS_REVOKE_ENDPOINT ?? `${baseUrl}/cas/oauth2/revoke`,
|
|
285
|
+
clientId: process.env.RUSH_CAS_CLIENT_ID ?? "rush-ai"
|
|
286
|
+
};
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/util/auth-session.ts
|
|
290
|
+
var VERIFY_PATH = "/api/skill-auth/me";
|
|
291
|
+
var REFRESH_SKEW_MS = 6e4;
|
|
292
|
+
var AUTH_REQUEST_TIMEOUT_MS = 8e3;
|
|
293
|
+
function getApiBaseUrl() {
|
|
294
|
+
return process.env.RUSH_API_URL ?? getGlobalConfig().api;
|
|
295
|
+
}
|
|
296
|
+
function buildBearerHeaders(token) {
|
|
297
|
+
return {
|
|
298
|
+
Authorization: `Bearer ${token}`,
|
|
299
|
+
"Content-Type": "application/json",
|
|
300
|
+
"User-Agent": "rush-ai/0.2.0"
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function shouldRefreshToken(options) {
|
|
304
|
+
const auth = getAuthConfig();
|
|
305
|
+
if (!auth.token || auth.method !== "cas" || !auth.refreshToken) {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
if (options?.force) {
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
if (!auth.expiresAt) {
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
const refreshWindowMs = options?.refreshWindowMs ?? REFRESH_SKEW_MS;
|
|
315
|
+
return Date.now() >= auth.expiresAt - refreshWindowMs;
|
|
316
|
+
}
|
|
317
|
+
async function refreshCasAccessToken(options) {
|
|
318
|
+
if (process.env.RUSH_API_KEY || !shouldRefreshToken(options)) {
|
|
319
|
+
return false;
|
|
320
|
+
}
|
|
321
|
+
const auth = getAuthConfig();
|
|
322
|
+
if (!auth.refreshToken) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
const cas = getCasConfig();
|
|
326
|
+
const body = new URLSearchParams({
|
|
327
|
+
grant_type: "refresh_token",
|
|
328
|
+
refresh_token: auth.refreshToken,
|
|
329
|
+
client_id: cas.clientId
|
|
330
|
+
}).toString();
|
|
331
|
+
try {
|
|
332
|
+
const response = await fetch(cas.tokenEndpoint, {
|
|
333
|
+
method: "POST",
|
|
334
|
+
headers: {
|
|
335
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
336
|
+
},
|
|
337
|
+
body,
|
|
338
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS)
|
|
339
|
+
});
|
|
340
|
+
if (!response.ok) {
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
const tokenData = await response.json();
|
|
344
|
+
if (!tokenData.access_token) {
|
|
345
|
+
return false;
|
|
346
|
+
}
|
|
347
|
+
const expiresAt = tokenData.expires_in ? Date.now() + tokenData.expires_in * 1e3 : auth.expiresAt;
|
|
348
|
+
setAuthConfig({
|
|
349
|
+
token: tokenData.access_token,
|
|
350
|
+
expiresAt,
|
|
351
|
+
refreshToken: tokenData.refresh_token ?? auth.refreshToken,
|
|
352
|
+
method: "cas"
|
|
353
|
+
});
|
|
354
|
+
return true;
|
|
355
|
+
} catch {
|
|
356
|
+
return false;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
async function verifyTokenOnline(token) {
|
|
360
|
+
const url = `${getApiBaseUrl()}${VERIFY_PATH}`;
|
|
361
|
+
try {
|
|
362
|
+
const response = await fetch(url, {
|
|
363
|
+
method: "GET",
|
|
364
|
+
headers: buildBearerHeaders(token),
|
|
365
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS)
|
|
366
|
+
});
|
|
367
|
+
if (response.ok) {
|
|
368
|
+
return { valid: true, status: response.status };
|
|
369
|
+
}
|
|
370
|
+
const message = await response.text().catch(() => void 0);
|
|
371
|
+
return {
|
|
372
|
+
valid: false,
|
|
373
|
+
status: response.status,
|
|
374
|
+
message: message || `Server rejected token (${response.status})`
|
|
375
|
+
};
|
|
376
|
+
} catch (error) {
|
|
377
|
+
return {
|
|
378
|
+
valid: false,
|
|
379
|
+
status: null,
|
|
380
|
+
message: `Network error: ${error instanceof Error ? error.message : "unknown"}`
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function verifyCurrentAuthSession() {
|
|
385
|
+
const token = getAuthToken();
|
|
386
|
+
if (!token) {
|
|
387
|
+
return {
|
|
388
|
+
valid: false,
|
|
389
|
+
status: null,
|
|
390
|
+
message: "Not authenticated"
|
|
391
|
+
};
|
|
392
|
+
}
|
|
393
|
+
await refreshCasAccessToken();
|
|
394
|
+
let activeToken = getAuthToken();
|
|
395
|
+
if (!activeToken) {
|
|
396
|
+
return {
|
|
397
|
+
valid: false,
|
|
398
|
+
status: null,
|
|
399
|
+
message: "Not authenticated"
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
const result = await verifyTokenOnline(activeToken);
|
|
403
|
+
if (result.valid || result.status !== 401 || process.env.RUSH_API_KEY) {
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
const refreshed = await refreshCasAccessToken({ force: true });
|
|
407
|
+
if (!refreshed) {
|
|
408
|
+
return result;
|
|
409
|
+
}
|
|
410
|
+
activeToken = getAuthToken();
|
|
411
|
+
if (!activeToken) {
|
|
412
|
+
return result;
|
|
413
|
+
}
|
|
414
|
+
return verifyTokenOnline(activeToken);
|
|
415
|
+
}
|
|
416
|
+
async function revokeToken(revokeEndpoint, clientId, token, tokenTypeHint) {
|
|
417
|
+
const body = new URLSearchParams({
|
|
418
|
+
token,
|
|
419
|
+
token_type_hint: tokenTypeHint,
|
|
420
|
+
client_id: clientId
|
|
421
|
+
}).toString();
|
|
422
|
+
try {
|
|
423
|
+
const response = await fetch(revokeEndpoint, {
|
|
424
|
+
method: "POST",
|
|
425
|
+
headers: {
|
|
426
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
427
|
+
},
|
|
428
|
+
body,
|
|
429
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS)
|
|
430
|
+
});
|
|
431
|
+
return response.ok;
|
|
432
|
+
} catch {
|
|
433
|
+
return false;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
async function revokeCurrentSession() {
|
|
437
|
+
if (process.env.RUSH_API_KEY) {
|
|
438
|
+
return false;
|
|
439
|
+
}
|
|
440
|
+
const auth = getAuthConfig();
|
|
441
|
+
if (auth.method === "platform_token") {
|
|
442
|
+
return revokePlatformToken();
|
|
443
|
+
}
|
|
444
|
+
if (auth.method !== "cas") {
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const cas = getCasConfig();
|
|
448
|
+
const candidates = [];
|
|
449
|
+
if (auth.refreshToken) {
|
|
450
|
+
candidates.push({ token: auth.refreshToken, hint: "refresh_token" });
|
|
451
|
+
}
|
|
452
|
+
if (auth.token) {
|
|
453
|
+
candidates.push({ token: auth.token, hint: "access_token" });
|
|
454
|
+
}
|
|
455
|
+
if (candidates.length === 0) {
|
|
456
|
+
return false;
|
|
457
|
+
}
|
|
458
|
+
let anyRevoked = false;
|
|
459
|
+
for (const candidate of candidates) {
|
|
460
|
+
const revoked = await revokeToken(
|
|
461
|
+
cas.revokeEndpoint,
|
|
462
|
+
cas.clientId,
|
|
463
|
+
candidate.token,
|
|
464
|
+
candidate.hint
|
|
465
|
+
);
|
|
466
|
+
if (revoked) {
|
|
467
|
+
anyRevoked = true;
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
return anyRevoked;
|
|
471
|
+
}
|
|
472
|
+
async function revokePlatformToken() {
|
|
473
|
+
const auth = getAuthConfig();
|
|
474
|
+
if (!auth.tokenId || !auth.token) {
|
|
475
|
+
return false;
|
|
476
|
+
}
|
|
477
|
+
const baseUrl = getApiBaseUrl();
|
|
478
|
+
const url = `${baseUrl}/api/platform-tokens?id=${encodeURIComponent(auth.tokenId)}`;
|
|
479
|
+
try {
|
|
480
|
+
const response = await fetch(url, {
|
|
481
|
+
method: "DELETE",
|
|
482
|
+
headers: {
|
|
483
|
+
Authorization: `Bearer ${auth.token}`,
|
|
484
|
+
"User-Agent": "rush-ai/0.2.0"
|
|
485
|
+
},
|
|
486
|
+
signal: AbortSignal.timeout(AUTH_REQUEST_TIMEOUT_MS)
|
|
487
|
+
});
|
|
488
|
+
return response.ok;
|
|
489
|
+
} catch {
|
|
490
|
+
return false;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// src/util/errors.ts
|
|
495
|
+
var RushError = class extends Error {
|
|
496
|
+
code;
|
|
497
|
+
meta;
|
|
498
|
+
exitCode;
|
|
499
|
+
constructor(message, meta = {}, code = "RUSH_ERROR", exitCode = 2) {
|
|
500
|
+
super(message);
|
|
501
|
+
this.name = "RushError";
|
|
502
|
+
this.code = code;
|
|
503
|
+
this.meta = meta;
|
|
504
|
+
this.exitCode = exitCode;
|
|
505
|
+
}
|
|
506
|
+
};
|
|
507
|
+
var AuthError = class extends RushError {
|
|
508
|
+
constructor(message = "Not authenticated. Run `rush-ai auth login` first.") {
|
|
509
|
+
super(message, {}, "AUTH_ERROR", 3);
|
|
510
|
+
this.name = "AuthError";
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
var TaskFailedError = class extends RushError {
|
|
514
|
+
constructor(message, meta = {}) {
|
|
515
|
+
super(message, meta, "TASK_FAILED", 1);
|
|
516
|
+
this.name = "TaskFailedError";
|
|
517
|
+
}
|
|
518
|
+
};
|
|
519
|
+
function friendlyMessage(status, serverMessage) {
|
|
520
|
+
if (serverMessage) return serverMessage;
|
|
521
|
+
switch (status) {
|
|
522
|
+
case 401:
|
|
523
|
+
return "Authentication expired. Run `rush-ai auth login` to re-authenticate.";
|
|
524
|
+
case 403:
|
|
525
|
+
return "Permission denied. You may not have access to this resource.";
|
|
526
|
+
case 404:
|
|
527
|
+
return "Resource not found.";
|
|
528
|
+
case 429:
|
|
529
|
+
return "Rate limited. The server is busy, please try again later.";
|
|
530
|
+
case 502:
|
|
531
|
+
case 503:
|
|
532
|
+
case 504:
|
|
533
|
+
return "Service temporarily unavailable. Please try again in a moment.";
|
|
534
|
+
default:
|
|
535
|
+
return `Request failed with status ${status}.`;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
var ApiError = class extends RushError {
|
|
539
|
+
status;
|
|
540
|
+
retryAfter;
|
|
541
|
+
constructor(message, status, body, retryAfter) {
|
|
542
|
+
let serverMessage;
|
|
543
|
+
if (body) {
|
|
544
|
+
try {
|
|
545
|
+
const parsed = JSON.parse(body);
|
|
546
|
+
if (parsed.error && typeof parsed.error === "string") {
|
|
547
|
+
serverMessage = parsed.error;
|
|
548
|
+
}
|
|
549
|
+
} catch {
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
super(
|
|
553
|
+
friendlyMessage(status, serverMessage),
|
|
554
|
+
{ status, body },
|
|
555
|
+
"API_ERROR"
|
|
556
|
+
);
|
|
557
|
+
this.name = "ApiError";
|
|
558
|
+
this.status = status;
|
|
559
|
+
this.retryAfter = retryAfter;
|
|
560
|
+
}
|
|
561
|
+
};
|
|
562
|
+
function isRushError(error) {
|
|
563
|
+
return error instanceof RushError;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// src/util/retry.ts
|
|
567
|
+
function parseRetryAfter(value) {
|
|
568
|
+
const seconds = Number(value);
|
|
569
|
+
if (!Number.isNaN(seconds) && seconds >= 0) {
|
|
570
|
+
return seconds * 1e3;
|
|
571
|
+
}
|
|
572
|
+
const date = new Date(value);
|
|
573
|
+
if (!Number.isNaN(date.getTime())) {
|
|
574
|
+
const delayMs = date.getTime() - Date.now();
|
|
575
|
+
return delayMs > 0 ? delayMs : 0;
|
|
576
|
+
}
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
function calculateDelay(attempt, baseDelay, maxDelay) {
|
|
580
|
+
const exponential = baseDelay * 2 ** attempt;
|
|
581
|
+
const jitter = Math.random() * baseDelay * 0.5;
|
|
582
|
+
return Math.min(exponential + jitter, maxDelay);
|
|
583
|
+
}
|
|
584
|
+
function isRetryableError(error, retryableStatuses) {
|
|
585
|
+
if (error instanceof RushError && error.code === "NETWORK_ERROR") {
|
|
586
|
+
return true;
|
|
587
|
+
}
|
|
588
|
+
if (error instanceof ApiError && retryableStatuses.includes(error.status)) {
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
return false;
|
|
592
|
+
}
|
|
593
|
+
function getRetryAfterFromError(error) {
|
|
594
|
+
if (error instanceof ApiError && error.status === 429) {
|
|
595
|
+
if (error.retryAfter) {
|
|
596
|
+
return parseRetryAfter(error.retryAfter);
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return null;
|
|
600
|
+
}
|
|
601
|
+
async function withRetry(fn, options = {}) {
|
|
602
|
+
const {
|
|
603
|
+
maxRetries = 3,
|
|
604
|
+
baseDelay = 1e3,
|
|
605
|
+
maxDelay = 3e4,
|
|
606
|
+
retryableStatuses = [429, 502, 503, 504],
|
|
607
|
+
signal,
|
|
608
|
+
onRetry
|
|
609
|
+
} = options;
|
|
610
|
+
let lastError;
|
|
611
|
+
for (let attempt = 0; attempt <= maxRetries; attempt++) {
|
|
612
|
+
if (signal?.aborted) {
|
|
613
|
+
throw lastError ?? new RushError("Request aborted", {}, "ABORT_ERROR");
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
return await fn();
|
|
617
|
+
} catch (error) {
|
|
618
|
+
lastError = error;
|
|
619
|
+
if (attempt >= maxRetries) {
|
|
620
|
+
throw error;
|
|
621
|
+
}
|
|
622
|
+
if (!isRetryableError(error, retryableStatuses)) {
|
|
623
|
+
throw error;
|
|
624
|
+
}
|
|
625
|
+
if (signal?.aborted) {
|
|
626
|
+
throw error;
|
|
627
|
+
}
|
|
628
|
+
let delayMs;
|
|
629
|
+
const retryAfterMs = getRetryAfterFromError(error);
|
|
630
|
+
if (retryAfterMs !== null) {
|
|
631
|
+
delayMs = Math.min(retryAfterMs, 12e4);
|
|
632
|
+
} else {
|
|
633
|
+
delayMs = calculateDelay(attempt, baseDelay, maxDelay);
|
|
634
|
+
}
|
|
635
|
+
const status = error instanceof ApiError ? error.status : void 0;
|
|
636
|
+
onRetry?.(attempt + 1, delayMs, status);
|
|
637
|
+
await new Promise((resolve3, reject) => {
|
|
638
|
+
const timer = setTimeout(resolve3, delayMs);
|
|
639
|
+
if (signal) {
|
|
640
|
+
const onAbort = () => {
|
|
641
|
+
clearTimeout(timer);
|
|
642
|
+
reject(lastError);
|
|
643
|
+
};
|
|
644
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
645
|
+
const origResolve = resolve3;
|
|
646
|
+
setTimeout(() => {
|
|
647
|
+
signal.removeEventListener("abort", onAbort);
|
|
648
|
+
origResolve();
|
|
649
|
+
}, delayMs);
|
|
650
|
+
}
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
}
|
|
654
|
+
throw lastError;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
// src/util/verbosity.ts
|
|
658
|
+
var _verbose = false;
|
|
659
|
+
var _debug = false;
|
|
660
|
+
function setVerbosity(verbose, debug) {
|
|
661
|
+
_debug = debug;
|
|
662
|
+
_verbose = verbose || debug;
|
|
663
|
+
}
|
|
664
|
+
var isVerbose = () => _verbose;
|
|
665
|
+
var isDebug = () => _debug;
|
|
666
|
+
|
|
667
|
+
// src/util/client.ts
|
|
668
|
+
var SENSITIVE_HEADERS = [
|
|
669
|
+
"authorization",
|
|
670
|
+
"cookie",
|
|
671
|
+
"set-cookie",
|
|
672
|
+
"x-api-key"
|
|
673
|
+
];
|
|
674
|
+
function maskSensitiveHeaders(headers) {
|
|
675
|
+
const result = {};
|
|
676
|
+
headers.forEach((value, key) => {
|
|
677
|
+
result[key] = SENSITIVE_HEADERS.includes(key.toLowerCase()) ? "[REDACTED]" : value;
|
|
678
|
+
});
|
|
679
|
+
return result;
|
|
680
|
+
}
|
|
681
|
+
var RushClient = class _RushClient {
|
|
682
|
+
baseUrl;
|
|
683
|
+
constructor() {
|
|
684
|
+
const config = getGlobalConfig();
|
|
685
|
+
const raw = process.env.RUSH_API_URL ?? config.api;
|
|
686
|
+
this.baseUrl = raw.replace(/\/+$/, "");
|
|
687
|
+
}
|
|
688
|
+
async getHeaders() {
|
|
689
|
+
const headers = {
|
|
690
|
+
"Content-Type": "application/json",
|
|
691
|
+
"User-Agent": `rush-ai/${VERSION}`
|
|
692
|
+
};
|
|
693
|
+
if (!process.env.RUSH_API_KEY) {
|
|
694
|
+
await refreshCasAccessToken();
|
|
695
|
+
}
|
|
696
|
+
const token = getAuthToken();
|
|
697
|
+
if (token) {
|
|
698
|
+
headers.Authorization = `Bearer ${token}`;
|
|
699
|
+
}
|
|
700
|
+
if (process.env.RUSH_TEST_MODE === "integration-test") {
|
|
701
|
+
headers["X-Test-Mode"] = "integration-test";
|
|
702
|
+
}
|
|
703
|
+
return headers;
|
|
704
|
+
}
|
|
705
|
+
async performRequest(url, options, retryOnUnauthorized = true) {
|
|
706
|
+
const requestHeaders = options.headers ?? {};
|
|
707
|
+
const headers = await this.getHeaders();
|
|
708
|
+
try {
|
|
709
|
+
const response = await fetch(url, {
|
|
710
|
+
...options,
|
|
711
|
+
headers: {
|
|
712
|
+
...headers,
|
|
713
|
+
...requestHeaders
|
|
714
|
+
}
|
|
715
|
+
});
|
|
716
|
+
if (response.status === 401 && retryOnUnauthorized && !process.env.RUSH_API_KEY) {
|
|
717
|
+
const auth = getAuthConfig();
|
|
718
|
+
const canRefresh = auth.method === "cas" && Boolean(auth.refreshToken);
|
|
719
|
+
if (canRefresh && await refreshCasAccessToken({ force: true })) {
|
|
720
|
+
const retryHeaders = await this.getHeaders();
|
|
721
|
+
return await fetch(url, {
|
|
722
|
+
...options,
|
|
723
|
+
headers: {
|
|
724
|
+
...retryHeaders,
|
|
725
|
+
...requestHeaders
|
|
726
|
+
}
|
|
727
|
+
});
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
return response;
|
|
731
|
+
} catch (err) {
|
|
732
|
+
throw new RushError(
|
|
733
|
+
`Network error: ${err instanceof Error ? err.message : "Failed to connect"}`,
|
|
734
|
+
{ url },
|
|
735
|
+
"NETWORK_ERROR"
|
|
736
|
+
);
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
/** Idempotent HTTP methods eligible for automatic retry */
|
|
740
|
+
static RETRYABLE_METHODS = ["GET", "HEAD", "DELETE"];
|
|
741
|
+
/**
|
|
742
|
+
* Internal fetch with response parsing and error handling (no retry).
|
|
743
|
+
*/
|
|
744
|
+
async _doFetch(path, options = {}) {
|
|
745
|
+
const { json = true, ...fetchOptions } = options;
|
|
746
|
+
const url = `${this.baseUrl}${path}`;
|
|
747
|
+
const method = (fetchOptions.method ?? "GET").toUpperCase();
|
|
748
|
+
const startTime = Date.now();
|
|
749
|
+
if (isVerbose()) {
|
|
750
|
+
output.dim(`\u2192 ${method} ${url}`);
|
|
751
|
+
}
|
|
752
|
+
const response = await this.performRequest(url, fetchOptions);
|
|
753
|
+
if (isVerbose()) {
|
|
754
|
+
output.dim(
|
|
755
|
+
`\u2190 ${response.status} ${response.statusText} (${Date.now() - startTime}ms)`
|
|
756
|
+
);
|
|
757
|
+
}
|
|
758
|
+
if (isDebug()) {
|
|
759
|
+
output.dim(
|
|
760
|
+
` Response headers: ${JSON.stringify(maskSensitiveHeaders(response.headers))}`
|
|
761
|
+
);
|
|
762
|
+
}
|
|
763
|
+
if (!response.ok) {
|
|
764
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
765
|
+
const retryAfter = response.headers.get("retry-after") ?? void 0;
|
|
766
|
+
throw new ApiError(
|
|
767
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
768
|
+
response.status,
|
|
769
|
+
errorBody,
|
|
770
|
+
retryAfter
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
const data = json ? await response.json() : await response.text();
|
|
774
|
+
return {
|
|
775
|
+
ok: true,
|
|
776
|
+
status: response.status,
|
|
777
|
+
data
|
|
778
|
+
};
|
|
779
|
+
}
|
|
780
|
+
/**
|
|
781
|
+
* Fetch with response parsing, error handling, and automatic retry
|
|
782
|
+
* for idempotent methods (GET, HEAD, DELETE).
|
|
783
|
+
*/
|
|
784
|
+
async fetch(path, options = {}) {
|
|
785
|
+
const method = (options.method ?? "GET").toUpperCase();
|
|
786
|
+
const isRetryable = _RushClient.RETRYABLE_METHODS.includes(method);
|
|
787
|
+
if (!isRetryable) {
|
|
788
|
+
return this._doFetch(path, options);
|
|
789
|
+
}
|
|
790
|
+
return withRetry(() => this._doFetch(path, options), {
|
|
791
|
+
signal: options.signal ?? void 0,
|
|
792
|
+
onRetry: (attempt, delayMs, status) => {
|
|
793
|
+
const statusInfo = status ? ` (${status})` : "";
|
|
794
|
+
console.error(
|
|
795
|
+
`\u27F3 Retrying ${method} ${path}${statusInfo} (attempt ${attempt}) in ${(delayMs / 1e3).toFixed(1)}s...`
|
|
796
|
+
);
|
|
797
|
+
}
|
|
798
|
+
});
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* Raw fetch for streaming (SSE) responses.
|
|
802
|
+
* Returns the raw Response object.
|
|
803
|
+
*/
|
|
804
|
+
async fetchRaw(path, options = {}) {
|
|
805
|
+
const url = `${this.baseUrl}${path}`;
|
|
806
|
+
const response = await this.performRequest(url, options);
|
|
807
|
+
if (!response.ok) {
|
|
808
|
+
const errorBody = await response.text().catch(() => "Unknown error");
|
|
809
|
+
throw new ApiError(
|
|
810
|
+
`API request failed: ${response.status} ${response.statusText}`,
|
|
811
|
+
response.status,
|
|
812
|
+
errorBody
|
|
813
|
+
);
|
|
814
|
+
}
|
|
815
|
+
return response;
|
|
816
|
+
}
|
|
817
|
+
async get(path) {
|
|
818
|
+
return this.fetch(path, { method: "GET" });
|
|
819
|
+
}
|
|
820
|
+
async post(path, body) {
|
|
821
|
+
return this.fetch(path, {
|
|
822
|
+
method: "POST",
|
|
823
|
+
body: body ? JSON.stringify(body) : void 0
|
|
824
|
+
});
|
|
825
|
+
}
|
|
826
|
+
async put(path, body) {
|
|
827
|
+
return this.fetch(path, {
|
|
828
|
+
method: "PUT",
|
|
829
|
+
body: body ? JSON.stringify(body) : void 0
|
|
830
|
+
});
|
|
831
|
+
}
|
|
832
|
+
async delete(path) {
|
|
833
|
+
return this.fetch(path, { method: "DELETE" });
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
function createClient() {
|
|
837
|
+
return new RushClient();
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export {
|
|
841
|
+
output,
|
|
842
|
+
VERSION,
|
|
843
|
+
getActiveProfile,
|
|
844
|
+
setActiveProfile,
|
|
845
|
+
listProfiles,
|
|
846
|
+
createProfile,
|
|
847
|
+
deleteProfile,
|
|
848
|
+
getAuthConfig,
|
|
849
|
+
setAuthConfig,
|
|
850
|
+
clearAuthConfig,
|
|
851
|
+
getGlobalConfig,
|
|
852
|
+
setGlobalConfig,
|
|
853
|
+
getConfigDir,
|
|
854
|
+
isLoggedIn,
|
|
855
|
+
getProfileConfig,
|
|
856
|
+
getProfileAuth,
|
|
857
|
+
getAuthMethod,
|
|
858
|
+
getAuthToken,
|
|
859
|
+
verifyCurrentAuthSession,
|
|
860
|
+
revokeCurrentSession,
|
|
861
|
+
RushError,
|
|
862
|
+
AuthError,
|
|
863
|
+
TaskFailedError,
|
|
864
|
+
ApiError,
|
|
865
|
+
isRushError,
|
|
866
|
+
setVerbosity,
|
|
867
|
+
RushClient,
|
|
868
|
+
createClient
|
|
869
|
+
};
|
|
870
|
+
//# sourceMappingURL=chunk-7H2T3KCJ.js.map
|