@quireco/cli 0.0.8 → 0.0.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -13
- package/dist/chunk-e9Ob2GDo.mjs +26 -0
- package/dist/cli-CqxFgCaa.mjs +50058 -0
- package/dist/dist-CW-Om7Oq.mjs +833 -0
- package/dist/index.mjs +1 -1
- package/dist/quire.mjs +1 -1
- package/package.json +2 -1
- package/dist/cli-vr0Pht7B.mjs +0 -2519
package/dist/cli-vr0Pht7B.mjs
DELETED
|
@@ -1,2519 +0,0 @@
|
|
|
1
|
-
import { createMain, defineCommand, runCommand, showUsage } from "citty";
|
|
2
|
-
import { isCancel, log, note, spinner, text } from "@clack/prompts";
|
|
3
|
-
import { loginGitHubCopilot, loginOpenAICodex } from "@earendil-works/pi-ai/oauth";
|
|
4
|
-
import { AuthStorage } from "@earendil-works/pi-coding-agent";
|
|
5
|
-
import { execFile, spawn } from "node:child_process";
|
|
6
|
-
import pc from "picocolors";
|
|
7
|
-
import { homedir } from "node:os";
|
|
8
|
-
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
9
|
-
import "@earendil-works/pi-ai";
|
|
10
|
-
import "@earendil-works/pi-ai/openai-completions";
|
|
11
|
-
import { access, constants, mkdir, readFile, unlink, writeFile } from "node:fs/promises";
|
|
12
|
-
import { closeSync, existsSync, fsyncSync, mkdirSync, openSync, readFileSync, readdirSync, renameSync, writeFileSync } from "node:fs";
|
|
13
|
-
import { Type } from "@sinclair/typebox";
|
|
14
|
-
import { Value } from "@sinclair/typebox/value";
|
|
15
|
-
import { promisify } from "node:util";
|
|
16
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
17
|
-
import { cwd } from "process";
|
|
18
|
-
//#region src/auth/open-browser.ts
|
|
19
|
-
async function openBrowser(url) {
|
|
20
|
-
const command = browserCommand(url);
|
|
21
|
-
if (command === null) return false;
|
|
22
|
-
return new Promise((resolve) => {
|
|
23
|
-
const child = spawn(command.command, command.args, {
|
|
24
|
-
detached: true,
|
|
25
|
-
stdio: "ignore",
|
|
26
|
-
shell: false
|
|
27
|
-
});
|
|
28
|
-
child.once("error", () => resolve(false));
|
|
29
|
-
child.once("spawn", () => {
|
|
30
|
-
child.unref();
|
|
31
|
-
resolve(true);
|
|
32
|
-
});
|
|
33
|
-
});
|
|
34
|
-
}
|
|
35
|
-
function browserCommand(url) {
|
|
36
|
-
if (process.platform === "darwin") return {
|
|
37
|
-
command: "open",
|
|
38
|
-
args: [url]
|
|
39
|
-
};
|
|
40
|
-
if (process.platform === "win32") return {
|
|
41
|
-
command: "cmd",
|
|
42
|
-
args: [
|
|
43
|
-
"/c",
|
|
44
|
-
"start",
|
|
45
|
-
"",
|
|
46
|
-
url
|
|
47
|
-
]
|
|
48
|
-
};
|
|
49
|
-
return {
|
|
50
|
-
command: "xdg-open",
|
|
51
|
-
args: [url]
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
//#endregion
|
|
55
|
-
//#region src/exit-codes.ts
|
|
56
|
-
const ExitCode = {
|
|
57
|
-
Success: 0,
|
|
58
|
-
InternalError: 1,
|
|
59
|
-
InvalidInput: 2,
|
|
60
|
-
AuthFailure: 10,
|
|
61
|
-
NetworkFailure: 11
|
|
62
|
-
};
|
|
63
|
-
var CliError = class extends Error {
|
|
64
|
-
constructor(message, exitCode = ExitCode.InternalError) {
|
|
65
|
-
super(message);
|
|
66
|
-
this.exitCode = exitCode;
|
|
67
|
-
this.name = "CliError";
|
|
68
|
-
}
|
|
69
|
-
};
|
|
70
|
-
function readExitCode(error) {
|
|
71
|
-
return error instanceof CliError ? error.exitCode : ExitCode.InternalError;
|
|
72
|
-
}
|
|
73
|
-
function toOneLineError(error) {
|
|
74
|
-
return (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim() || "unknown error";
|
|
75
|
-
}
|
|
76
|
-
//#endregion
|
|
77
|
-
//#region src/output.ts
|
|
78
|
-
function writeLine(output, message = "") {
|
|
79
|
-
output?.write(`${message}\n`);
|
|
80
|
-
}
|
|
81
|
-
function writeJson(output, value) {
|
|
82
|
-
writeLine(output, JSON.stringify(value));
|
|
83
|
-
}
|
|
84
|
-
const color = {
|
|
85
|
-
heading(value) {
|
|
86
|
-
return pc.bold(value);
|
|
87
|
-
},
|
|
88
|
-
label(value) {
|
|
89
|
-
return pc.gray(value);
|
|
90
|
-
},
|
|
91
|
-
success(value) {
|
|
92
|
-
return pc.green(value);
|
|
93
|
-
},
|
|
94
|
-
warn(value) {
|
|
95
|
-
return pc.yellow(value);
|
|
96
|
-
},
|
|
97
|
-
error(value) {
|
|
98
|
-
return pc.red(value);
|
|
99
|
-
},
|
|
100
|
-
info(value) {
|
|
101
|
-
return pc.cyan(value);
|
|
102
|
-
},
|
|
103
|
-
path(value) {
|
|
104
|
-
return pc.dim(value);
|
|
105
|
-
},
|
|
106
|
-
command(value) {
|
|
107
|
-
return pc.cyan(value);
|
|
108
|
-
},
|
|
109
|
-
value(value) {
|
|
110
|
-
return pc.bold(value);
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
//#endregion
|
|
114
|
-
//#region src/auth/constants.ts
|
|
115
|
-
const QUIRE_API_TOKEN_ENV = "QUIRE_API_TOKEN";
|
|
116
|
-
const QUIRE_API_TOKEN_TYPE = "QuireApiKey";
|
|
117
|
-
//#endregion
|
|
118
|
-
//#region src/utils/runtime.ts
|
|
119
|
-
function isRecord(value) {
|
|
120
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
121
|
-
}
|
|
122
|
-
function readString(value) {
|
|
123
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
124
|
-
}
|
|
125
|
-
//#endregion
|
|
126
|
-
//#region src/auth/api.ts
|
|
127
|
-
const CLIENT_ID = "quire-cli";
|
|
128
|
-
const DEVICE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
129
|
-
var HttpError = class extends CliError {
|
|
130
|
-
constructor(message, status, body, code) {
|
|
131
|
-
super(message, status === 401 || status === 403 ? ExitCode.AuthFailure : ExitCode.NetworkFailure);
|
|
132
|
-
this.status = status;
|
|
133
|
-
this.body = body;
|
|
134
|
-
this.code = code;
|
|
135
|
-
this.name = "HttpError";
|
|
136
|
-
}
|
|
137
|
-
};
|
|
138
|
-
function readApiBaseUrl(value = process.env.QUIRE_API_URL ?? "http://localhost:3000") {
|
|
139
|
-
return normalizeApiBaseUrl(value);
|
|
140
|
-
}
|
|
141
|
-
function normalizeApiBaseUrl(value) {
|
|
142
|
-
try {
|
|
143
|
-
const url = new URL(value);
|
|
144
|
-
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
145
|
-
url.search = "";
|
|
146
|
-
url.hash = "";
|
|
147
|
-
return url.toString().replace(/\/$/, "");
|
|
148
|
-
} catch {
|
|
149
|
-
throw new CliError(`Invalid Quire API URL: ${value}`, ExitCode.InvalidInput);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
async function requestDeviceCode(options) {
|
|
153
|
-
return parseDeviceCodeResponse((await requestJson({
|
|
154
|
-
url: authUrl(options.apiBaseUrl, "/device/code"),
|
|
155
|
-
fetchFn: options.fetchFn,
|
|
156
|
-
init: {
|
|
157
|
-
method: "POST",
|
|
158
|
-
headers: jsonHeaders(),
|
|
159
|
-
body: JSON.stringify({
|
|
160
|
-
client_id: CLIENT_ID,
|
|
161
|
-
...options.scope === void 0 ? {} : { scope: options.scope }
|
|
162
|
-
})
|
|
163
|
-
}
|
|
164
|
-
})).body);
|
|
165
|
-
}
|
|
166
|
-
async function requestDeviceToken(options) {
|
|
167
|
-
return parseDeviceTokenResponse((await requestJson({
|
|
168
|
-
url: authUrl(options.apiBaseUrl, "/device/token"),
|
|
169
|
-
fetchFn: options.fetchFn,
|
|
170
|
-
init: {
|
|
171
|
-
method: "POST",
|
|
172
|
-
headers: jsonHeaders(),
|
|
173
|
-
body: JSON.stringify({
|
|
174
|
-
grant_type: DEVICE_GRANT_TYPE,
|
|
175
|
-
device_code: options.deviceCode,
|
|
176
|
-
client_id: CLIENT_ID
|
|
177
|
-
})
|
|
178
|
-
}
|
|
179
|
-
})).body);
|
|
180
|
-
}
|
|
181
|
-
async function fetchCurrentSession(options) {
|
|
182
|
-
const response = await requestJson({
|
|
183
|
-
url: authUrl(options.apiBaseUrl, "/get-session"),
|
|
184
|
-
fetchFn: options.fetchFn,
|
|
185
|
-
init: {
|
|
186
|
-
method: "GET",
|
|
187
|
-
headers: {
|
|
188
|
-
Accept: "application/json",
|
|
189
|
-
...authHeaders(options)
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
});
|
|
193
|
-
const refreshedAccessToken = response.response.headers.get("set-auth-token") ?? void 0;
|
|
194
|
-
return {
|
|
195
|
-
data: parseCurrentSession(response.body),
|
|
196
|
-
...refreshedAccessToken === void 0 ? {} : { refreshedAccessToken }
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
async function fetchModelSourceBrokerStatus(options) {
|
|
200
|
-
return parseModelSourceBrokerStatus((await requestJson({
|
|
201
|
-
url: apiUrl(options.apiBaseUrl, "/api/model-sources"),
|
|
202
|
-
fetchFn: options.fetchFn,
|
|
203
|
-
init: {
|
|
204
|
-
method: "GET",
|
|
205
|
-
headers: {
|
|
206
|
-
Accept: "application/json",
|
|
207
|
-
...authHeaders(options),
|
|
208
|
-
"User-Agent": "quire-cli"
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
})).body);
|
|
212
|
-
}
|
|
213
|
-
function authUrl(apiBaseUrl, path) {
|
|
214
|
-
return apiUrl(apiBaseUrl, `/api/auth${path}`);
|
|
215
|
-
}
|
|
216
|
-
function apiUrl(apiBaseUrl, path) {
|
|
217
|
-
return new URL(path, apiBaseUrl).toString();
|
|
218
|
-
}
|
|
219
|
-
function jsonHeaders() {
|
|
220
|
-
return {
|
|
221
|
-
Accept: "application/json",
|
|
222
|
-
"Content-Type": "application/json",
|
|
223
|
-
"User-Agent": "quire-cli"
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
function authHeaders(credentials) {
|
|
227
|
-
if (credentials.tokenType === "QuireApiKey") return { "X-Quire-API-Key": credentials.accessToken };
|
|
228
|
-
return { Authorization: `Bearer ${credentials.accessToken}` };
|
|
229
|
-
}
|
|
230
|
-
async function requestJson(options) {
|
|
231
|
-
let response;
|
|
232
|
-
try {
|
|
233
|
-
response = await (options.fetchFn ?? fetch)(options.url, options.init);
|
|
234
|
-
} catch (error) {
|
|
235
|
-
throw new CliError(`Could not reach Quire API: ${error instanceof Error ? error.message : String(error)}`, ExitCode.NetworkFailure);
|
|
236
|
-
}
|
|
237
|
-
const body = await readResponseBody(response);
|
|
238
|
-
if (!response.ok) {
|
|
239
|
-
const { code, description } = readErrorBody(body);
|
|
240
|
-
throw new HttpError(description ?? `Quire API request failed with HTTP ${response.status}`, response.status, body, code);
|
|
241
|
-
}
|
|
242
|
-
return {
|
|
243
|
-
body,
|
|
244
|
-
response
|
|
245
|
-
};
|
|
246
|
-
}
|
|
247
|
-
async function readResponseBody(response) {
|
|
248
|
-
const text = await response.text();
|
|
249
|
-
if (!text) return null;
|
|
250
|
-
try {
|
|
251
|
-
return JSON.parse(text);
|
|
252
|
-
} catch {
|
|
253
|
-
return text;
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
function readErrorBody(body) {
|
|
257
|
-
if (!isRecord(body)) return {};
|
|
258
|
-
return {
|
|
259
|
-
code: typeof body.error === "string" ? body.error : void 0,
|
|
260
|
-
description: typeof body.error_description === "string" ? body.error_description : typeof body.message === "string" ? body.message : void 0
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
function parseDeviceCodeResponse(value) {
|
|
264
|
-
if (!isRecord(value)) throw new CliError("Quire API returned an invalid device code response", ExitCode.NetworkFailure);
|
|
265
|
-
const response = {
|
|
266
|
-
device_code: value.device_code,
|
|
267
|
-
user_code: value.user_code,
|
|
268
|
-
verification_uri: value.verification_uri,
|
|
269
|
-
verification_uri_complete: value.verification_uri_complete,
|
|
270
|
-
expires_in: value.expires_in,
|
|
271
|
-
interval: value.interval
|
|
272
|
-
};
|
|
273
|
-
if (typeof response.device_code !== "string" || typeof response.user_code !== "string" || typeof response.verification_uri !== "string" || typeof response.verification_uri_complete !== "string" || typeof response.expires_in !== "number" || typeof response.interval !== "number") throw new CliError("Quire API returned an invalid device code response", ExitCode.NetworkFailure);
|
|
274
|
-
return response;
|
|
275
|
-
}
|
|
276
|
-
function parseDeviceTokenResponse(value) {
|
|
277
|
-
if (!isRecord(value) || typeof value.access_token !== "string") throw new CliError("Quire API returned an invalid device token response", ExitCode.NetworkFailure);
|
|
278
|
-
if (value.expires_in !== void 0 && typeof value.expires_in !== "number") throw new CliError("Quire API returned an invalid device token response", ExitCode.NetworkFailure);
|
|
279
|
-
if (value.scope !== void 0 && typeof value.scope !== "string") throw new CliError("Quire API returned an invalid device token response", ExitCode.NetworkFailure);
|
|
280
|
-
return {
|
|
281
|
-
access_token: value.access_token,
|
|
282
|
-
token_type: typeof value.token_type === "string" ? value.token_type : "Bearer",
|
|
283
|
-
...value.expires_in === void 0 ? {} : { expires_in: value.expires_in },
|
|
284
|
-
...value.scope === void 0 ? {} : { scope: value.scope }
|
|
285
|
-
};
|
|
286
|
-
}
|
|
287
|
-
function parseCurrentSession(value) {
|
|
288
|
-
if (value === null) return null;
|
|
289
|
-
if (!isRecord(value) || !isRecord(value.session) || !isRecord(value.user)) throw new CliError("Quire API returned an invalid session response", ExitCode.NetworkFailure);
|
|
290
|
-
if (typeof value.session.id !== "string" || typeof value.user.id !== "string") throw new CliError("Quire API returned an invalid session response", ExitCode.NetworkFailure);
|
|
291
|
-
if (value.user.email !== void 0 && value.user.email !== null && typeof value.user.email !== "string") throw new CliError("Quire API returned an invalid session response", ExitCode.NetworkFailure);
|
|
292
|
-
if (value.user.name !== void 0 && value.user.name !== null && typeof value.user.name !== "string") throw new CliError("Quire API returned an invalid session response", ExitCode.NetworkFailure);
|
|
293
|
-
return {
|
|
294
|
-
session: {
|
|
295
|
-
id: value.session.id,
|
|
296
|
-
...typeof value.session.expiresAt === "string" ? { expiresAt: value.session.expiresAt } : {}
|
|
297
|
-
},
|
|
298
|
-
user: {
|
|
299
|
-
id: value.user.id,
|
|
300
|
-
...value.user.email === void 0 ? {} : { email: value.user.email },
|
|
301
|
-
...value.user.name === void 0 ? {} : { name: value.user.name }
|
|
302
|
-
}
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
function parseModelSourceBrokerStatus(value) {
|
|
306
|
-
if (!isRecord(value) || !Array.isArray(value.selectedOrder)) throw new CliError("Quire API returned an invalid model-source status response", ExitCode.NetworkFailure);
|
|
307
|
-
const resolvedAvailability = value.resolvedAvailability;
|
|
308
|
-
if (!isRecord(resolvedAvailability)) throw new CliError("Quire API returned an invalid model-source status response", ExitCode.NetworkFailure);
|
|
309
|
-
const status = resolvedAvailability.status;
|
|
310
|
-
const reason = resolvedAvailability.reason;
|
|
311
|
-
const selectedProviderMode = resolvedAvailability.mode;
|
|
312
|
-
const requiredNextAction = resolvedAvailability.nextAction;
|
|
313
|
-
if (status !== "available" && status !== "unavailable" || typeof reason !== "string" || selectedProviderMode !== null && typeof selectedProviderMode !== "string" || requiredNextAction !== null && typeof requiredNextAction !== "string" || !value.selectedOrder.every((mode) => typeof mode === "string")) throw new CliError("Quire API returned an invalid model-source status response", ExitCode.NetworkFailure);
|
|
314
|
-
const actor = readActor(value);
|
|
315
|
-
return {
|
|
316
|
-
...actor === void 0 ? {} : { actor },
|
|
317
|
-
available: status === "available",
|
|
318
|
-
status,
|
|
319
|
-
selectedProviderMode,
|
|
320
|
-
selectedOrder: value.selectedOrder,
|
|
321
|
-
reason,
|
|
322
|
-
requiredNextAction,
|
|
323
|
-
requiredBalance: readWalletRequiredBalance(value),
|
|
324
|
-
remainingBalance: readWalletRemainingBalance(value),
|
|
325
|
-
recentUsage: readRecentUsage(value)
|
|
326
|
-
};
|
|
327
|
-
}
|
|
328
|
-
function readActor(value) {
|
|
329
|
-
const actor = value.actor;
|
|
330
|
-
if (!isRecord(actor)) return;
|
|
331
|
-
const actorType = actor.actorType;
|
|
332
|
-
if (actorType !== "user_session" && actorType !== "api_key") return;
|
|
333
|
-
return {
|
|
334
|
-
actorType,
|
|
335
|
-
actorId: typeof actor.actorId === "string" ? actor.actorId : null,
|
|
336
|
-
environmentId: typeof actor.environmentId === "string" ? actor.environmentId : null,
|
|
337
|
-
environmentName: typeof actor.environmentName === "string" ? actor.environmentName : null,
|
|
338
|
-
triggerSource: typeof actor.triggerSource === "string" ? actor.triggerSource : null
|
|
339
|
-
};
|
|
340
|
-
}
|
|
341
|
-
function readWalletRequiredBalance(value) {
|
|
342
|
-
const source = readFirstSource(value);
|
|
343
|
-
const requiredBalance = (isRecord(source?.wallet) ? source.wallet : null)?.requiredBalance;
|
|
344
|
-
return typeof requiredBalance === "number" && Number.isFinite(requiredBalance) ? requiredBalance : null;
|
|
345
|
-
}
|
|
346
|
-
function readWalletRemainingBalance(value) {
|
|
347
|
-
const source = readFirstSource(value);
|
|
348
|
-
const wallet = isRecord(source?.wallet) ? source.wallet : null;
|
|
349
|
-
const remaining = (isRecord(wallet?.balance) ? wallet.balance : null)?.remaining;
|
|
350
|
-
return typeof remaining === "number" && Number.isFinite(remaining) ? remaining : null;
|
|
351
|
-
}
|
|
352
|
-
function readFirstSource(value) {
|
|
353
|
-
if (!Array.isArray(value.sources)) return null;
|
|
354
|
-
const [source] = value.sources;
|
|
355
|
-
return isRecord(source) ? source : null;
|
|
356
|
-
}
|
|
357
|
-
function readRecentUsage(value) {
|
|
358
|
-
if (!Array.isArray(value.recentUsage)) return [];
|
|
359
|
-
return value.recentUsage.flatMap((event) => {
|
|
360
|
-
if (!isRecord(event)) return [];
|
|
361
|
-
if (typeof event.modelId !== "string" || typeof event.status !== "string" || typeof event.totalTokens !== "number" || typeof event.createdAt !== "string") return [];
|
|
362
|
-
return [{
|
|
363
|
-
modelId: event.modelId,
|
|
364
|
-
status: event.status,
|
|
365
|
-
totalTokens: event.totalTokens,
|
|
366
|
-
runId: typeof event.runId === "string" ? event.runId : null,
|
|
367
|
-
createdAt: event.createdAt
|
|
368
|
-
}];
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
//#endregion
|
|
372
|
-
//#region src/auth/store.ts
|
|
373
|
-
function defaultAuthPath(env = process.env) {
|
|
374
|
-
if (env.QUIRE_AUTH_FILE) return resolve(env.QUIRE_AUTH_FILE);
|
|
375
|
-
return join(resolve(env.QUIRE_HOME ?? join(homedir(), ".quire")), "auth.json");
|
|
376
|
-
}
|
|
377
|
-
function createAuthStore(path = defaultAuthPath()) {
|
|
378
|
-
async function get() {
|
|
379
|
-
try {
|
|
380
|
-
const contents = await readFile(path, "utf8");
|
|
381
|
-
return parseStoredCredentials(JSON.parse(contents));
|
|
382
|
-
} catch {
|
|
383
|
-
return null;
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
async function set(credentials) {
|
|
387
|
-
await mkdir(dirname(path), {
|
|
388
|
-
recursive: true,
|
|
389
|
-
mode: 448
|
|
390
|
-
});
|
|
391
|
-
await writeFile(path, `${JSON.stringify(credentials, null, 2)}\n`, { mode: 384 });
|
|
392
|
-
}
|
|
393
|
-
async function clear() {
|
|
394
|
-
try {
|
|
395
|
-
await unlink(path);
|
|
396
|
-
} catch {}
|
|
397
|
-
}
|
|
398
|
-
return {
|
|
399
|
-
path,
|
|
400
|
-
get,
|
|
401
|
-
set,
|
|
402
|
-
clear
|
|
403
|
-
};
|
|
404
|
-
}
|
|
405
|
-
const authStore = createAuthStore();
|
|
406
|
-
async function resolveAuthCredentials(options = {}) {
|
|
407
|
-
const env = options.env ?? process.env;
|
|
408
|
-
const apiToken = env[QUIRE_API_TOKEN_ENV]?.trim();
|
|
409
|
-
if (apiToken) return createStoredCredentials({
|
|
410
|
-
apiBaseUrl: readApiBaseUrl(env.QUIRE_API_URL),
|
|
411
|
-
accessToken: apiToken,
|
|
412
|
-
tokenType: QUIRE_API_TOKEN_TYPE,
|
|
413
|
-
user: {
|
|
414
|
-
id: "api-token",
|
|
415
|
-
name: "API token"
|
|
416
|
-
}
|
|
417
|
-
});
|
|
418
|
-
return (options.store ?? authStore).get();
|
|
419
|
-
}
|
|
420
|
-
function createStoredCredentials(input) {
|
|
421
|
-
const now = input.now ?? /* @__PURE__ */ new Date();
|
|
422
|
-
const timestamp = now.toISOString();
|
|
423
|
-
const expiresAt = input.expiresAt ?? expiresAtFromSeconds(input.expiresIn, now);
|
|
424
|
-
return {
|
|
425
|
-
version: 1,
|
|
426
|
-
apiBaseUrl: input.apiBaseUrl,
|
|
427
|
-
accessToken: input.accessToken,
|
|
428
|
-
tokenType: input.tokenType ?? "Bearer",
|
|
429
|
-
...expiresAt === void 0 ? {} : { expiresAt },
|
|
430
|
-
...input.user === void 0 ? {} : { user: input.user },
|
|
431
|
-
createdAt: timestamp,
|
|
432
|
-
updatedAt: timestamp
|
|
433
|
-
};
|
|
434
|
-
}
|
|
435
|
-
function updateStoredCredentials(credentials, input) {
|
|
436
|
-
return {
|
|
437
|
-
...credentials,
|
|
438
|
-
accessToken: input.accessToken ?? credentials.accessToken,
|
|
439
|
-
tokenType: input.tokenType ?? credentials.tokenType,
|
|
440
|
-
...input.expiresAt === void 0 ? {} : { expiresAt: input.expiresAt },
|
|
441
|
-
...input.user === void 0 ? {} : { user: input.user },
|
|
442
|
-
updatedAt: (input.now ?? /* @__PURE__ */ new Date()).toISOString()
|
|
443
|
-
};
|
|
444
|
-
}
|
|
445
|
-
function expiresAtFromSeconds(expiresIn, now) {
|
|
446
|
-
return typeof expiresIn === "number" ? new Date(now.getTime() + expiresIn * 1e3).toISOString() : void 0;
|
|
447
|
-
}
|
|
448
|
-
function parseStoredCredentials(value) {
|
|
449
|
-
if (!isRecord(value)) return null;
|
|
450
|
-
if (value.version !== 1 || typeof value.apiBaseUrl !== "string" || typeof value.accessToken !== "string" || typeof value.tokenType !== "string" || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") return null;
|
|
451
|
-
if (value.expiresAt !== void 0 && typeof value.expiresAt !== "string") return null;
|
|
452
|
-
const user = parseStoredUser(value.user);
|
|
453
|
-
if (value.user !== void 0 && user === null) return null;
|
|
454
|
-
return {
|
|
455
|
-
version: 1,
|
|
456
|
-
apiBaseUrl: value.apiBaseUrl,
|
|
457
|
-
accessToken: value.accessToken,
|
|
458
|
-
tokenType: value.tokenType,
|
|
459
|
-
...value.expiresAt === void 0 ? {} : { expiresAt: value.expiresAt },
|
|
460
|
-
...user === void 0 || user === null ? {} : { user },
|
|
461
|
-
createdAt: value.createdAt,
|
|
462
|
-
updatedAt: value.updatedAt
|
|
463
|
-
};
|
|
464
|
-
}
|
|
465
|
-
function parseStoredUser(value) {
|
|
466
|
-
if (value === void 0) return;
|
|
467
|
-
if (!isRecord(value) || typeof value.id !== "string") return null;
|
|
468
|
-
if (value.email !== void 0 && value.email !== null && typeof value.email !== "string") return null;
|
|
469
|
-
if (value.name !== void 0 && value.name !== null && typeof value.name !== "string") return null;
|
|
470
|
-
return {
|
|
471
|
-
id: value.id,
|
|
472
|
-
...value.email === void 0 ? {} : { email: value.email },
|
|
473
|
-
...value.name === void 0 ? {} : { name: value.name }
|
|
474
|
-
};
|
|
475
|
-
}
|
|
476
|
-
//#endregion
|
|
477
|
-
//#region src/pipeline/quire-model-provider.ts
|
|
478
|
-
const PI_CODEX_MODEL_PROVIDER = "openai-codex";
|
|
479
|
-
const PI_GITHUB_COPILOT_MODEL_PROVIDER = "github-copilot";
|
|
480
|
-
const PI_GITHUB_COPILOT_MODEL_ID = "gpt-5.5";
|
|
481
|
-
const QUIRE_MODEL_AUTH_PATH_ENV = "QUIRE_MODEL_AUTH_PATH";
|
|
482
|
-
function readQuireModelAuthPath() {
|
|
483
|
-
const envPath = process.env[QUIRE_MODEL_AUTH_PATH_ENV]?.trim();
|
|
484
|
-
return envPath && envPath.length > 0 ? envPath : join(homedir(), ".quire", "model-auth.json");
|
|
485
|
-
}
|
|
486
|
-
//#endregion
|
|
487
|
-
//#region src/commands/connect.ts
|
|
488
|
-
const CHATGPT_MANUAL_CODE_PROMPT = "Paste the localhost redirect URL from your browser";
|
|
489
|
-
const CHATGPT_MANUAL_CODE_PLACEHOLDER = "http://localhost:1455/auth/callback?code=...";
|
|
490
|
-
const connectCommand = defineCommand({
|
|
491
|
-
meta: {
|
|
492
|
-
name: "connect",
|
|
493
|
-
description: "Connect local provider accounts used by Quire investigations."
|
|
494
|
-
},
|
|
495
|
-
subCommands: {
|
|
496
|
-
chatgpt: defineCommand({
|
|
497
|
-
meta: {
|
|
498
|
-
name: "chatgpt",
|
|
499
|
-
description: "Connect ChatGPT/Codex subscription auth for local investigations."
|
|
500
|
-
},
|
|
501
|
-
args: {
|
|
502
|
-
"no-open": {
|
|
503
|
-
type: "boolean",
|
|
504
|
-
description: "Print the authorization URL without trying to open a browser."
|
|
505
|
-
},
|
|
506
|
-
json: {
|
|
507
|
-
type: "boolean",
|
|
508
|
-
description: "Print machine-readable connection state to stdout."
|
|
509
|
-
}
|
|
510
|
-
},
|
|
511
|
-
async run({ args }) {
|
|
512
|
-
await runConnectChatgpt({
|
|
513
|
-
noOpen: args["no-open"] === true,
|
|
514
|
-
json: args.json === true
|
|
515
|
-
});
|
|
516
|
-
}
|
|
517
|
-
}),
|
|
518
|
-
copilot: defineCommand({
|
|
519
|
-
meta: {
|
|
520
|
-
name: "copilot",
|
|
521
|
-
alias: "github",
|
|
522
|
-
description: "Connect GitHub Copilot subscription auth for local investigations."
|
|
523
|
-
},
|
|
524
|
-
args: {
|
|
525
|
-
"github-enterprise": {
|
|
526
|
-
type: "string",
|
|
527
|
-
description: "GitHub Enterprise URL/domain for Copilot login. Omit or pass an empty value for github.com."
|
|
528
|
-
},
|
|
529
|
-
"no-open": {
|
|
530
|
-
type: "boolean",
|
|
531
|
-
description: "Print the device authorization URL without trying to open a browser."
|
|
532
|
-
},
|
|
533
|
-
json: {
|
|
534
|
-
type: "boolean",
|
|
535
|
-
description: "Print machine-readable connection state to stdout."
|
|
536
|
-
}
|
|
537
|
-
},
|
|
538
|
-
async run({ args }) {
|
|
539
|
-
await runConnectCopilot({
|
|
540
|
-
githubEnterprise: args["github-enterprise"],
|
|
541
|
-
noOpen: args["no-open"] === true,
|
|
542
|
-
json: args.json === true
|
|
543
|
-
});
|
|
544
|
-
}
|
|
545
|
-
})
|
|
546
|
-
}
|
|
547
|
-
});
|
|
548
|
-
async function runConnectChatgpt(options = {}) {
|
|
549
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
550
|
-
const stderr = options.io?.stderr ?? process.stderr;
|
|
551
|
-
const modelAuthPath = options.modelAuthPath ?? readQuireModelAuthPath();
|
|
552
|
-
const modelAuthStorage = options.modelAuthStorage ?? AuthStorage.create(modelAuthPath);
|
|
553
|
-
const loginChatgpt = options.loginChatgpt ?? loginOpenAICodex;
|
|
554
|
-
const promptOutput = options.json === true ? stderr : stdout;
|
|
555
|
-
const promptForInput = options.promptForLine ?? ((message, promptOptions) => promptForLine(message, promptOutput, promptOptions));
|
|
556
|
-
let launchAttempt;
|
|
557
|
-
if (options.json !== true) {
|
|
558
|
-
log.step("Connect ChatGPT for local Quire investigations", {
|
|
559
|
-
output: stdout,
|
|
560
|
-
spacing: 0
|
|
561
|
-
});
|
|
562
|
-
log.message("Quire will use local Codex subscription auth before Quire Credits.", {
|
|
563
|
-
output: stdout,
|
|
564
|
-
spacing: 0,
|
|
565
|
-
withGuide: true
|
|
566
|
-
});
|
|
567
|
-
}
|
|
568
|
-
const credentials = await loginChatgpt({
|
|
569
|
-
originator: "quire",
|
|
570
|
-
onAuth: (info) => {
|
|
571
|
-
const authOutput = options.json === true ? stderr : stdout;
|
|
572
|
-
if (options.json === true) {
|
|
573
|
-
writeLine(authOutput, `${color.label("Authorization URL")}: ${info.url}`);
|
|
574
|
-
if (info.instructions) writeLine(authOutput, info.instructions);
|
|
575
|
-
} else writeChatgptAuthorizationNote(authOutput, info.url, info.instructions);
|
|
576
|
-
if (options.noOpen === true) return;
|
|
577
|
-
launchAttempt = (async () => {
|
|
578
|
-
let opened = false;
|
|
579
|
-
try {
|
|
580
|
-
opened = await (options.opener ?? openBrowser)(info.url);
|
|
581
|
-
} catch {
|
|
582
|
-
opened = false;
|
|
583
|
-
}
|
|
584
|
-
if (opened) log.success("Opened ChatGPT authorization in your browser.", {
|
|
585
|
-
output: stderr,
|
|
586
|
-
spacing: 0
|
|
587
|
-
});
|
|
588
|
-
else log.warn("Could not open a browser automatically. Open the authorization URL manually.", {
|
|
589
|
-
output: stderr,
|
|
590
|
-
spacing: 0
|
|
591
|
-
});
|
|
592
|
-
})();
|
|
593
|
-
},
|
|
594
|
-
onPrompt: async (prompt) => {
|
|
595
|
-
return promptForInput(prompt.placeholder ? `${prompt.message} (${prompt.placeholder})` : prompt.message);
|
|
596
|
-
},
|
|
597
|
-
onManualCodeInput: async () => {
|
|
598
|
-
await launchAttempt;
|
|
599
|
-
if (options.json !== true) log.message([
|
|
600
|
-
"Complete ChatGPT authorization in the browser.",
|
|
601
|
-
"When it redirects to localhost, the page may fail to load in a VM.",
|
|
602
|
-
"Copy the full address-bar URL and paste it below."
|
|
603
|
-
], {
|
|
604
|
-
output: promptOutput,
|
|
605
|
-
spacing: 0,
|
|
606
|
-
withGuide: true
|
|
607
|
-
});
|
|
608
|
-
return promptForInput(CHATGPT_MANUAL_CODE_PROMPT, { placeholder: CHATGPT_MANUAL_CODE_PLACEHOLDER });
|
|
609
|
-
},
|
|
610
|
-
onProgress: (message) => {
|
|
611
|
-
writeLine(stderr, message);
|
|
612
|
-
}
|
|
613
|
-
});
|
|
614
|
-
modelAuthStorage.set(PI_CODEX_MODEL_PROVIDER, oauthCredential(credentials));
|
|
615
|
-
if (options.json === true) {
|
|
616
|
-
writeJson(stdout, {
|
|
617
|
-
connected: true,
|
|
618
|
-
provider: PI_CODEX_MODEL_PROVIDER,
|
|
619
|
-
authPath: modelAuthPath,
|
|
620
|
-
source: "quire_model_auth"
|
|
621
|
-
});
|
|
622
|
-
return;
|
|
623
|
-
}
|
|
624
|
-
log.success("ChatGPT connected for local Quire investigations.", { output: stdout });
|
|
625
|
-
}
|
|
626
|
-
function writeChatgptAuthorizationNote(output, url, instructions) {
|
|
627
|
-
const lines = [
|
|
628
|
-
"If you're in a VM or remote shell, browser launch may not work.",
|
|
629
|
-
"Open the sign-in URL below in your local browser.",
|
|
630
|
-
"After signing in, ChatGPT will redirect to localhost.",
|
|
631
|
-
"Copy that final localhost URL back into this terminal."
|
|
632
|
-
];
|
|
633
|
-
if (instructions) lines.push("", instructions);
|
|
634
|
-
note(lines.join("\n"), "ChatGPT authorization", { output });
|
|
635
|
-
writeLine(output);
|
|
636
|
-
writeLine(output, `${color.label("Open")}: ${terminalLink("ChatGPT authorization", url, output)}`);
|
|
637
|
-
writeLine(output, color.label("Sign-in URL to paste into your browser:"));
|
|
638
|
-
writeLine(output, url);
|
|
639
|
-
writeLine(output);
|
|
640
|
-
}
|
|
641
|
-
function terminalLink(label, url, output) {
|
|
642
|
-
if (!isInteractiveOutput(output)) return label;
|
|
643
|
-
return `\u001B]8;;${url}\u001B\\${label}\u001B]8;;\u001B\\`;
|
|
644
|
-
}
|
|
645
|
-
function isInteractiveOutput(output) {
|
|
646
|
-
return Boolean(output && "isTTY" in output && output.isTTY === true && process.env.TERM !== "dumb" && process.env.CI !== "true");
|
|
647
|
-
}
|
|
648
|
-
async function runConnectCopilot(options = {}) {
|
|
649
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
650
|
-
const stderr = options.io?.stderr ?? process.stderr;
|
|
651
|
-
const modelAuthPath = options.modelAuthPath ?? readQuireModelAuthPath();
|
|
652
|
-
const modelAuthStorage = options.modelAuthStorage ?? AuthStorage.create(modelAuthPath);
|
|
653
|
-
const loginCopilot = options.loginCopilot ?? loginGitHubCopilot;
|
|
654
|
-
const promptOutput = options.json === true ? stderr : stdout;
|
|
655
|
-
const promptForInput = options.promptForLine ?? ((message) => promptForLine(message, promptOutput));
|
|
656
|
-
if (options.json !== true) {
|
|
657
|
-
log.step("Connect GitHub Copilot for local Quire investigations", {
|
|
658
|
-
output: stdout,
|
|
659
|
-
spacing: 0
|
|
660
|
-
});
|
|
661
|
-
log.message(`Quire will use local GitHub Copilot auth with ${PI_GITHUB_COPILOT_MODEL_ID} before Quire Credits.`, {
|
|
662
|
-
output: stdout,
|
|
663
|
-
spacing: 0,
|
|
664
|
-
withGuide: true
|
|
665
|
-
});
|
|
666
|
-
}
|
|
667
|
-
let usedEnterpriseFlag = false;
|
|
668
|
-
const credentials = await loginCopilot({
|
|
669
|
-
onAuth: (url, instructions) => {
|
|
670
|
-
const authOutput = options.json === true ? stderr : stdout;
|
|
671
|
-
writeLine(authOutput, `${color.label("Authorization URL")}: ${url}`);
|
|
672
|
-
if (instructions) writeLine(authOutput, instructions);
|
|
673
|
-
if (options.noOpen === true) return;
|
|
674
|
-
(async () => {
|
|
675
|
-
let opened = false;
|
|
676
|
-
try {
|
|
677
|
-
opened = await (options.opener ?? openBrowser)(url);
|
|
678
|
-
} catch {
|
|
679
|
-
opened = false;
|
|
680
|
-
}
|
|
681
|
-
if (opened) log.success("Opened GitHub Copilot authorization in your browser.", {
|
|
682
|
-
output: stderr,
|
|
683
|
-
spacing: 0
|
|
684
|
-
});
|
|
685
|
-
else log.warn("Could not open a browser automatically. Open the authorization URL manually.", {
|
|
686
|
-
output: stderr,
|
|
687
|
-
spacing: 0
|
|
688
|
-
});
|
|
689
|
-
})();
|
|
690
|
-
},
|
|
691
|
-
onPrompt: async (prompt) => {
|
|
692
|
-
if (options.githubEnterprise !== void 0 && !usedEnterpriseFlag && isGithubEnterprisePrompt(prompt)) {
|
|
693
|
-
usedEnterpriseFlag = true;
|
|
694
|
-
return options.githubEnterprise;
|
|
695
|
-
}
|
|
696
|
-
return promptForInput(prompt.placeholder ? `${prompt.message} (${prompt.placeholder})` : prompt.message);
|
|
697
|
-
},
|
|
698
|
-
onProgress: (message) => {
|
|
699
|
-
writeLine(stderr, message);
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
modelAuthStorage.set(PI_GITHUB_COPILOT_MODEL_PROVIDER, oauthCredential(credentials));
|
|
703
|
-
if (options.json === true) {
|
|
704
|
-
writeJson(stdout, {
|
|
705
|
-
connected: true,
|
|
706
|
-
provider: PI_GITHUB_COPILOT_MODEL_PROVIDER,
|
|
707
|
-
model: PI_GITHUB_COPILOT_MODEL_ID,
|
|
708
|
-
authPath: modelAuthPath,
|
|
709
|
-
source: "quire_model_auth"
|
|
710
|
-
});
|
|
711
|
-
return;
|
|
712
|
-
}
|
|
713
|
-
log.success("GitHub Copilot connected for local Quire investigations.", { output: stdout });
|
|
714
|
-
}
|
|
715
|
-
function oauthCredential(credentials) {
|
|
716
|
-
return {
|
|
717
|
-
type: "oauth",
|
|
718
|
-
...credentials
|
|
719
|
-
};
|
|
720
|
-
}
|
|
721
|
-
function isGithubEnterprisePrompt(prompt) {
|
|
722
|
-
return /github enterprise/i.test(prompt.message);
|
|
723
|
-
}
|
|
724
|
-
async function promptForLine(message, output = process.stdout, options = {}) {
|
|
725
|
-
const value = await text({
|
|
726
|
-
message,
|
|
727
|
-
placeholder: options.placeholder,
|
|
728
|
-
input: process.stdin,
|
|
729
|
-
output
|
|
730
|
-
});
|
|
731
|
-
if (isCancel(value)) throw new CliError("Authorization input was cancelled.", ExitCode.InvalidInput);
|
|
732
|
-
return value;
|
|
733
|
-
}
|
|
734
|
-
//#endregion
|
|
735
|
-
//#region src/commands/continue.ts
|
|
736
|
-
const continueCommand = defineCommand({
|
|
737
|
-
meta: {
|
|
738
|
-
name: "continue",
|
|
739
|
-
alias: "resume",
|
|
740
|
-
description: "Continue a previous Quire investigation with more context."
|
|
741
|
-
},
|
|
742
|
-
args: {
|
|
743
|
-
run: {
|
|
744
|
-
type: "positional",
|
|
745
|
-
description: "Run id or run directory to continue.",
|
|
746
|
-
required: true
|
|
747
|
-
},
|
|
748
|
-
prompt: {
|
|
749
|
-
type: "positional",
|
|
750
|
-
description: "Additional context or instruction for the continued run.",
|
|
751
|
-
required: false
|
|
752
|
-
},
|
|
753
|
-
stdin: {
|
|
754
|
-
type: "boolean",
|
|
755
|
-
description: "Reserved for the rebuilt continuation command."
|
|
756
|
-
},
|
|
757
|
-
json: {
|
|
758
|
-
type: "boolean",
|
|
759
|
-
description: "Reserved for the rebuilt continuation command."
|
|
760
|
-
},
|
|
761
|
-
watch: {
|
|
762
|
-
type: "boolean",
|
|
763
|
-
description: "Reserved for the rebuilt continuation command."
|
|
764
|
-
},
|
|
765
|
-
detach: {
|
|
766
|
-
type: "boolean",
|
|
767
|
-
description: "Reserved for the rebuilt continuation command."
|
|
768
|
-
},
|
|
769
|
-
headed: {
|
|
770
|
-
type: "boolean",
|
|
771
|
-
description: "Reserved for the rebuilt continuation command."
|
|
772
|
-
}
|
|
773
|
-
},
|
|
774
|
-
async run({ args }) {
|
|
775
|
-
await runContinue({
|
|
776
|
-
run: readRequiredString$1(args.run, "run"),
|
|
777
|
-
prompt: readOptionalString$2(args.prompt),
|
|
778
|
-
stdin: args.stdin === true,
|
|
779
|
-
json: args.json === true,
|
|
780
|
-
watch: args.watch === true,
|
|
781
|
-
detach: args.detach === true,
|
|
782
|
-
headed: args.headed === true
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
});
|
|
786
|
-
async function runContinue(_options) {
|
|
787
|
-
throw new CliError("Investigation continuation is intentionally a clean-slate learning stub on this branch. Rebuild it after the run protocol and investigate command.", ExitCode.InternalError);
|
|
788
|
-
}
|
|
789
|
-
function readRequiredString$1(value, name) {
|
|
790
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
791
|
-
throw new CliError(`Missing required ${name}.`, ExitCode.InvalidInput);
|
|
792
|
-
}
|
|
793
|
-
function readOptionalString$2(value) {
|
|
794
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
795
|
-
}
|
|
796
|
-
//#endregion
|
|
797
|
-
//#region src/doctor/render.ts
|
|
798
|
-
const SECTION_TITLES = {
|
|
799
|
-
identity: "Identity",
|
|
800
|
-
workspace: "Workspace",
|
|
801
|
-
runs: "Runs"
|
|
802
|
-
};
|
|
803
|
-
const SECTION_ORDER = [
|
|
804
|
-
"identity",
|
|
805
|
-
"workspace",
|
|
806
|
-
"runs"
|
|
807
|
-
];
|
|
808
|
-
const GLYPHS = {
|
|
809
|
-
pass: color.success("✓"),
|
|
810
|
-
warn: color.warn("!"),
|
|
811
|
-
fail: color.error("✗")
|
|
812
|
-
};
|
|
813
|
-
function renderDoctorReport(output, report, strict = false) {
|
|
814
|
-
writeLine(output, color.heading("Quire doctor"));
|
|
815
|
-
writeLine(output);
|
|
816
|
-
for (const section of SECTION_ORDER) {
|
|
817
|
-
const checks = report.checks.filter((check) => check.section === section);
|
|
818
|
-
if (checks.length === 0) continue;
|
|
819
|
-
writeLine(output, color.heading(SECTION_TITLES[section]));
|
|
820
|
-
for (const check of checks) {
|
|
821
|
-
writeLine(output, ` ${GLYPHS[check.severity]} ${check.message}`);
|
|
822
|
-
if (check.fix !== void 0) writeLine(output, ` ${color.label("→")} ${color.command(check.fix.command)}`);
|
|
823
|
-
}
|
|
824
|
-
writeLine(output);
|
|
825
|
-
}
|
|
826
|
-
writeLine(output, formatSummary(report, strict));
|
|
827
|
-
}
|
|
828
|
-
function formatSummary(report, strict) {
|
|
829
|
-
const { warn, fail } = report.summary;
|
|
830
|
-
if (fail === 0 && warn === 0) return "All checks passed.";
|
|
831
|
-
const parts = [];
|
|
832
|
-
if (fail > 0) parts.push(`${fail} ${pluralize(fail, "failure", "failures")}`);
|
|
833
|
-
if (warn > 0) parts.push(`${warn} ${pluralize(warn, "warning", "warnings")}`);
|
|
834
|
-
const sentence = `${parts.join(", ")}.`;
|
|
835
|
-
if (fail === 0 && warn > 0 && !strict) return `${sentence} Run with --strict to fail on warnings.`;
|
|
836
|
-
return sentence;
|
|
837
|
-
}
|
|
838
|
-
function pluralize(count, singular, plural) {
|
|
839
|
-
return count === 1 ? singular : plural;
|
|
840
|
-
}
|
|
841
|
-
//#endregion
|
|
842
|
-
//#region src/schema/environment.ts
|
|
843
|
-
const VerificationCommandSchema = Type.Object({
|
|
844
|
-
command: Type.String({ minLength: 1 }),
|
|
845
|
-
purpose: Type.Optional(Type.String({ minLength: 1 }))
|
|
846
|
-
}, { additionalProperties: false });
|
|
847
|
-
const WebTargetSchema = Type.Object({
|
|
848
|
-
kind: Type.Literal("web"),
|
|
849
|
-
url: Type.String({ minLength: 1 }),
|
|
850
|
-
description: Type.Optional(Type.String({ minLength: 1 })),
|
|
851
|
-
entrypoint: Type.Optional(Type.String({ minLength: 1 })),
|
|
852
|
-
readOnly: Type.Optional(Type.Boolean()),
|
|
853
|
-
codePaths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
|
|
854
|
-
verify: Type.Optional(Type.Array(VerificationCommandSchema))
|
|
855
|
-
}, { additionalProperties: false });
|
|
856
|
-
const EnvironmentFileSchema = Type.Object({
|
|
857
|
-
version: Type.Literal(2),
|
|
858
|
-
defaultTarget: Type.Optional(Type.String({ minLength: 1 })),
|
|
859
|
-
targets: Type.Record(Type.String({ minLength: 1 }), WebTargetSchema)
|
|
860
|
-
}, { additionalProperties: false });
|
|
861
|
-
function environmentFilePath(repoPath) {
|
|
862
|
-
return join(repoPath, ".quire", "environment.json");
|
|
863
|
-
}
|
|
864
|
-
function sanitizeTargetName(value) {
|
|
865
|
-
const sanitized = value.trim().toLowerCase().replace(/[^a-z0-9._-]+/g, "-");
|
|
866
|
-
if (sanitized.length === 0 || sanitized === "." || sanitized === "..") throw new EnvironmentError("schema_violation", "Invalid environment name", {
|
|
867
|
-
fieldPath: "/target",
|
|
868
|
-
filePath: ""
|
|
869
|
-
});
|
|
870
|
-
return sanitized;
|
|
871
|
-
}
|
|
872
|
-
var EnvironmentError = class extends Error {
|
|
873
|
-
code;
|
|
874
|
-
fieldPath;
|
|
875
|
-
filePath;
|
|
876
|
-
constructor(code, message, options) {
|
|
877
|
-
super(`${message} at ${options.fieldPath}`, { cause: options.cause });
|
|
878
|
-
this.name = "EnvironmentError";
|
|
879
|
-
this.code = code;
|
|
880
|
-
this.fieldPath = options.fieldPath;
|
|
881
|
-
this.filePath = options.filePath;
|
|
882
|
-
}
|
|
883
|
-
};
|
|
884
|
-
function loadEnvironment(repoPath, targetName) {
|
|
885
|
-
const filePath = environmentFilePath(repoPath);
|
|
886
|
-
if (!existsSync(filePath)) throw new EnvironmentError("missing_file", "Missing .quire/environment.json", {
|
|
887
|
-
fieldPath: "/",
|
|
888
|
-
filePath
|
|
889
|
-
});
|
|
890
|
-
let parsed;
|
|
891
|
-
try {
|
|
892
|
-
parsed = JSON.parse(readFileSync(filePath, "utf8"));
|
|
893
|
-
} catch (error) {
|
|
894
|
-
throw new EnvironmentError("malformed_json", "Malformed environment JSON", {
|
|
895
|
-
fieldPath: "/",
|
|
896
|
-
filePath,
|
|
897
|
-
cause: error
|
|
898
|
-
});
|
|
899
|
-
}
|
|
900
|
-
if (!Value.Check(EnvironmentFileSchema, parsed)) {
|
|
901
|
-
const firstError = Value.Errors(EnvironmentFileSchema, parsed).First();
|
|
902
|
-
const fieldPath = normalizeFieldPath(firstError?.path);
|
|
903
|
-
throw new EnvironmentError("schema_violation", firstError?.message ?? "Environment schema validation failed", {
|
|
904
|
-
fieldPath,
|
|
905
|
-
filePath
|
|
906
|
-
});
|
|
907
|
-
}
|
|
908
|
-
return resolveEnvironmentTarget(parsed, targetName, filePath);
|
|
909
|
-
}
|
|
910
|
-
function isMobileEnvironment(env) {
|
|
911
|
-
return env.app.kind === "mobile";
|
|
912
|
-
}
|
|
913
|
-
function isWebEnvironment(env) {
|
|
914
|
-
return env.app.kind === "web";
|
|
915
|
-
}
|
|
916
|
-
function environmentBaseUrl(env) {
|
|
917
|
-
return isWebEnvironment(env) ? env.app.url : void 0;
|
|
918
|
-
}
|
|
919
|
-
function resolveEnvironmentTarget(env, requestedTarget, filePath) {
|
|
920
|
-
const targetName = requestedTarget ?? env.defaultTarget ?? Object.keys(env.targets)[0];
|
|
921
|
-
if (targetName === void 0) throw new EnvironmentError("schema_violation", "Environment must define at least one target", {
|
|
922
|
-
fieldPath: "/targets",
|
|
923
|
-
filePath
|
|
924
|
-
});
|
|
925
|
-
const sanitizedTargetName = sanitizeTargetName(targetName);
|
|
926
|
-
const target = env.targets[sanitizedTargetName] ?? env.targets[targetName];
|
|
927
|
-
if (target === void 0) throw new EnvironmentError("schema_violation", `Unknown target ${targetName}`, {
|
|
928
|
-
fieldPath: `/targets/${sanitizedTargetName}`,
|
|
929
|
-
filePath
|
|
930
|
-
});
|
|
931
|
-
return {
|
|
932
|
-
version: 2,
|
|
933
|
-
targetName: sanitizedTargetName,
|
|
934
|
-
app: {
|
|
935
|
-
...target,
|
|
936
|
-
baseUrl: target.url
|
|
937
|
-
}
|
|
938
|
-
};
|
|
939
|
-
}
|
|
940
|
-
function normalizeFieldPath(path) {
|
|
941
|
-
if (path === void 0 || path === "") return "/";
|
|
942
|
-
return path;
|
|
943
|
-
}
|
|
944
|
-
//#endregion
|
|
945
|
-
//#region src/run-dir.ts
|
|
946
|
-
function createRunDir(repoPath, options = {}) {
|
|
947
|
-
const workspaceKey = workspaceKeyForRepoPath(resolve(repoPath));
|
|
948
|
-
const runsRoot = resolve(options.runsRoot ?? defaultRunsRoot());
|
|
949
|
-
const runId = options.runId ?? formatRunId(options.now ?? /* @__PURE__ */ new Date());
|
|
950
|
-
const root = join(runsRoot, workspaceKey, runId);
|
|
951
|
-
const paths = runDirPaths(root);
|
|
952
|
-
mkdirSync(paths.evidence, {
|
|
953
|
-
recursive: true,
|
|
954
|
-
mode: 448
|
|
955
|
-
});
|
|
956
|
-
return {
|
|
957
|
-
runId,
|
|
958
|
-
workspaceKey,
|
|
959
|
-
runsRoot,
|
|
960
|
-
root,
|
|
961
|
-
paths
|
|
962
|
-
};
|
|
963
|
-
}
|
|
964
|
-
function runStatusPath(runPath) {
|
|
965
|
-
return join(resolve(runPath), "status.json");
|
|
966
|
-
}
|
|
967
|
-
function runProgressPath(runPath) {
|
|
968
|
-
return join(resolve(runPath), "progress.jsonl");
|
|
969
|
-
}
|
|
970
|
-
function runAgentSessionPath(runPath) {
|
|
971
|
-
return join(resolve(runPath), "agent-session.jsonl");
|
|
972
|
-
}
|
|
973
|
-
function runHandoffPath(runPath) {
|
|
974
|
-
return join(resolve(runPath), "handoff.md");
|
|
975
|
-
}
|
|
976
|
-
function runDirPaths(root) {
|
|
977
|
-
const evidence = join(root, "evidence");
|
|
978
|
-
return {
|
|
979
|
-
status: runStatusPath(root),
|
|
980
|
-
progress: runProgressPath(root),
|
|
981
|
-
agentSession: runAgentSessionPath(root),
|
|
982
|
-
handoff: runHandoffPath(root),
|
|
983
|
-
evidence,
|
|
984
|
-
screenshots: join(evidence, "screenshots"),
|
|
985
|
-
video: join(evidence, "video"),
|
|
986
|
-
trace: join(evidence, "trace"),
|
|
987
|
-
console: join(evidence, "console"),
|
|
988
|
-
network: join(evidence, "network"),
|
|
989
|
-
harnessSessions: join(root, ".harness-sessions")
|
|
990
|
-
};
|
|
991
|
-
}
|
|
992
|
-
function defaultRunsRoot(env = process.env) {
|
|
993
|
-
if (env.QUIRE_RUNS_DIR !== void 0 && env.QUIRE_RUNS_DIR.length > 0) return resolve(env.QUIRE_RUNS_DIR);
|
|
994
|
-
return join(resolve(env.QUIRE_HOME ?? join(homedir(), ".quire")), "runs");
|
|
995
|
-
}
|
|
996
|
-
function workspaceKeyForRepoPath(repoPath) {
|
|
997
|
-
const resolvedRepoPath = resolve(repoPath);
|
|
998
|
-
return `${sanitizeWorkspaceName(basename(resolvedRepoPath) ?? "workspace")}-${createHash("sha256").update(resolvedRepoPath, "utf8").digest("hex").slice(0, 10)}`;
|
|
999
|
-
}
|
|
1000
|
-
function formatRunId(date) {
|
|
1001
|
-
return [
|
|
1002
|
-
date.getFullYear().toString().padStart(4, "0"),
|
|
1003
|
-
(date.getMonth() + 1).toString().padStart(2, "0"),
|
|
1004
|
-
date.getDate().toString().padStart(2, "0"),
|
|
1005
|
-
"-",
|
|
1006
|
-
date.getHours().toString().padStart(2, "0"),
|
|
1007
|
-
date.getMinutes().toString().padStart(2, "0"),
|
|
1008
|
-
date.getSeconds().toString().padStart(2, "0"),
|
|
1009
|
-
"-",
|
|
1010
|
-
date.getMilliseconds().toString().padStart(3, "0"),
|
|
1011
|
-
"-",
|
|
1012
|
-
randomBytes(3).toString("hex")
|
|
1013
|
-
].join("");
|
|
1014
|
-
}
|
|
1015
|
-
function sanitizeWorkspaceName(value) {
|
|
1016
|
-
return value.toLowerCase().replace(/[^a-z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") ?? "workspace";
|
|
1017
|
-
}
|
|
1018
|
-
//#endregion
|
|
1019
|
-
//#region src/run-status.ts
|
|
1020
|
-
function updateRunStatus(runPath, patch) {
|
|
1021
|
-
const existing = readRunStatus(runPath);
|
|
1022
|
-
if (existing === void 0) throw new CliError(`Run status not found: ${runPath}`, ExitCode.InvalidInput);
|
|
1023
|
-
const timestamp = (patch.now ?? /* @__PURE__ */ new Date()).toISOString();
|
|
1024
|
-
const nextStatus = patch.status ?? existing.status;
|
|
1025
|
-
const status = {
|
|
1026
|
-
...existing,
|
|
1027
|
-
...definedPatch(patch),
|
|
1028
|
-
status: nextStatus,
|
|
1029
|
-
updatedAt: timestamp,
|
|
1030
|
-
...nextStatus === "running" && existing.startedAt === void 0 ? { startedAt: timestamp } : {},
|
|
1031
|
-
...isTerminalRunStatus(nextStatus) && existing.completedAt === void 0 ? { completedAt: timestamp } : {}
|
|
1032
|
-
};
|
|
1033
|
-
writeStatus(runStatusPath(runPath), status);
|
|
1034
|
-
return status;
|
|
1035
|
-
}
|
|
1036
|
-
function readRunStatus(runPath) {
|
|
1037
|
-
const filePath = runStatusPath(runPath);
|
|
1038
|
-
if (!existsSync(filePath)) return;
|
|
1039
|
-
try {
|
|
1040
|
-
return parseRunStatus(JSON.parse(readFileSync(filePath, "utf8")));
|
|
1041
|
-
} catch {
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
}
|
|
1045
|
-
function readFreshRunStatus(runPath) {
|
|
1046
|
-
const status = readRequiredRunStatus(runPath);
|
|
1047
|
-
if (status.status === "queued" && !isPidAlive(status.pid)) {
|
|
1048
|
-
if (existsSync(runHandoffPath(runPath))) return updateRunStatus(runPath, { status: "completed" });
|
|
1049
|
-
return updateRunStatus(runPath, {
|
|
1050
|
-
status: "stale",
|
|
1051
|
-
error: "Worker process exited before marking the run as running."
|
|
1052
|
-
});
|
|
1053
|
-
}
|
|
1054
|
-
if ((status.status === "running" || status.status === "canceling") && !isPidAlive(status.pid)) {
|
|
1055
|
-
if (existsSync(runHandoffPath(runPath))) return updateRunStatus(runPath, { status: "completed" });
|
|
1056
|
-
return updateRunStatus(runPath, {
|
|
1057
|
-
status: status.status === "canceling" ? "canceled" : "stale",
|
|
1058
|
-
error: status.status === "canceling" ? void 0 : "Worker process is no longer running."
|
|
1059
|
-
});
|
|
1060
|
-
}
|
|
1061
|
-
return status;
|
|
1062
|
-
}
|
|
1063
|
-
function readRequiredRunStatus(runPath) {
|
|
1064
|
-
const status = readRunStatus(runPath);
|
|
1065
|
-
if (status === void 0) throw new CliError(`Run status not found: ${runPath}`, ExitCode.InvalidInput);
|
|
1066
|
-
return status;
|
|
1067
|
-
}
|
|
1068
|
-
function listRunStatuses(options = {}) {
|
|
1069
|
-
const runsRoot = resolve(options.runsRoot ?? defaultRunsRoot());
|
|
1070
|
-
if (!existsSync(runsRoot)) return [];
|
|
1071
|
-
return readdirSync(runsRoot, { withFileTypes: true }).filter((workspaceEntry) => workspaceEntry.isDirectory()).flatMap((workspaceEntry) => {
|
|
1072
|
-
const workspacePath = join(runsRoot, workspaceEntry.name);
|
|
1073
|
-
return readdirSync(workspacePath, { withFileTypes: true }).filter((runEntry) => runEntry.isDirectory()).map((runEntry) => readRunStatus(join(workspacePath, runEntry.name))).filter((status) => status !== void 0);
|
|
1074
|
-
}).map((status) => readFreshRunStatus(status.runPath)).sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
1075
|
-
}
|
|
1076
|
-
function isTerminalRunStatus(status) {
|
|
1077
|
-
return status === "completed" || status === "failed" || status === "canceled" || status === "stale";
|
|
1078
|
-
}
|
|
1079
|
-
function isPidAlive(pid) {
|
|
1080
|
-
if (pid === void 0 || !Number.isInteger(pid) || pid <= 0) return false;
|
|
1081
|
-
try {
|
|
1082
|
-
process.kill(pid, 0);
|
|
1083
|
-
return true;
|
|
1084
|
-
} catch (error) {
|
|
1085
|
-
return isRecord(error) && error.code === "EPERM";
|
|
1086
|
-
}
|
|
1087
|
-
}
|
|
1088
|
-
function writeStatus(filePath, status) {
|
|
1089
|
-
mkdirSync(dirname(filePath), {
|
|
1090
|
-
recursive: true,
|
|
1091
|
-
mode: 448
|
|
1092
|
-
});
|
|
1093
|
-
const tempPath = join(dirname(filePath), `.${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}.status.tmp`);
|
|
1094
|
-
writeFileSync(tempPath, `${JSON.stringify(status, null, 2)}\n`, { mode: 384 });
|
|
1095
|
-
fsyncFile(tempPath);
|
|
1096
|
-
renameSync(tempPath, filePath);
|
|
1097
|
-
fsyncDirectory(dirname(filePath));
|
|
1098
|
-
}
|
|
1099
|
-
function fsyncFile(filePath) {
|
|
1100
|
-
const file = openSync(filePath, "r");
|
|
1101
|
-
try {
|
|
1102
|
-
fsyncSync(file);
|
|
1103
|
-
} finally {
|
|
1104
|
-
closeSync(file);
|
|
1105
|
-
}
|
|
1106
|
-
}
|
|
1107
|
-
function fsyncDirectory(directoryPath) {
|
|
1108
|
-
try {
|
|
1109
|
-
const directory = openSync(directoryPath, "r");
|
|
1110
|
-
try {
|
|
1111
|
-
fsyncSync(directory);
|
|
1112
|
-
} finally {
|
|
1113
|
-
closeSync(directory);
|
|
1114
|
-
}
|
|
1115
|
-
} catch {}
|
|
1116
|
-
}
|
|
1117
|
-
function definedPatch(patch) {
|
|
1118
|
-
const { now: _now, ...rest } = patch;
|
|
1119
|
-
return Object.fromEntries(Object.entries(rest).filter((entry) => {
|
|
1120
|
-
const [, value] = entry;
|
|
1121
|
-
return value !== void 0;
|
|
1122
|
-
}));
|
|
1123
|
-
}
|
|
1124
|
-
function parseRunStatus(value) {
|
|
1125
|
-
if (!isRecord(value)) return;
|
|
1126
|
-
if (value.version !== 1 || typeof value.runId !== "string" || typeof value.workspaceKey !== "string" || typeof value.runPath !== "string" || typeof value.repoPath !== "string" || !isRunStatusName(value.status) || typeof value.createdAt !== "string" || typeof value.updatedAt !== "string") return;
|
|
1127
|
-
return {
|
|
1128
|
-
version: 1,
|
|
1129
|
-
runId: value.runId,
|
|
1130
|
-
workspaceKey: value.workspaceKey,
|
|
1131
|
-
runPath: value.runPath,
|
|
1132
|
-
files: parseRunStatusFiles(value.files),
|
|
1133
|
-
repoPath: value.repoPath,
|
|
1134
|
-
status: value.status,
|
|
1135
|
-
createdAt: value.createdAt,
|
|
1136
|
-
updatedAt: value.updatedAt,
|
|
1137
|
-
...typeof value.pid === "number" ? { pid: value.pid } : {},
|
|
1138
|
-
...typeof value.startedAt === "string" ? { startedAt: value.startedAt } : {},
|
|
1139
|
-
...typeof value.completedAt === "string" ? { completedAt: value.completedAt } : {},
|
|
1140
|
-
...value.modelUsage === void 0 ? {} : { modelUsage: value.modelUsage },
|
|
1141
|
-
...value.agent === void 0 ? {} : { agent: value.agent },
|
|
1142
|
-
...value.sync === void 0 ? {} : { sync: value.sync },
|
|
1143
|
-
...typeof value.error === "string" ? { error: value.error } : {}
|
|
1144
|
-
};
|
|
1145
|
-
}
|
|
1146
|
-
function parseRunStatusFiles(value) {
|
|
1147
|
-
if (isRecord(value) && typeof value.status === "string" && typeof value.progress === "string" && typeof value.agentSession === "string" && typeof value.handoff === "string" && typeof value.evidence === "string") return {
|
|
1148
|
-
status: value.status,
|
|
1149
|
-
progress: value.progress,
|
|
1150
|
-
agentSession: value.agentSession,
|
|
1151
|
-
handoff: value.handoff,
|
|
1152
|
-
evidence: value.evidence
|
|
1153
|
-
};
|
|
1154
|
-
return {
|
|
1155
|
-
status: "status.json",
|
|
1156
|
-
progress: "progress.jsonl",
|
|
1157
|
-
agentSession: "agent-session.jsonl",
|
|
1158
|
-
handoff: "handoff.md",
|
|
1159
|
-
evidence: "evidence"
|
|
1160
|
-
};
|
|
1161
|
-
}
|
|
1162
|
-
function isRunStatusName(value) {
|
|
1163
|
-
return value === "queued" || value === "running" || value === "canceling" || value === "completed" || value === "failed" || value === "canceled" || value === "stale";
|
|
1164
|
-
}
|
|
1165
|
-
//#endregion
|
|
1166
|
-
//#region src/doctor/checks.ts
|
|
1167
|
-
const execFileAsync = promisify(execFile);
|
|
1168
|
-
function createDoctorContext(overrides = {}) {
|
|
1169
|
-
return {
|
|
1170
|
-
cwd: resolve(overrides.cwd ?? process.cwd()),
|
|
1171
|
-
runsRoot: resolve(overrides.runsRoot ?? defaultRunsRoot()),
|
|
1172
|
-
authStore: overrides.authStore ?? authStore,
|
|
1173
|
-
fetchFn: overrides.fetchFn ?? fetch,
|
|
1174
|
-
execFn: overrides.execFn ?? defaultExecFn,
|
|
1175
|
-
probeTimeoutMs: overrides.probeTimeoutMs ?? 2e3
|
|
1176
|
-
};
|
|
1177
|
-
}
|
|
1178
|
-
async function defaultExecFn(file, args, options) {
|
|
1179
|
-
const result = await execFileAsync(file, [...args], {
|
|
1180
|
-
timeout: options?.timeout ?? 5e3,
|
|
1181
|
-
encoding: "utf8"
|
|
1182
|
-
});
|
|
1183
|
-
return {
|
|
1184
|
-
stdout: result.stdout ?? "",
|
|
1185
|
-
stderr: result.stderr ?? ""
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
async function checkAuthPresent(context) {
|
|
1189
|
-
const credentials = await resolveAuthCredentials({ store: context.authStore });
|
|
1190
|
-
if (credentials === null) return { result: {
|
|
1191
|
-
id: "auth.present",
|
|
1192
|
-
section: "identity",
|
|
1193
|
-
severity: "fail",
|
|
1194
|
-
message: "No Quire credentials on this machine.",
|
|
1195
|
-
fix: { command: "quire login" }
|
|
1196
|
-
} };
|
|
1197
|
-
return {
|
|
1198
|
-
result: {
|
|
1199
|
-
id: "auth.present",
|
|
1200
|
-
section: "identity",
|
|
1201
|
-
severity: "pass",
|
|
1202
|
-
message: "Credentials present."
|
|
1203
|
-
},
|
|
1204
|
-
auth: {
|
|
1205
|
-
apiBaseUrl: credentials.apiBaseUrl,
|
|
1206
|
-
accessToken: credentials.accessToken,
|
|
1207
|
-
tokenType: credentials.tokenType,
|
|
1208
|
-
...credentials.user === void 0 ? {} : { user: credentials.user }
|
|
1209
|
-
}
|
|
1210
|
-
};
|
|
1211
|
-
}
|
|
1212
|
-
async function checkAuthValid(context, auth) {
|
|
1213
|
-
if (auth.tokenType === "QuireApiKey") try {
|
|
1214
|
-
return { result: {
|
|
1215
|
-
id: "auth.valid",
|
|
1216
|
-
section: "identity",
|
|
1217
|
-
severity: "pass",
|
|
1218
|
-
message: `API token valid for ${(await fetchModelSourceBrokerStatus({
|
|
1219
|
-
apiBaseUrl: auth.apiBaseUrl,
|
|
1220
|
-
accessToken: auth.accessToken,
|
|
1221
|
-
tokenType: auth.tokenType,
|
|
1222
|
-
fetchFn: context.fetchFn
|
|
1223
|
-
})).actor?.environmentName ?? "remote agent environment"}.`
|
|
1224
|
-
} };
|
|
1225
|
-
} catch (error) {
|
|
1226
|
-
return { result: {
|
|
1227
|
-
id: "auth.valid",
|
|
1228
|
-
section: "identity",
|
|
1229
|
-
severity: "fail",
|
|
1230
|
-
message: `Could not validate Quire API token: ${toMessage(error)}`,
|
|
1231
|
-
fix: { command: "Set QUIRE_API_TOKEN to an active token from Quire settings" }
|
|
1232
|
-
} };
|
|
1233
|
-
}
|
|
1234
|
-
try {
|
|
1235
|
-
const lookup = await fetchCurrentSession({
|
|
1236
|
-
apiBaseUrl: auth.apiBaseUrl,
|
|
1237
|
-
accessToken: auth.accessToken,
|
|
1238
|
-
tokenType: auth.tokenType,
|
|
1239
|
-
fetchFn: context.fetchFn
|
|
1240
|
-
});
|
|
1241
|
-
if (lookup.data === null) return { result: {
|
|
1242
|
-
id: "auth.valid",
|
|
1243
|
-
section: "identity",
|
|
1244
|
-
severity: "fail",
|
|
1245
|
-
message: "Stored credentials were rejected by Quire.",
|
|
1246
|
-
fix: { command: "quire login" }
|
|
1247
|
-
} };
|
|
1248
|
-
const user = lookup.data.user;
|
|
1249
|
-
return {
|
|
1250
|
-
result: {
|
|
1251
|
-
id: "auth.valid",
|
|
1252
|
-
section: "identity",
|
|
1253
|
-
severity: "pass",
|
|
1254
|
-
message: `Logged in as ${formatUser$2(user)}.`
|
|
1255
|
-
},
|
|
1256
|
-
user
|
|
1257
|
-
};
|
|
1258
|
-
} catch (error) {
|
|
1259
|
-
return { result: {
|
|
1260
|
-
id: "auth.valid",
|
|
1261
|
-
section: "identity",
|
|
1262
|
-
severity: "fail",
|
|
1263
|
-
message: `Could not validate Quire session: ${toMessage(error)}`,
|
|
1264
|
-
fix: { command: "quire login" }
|
|
1265
|
-
} };
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
async function checkAuthBroker(context, auth) {
|
|
1269
|
-
try {
|
|
1270
|
-
return brokerToCheckResult(await fetchModelSourceBrokerStatus({
|
|
1271
|
-
apiBaseUrl: auth.apiBaseUrl,
|
|
1272
|
-
accessToken: auth.accessToken,
|
|
1273
|
-
tokenType: auth.tokenType,
|
|
1274
|
-
fetchFn: context.fetchFn
|
|
1275
|
-
}));
|
|
1276
|
-
} catch (error) {
|
|
1277
|
-
return {
|
|
1278
|
-
id: "auth.broker",
|
|
1279
|
-
section: "identity",
|
|
1280
|
-
severity: "warn",
|
|
1281
|
-
message: `Could not read model-source status: ${toMessage(error)}`
|
|
1282
|
-
};
|
|
1283
|
-
}
|
|
1284
|
-
}
|
|
1285
|
-
function brokerToCheckResult(status) {
|
|
1286
|
-
if (status.requiredNextAction !== null) return {
|
|
1287
|
-
id: "auth.broker",
|
|
1288
|
-
section: "identity",
|
|
1289
|
-
severity: "fail",
|
|
1290
|
-
message: `Model broker requires action: ${status.requiredNextAction}.`,
|
|
1291
|
-
fix: { command: "quire whoami --json" }
|
|
1292
|
-
};
|
|
1293
|
-
if (!status.available) return {
|
|
1294
|
-
id: "auth.broker",
|
|
1295
|
-
section: "identity",
|
|
1296
|
-
severity: "warn",
|
|
1297
|
-
message: `Model broker unavailable (${status.reason}).`
|
|
1298
|
-
};
|
|
1299
|
-
return {
|
|
1300
|
-
id: "auth.broker",
|
|
1301
|
-
section: "identity",
|
|
1302
|
-
severity: "pass",
|
|
1303
|
-
message: `Model broker available (${status.selectedProviderMode ?? "default"}).`
|
|
1304
|
-
};
|
|
1305
|
-
}
|
|
1306
|
-
function checkEnvironment(context) {
|
|
1307
|
-
try {
|
|
1308
|
-
const environment = loadEnvironment(context.cwd);
|
|
1309
|
-
return {
|
|
1310
|
-
presence: {
|
|
1311
|
-
id: "env.present",
|
|
1312
|
-
section: "workspace",
|
|
1313
|
-
severity: "pass",
|
|
1314
|
-
message: ".quire/environment.json found."
|
|
1315
|
-
},
|
|
1316
|
-
validity: {
|
|
1317
|
-
id: "env.valid",
|
|
1318
|
-
section: "workspace",
|
|
1319
|
-
severity: "pass",
|
|
1320
|
-
message: `Target: ${describeTarget(environment)}.`
|
|
1321
|
-
},
|
|
1322
|
-
env: { environment }
|
|
1323
|
-
};
|
|
1324
|
-
} catch (error) {
|
|
1325
|
-
if (error instanceof EnvironmentError && error.code === "missing_file") return { presence: {
|
|
1326
|
-
id: "env.present",
|
|
1327
|
-
section: "workspace",
|
|
1328
|
-
severity: "fail",
|
|
1329
|
-
message: "No .quire/environment.json in this workspace.",
|
|
1330
|
-
fix: { command: "quire setup" }
|
|
1331
|
-
} };
|
|
1332
|
-
return {
|
|
1333
|
-
presence: {
|
|
1334
|
-
id: "env.present",
|
|
1335
|
-
section: "workspace",
|
|
1336
|
-
severity: "pass",
|
|
1337
|
-
message: ".quire/environment.json found."
|
|
1338
|
-
},
|
|
1339
|
-
validity: {
|
|
1340
|
-
id: "env.valid",
|
|
1341
|
-
section: "workspace",
|
|
1342
|
-
severity: "fail",
|
|
1343
|
-
message: error instanceof EnvironmentError ? `Invalid environment: ${error.message}` : `Could not read environment: ${toMessage(error)}`,
|
|
1344
|
-
fix: { command: "quire setup" }
|
|
1345
|
-
}
|
|
1346
|
-
};
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
function describeTarget(environment) {
|
|
1350
|
-
if (isMobileEnvironment(environment)) return `mobile/${environment.app.platform ?? "unknown"} → ${environment.app.appId ?? environment.app.appPath ?? "unspecified"}`;
|
|
1351
|
-
return `web → ${environmentBaseUrl(environment) ?? "unspecified"}`;
|
|
1352
|
-
}
|
|
1353
|
-
async function checkWebReachable(context, baseUrl) {
|
|
1354
|
-
const started = Date.now();
|
|
1355
|
-
try {
|
|
1356
|
-
const response = await fetchWithTimeout(context.fetchFn, baseUrl, { method: "HEAD" }, context.probeTimeoutMs);
|
|
1357
|
-
const elapsed = Date.now() - started;
|
|
1358
|
-
if (!response.ok) return {
|
|
1359
|
-
id: "target.web.reachable",
|
|
1360
|
-
section: "workspace",
|
|
1361
|
-
severity: "warn",
|
|
1362
|
-
message: `Target responded with HTTP ${response.status} (${elapsed}ms).`
|
|
1363
|
-
};
|
|
1364
|
-
return {
|
|
1365
|
-
id: "target.web.reachable",
|
|
1366
|
-
section: "workspace",
|
|
1367
|
-
severity: "pass",
|
|
1368
|
-
message: `Target reachable (${response.status}, ${elapsed}ms).`
|
|
1369
|
-
};
|
|
1370
|
-
} catch (error) {
|
|
1371
|
-
return {
|
|
1372
|
-
id: "target.web.reachable",
|
|
1373
|
-
section: "workspace",
|
|
1374
|
-
severity: "fail",
|
|
1375
|
-
message: `Target unreachable: ${toMessage(error)}`
|
|
1376
|
-
};
|
|
1377
|
-
}
|
|
1378
|
-
}
|
|
1379
|
-
async function checkMobileDevice(context, app) {
|
|
1380
|
-
const provider = app.provider ?? "local";
|
|
1381
|
-
if (provider === "local") {
|
|
1382
|
-
if (app.platform === "ios") return [await checkIosSimulator(context, app)];
|
|
1383
|
-
if (app.platform === "android") return [await checkAndroidDevice(context, app)];
|
|
1384
|
-
return [{
|
|
1385
|
-
id: "target.mobile.device",
|
|
1386
|
-
section: "workspace",
|
|
1387
|
-
severity: "warn",
|
|
1388
|
-
message: "Mobile platform not specified."
|
|
1389
|
-
}];
|
|
1390
|
-
}
|
|
1391
|
-
if (provider === "appium") return [await checkAppiumServer(context, app)];
|
|
1392
|
-
return [{
|
|
1393
|
-
id: "target.mobile.device",
|
|
1394
|
-
section: "workspace",
|
|
1395
|
-
severity: "pass",
|
|
1396
|
-
message: `Provider ${provider} configured; remote device checks deferred.`
|
|
1397
|
-
}];
|
|
1398
|
-
}
|
|
1399
|
-
async function checkIosSimulator(context, app) {
|
|
1400
|
-
try {
|
|
1401
|
-
const { stdout } = await context.execFn("xcrun", [
|
|
1402
|
-
"simctl",
|
|
1403
|
-
"list",
|
|
1404
|
-
"devices",
|
|
1405
|
-
"booted",
|
|
1406
|
-
"-j"
|
|
1407
|
-
], { timeout: context.probeTimeoutMs });
|
|
1408
|
-
const parsed = parseSimctlBooted(stdout);
|
|
1409
|
-
if (parsed.length === 0) return {
|
|
1410
|
-
id: "target.mobile.device",
|
|
1411
|
-
section: "workspace",
|
|
1412
|
-
severity: "fail",
|
|
1413
|
-
message: "No booted iOS simulator found.",
|
|
1414
|
-
fix: { command: "xcrun simctl boot \"iPhone 15\"" }
|
|
1415
|
-
};
|
|
1416
|
-
if (app.device !== void 0 && !parsed.some((entry) => matchesIosDevice(entry, app.device))) return {
|
|
1417
|
-
id: "target.mobile.device",
|
|
1418
|
-
section: "workspace",
|
|
1419
|
-
severity: "warn",
|
|
1420
|
-
message: `Configured device ${app.device} is not currently booted.`,
|
|
1421
|
-
fix: { command: `xcrun simctl boot "${app.device}"` }
|
|
1422
|
-
};
|
|
1423
|
-
return {
|
|
1424
|
-
id: "target.mobile.device",
|
|
1425
|
-
section: "workspace",
|
|
1426
|
-
severity: "pass",
|
|
1427
|
-
message: `iOS simulator booted: ${parsed.map((entry) => entry.name).join(", ")}.`
|
|
1428
|
-
};
|
|
1429
|
-
} catch (error) {
|
|
1430
|
-
return {
|
|
1431
|
-
id: "target.mobile.device",
|
|
1432
|
-
section: "workspace",
|
|
1433
|
-
severity: "fail",
|
|
1434
|
-
message: `xcrun simctl failed: ${toMessage(error)}`
|
|
1435
|
-
};
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
async function checkAndroidDevice(context, app) {
|
|
1439
|
-
try {
|
|
1440
|
-
const { stdout } = await context.execFn("adb", ["devices"], { timeout: context.probeTimeoutMs });
|
|
1441
|
-
const devices = parseAdbDevices(stdout);
|
|
1442
|
-
if (devices.length === 0) return {
|
|
1443
|
-
id: "target.mobile.device",
|
|
1444
|
-
section: "workspace",
|
|
1445
|
-
severity: "fail",
|
|
1446
|
-
message: "No connected Android devices.",
|
|
1447
|
-
fix: { command: "adb devices" }
|
|
1448
|
-
};
|
|
1449
|
-
if (app.device !== void 0 && !devices.includes(app.device)) return {
|
|
1450
|
-
id: "target.mobile.device",
|
|
1451
|
-
section: "workspace",
|
|
1452
|
-
severity: "warn",
|
|
1453
|
-
message: `Configured device ${app.device} is not in adb devices.`
|
|
1454
|
-
};
|
|
1455
|
-
return {
|
|
1456
|
-
id: "target.mobile.device",
|
|
1457
|
-
section: "workspace",
|
|
1458
|
-
severity: "pass",
|
|
1459
|
-
message: `Android device(s) available: ${devices.join(", ")}.`
|
|
1460
|
-
};
|
|
1461
|
-
} catch (error) {
|
|
1462
|
-
return {
|
|
1463
|
-
id: "target.mobile.device",
|
|
1464
|
-
section: "workspace",
|
|
1465
|
-
severity: "fail",
|
|
1466
|
-
message: `adb failed: ${toMessage(error)}`
|
|
1467
|
-
};
|
|
1468
|
-
}
|
|
1469
|
-
}
|
|
1470
|
-
async function checkAppiumServer(context, app) {
|
|
1471
|
-
if (app.appiumUrl === void 0) return {
|
|
1472
|
-
id: "target.mobile.appium",
|
|
1473
|
-
section: "workspace",
|
|
1474
|
-
severity: "fail",
|
|
1475
|
-
message: "Appium provider configured without appiumUrl."
|
|
1476
|
-
};
|
|
1477
|
-
try {
|
|
1478
|
-
const response = await fetchWithTimeout(context.fetchFn, `${stripTrailingSlash(app.appiumUrl)}/status`, { method: "GET" }, context.probeTimeoutMs);
|
|
1479
|
-
if (!response.ok) return {
|
|
1480
|
-
id: "target.mobile.appium",
|
|
1481
|
-
section: "workspace",
|
|
1482
|
-
severity: "fail",
|
|
1483
|
-
message: `Appium /status returned HTTP ${response.status}.`
|
|
1484
|
-
};
|
|
1485
|
-
return {
|
|
1486
|
-
id: "target.mobile.appium",
|
|
1487
|
-
section: "workspace",
|
|
1488
|
-
severity: "pass",
|
|
1489
|
-
message: `Appium server reachable at ${app.appiumUrl}.`
|
|
1490
|
-
};
|
|
1491
|
-
} catch (error) {
|
|
1492
|
-
return {
|
|
1493
|
-
id: "target.mobile.appium",
|
|
1494
|
-
section: "workspace",
|
|
1495
|
-
severity: "fail",
|
|
1496
|
-
message: `Appium server unreachable: ${toMessage(error)}`
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
}
|
|
1500
|
-
function checkMobileAppPath(app) {
|
|
1501
|
-
if (app.appPath === void 0) return {
|
|
1502
|
-
id: "target.mobile.appPath",
|
|
1503
|
-
section: "workspace",
|
|
1504
|
-
severity: "warn",
|
|
1505
|
-
message: "appPath not set; agent-mobile will not auto-install."
|
|
1506
|
-
};
|
|
1507
|
-
if (!existsSync(app.appPath)) return {
|
|
1508
|
-
id: "target.mobile.appPath",
|
|
1509
|
-
section: "workspace",
|
|
1510
|
-
severity: "fail",
|
|
1511
|
-
message: `appPath does not exist: ${app.appPath}.`
|
|
1512
|
-
};
|
|
1513
|
-
return {
|
|
1514
|
-
id: "target.mobile.appPath",
|
|
1515
|
-
section: "workspace",
|
|
1516
|
-
severity: "pass",
|
|
1517
|
-
message: `appPath exists: ${app.appPath}.`
|
|
1518
|
-
};
|
|
1519
|
-
}
|
|
1520
|
-
async function checkRunsRoot(context) {
|
|
1521
|
-
try {
|
|
1522
|
-
await mkdir(context.runsRoot, {
|
|
1523
|
-
recursive: true,
|
|
1524
|
-
mode: 448
|
|
1525
|
-
});
|
|
1526
|
-
await access(context.runsRoot, constants.W_OK);
|
|
1527
|
-
return {
|
|
1528
|
-
id: "runs.rootWritable",
|
|
1529
|
-
section: "runs",
|
|
1530
|
-
severity: "pass",
|
|
1531
|
-
message: `Runs root writable (${context.runsRoot}).`
|
|
1532
|
-
};
|
|
1533
|
-
} catch (error) {
|
|
1534
|
-
return {
|
|
1535
|
-
id: "runs.rootWritable",
|
|
1536
|
-
section: "runs",
|
|
1537
|
-
severity: "fail",
|
|
1538
|
-
message: `Runs root not writable: ${toMessage(error)}`
|
|
1539
|
-
};
|
|
1540
|
-
}
|
|
1541
|
-
}
|
|
1542
|
-
function checkStuckRuns(context) {
|
|
1543
|
-
try {
|
|
1544
|
-
const workspaceKey = workspaceKeyForRepoPath(context.cwd);
|
|
1545
|
-
const stuck = listRunStatuses({
|
|
1546
|
-
cwd: context.cwd,
|
|
1547
|
-
runsRoot: context.runsRoot
|
|
1548
|
-
}).filter((status) => status.workspaceKey === workspaceKey && (status.status === "running" || status.status === "canceling") && !isPidAlive(status.pid));
|
|
1549
|
-
if (stuck.length === 0) return {
|
|
1550
|
-
id: "runs.stuck",
|
|
1551
|
-
section: "runs",
|
|
1552
|
-
severity: "pass",
|
|
1553
|
-
message: "No stuck runs."
|
|
1554
|
-
};
|
|
1555
|
-
const first = stuck[0];
|
|
1556
|
-
if (first === void 0) return {
|
|
1557
|
-
id: "runs.stuck",
|
|
1558
|
-
section: "runs",
|
|
1559
|
-
severity: "pass",
|
|
1560
|
-
message: "No stuck runs."
|
|
1561
|
-
};
|
|
1562
|
-
return {
|
|
1563
|
-
id: "runs.stuck",
|
|
1564
|
-
section: "runs",
|
|
1565
|
-
severity: "warn",
|
|
1566
|
-
message: `${stuck.length} stuck run(s); first: ${first.runId}.`,
|
|
1567
|
-
fix: { command: `quire runs cancel ${first.runId}` }
|
|
1568
|
-
};
|
|
1569
|
-
} catch (error) {
|
|
1570
|
-
return {
|
|
1571
|
-
id: "runs.stuck",
|
|
1572
|
-
section: "runs",
|
|
1573
|
-
severity: "warn",
|
|
1574
|
-
message: `Could not inspect run statuses: ${toMessage(error)}`
|
|
1575
|
-
};
|
|
1576
|
-
}
|
|
1577
|
-
}
|
|
1578
|
-
async function fetchWithTimeout(fetchFn, url, init, timeoutMs) {
|
|
1579
|
-
return fetchFn(url, {
|
|
1580
|
-
...init,
|
|
1581
|
-
signal: AbortSignal.timeout(timeoutMs)
|
|
1582
|
-
});
|
|
1583
|
-
}
|
|
1584
|
-
function stripTrailingSlash(value) {
|
|
1585
|
-
return value.endsWith("/") ? value.slice(0, -1) : value;
|
|
1586
|
-
}
|
|
1587
|
-
function formatUser$2(user) {
|
|
1588
|
-
if (user.email !== void 0 && user.email !== null && user.email.length > 0) return user.email;
|
|
1589
|
-
if (user.name !== void 0 && user.name !== null && user.name.length > 0) return user.name;
|
|
1590
|
-
return user.id;
|
|
1591
|
-
}
|
|
1592
|
-
function toMessage(error) {
|
|
1593
|
-
return error instanceof Error ? error.message : String(error);
|
|
1594
|
-
}
|
|
1595
|
-
function parseSimctlBooted(stdout) {
|
|
1596
|
-
try {
|
|
1597
|
-
const devices = JSON.parse(stdout).devices ?? {};
|
|
1598
|
-
const entries = [];
|
|
1599
|
-
for (const list of Object.values(devices)) {
|
|
1600
|
-
if (!Array.isArray(list)) continue;
|
|
1601
|
-
for (const device of list) if (typeof device === "object" && device !== null && device.state === "Booted" && typeof device.name === "string" && typeof device.udid === "string") entries.push({
|
|
1602
|
-
name: device.name,
|
|
1603
|
-
udid: device.udid
|
|
1604
|
-
});
|
|
1605
|
-
}
|
|
1606
|
-
return entries;
|
|
1607
|
-
} catch {
|
|
1608
|
-
return [];
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
function matchesIosDevice(entry, device) {
|
|
1612
|
-
if (device === void 0) return true;
|
|
1613
|
-
return entry.udid === device || entry.name === device;
|
|
1614
|
-
}
|
|
1615
|
-
function parseAdbDevices(stdout) {
|
|
1616
|
-
return stdout.split("\n").slice(1).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("*")).map((line) => line.split(/\s+/)[0]).filter((id) => id !== void 0 && id.length > 0 && id !== "List");
|
|
1617
|
-
}
|
|
1618
|
-
//#endregion
|
|
1619
|
-
//#region src/doctor/runner.ts
|
|
1620
|
-
async function runDoctor(overrides = {}) {
|
|
1621
|
-
const context = createDoctorContext(overrides);
|
|
1622
|
-
const [identityResults, workspaceResults, runsResults] = await Promise.all([
|
|
1623
|
-
runIdentitySection(context),
|
|
1624
|
-
runWorkspaceSection(context),
|
|
1625
|
-
runRunsSection(context)
|
|
1626
|
-
]);
|
|
1627
|
-
return buildReport([
|
|
1628
|
-
...identityResults,
|
|
1629
|
-
...workspaceResults,
|
|
1630
|
-
...runsResults
|
|
1631
|
-
]);
|
|
1632
|
-
}
|
|
1633
|
-
async function runIdentitySection(context) {
|
|
1634
|
-
const results = [];
|
|
1635
|
-
const presence = await checkAuthPresent(context);
|
|
1636
|
-
results.push(presence.result);
|
|
1637
|
-
if (presence.auth === void 0) return results;
|
|
1638
|
-
const validity = await checkAuthValid(context, presence.auth);
|
|
1639
|
-
results.push(validity.result);
|
|
1640
|
-
if (validity.result.severity === "fail") return results;
|
|
1641
|
-
results.push(await checkAuthBroker(context, presence.auth));
|
|
1642
|
-
return results;
|
|
1643
|
-
}
|
|
1644
|
-
async function runWorkspaceSection(context) {
|
|
1645
|
-
const env = checkEnvironment(context);
|
|
1646
|
-
const results = [env.presence];
|
|
1647
|
-
if (env.validity !== void 0) results.push(env.validity);
|
|
1648
|
-
if (env.env === void 0) return results;
|
|
1649
|
-
if (isWebEnvironment(env.env.environment)) {
|
|
1650
|
-
results.push(await checkWebReachable(context, env.env.environment.app.baseUrl));
|
|
1651
|
-
return results;
|
|
1652
|
-
}
|
|
1653
|
-
if (isMobileEnvironment(env.env.environment)) {
|
|
1654
|
-
const app = env.env.environment.app;
|
|
1655
|
-
results.push(...await checkMobileDevice(context, app));
|
|
1656
|
-
results.push(checkMobileAppPath(app));
|
|
1657
|
-
}
|
|
1658
|
-
return results;
|
|
1659
|
-
}
|
|
1660
|
-
async function runRunsSection(context) {
|
|
1661
|
-
return [await checkRunsRoot(context), checkStuckRuns(context)];
|
|
1662
|
-
}
|
|
1663
|
-
function buildReport(results) {
|
|
1664
|
-
const summary = {
|
|
1665
|
-
pass: 0,
|
|
1666
|
-
warn: 0,
|
|
1667
|
-
fail: 0
|
|
1668
|
-
};
|
|
1669
|
-
for (const result of results) summary[result.severity] += 1;
|
|
1670
|
-
return {
|
|
1671
|
-
ok: summary.fail === 0,
|
|
1672
|
-
summary,
|
|
1673
|
-
checks: results
|
|
1674
|
-
};
|
|
1675
|
-
}
|
|
1676
|
-
function isFailingReport(report, options = {}) {
|
|
1677
|
-
if (report.summary.fail > 0) return true;
|
|
1678
|
-
return options.strict === true && report.summary.warn > 0;
|
|
1679
|
-
}
|
|
1680
|
-
//#endregion
|
|
1681
|
-
//#region src/commands/doctor.ts
|
|
1682
|
-
const doctorCommand = defineCommand({
|
|
1683
|
-
meta: {
|
|
1684
|
-
name: "doctor",
|
|
1685
|
-
description: "Diagnose Quire setup: auth, workspace target, and run state."
|
|
1686
|
-
},
|
|
1687
|
-
args: {
|
|
1688
|
-
json: {
|
|
1689
|
-
type: "boolean",
|
|
1690
|
-
description: "Emit the doctor report as JSON."
|
|
1691
|
-
},
|
|
1692
|
-
strict: {
|
|
1693
|
-
type: "boolean",
|
|
1694
|
-
description: "Exit non-zero on warnings as well as failures."
|
|
1695
|
-
}
|
|
1696
|
-
},
|
|
1697
|
-
async run({ args }) {
|
|
1698
|
-
await runDoctorCommand({
|
|
1699
|
-
json: args.json === true,
|
|
1700
|
-
strict: args.strict === true
|
|
1701
|
-
});
|
|
1702
|
-
}
|
|
1703
|
-
});
|
|
1704
|
-
async function runDoctorCommand(options = {}) {
|
|
1705
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
1706
|
-
const report = await withDoctorSpinner(startDoctorSpinner(options, stdout), () => runDoctor(options.context ?? {}));
|
|
1707
|
-
if (options.json === true) writeJson(stdout, report);
|
|
1708
|
-
else renderDoctorReport(stdout, report, options.strict === true);
|
|
1709
|
-
if (isFailingReport(report, { strict: options.strict === true })) process.exitCode = 1;
|
|
1710
|
-
return report;
|
|
1711
|
-
}
|
|
1712
|
-
function startDoctorSpinner(options, output) {
|
|
1713
|
-
if (options.json === true) return;
|
|
1714
|
-
const doctorSpinner = spinner({ output: output ?? process.stdout });
|
|
1715
|
-
doctorSpinner.start("Checking Quire setup");
|
|
1716
|
-
return doctorSpinner;
|
|
1717
|
-
}
|
|
1718
|
-
async function withDoctorSpinner(doctorSpinner, callback) {
|
|
1719
|
-
try {
|
|
1720
|
-
return await callback();
|
|
1721
|
-
} finally {
|
|
1722
|
-
doctorSpinner?.clear();
|
|
1723
|
-
}
|
|
1724
|
-
}
|
|
1725
|
-
//#endregion
|
|
1726
|
-
//#region src/utils/command-args.ts
|
|
1727
|
-
function readOptionalString$1(value) {
|
|
1728
|
-
return readString(value);
|
|
1729
|
-
}
|
|
1730
|
-
//#endregion
|
|
1731
|
-
//#region src/commands/env.ts
|
|
1732
|
-
const envCommand = defineCommand({
|
|
1733
|
-
meta: {
|
|
1734
|
-
name: "env",
|
|
1735
|
-
description: "Show the workspace target configuration Quire uses for investigations."
|
|
1736
|
-
},
|
|
1737
|
-
args: {
|
|
1738
|
-
json: {
|
|
1739
|
-
type: "boolean",
|
|
1740
|
-
description: "Print the environment as JSON."
|
|
1741
|
-
},
|
|
1742
|
-
target: {
|
|
1743
|
-
type: "string",
|
|
1744
|
-
description: "Show a named target from .quire/environment.json.",
|
|
1745
|
-
valueHint: "name"
|
|
1746
|
-
}
|
|
1747
|
-
},
|
|
1748
|
-
async run({ args }) {
|
|
1749
|
-
await runEnvPrint({
|
|
1750
|
-
json: args.json === true,
|
|
1751
|
-
target: readOptionalString$1(args.target)
|
|
1752
|
-
});
|
|
1753
|
-
},
|
|
1754
|
-
subCommands: { show: defineCommand({
|
|
1755
|
-
meta: {
|
|
1756
|
-
name: "show",
|
|
1757
|
-
description: "Show the current workspace target configuration."
|
|
1758
|
-
},
|
|
1759
|
-
args: {
|
|
1760
|
-
json: {
|
|
1761
|
-
type: "boolean",
|
|
1762
|
-
description: "Print the environment as JSON."
|
|
1763
|
-
},
|
|
1764
|
-
target: {
|
|
1765
|
-
type: "string",
|
|
1766
|
-
description: "Show a named target from .quire/environment.json.",
|
|
1767
|
-
valueHint: "name"
|
|
1768
|
-
}
|
|
1769
|
-
},
|
|
1770
|
-
async run({ args }) {
|
|
1771
|
-
await runEnvPrint({
|
|
1772
|
-
json: args.json === true,
|
|
1773
|
-
target: readOptionalString$1(args.target)
|
|
1774
|
-
});
|
|
1775
|
-
}
|
|
1776
|
-
}) }
|
|
1777
|
-
});
|
|
1778
|
-
async function runEnvPrint(options = {}) {
|
|
1779
|
-
const cwd = resolve(options.cwd ?? process.cwd());
|
|
1780
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
1781
|
-
const env = readEnvIfPresent(cwd, normalizeTargetName(options.target, cwd));
|
|
1782
|
-
if (env === null) {
|
|
1783
|
-
if (options.json === true) writeJson(stdout, null);
|
|
1784
|
-
else {
|
|
1785
|
-
log.warn(`No ${formatEnvironmentPath(cwd)} in this workspace.`, {
|
|
1786
|
-
output: stdout,
|
|
1787
|
-
spacing: 0
|
|
1788
|
-
});
|
|
1789
|
-
log.message(`Create one with ${color.command("quire setup")}.`, {
|
|
1790
|
-
output: stdout,
|
|
1791
|
-
spacing: 0,
|
|
1792
|
-
withGuide: true
|
|
1793
|
-
});
|
|
1794
|
-
}
|
|
1795
|
-
return null;
|
|
1796
|
-
}
|
|
1797
|
-
if (options.json === true) {
|
|
1798
|
-
writeJson(stdout, env);
|
|
1799
|
-
return env;
|
|
1800
|
-
}
|
|
1801
|
-
log.success("Quire environment", {
|
|
1802
|
-
output: stdout,
|
|
1803
|
-
spacing: 0
|
|
1804
|
-
});
|
|
1805
|
-
writeLine(stdout, `${color.label("Path")}: ${color.path(formatEnvironmentPath(cwd))}`);
|
|
1806
|
-
for (const line of describeEnvironment(env)) writeLine(stdout, line);
|
|
1807
|
-
return env;
|
|
1808
|
-
}
|
|
1809
|
-
function readEnvIfPresent(cwd, target) {
|
|
1810
|
-
try {
|
|
1811
|
-
return loadEnvironment(cwd, target);
|
|
1812
|
-
} catch (error) {
|
|
1813
|
-
if (error instanceof EnvironmentError && error.code === "missing_file") return null;
|
|
1814
|
-
if (error instanceof EnvironmentError) throw environmentCliError(error, target, cwd);
|
|
1815
|
-
throw error;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
function formatEnvironmentPath(cwd) {
|
|
1819
|
-
const path = environmentFilePath(cwd);
|
|
1820
|
-
const rel = relative(cwd, path);
|
|
1821
|
-
return rel.startsWith("..") || rel.startsWith("/") ? path : rel;
|
|
1822
|
-
}
|
|
1823
|
-
function normalizeTargetName(target, cwd) {
|
|
1824
|
-
if (target === void 0) return;
|
|
1825
|
-
try {
|
|
1826
|
-
return sanitizeTargetName(target);
|
|
1827
|
-
} catch (error) {
|
|
1828
|
-
if (error instanceof EnvironmentError) throw environmentCliError(error, target, cwd);
|
|
1829
|
-
throw error;
|
|
1830
|
-
}
|
|
1831
|
-
}
|
|
1832
|
-
function environmentCliError(error, target, cwd) {
|
|
1833
|
-
if (error.code === "schema_violation" && error.filePath.length === 0) return new CliError(`Invalid target name: ${target ?? ""}`, ExitCode.InvalidInput);
|
|
1834
|
-
const relPath = error.filePath.length === 0 ? "environment configuration" : formatEnvironmentPath(cwd);
|
|
1835
|
-
return new CliError(`${error.message} in ${relPath}`, ExitCode.InvalidInput);
|
|
1836
|
-
}
|
|
1837
|
-
function describeEnvironment(env) {
|
|
1838
|
-
const baseUrl = isWebEnvironment(env) ? env.app.url : "(unset)";
|
|
1839
|
-
const lines = [
|
|
1840
|
-
`${color.label("Target")}: ${color.value(env.targetName)}`,
|
|
1841
|
-
`${color.label("Kind")}: ${color.value(env.app.kind)}`,
|
|
1842
|
-
`${color.label("URL")}: ${baseUrl}`
|
|
1843
|
-
];
|
|
1844
|
-
if (!isWebEnvironment(env)) return lines;
|
|
1845
|
-
if (env.app.description !== void 0) lines.push(`${color.label("Description")}: ${env.app.description}`);
|
|
1846
|
-
if (env.app.entrypoint !== void 0) lines.push(`${color.label("Entrypoint")}: ${env.app.entrypoint}`);
|
|
1847
|
-
if (env.app.readOnly !== void 0) lines.push(`${color.label("Read only")}: ${String(env.app.readOnly)}`);
|
|
1848
|
-
return lines;
|
|
1849
|
-
}
|
|
1850
|
-
//#endregion
|
|
1851
|
-
//#region src/commands/investigate.ts
|
|
1852
|
-
const investigationCommandSchema = {
|
|
1853
|
-
version: 1,
|
|
1854
|
-
command: "quire investigate",
|
|
1855
|
-
description: "Investigation command learning stub. Rebuild the implementation from the guide.",
|
|
1856
|
-
status: "not_implemented",
|
|
1857
|
-
arguments: [
|
|
1858
|
-
{
|
|
1859
|
-
name: "prompt",
|
|
1860
|
-
type: "string",
|
|
1861
|
-
positional: true,
|
|
1862
|
-
requiredUnless: "--stdin",
|
|
1863
|
-
description: "Natural-language investigation query."
|
|
1864
|
-
},
|
|
1865
|
-
{
|
|
1866
|
-
name: "--stdin",
|
|
1867
|
-
type: "boolean",
|
|
1868
|
-
description: "Read additional plain-text investigation context from stdin."
|
|
1869
|
-
},
|
|
1870
|
-
{
|
|
1871
|
-
name: "--json",
|
|
1872
|
-
type: "boolean",
|
|
1873
|
-
description: "Reserved for the rebuilt command's machine-readable start handle."
|
|
1874
|
-
},
|
|
1875
|
-
{
|
|
1876
|
-
name: "--watch",
|
|
1877
|
-
type: "boolean",
|
|
1878
|
-
description: "Reserved for the rebuilt command's attached run watcher."
|
|
1879
|
-
},
|
|
1880
|
-
{
|
|
1881
|
-
name: "--detach",
|
|
1882
|
-
type: "boolean",
|
|
1883
|
-
description: "Reserved for the rebuilt command's detached run mode."
|
|
1884
|
-
},
|
|
1885
|
-
{
|
|
1886
|
-
name: "--url",
|
|
1887
|
-
type: "string",
|
|
1888
|
-
description: "Reserved for the rebuilt command's web target override."
|
|
1889
|
-
},
|
|
1890
|
-
{
|
|
1891
|
-
name: "--target",
|
|
1892
|
-
type: "string",
|
|
1893
|
-
description: "Reserved for the rebuilt command's named target selection."
|
|
1894
|
-
},
|
|
1895
|
-
{
|
|
1896
|
-
name: "--schema",
|
|
1897
|
-
type: "boolean",
|
|
1898
|
-
description: "Print this machine-readable invocation schema and exit."
|
|
1899
|
-
}
|
|
1900
|
-
]
|
|
1901
|
-
};
|
|
1902
|
-
const investigateCommand = defineCommand({
|
|
1903
|
-
meta: {
|
|
1904
|
-
name: "investigate",
|
|
1905
|
-
alias: "ask",
|
|
1906
|
-
description: "Investigate a question against the current workspace."
|
|
1907
|
-
},
|
|
1908
|
-
args: {
|
|
1909
|
-
prompt: {
|
|
1910
|
-
type: "positional",
|
|
1911
|
-
description: "Natural-language investigation query.",
|
|
1912
|
-
required: false
|
|
1913
|
-
},
|
|
1914
|
-
url: {
|
|
1915
|
-
type: "string",
|
|
1916
|
-
description: "Reserved for the rebuilt web target override.",
|
|
1917
|
-
valueHint: "url"
|
|
1918
|
-
},
|
|
1919
|
-
target: {
|
|
1920
|
-
type: "string",
|
|
1921
|
-
description: "Reserved for the rebuilt named target selection.",
|
|
1922
|
-
valueHint: "name"
|
|
1923
|
-
},
|
|
1924
|
-
stdin: {
|
|
1925
|
-
type: "boolean",
|
|
1926
|
-
description: "Read additional plain-text investigation context from stdin."
|
|
1927
|
-
},
|
|
1928
|
-
json: {
|
|
1929
|
-
type: "boolean",
|
|
1930
|
-
description: "Reserved for the rebuilt JSON start handle."
|
|
1931
|
-
},
|
|
1932
|
-
watch: {
|
|
1933
|
-
type: "boolean",
|
|
1934
|
-
description: "Reserved for the rebuilt run watcher."
|
|
1935
|
-
},
|
|
1936
|
-
detach: {
|
|
1937
|
-
type: "boolean",
|
|
1938
|
-
description: "Reserved for the rebuilt detached run mode."
|
|
1939
|
-
},
|
|
1940
|
-
headed: {
|
|
1941
|
-
type: "boolean",
|
|
1942
|
-
description: "Reserved for the rebuilt browser tooling."
|
|
1943
|
-
},
|
|
1944
|
-
schema: {
|
|
1945
|
-
type: "boolean",
|
|
1946
|
-
description: "Emit the machine-readable investigation invocation schema and exit."
|
|
1947
|
-
}
|
|
1948
|
-
},
|
|
1949
|
-
async run({ args }) {
|
|
1950
|
-
if (args.schema === true) {
|
|
1951
|
-
writeInvestigationCommandSchema();
|
|
1952
|
-
return;
|
|
1953
|
-
}
|
|
1954
|
-
await runInvestigate({
|
|
1955
|
-
query: readOptionalString(args.prompt),
|
|
1956
|
-
target: readOptionalString(args.target),
|
|
1957
|
-
url: readOptionalString(args.url),
|
|
1958
|
-
stdin: args.stdin === true,
|
|
1959
|
-
json: args.json === true,
|
|
1960
|
-
watch: args.watch === true,
|
|
1961
|
-
detach: args.detach === true,
|
|
1962
|
-
headed: args.headed === true
|
|
1963
|
-
});
|
|
1964
|
-
}
|
|
1965
|
-
});
|
|
1966
|
-
function writeInvestigationCommandSchema(options = {}) {
|
|
1967
|
-
writeJson(options.io?.stdout ?? process.stdout, investigationCommandSchema);
|
|
1968
|
-
}
|
|
1969
|
-
async function runInvestigate(_options) {
|
|
1970
|
-
createRunDir(cwd());
|
|
1971
|
-
}
|
|
1972
|
-
function readOptionalString(value) {
|
|
1973
|
-
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
1974
|
-
}
|
|
1975
|
-
//#endregion
|
|
1976
|
-
//#region src/auth/device-flow.ts
|
|
1977
|
-
var DeviceAuthError = class extends CliError {
|
|
1978
|
-
constructor(message, code) {
|
|
1979
|
-
super(message, ExitCode.AuthFailure);
|
|
1980
|
-
this.code = code;
|
|
1981
|
-
this.name = "DeviceAuthError";
|
|
1982
|
-
}
|
|
1983
|
-
};
|
|
1984
|
-
async function pollForToken(options) {
|
|
1985
|
-
const sleep = options.sleep ?? defaultSleep;
|
|
1986
|
-
const now = options.now ?? Date.now;
|
|
1987
|
-
const expiresAt = now() + options.expiresIn * 1e3;
|
|
1988
|
-
let interval = Math.max(options.interval, 1);
|
|
1989
|
-
while (now() < expiresAt) try {
|
|
1990
|
-
return await (options.requestToken?.() ?? requestDeviceToken({
|
|
1991
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
1992
|
-
deviceCode: options.deviceCode,
|
|
1993
|
-
fetchFn: options.fetchFn
|
|
1994
|
-
}));
|
|
1995
|
-
} catch (error) {
|
|
1996
|
-
const code = readDevicePollErrorCode(error);
|
|
1997
|
-
if (code === void 0) throw error;
|
|
1998
|
-
if (code === "authorization_pending") {
|
|
1999
|
-
options.onStatus?.(code, interval);
|
|
2000
|
-
await sleep(interval * 1e3);
|
|
2001
|
-
continue;
|
|
2002
|
-
}
|
|
2003
|
-
if (code === "slow_down") {
|
|
2004
|
-
interval += 5;
|
|
2005
|
-
options.onStatus?.(code, interval);
|
|
2006
|
-
await sleep(interval * 1e3);
|
|
2007
|
-
continue;
|
|
2008
|
-
}
|
|
2009
|
-
throw new DeviceAuthError(deviceErrorMessage(code), code);
|
|
2010
|
-
}
|
|
2011
|
-
throw new DeviceAuthError(deviceErrorMessage("expired_token"), "expired_token");
|
|
2012
|
-
}
|
|
2013
|
-
function readDevicePollErrorCode(error) {
|
|
2014
|
-
if (error instanceof DeviceAuthError) return error.code;
|
|
2015
|
-
if (error instanceof HttpError && isDevicePollErrorCode(error.code)) return error.code;
|
|
2016
|
-
}
|
|
2017
|
-
function isDevicePollErrorCode(value) {
|
|
2018
|
-
return value === "authorization_pending" || value === "slow_down" || value === "access_denied" || value === "expired_token" || value === "invalid_grant";
|
|
2019
|
-
}
|
|
2020
|
-
function deviceErrorMessage(code) {
|
|
2021
|
-
switch (code) {
|
|
2022
|
-
case "access_denied": return "Device login was denied in the browser.";
|
|
2023
|
-
case "expired_token": return "Device login code expired. Run `quire login` again.";
|
|
2024
|
-
case "invalid_grant": return "Device login code is invalid. Run `quire login` again.";
|
|
2025
|
-
case "authorization_pending": return "Device login is still waiting for browser approval.";
|
|
2026
|
-
case "slow_down": return "Device login polling was too frequent.";
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
function defaultSleep(ms) {
|
|
2030
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2031
|
-
}
|
|
2032
|
-
//#endregion
|
|
2033
|
-
//#region src/commands/login.ts
|
|
2034
|
-
const loginCommand = defineCommand({
|
|
2035
|
-
meta: {
|
|
2036
|
-
name: "login",
|
|
2037
|
-
description: "Authenticate this machine with Quire using browser device authorization."
|
|
2038
|
-
},
|
|
2039
|
-
args: {
|
|
2040
|
-
"api-url": {
|
|
2041
|
-
type: "string",
|
|
2042
|
-
description: "Quire web app base URL. Defaults to QUIRE_API_URL or https://quire.sh.",
|
|
2043
|
-
valueHint: "url"
|
|
2044
|
-
},
|
|
2045
|
-
"no-open": {
|
|
2046
|
-
type: "boolean",
|
|
2047
|
-
description: "Print the device URL without trying to open a browser."
|
|
2048
|
-
}
|
|
2049
|
-
},
|
|
2050
|
-
async run({ args }) {
|
|
2051
|
-
await runLogin({
|
|
2052
|
-
apiBaseUrl: readApiBaseUrl(args["api-url"]),
|
|
2053
|
-
noOpen: args["no-open"] === true
|
|
2054
|
-
});
|
|
2055
|
-
}
|
|
2056
|
-
});
|
|
2057
|
-
async function runLogin(options) {
|
|
2058
|
-
const store = options.store ?? authStore;
|
|
2059
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
2060
|
-
const stderr = options.io?.stderr ?? process.stderr;
|
|
2061
|
-
const device = await (options.requestCode?.() ?? requestDeviceCode({
|
|
2062
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2063
|
-
fetchFn: options.fetchFn
|
|
2064
|
-
}));
|
|
2065
|
-
log.step("Authorize Quire CLI", {
|
|
2066
|
-
output: stdout,
|
|
2067
|
-
spacing: 0
|
|
2068
|
-
});
|
|
2069
|
-
log.message([`${color.label("Open")}: ${device.verification_uri}`, `${color.label("Code")}: ${color.value(device.user_code)}`], {
|
|
2070
|
-
output: stdout,
|
|
2071
|
-
spacing: 0,
|
|
2072
|
-
withGuide: true
|
|
2073
|
-
});
|
|
2074
|
-
if (options.noOpen !== true) if (await (options.opener ?? openBrowser)(device.verification_uri_complete)) log.success("Opened browser for Quire authorization.", {
|
|
2075
|
-
output: stderr,
|
|
2076
|
-
spacing: 0
|
|
2077
|
-
});
|
|
2078
|
-
else log.warn("Could not open a browser automatically. Open the URL above.", {
|
|
2079
|
-
output: stderr,
|
|
2080
|
-
spacing: 0
|
|
2081
|
-
});
|
|
2082
|
-
log.info("Waiting for browser approval...", {
|
|
2083
|
-
output: stderr,
|
|
2084
|
-
spacing: 0
|
|
2085
|
-
});
|
|
2086
|
-
const token = await (options.pollToken?.() ?? pollForToken({
|
|
2087
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2088
|
-
deviceCode: device.device_code,
|
|
2089
|
-
interval: device.interval,
|
|
2090
|
-
expiresIn: device.expires_in,
|
|
2091
|
-
fetchFn: options.fetchFn,
|
|
2092
|
-
sleep: options.sleep,
|
|
2093
|
-
onStatus(status, nextInterval) {
|
|
2094
|
-
if (status === "slow_down") log.warn(`Server asked Quire to slow down; polling every ${nextInterval}s.`, {
|
|
2095
|
-
output: stderr,
|
|
2096
|
-
spacing: 0
|
|
2097
|
-
});
|
|
2098
|
-
}
|
|
2099
|
-
}));
|
|
2100
|
-
const sessionLookup = await (options.getSession?.(token.access_token) ?? fetchCurrentSession({
|
|
2101
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2102
|
-
accessToken: token.access_token,
|
|
2103
|
-
tokenType: token.token_type,
|
|
2104
|
-
fetchFn: options.fetchFn
|
|
2105
|
-
}));
|
|
2106
|
-
const session = sessionLookup.data;
|
|
2107
|
-
if (session === null) throw new CliError("Login succeeded but Quire could not read the current account.", ExitCode.AuthFailure);
|
|
2108
|
-
const credentials = createStoredCredentials({
|
|
2109
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2110
|
-
accessToken: sessionLookup.refreshedAccessToken ?? token.access_token,
|
|
2111
|
-
tokenType: token.token_type,
|
|
2112
|
-
expiresAt: session.session.expiresAt,
|
|
2113
|
-
expiresIn: token.expires_in,
|
|
2114
|
-
user: session.user
|
|
2115
|
-
});
|
|
2116
|
-
await store.set(credentials);
|
|
2117
|
-
log.success(`Logged in as ${color.value(formatUser$1(session.user))}.`, { output: stdout });
|
|
2118
|
-
}
|
|
2119
|
-
function formatUser$1(user) {
|
|
2120
|
-
if (user.name && user.email && user.name !== user.email) return `${user.name} <${user.email}>`;
|
|
2121
|
-
return user.email ?? user.name ?? user.id;
|
|
2122
|
-
}
|
|
2123
|
-
//#endregion
|
|
2124
|
-
//#region src/commands/logout.ts
|
|
2125
|
-
const logoutCommand = defineCommand({
|
|
2126
|
-
meta: {
|
|
2127
|
-
name: "logout",
|
|
2128
|
-
description: "Remove stored Quire CLI credentials from this machine."
|
|
2129
|
-
},
|
|
2130
|
-
async run() {
|
|
2131
|
-
await runLogout({});
|
|
2132
|
-
}
|
|
2133
|
-
});
|
|
2134
|
-
async function runLogout(options) {
|
|
2135
|
-
const store = options.store ?? authStore;
|
|
2136
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
2137
|
-
const existing = await store.get();
|
|
2138
|
-
await store.clear();
|
|
2139
|
-
if (existing === null) log.info("Already logged out.", {
|
|
2140
|
-
output: stdout,
|
|
2141
|
-
spacing: 0
|
|
2142
|
-
});
|
|
2143
|
-
else log.success("Logged out of Quire.", {
|
|
2144
|
-
output: stdout,
|
|
2145
|
-
spacing: 0
|
|
2146
|
-
});
|
|
2147
|
-
}
|
|
2148
|
-
//#endregion
|
|
2149
|
-
//#region src/commands/runs.ts
|
|
2150
|
-
const runsCommand = defineCommand({
|
|
2151
|
-
meta: {
|
|
2152
|
-
name: "runs",
|
|
2153
|
-
description: "Inspect, watch, and stop durable Quire investigation runs."
|
|
2154
|
-
},
|
|
2155
|
-
subCommands: {
|
|
2156
|
-
status: defineCommand({
|
|
2157
|
-
meta: {
|
|
2158
|
-
name: "status",
|
|
2159
|
-
description: "Reserved for the rebuilt investigation run status command."
|
|
2160
|
-
},
|
|
2161
|
-
args: {
|
|
2162
|
-
run: {
|
|
2163
|
-
type: "positional",
|
|
2164
|
-
description: "Run id or run directory.",
|
|
2165
|
-
required: true
|
|
2166
|
-
},
|
|
2167
|
-
json: {
|
|
2168
|
-
type: "boolean",
|
|
2169
|
-
description: "Emit status JSON."
|
|
2170
|
-
}
|
|
2171
|
-
},
|
|
2172
|
-
async run({ args }) {
|
|
2173
|
-
await runStatus(readRequiredString(args.run, "run"), { json: args.json === true });
|
|
2174
|
-
}
|
|
2175
|
-
}),
|
|
2176
|
-
watch: defineCommand({
|
|
2177
|
-
meta: {
|
|
2178
|
-
name: "watch",
|
|
2179
|
-
description: "Reserved for the rebuilt investigation run watcher."
|
|
2180
|
-
},
|
|
2181
|
-
args: { run: {
|
|
2182
|
-
type: "positional",
|
|
2183
|
-
description: "Run id or run directory.",
|
|
2184
|
-
required: true
|
|
2185
|
-
} },
|
|
2186
|
-
async run({ args }) {
|
|
2187
|
-
await watchRun(readRequiredString(args.run, "run"));
|
|
2188
|
-
}
|
|
2189
|
-
}),
|
|
2190
|
-
cancel: defineCommand({
|
|
2191
|
-
meta: {
|
|
2192
|
-
name: "cancel",
|
|
2193
|
-
description: "Reserved for the rebuilt investigation run cancel command."
|
|
2194
|
-
},
|
|
2195
|
-
args: {
|
|
2196
|
-
run: {
|
|
2197
|
-
type: "positional",
|
|
2198
|
-
description: "Run id or run directory.",
|
|
2199
|
-
required: true
|
|
2200
|
-
},
|
|
2201
|
-
json: {
|
|
2202
|
-
type: "boolean",
|
|
2203
|
-
description: "Emit status JSON after the cancel request."
|
|
2204
|
-
}
|
|
2205
|
-
},
|
|
2206
|
-
async run({ args }) {
|
|
2207
|
-
await cancelRun(readRequiredString(args.run, "run"), { json: args.json === true });
|
|
2208
|
-
}
|
|
2209
|
-
}),
|
|
2210
|
-
list: defineCommand({
|
|
2211
|
-
meta: {
|
|
2212
|
-
name: "list",
|
|
2213
|
-
description: "Reserved for the rebuilt investigation run list command."
|
|
2214
|
-
},
|
|
2215
|
-
args: { json: {
|
|
2216
|
-
type: "boolean",
|
|
2217
|
-
description: "Emit run status JSON."
|
|
2218
|
-
} },
|
|
2219
|
-
async run({ args }) {
|
|
2220
|
-
await listRuns({ json: args.json === true });
|
|
2221
|
-
}
|
|
2222
|
-
})
|
|
2223
|
-
}
|
|
2224
|
-
});
|
|
2225
|
-
async function runStatus(run, options = {}) {
|
|
2226
|
-
const status = {
|
|
2227
|
-
runId: run,
|
|
2228
|
-
status: "not_implemented"
|
|
2229
|
-
};
|
|
2230
|
-
if (options.json === true) {
|
|
2231
|
-
writeJson(options.io?.stdout ?? process.stdout, status);
|
|
2232
|
-
return status;
|
|
2233
|
-
}
|
|
2234
|
-
throw notImplemented("runs status");
|
|
2235
|
-
}
|
|
2236
|
-
async function watchRun(run) {
|
|
2237
|
-
throw notImplemented(`runs watch ${run}`);
|
|
2238
|
-
}
|
|
2239
|
-
async function cancelRun(run, options = {}) {
|
|
2240
|
-
const status = {
|
|
2241
|
-
runId: run,
|
|
2242
|
-
status: "not_implemented"
|
|
2243
|
-
};
|
|
2244
|
-
if (options.json === true) {
|
|
2245
|
-
writeJson(options.io?.stdout ?? process.stdout, status);
|
|
2246
|
-
return status;
|
|
2247
|
-
}
|
|
2248
|
-
throw notImplemented("runs cancel");
|
|
2249
|
-
}
|
|
2250
|
-
async function listRuns(options = {}) {
|
|
2251
|
-
if (options.json === true) {
|
|
2252
|
-
writeJson(options.io?.stdout ?? process.stdout, []);
|
|
2253
|
-
return [];
|
|
2254
|
-
}
|
|
2255
|
-
throw notImplemented("runs list");
|
|
2256
|
-
}
|
|
2257
|
-
function notImplemented(command) {
|
|
2258
|
-
return new CliError(`${command} is intentionally a clean-slate learning stub on this branch. Rebuild it from the implementation guide.`, ExitCode.InternalError);
|
|
2259
|
-
}
|
|
2260
|
-
function readRequiredString(value, name) {
|
|
2261
|
-
if (typeof value === "string" && value.length > 0) return value;
|
|
2262
|
-
throw new CliError(`Missing required ${name}.`, ExitCode.InvalidInput);
|
|
2263
|
-
}
|
|
2264
|
-
//#endregion
|
|
2265
|
-
//#region src/commands/setup.ts
|
|
2266
|
-
const SETUP_URL = "https://quire.sh/agent-setup";
|
|
2267
|
-
const AGENT_SETUP_PROMPT = `Set up this repository for Quire investigations.
|
|
2268
|
-
|
|
2269
|
-
Please load and follow the Quire agent setup instructions from ${SETUP_URL}.
|
|
2270
|
-
|
|
2271
|
-
Inspect the project, create the smallest useful .quire/environment.json launch brief, update AGENTS.md with concise run and verification instructions for future agents, then run quire doctor if available and report anything still blocked.`;
|
|
2272
|
-
const setupCommand = defineCommand({
|
|
2273
|
-
meta: {
|
|
2274
|
-
name: "setup",
|
|
2275
|
-
description: "Prepare this workspace for Quire investigations."
|
|
2276
|
-
},
|
|
2277
|
-
async run() {
|
|
2278
|
-
await runSetup();
|
|
2279
|
-
}
|
|
2280
|
-
});
|
|
2281
|
-
async function runSetup(options = {}) {
|
|
2282
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
2283
|
-
log.step("Set up Quire for this workspace", {
|
|
2284
|
-
output: stdout,
|
|
2285
|
-
spacing: 0
|
|
2286
|
-
});
|
|
2287
|
-
log.message([`Open ${color.info(SETUP_URL)} to load the Quire setup skill, or copy this prompt into your coding agent like Claude Code, Codex, Pi, or Amp:`], {
|
|
2288
|
-
output: stdout,
|
|
2289
|
-
spacing: 0,
|
|
2290
|
-
withGuide: true
|
|
2291
|
-
});
|
|
2292
|
-
writeLine(stdout);
|
|
2293
|
-
writeLine(stdout, color.label("--- copy prompt ---"));
|
|
2294
|
-
writeLine(stdout, AGENT_SETUP_PROMPT);
|
|
2295
|
-
writeLine(stdout, color.label("--- end prompt ---"));
|
|
2296
|
-
}
|
|
2297
|
-
//#endregion
|
|
2298
|
-
//#region src/commands/whoami.ts
|
|
2299
|
-
const whoamiCommand = defineCommand({
|
|
2300
|
-
meta: {
|
|
2301
|
-
name: "whoami",
|
|
2302
|
-
alias: "me",
|
|
2303
|
-
description: "Print the Quire account for the stored CLI credentials."
|
|
2304
|
-
},
|
|
2305
|
-
args: { json: {
|
|
2306
|
-
type: "boolean",
|
|
2307
|
-
description: "Print machine-readable account state to stdout."
|
|
2308
|
-
} },
|
|
2309
|
-
async run({ args }) {
|
|
2310
|
-
await runWhoami({ json: args.json === true });
|
|
2311
|
-
}
|
|
2312
|
-
});
|
|
2313
|
-
async function runWhoami(options) {
|
|
2314
|
-
const store = options.store ?? authStore;
|
|
2315
|
-
const stdout = options.io?.stdout ?? process.stdout;
|
|
2316
|
-
const credentials = await resolveAuthCredentials({ store });
|
|
2317
|
-
if (credentials === null) {
|
|
2318
|
-
if (options.json === true) writeJson(stdout, { authenticated: false });
|
|
2319
|
-
throw new CliError("Not logged in. Run `quire login`.", ExitCode.AuthFailure);
|
|
2320
|
-
}
|
|
2321
|
-
if (credentials.tokenType === "QuireApiKey") {
|
|
2322
|
-
const modelSourceBroker = await withProfileSpinner(startProfileSpinner(options, stdout), () => readModelSourceBrokerStatus({
|
|
2323
|
-
accessToken: credentials.accessToken,
|
|
2324
|
-
tokenType: credentials.tokenType,
|
|
2325
|
-
apiBaseUrl: credentials.apiBaseUrl,
|
|
2326
|
-
fetchFn: options.fetchFn,
|
|
2327
|
-
getModelSourceBrokerStatus: options.getModelSourceBrokerStatus
|
|
2328
|
-
}));
|
|
2329
|
-
const actor = modelSourceBroker?.actor ?? null;
|
|
2330
|
-
if (options.json === true) {
|
|
2331
|
-
writeJson(stdout, {
|
|
2332
|
-
authenticated: true,
|
|
2333
|
-
authMethod: "api_token",
|
|
2334
|
-
apiBaseUrl: credentials.apiBaseUrl,
|
|
2335
|
-
actor,
|
|
2336
|
-
modelSourceBroker
|
|
2337
|
-
});
|
|
2338
|
-
return;
|
|
2339
|
-
}
|
|
2340
|
-
writeWhoamiSummary(stdout, {
|
|
2341
|
-
title: `Authenticated with API token for ${formatActor(actor)}.`,
|
|
2342
|
-
modelSourceBroker
|
|
2343
|
-
});
|
|
2344
|
-
return;
|
|
2345
|
-
}
|
|
2346
|
-
const profile = await withProfileSpinner(startProfileSpinner(options, stdout), async () => {
|
|
2347
|
-
const sessionLookup = await (options.getSession?.(credentials.accessToken) ?? fetchCurrentSession({
|
|
2348
|
-
apiBaseUrl: credentials.apiBaseUrl,
|
|
2349
|
-
accessToken: credentials.accessToken,
|
|
2350
|
-
tokenType: credentials.tokenType,
|
|
2351
|
-
fetchFn: options.fetchFn
|
|
2352
|
-
}));
|
|
2353
|
-
const session = sessionLookup.data;
|
|
2354
|
-
if (session === null) {
|
|
2355
|
-
await store.clear();
|
|
2356
|
-
throw new CliError("Stored Quire credentials are no longer valid. Run `quire login`.", ExitCode.AuthFailure);
|
|
2357
|
-
}
|
|
2358
|
-
const refreshedCredentials = updateStoredCredentials(credentials, {
|
|
2359
|
-
accessToken: sessionLookup.refreshedAccessToken,
|
|
2360
|
-
expiresAt: session.session.expiresAt,
|
|
2361
|
-
user: session.user
|
|
2362
|
-
});
|
|
2363
|
-
await store.set(refreshedCredentials);
|
|
2364
|
-
return {
|
|
2365
|
-
refreshedCredentials,
|
|
2366
|
-
session,
|
|
2367
|
-
modelSourceBroker: await readModelSourceBrokerStatus({
|
|
2368
|
-
accessToken: refreshedCredentials.accessToken,
|
|
2369
|
-
tokenType: refreshedCredentials.tokenType,
|
|
2370
|
-
apiBaseUrl: refreshedCredentials.apiBaseUrl,
|
|
2371
|
-
fetchFn: options.fetchFn,
|
|
2372
|
-
getModelSourceBrokerStatus: options.getModelSourceBrokerStatus
|
|
2373
|
-
})
|
|
2374
|
-
};
|
|
2375
|
-
});
|
|
2376
|
-
if (options.json === true) {
|
|
2377
|
-
writeJson(stdout, {
|
|
2378
|
-
authenticated: true,
|
|
2379
|
-
apiBaseUrl: profile.refreshedCredentials.apiBaseUrl,
|
|
2380
|
-
user: profile.session.user,
|
|
2381
|
-
session: profile.session.session,
|
|
2382
|
-
modelSourceBroker: profile.modelSourceBroker
|
|
2383
|
-
});
|
|
2384
|
-
return;
|
|
2385
|
-
}
|
|
2386
|
-
writeWhoamiSummary(stdout, {
|
|
2387
|
-
title: `Logged in as ${color.value(formatUser(profile.session.user))}.`,
|
|
2388
|
-
modelSourceBroker: profile.modelSourceBroker
|
|
2389
|
-
});
|
|
2390
|
-
}
|
|
2391
|
-
function startProfileSpinner(options, output) {
|
|
2392
|
-
if (options.json === true) return;
|
|
2393
|
-
const profileSpinner = spinner({ output: output ?? process.stdout });
|
|
2394
|
-
profileSpinner.start("Loading profile");
|
|
2395
|
-
return profileSpinner;
|
|
2396
|
-
}
|
|
2397
|
-
async function withProfileSpinner(profileSpinner, callback) {
|
|
2398
|
-
try {
|
|
2399
|
-
return await callback();
|
|
2400
|
-
} finally {
|
|
2401
|
-
profileSpinner?.clear();
|
|
2402
|
-
}
|
|
2403
|
-
}
|
|
2404
|
-
async function readModelSourceBrokerStatus(options) {
|
|
2405
|
-
try {
|
|
2406
|
-
return await (options.getModelSourceBrokerStatus?.(options.accessToken, options.apiBaseUrl) ?? fetchModelSourceBrokerStatus({
|
|
2407
|
-
apiBaseUrl: options.apiBaseUrl,
|
|
2408
|
-
accessToken: options.accessToken,
|
|
2409
|
-
tokenType: options.tokenType,
|
|
2410
|
-
fetchFn: options.fetchFn
|
|
2411
|
-
}));
|
|
2412
|
-
} catch {
|
|
2413
|
-
return null;
|
|
2414
|
-
}
|
|
2415
|
-
}
|
|
2416
|
-
function formatActor(actor) {
|
|
2417
|
-
return actor?.environmentName ?? actor?.triggerSource ?? "remote agent environment";
|
|
2418
|
-
}
|
|
2419
|
-
function formatUser(user) {
|
|
2420
|
-
if (user.name && user.email && user.name !== user.email) return `${user.name} <${user.email}>`;
|
|
2421
|
-
return user.email ?? user.name ?? user.id;
|
|
2422
|
-
}
|
|
2423
|
-
function writeWhoamiSummary(output, profile) {
|
|
2424
|
-
log.success(profile.title, {
|
|
2425
|
-
output: output ?? process.stdout,
|
|
2426
|
-
spacing: 0
|
|
2427
|
-
});
|
|
2428
|
-
writeWalletBalance(output, profile.modelSourceBroker);
|
|
2429
|
-
}
|
|
2430
|
-
function writeWalletBalance(output, status) {
|
|
2431
|
-
if (status?.remainingBalance === null || status?.remainingBalance === void 0) return;
|
|
2432
|
-
log.message(`${color.label("Quire Wallet")}: ${color.value(formatWalletBalance(status.remainingBalance))}`, {
|
|
2433
|
-
output: output ?? process.stdout,
|
|
2434
|
-
spacing: 0,
|
|
2435
|
-
withGuide: true
|
|
2436
|
-
});
|
|
2437
|
-
}
|
|
2438
|
-
function formatWalletBalance(value) {
|
|
2439
|
-
return new Intl.NumberFormat("en-US", {
|
|
2440
|
-
style: "currency",
|
|
2441
|
-
currency: "USD",
|
|
2442
|
-
maximumFractionDigits: 2
|
|
2443
|
-
}).format(value);
|
|
2444
|
-
}
|
|
2445
|
-
//#endregion
|
|
2446
|
-
//#region package.json
|
|
2447
|
-
var version$1 = "0.0.6";
|
|
2448
|
-
//#endregion
|
|
2449
|
-
//#region src/cli.ts
|
|
2450
|
-
const subCommands = {
|
|
2451
|
-
login: loginCommand,
|
|
2452
|
-
logout: logoutCommand,
|
|
2453
|
-
whoami: whoamiCommand,
|
|
2454
|
-
connect: connectCommand,
|
|
2455
|
-
continue: continueCommand,
|
|
2456
|
-
doctor: doctorCommand,
|
|
2457
|
-
env: envCommand,
|
|
2458
|
-
investigate: investigateCommand,
|
|
2459
|
-
runs: runsCommand,
|
|
2460
|
-
setup: setupCommand
|
|
2461
|
-
};
|
|
2462
|
-
const subCommandAliases = new Set([
|
|
2463
|
-
"ask",
|
|
2464
|
-
"me",
|
|
2465
|
-
"resume"
|
|
2466
|
-
]);
|
|
2467
|
-
const cli = defineCommand({
|
|
2468
|
-
meta: {
|
|
2469
|
-
name: "quire",
|
|
2470
|
-
version: version$1,
|
|
2471
|
-
description: "Triage and investigation specialist for software teams and coding agents."
|
|
2472
|
-
},
|
|
2473
|
-
subCommands,
|
|
2474
|
-
async run({ cmd, rawArgs }) {
|
|
2475
|
-
if (rawArgs.length === 0) await showUsage(cmd);
|
|
2476
|
-
}
|
|
2477
|
-
});
|
|
2478
|
-
const runMain = createMain(cli);
|
|
2479
|
-
async function main(rawArgs = process.argv.slice(2)) {
|
|
2480
|
-
const commandArgs = normalizeRawArgs(rawArgs);
|
|
2481
|
-
if (shouldUseCittyBuiltinHandling(commandArgs)) {
|
|
2482
|
-
await runMain({ rawArgs: commandArgs });
|
|
2483
|
-
return;
|
|
2484
|
-
}
|
|
2485
|
-
try {
|
|
2486
|
-
await runCommand(cli, { rawArgs: commandArgs });
|
|
2487
|
-
} catch (error) {
|
|
2488
|
-
writeCliError(error, commandArgs);
|
|
2489
|
-
}
|
|
2490
|
-
}
|
|
2491
|
-
function shouldUseCittyBuiltinHandling(rawArgs) {
|
|
2492
|
-
return rawArgs.length === 0 || rawArgs.some((arg) => arg === "--help" || arg === "-h") || isVersionRequest(rawArgs);
|
|
2493
|
-
}
|
|
2494
|
-
function isVersionRequest(rawArgs) {
|
|
2495
|
-
return rawArgs.length === 1 && (rawArgs[0] === "--version" || rawArgs[0] === "-v");
|
|
2496
|
-
}
|
|
2497
|
-
function normalizeRawArgs(rawArgs) {
|
|
2498
|
-
const first = rawArgs[0];
|
|
2499
|
-
if (first === void 0 || first.startsWith("-") || isSubCommandName(first)) return rawArgs;
|
|
2500
|
-
return ["investigate", ...rawArgs];
|
|
2501
|
-
}
|
|
2502
|
-
function isSubCommandName(name) {
|
|
2503
|
-
return Object.hasOwn(subCommands, name) || subCommandAliases.has(name);
|
|
2504
|
-
}
|
|
2505
|
-
function writeCliError(error, rawArgs) {
|
|
2506
|
-
const message = toOneLineError(error);
|
|
2507
|
-
const exitCode = readExitCode(error);
|
|
2508
|
-
if (wantsJsonError(rawArgs)) console.error(JSON.stringify({ error: {
|
|
2509
|
-
message,
|
|
2510
|
-
exitCode
|
|
2511
|
-
} }));
|
|
2512
|
-
else console.error(`${color.error("Error")}: ${message}`);
|
|
2513
|
-
process.exitCode = exitCode;
|
|
2514
|
-
}
|
|
2515
|
-
function wantsJsonError(rawArgs) {
|
|
2516
|
-
return rawArgs.includes("--json");
|
|
2517
|
-
}
|
|
2518
|
-
//#endregion
|
|
2519
|
-
export { ExitCode as i, main as n, CliError as r, cli as t };
|