@monadeo.com/grimoire-cli 0.3.6 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +5 -2
- package/dist/index.js +449 -233
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -10,7 +10,7 @@ import { join as join6 } from "node:path";
|
|
|
10
10
|
import { readFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
11
11
|
import { homedir } from "node:os";
|
|
12
12
|
import { dirname, join } from "node:path";
|
|
13
|
-
var DEFAULT_API_BASE = "https://grimoire
|
|
13
|
+
var DEFAULT_API_BASE = "https://cli.grimoire.monadeo.com";
|
|
14
14
|
function globalConfigPath() {
|
|
15
15
|
return join(process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "grimoire", "config.json");
|
|
16
16
|
}
|
|
@@ -99,116 +99,224 @@ async function fetchWithTimeout(url, init = {}) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// ../core/dist/auth.js
|
|
102
|
+
function configDir() {
|
|
103
|
+
return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "grimoire");
|
|
104
|
+
}
|
|
102
105
|
function credentialsPath() {
|
|
103
|
-
return join2(
|
|
106
|
+
return join2(configDir(), "credentials.json");
|
|
104
107
|
}
|
|
105
|
-
function
|
|
108
|
+
function storeSession(session) {
|
|
106
109
|
const path = credentialsPath();
|
|
107
110
|
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
108
|
-
writeFileSync2(path, JSON.stringify(
|
|
111
|
+
writeFileSync2(path, JSON.stringify(session) + "\n", { mode: 384 });
|
|
109
112
|
}
|
|
110
|
-
function
|
|
113
|
+
function readSession() {
|
|
111
114
|
try {
|
|
112
115
|
const parsed = JSON.parse(readFileSync2(credentialsPath(), "utf8"));
|
|
113
|
-
|
|
116
|
+
if (typeof parsed.refresh_token === "string" && parsed.refresh_token !== "" && typeof parsed.supabase_url === "string" && typeof parsed.supabase_anon_key === "string") {
|
|
117
|
+
return {
|
|
118
|
+
refresh_token: parsed.refresh_token,
|
|
119
|
+
supabase_url: parsed.supabase_url,
|
|
120
|
+
supabase_anon_key: parsed.supabase_anon_key
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
return void 0;
|
|
114
124
|
} catch {
|
|
115
125
|
return void 0;
|
|
116
126
|
}
|
|
117
127
|
}
|
|
118
|
-
function
|
|
128
|
+
function clearSession() {
|
|
119
129
|
if (existsSync2(credentialsPath()))
|
|
120
130
|
rmSync(credentialsPath());
|
|
121
131
|
}
|
|
132
|
+
function machineTokenPath() {
|
|
133
|
+
return join2(configDir(), "machine-token");
|
|
134
|
+
}
|
|
135
|
+
function storeMachineToken(token) {
|
|
136
|
+
const path = machineTokenPath();
|
|
137
|
+
mkdirSync2(dirname2(path), { recursive: true, mode: 448 });
|
|
138
|
+
writeFileSync2(path, token + "\n", { mode: 384 });
|
|
139
|
+
}
|
|
140
|
+
function readMachineToken() {
|
|
141
|
+
try {
|
|
142
|
+
const token = readFileSync2(machineTokenPath(), "utf8").trim();
|
|
143
|
+
return token !== "" ? token : void 0;
|
|
144
|
+
} catch {
|
|
145
|
+
return void 0;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
function clearMachineToken() {
|
|
149
|
+
if (existsSync2(machineTokenPath()))
|
|
150
|
+
rmSync(machineTokenPath());
|
|
151
|
+
}
|
|
152
|
+
async function fetchAuthConfig(apiBase) {
|
|
153
|
+
const res = await fetchWithTimeout(`${apiBase.replace(/\/+$/, "")}/v1/auth/config`);
|
|
154
|
+
if (!res.ok)
|
|
155
|
+
throw new ApiError(res.status, "auth_config_unavailable", await res.text());
|
|
156
|
+
const body = await res.json();
|
|
157
|
+
if (typeof body.supabase_url !== "string" || typeof body.supabase_anon_key !== "string" || typeof body.oauth_provider !== "string" || !Array.isArray(body.redirect_urls) || body.redirect_urls.length === 0) {
|
|
158
|
+
throw new ApiError(res.status, "auth_config_malformed", body);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
supabase_url: body.supabase_url,
|
|
162
|
+
supabase_anon_key: body.supabase_anon_key,
|
|
163
|
+
oauth_provider: body.oauth_provider,
|
|
164
|
+
redirect_urls: body.redirect_urls.map(String)
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
function parseDetail(text) {
|
|
168
|
+
try {
|
|
169
|
+
return JSON.parse(text);
|
|
170
|
+
} catch {
|
|
171
|
+
return text;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
async function gotrueToken(supabaseUrl, anonKey, grantType, body) {
|
|
175
|
+
const res = await fetchWithTimeout(`${supabaseUrl.replace(/\/+$/, "")}/auth/v1/token?grant_type=${grantType}`, {
|
|
176
|
+
method: "POST",
|
|
177
|
+
headers: { "Content-Type": "application/json", apikey: anonKey },
|
|
178
|
+
body: JSON.stringify(body)
|
|
179
|
+
});
|
|
180
|
+
const text = await res.text();
|
|
181
|
+
if (!res.ok) {
|
|
182
|
+
throw new ApiError(res.status, grantType === "pkce" ? "login_failed" : "refresh_failed", parseDetail(text));
|
|
183
|
+
}
|
|
184
|
+
const parsed = JSON.parse(text);
|
|
185
|
+
if (typeof parsed.access_token !== "string" || typeof parsed.refresh_token !== "string" || typeof parsed.expires_in !== "number") {
|
|
186
|
+
throw new ApiError(res.status, "token_malformed", parsed);
|
|
187
|
+
}
|
|
188
|
+
return {
|
|
189
|
+
access_token: parsed.access_token,
|
|
190
|
+
refresh_token: parsed.refresh_token,
|
|
191
|
+
expires_in: parsed.expires_in
|
|
192
|
+
};
|
|
193
|
+
}
|
|
122
194
|
function base64url(buf) {
|
|
123
195
|
return buf.toString("base64url");
|
|
124
196
|
}
|
|
125
|
-
async function
|
|
126
|
-
const
|
|
197
|
+
async function bindCallback(server, redirectUrls) {
|
|
198
|
+
for (const candidate of redirectUrls) {
|
|
199
|
+
const port = Number(new URL(candidate).port);
|
|
200
|
+
const bound = await new Promise((resolve) => {
|
|
201
|
+
const onError = () => {
|
|
202
|
+
server.off("error", onError);
|
|
203
|
+
resolve(false);
|
|
204
|
+
};
|
|
205
|
+
server.once("error", onError);
|
|
206
|
+
server.listen(port, "127.0.0.1", () => {
|
|
207
|
+
server.off("error", onError);
|
|
208
|
+
resolve(true);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
if (bound)
|
|
212
|
+
return candidate;
|
|
213
|
+
}
|
|
214
|
+
throw new Error(`No free login callback port among: ${redirectUrls.join(", ")}`);
|
|
215
|
+
}
|
|
216
|
+
async function browserLogin(apiBase, openBrowser, timeoutMs = 3e5) {
|
|
217
|
+
const config = await fetchAuthConfig(apiBase);
|
|
127
218
|
const verifier = base64url(randomBytes(32));
|
|
128
219
|
const challenge = base64url(createHash("sha256").update(verifier).digest());
|
|
129
220
|
const state = base64url(randomBytes(16));
|
|
130
221
|
const code = await new Promise((resolve, reject) => {
|
|
131
|
-
|
|
222
|
+
let timer;
|
|
223
|
+
const finish2 = (outcome) => {
|
|
224
|
+
if (timer)
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
server.closeAllConnections();
|
|
227
|
+
server.close();
|
|
228
|
+
if ("code" in outcome)
|
|
229
|
+
resolve(outcome.code);
|
|
230
|
+
else
|
|
231
|
+
reject(outcome.error);
|
|
232
|
+
};
|
|
233
|
+
const server = createServer((req, res) => {
|
|
132
234
|
const url = new URL(req.url ?? "/", "http://127.0.0.1");
|
|
235
|
+
if (req.method !== "GET" || url.pathname !== `/callback/${state}`) {
|
|
236
|
+
res.writeHead(404).end();
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const failure = url.searchParams.get("error_description") ?? url.searchParams.get("error");
|
|
133
240
|
const received = url.searchParams.get("code");
|
|
134
|
-
if (
|
|
135
|
-
|
|
241
|
+
if (failure || !received) {
|
|
242
|
+
res.writeHead(400, { "Content-Type": "text/plain" }).end(`Login failed: ${failure ?? "no code"}`);
|
|
243
|
+
finish2({ error: new Error(`Login failed: ${failure ?? "no code returned"}`) });
|
|
136
244
|
return;
|
|
137
245
|
}
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
server.close();
|
|
141
|
-
resolve(received);
|
|
246
|
+
res.writeHead(200, { "Content-Type": "text/plain" }).end("Logged in to Grimoire. You can close this tab.");
|
|
247
|
+
finish2({ code: received });
|
|
142
248
|
});
|
|
143
|
-
server
|
|
144
|
-
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
}
|
|
249
|
+
bindCallback(server, config.redirect_urls).then((redirectBase) => {
|
|
250
|
+
timer = setTimeout(() => finish2({ error: new Error("Login timed out") }), timeoutMs);
|
|
251
|
+
const authorize = new URL(`${config.supabase_url.replace(/\/+$/, "")}/auth/v1/authorize`);
|
|
252
|
+
authorize.searchParams.set("provider", config.oauth_provider);
|
|
253
|
+
authorize.searchParams.set("redirect_to", `${redirectBase.replace(/\/+$/, "")}/${state}`);
|
|
254
|
+
authorize.searchParams.set("code_challenge", challenge);
|
|
255
|
+
authorize.searchParams.set("code_challenge_method", "s256");
|
|
256
|
+
openBrowser(authorize.toString());
|
|
257
|
+
}).catch((err) => finish2({ error: err }));
|
|
152
258
|
});
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
259
|
+
const tokens = await gotrueToken(config.supabase_url, config.supabase_anon_key, "pkce", {
|
|
260
|
+
auth_code: code,
|
|
261
|
+
code_verifier: verifier
|
|
262
|
+
});
|
|
263
|
+
storeSession({
|
|
264
|
+
refresh_token: tokens.refresh_token,
|
|
265
|
+
supabase_url: config.supabase_url,
|
|
266
|
+
supabase_anon_key: config.supabase_anon_key
|
|
157
267
|
});
|
|
158
|
-
if (!res.ok)
|
|
159
|
-
throw new Error(`Token exchange failed: ${res.status}`);
|
|
160
|
-
const { refresh_token } = await res.json();
|
|
161
|
-
if (typeof refresh_token !== "string" || refresh_token.length === 0) {
|
|
162
|
-
throw new Error("Token exchange succeeded but returned no refresh token");
|
|
163
|
-
}
|
|
164
|
-
storeRefreshToken(refresh_token);
|
|
165
268
|
}
|
|
166
269
|
|
|
167
270
|
// ../core/dist/client.js
|
|
271
|
+
var CODES = {
|
|
272
|
+
401: "unauthorized",
|
|
273
|
+
403: "forbidden",
|
|
274
|
+
404: "not_found",
|
|
275
|
+
422: "invalid_request",
|
|
276
|
+
429: "quota_exceeded"
|
|
277
|
+
};
|
|
168
278
|
var GrimoireClient = class {
|
|
169
279
|
baseUrl;
|
|
170
280
|
machineToken;
|
|
171
|
-
|
|
281
|
+
cachedAccessToken;
|
|
172
282
|
refreshInFlight;
|
|
173
283
|
constructor(opts = {}) {
|
|
174
284
|
this.baseUrl = (opts.baseUrl ?? loadGlobalConfig().apiBaseUrl).replace(/\/+$/, "");
|
|
175
|
-
this.machineToken = opts.machineToken ?? process.env.GRIMOIRE_AUTH_TOKEN;
|
|
285
|
+
this.machineToken = opts.machineToken ?? process.env.GRIMOIRE_AUTH_TOKEN ?? readMachineToken();
|
|
176
286
|
}
|
|
177
287
|
async bearer() {
|
|
178
288
|
if (this.machineToken)
|
|
179
289
|
return this.machineToken;
|
|
180
|
-
if (this.
|
|
181
|
-
return this.
|
|
290
|
+
if (this.cachedAccessToken && this.cachedAccessToken.expiresAt > Date.now() + 6e4) {
|
|
291
|
+
return this.cachedAccessToken.token;
|
|
182
292
|
}
|
|
183
|
-
this.refreshInFlight ??= this.
|
|
293
|
+
this.refreshInFlight ??= this.refreshAccessToken().finally(() => {
|
|
184
294
|
this.refreshInFlight = void 0;
|
|
185
295
|
});
|
|
186
296
|
return this.refreshInFlight;
|
|
187
297
|
}
|
|
188
|
-
// Exchange the
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
298
|
+
// Exchange the stored refresh token for a fresh access token. GoTrue rotates
|
|
299
|
+
// refresh tokens, so the new one replaces the stored one every time.
|
|
300
|
+
async refreshAccessToken() {
|
|
301
|
+
const session = readSession();
|
|
302
|
+
if (!session)
|
|
192
303
|
throw new ApiError(401, "not_logged_in", "Run `grimoire login`");
|
|
193
|
-
|
|
194
|
-
method: "POST",
|
|
195
|
-
headers: { "Content-Type": "application/json" },
|
|
196
|
-
body: JSON.stringify({ refresh_token: refresh })
|
|
197
|
-
});
|
|
198
|
-
if (!res.ok)
|
|
199
|
-
throw new ApiError(res.status, "refresh_failed", "Run `grimoire login`");
|
|
200
|
-
let payload;
|
|
304
|
+
let tokens;
|
|
201
305
|
try {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
throw
|
|
306
|
+
tokens = await gotrueToken(session.supabase_url, session.supabase_anon_key, "refresh_token", {
|
|
307
|
+
refresh_token: session.refresh_token
|
|
308
|
+
});
|
|
309
|
+
} catch (err) {
|
|
310
|
+
if (err instanceof ApiError)
|
|
311
|
+
throw new ApiError(err.status, "refresh_failed", "Run `grimoire login`");
|
|
312
|
+
throw err;
|
|
209
313
|
}
|
|
210
|
-
|
|
211
|
-
|
|
314
|
+
storeSession({ ...session, refresh_token: tokens.refresh_token });
|
|
315
|
+
this.cachedAccessToken = {
|
|
316
|
+
token: tokens.access_token,
|
|
317
|
+
expiresAt: Date.now() + tokens.expires_in * 1e3
|
|
318
|
+
};
|
|
319
|
+
return tokens.access_token;
|
|
212
320
|
}
|
|
213
321
|
async refreshSession() {
|
|
214
322
|
await this.bearer();
|
|
@@ -225,11 +333,12 @@ var GrimoireClient = class {
|
|
|
225
333
|
async request(path, init = {}, auth = true) {
|
|
226
334
|
let res = await this.send(path, init, auth);
|
|
227
335
|
if (res.status === 401 && auth && !this.machineToken) {
|
|
228
|
-
this.
|
|
336
|
+
this.cachedAccessToken = void 0;
|
|
229
337
|
res = await this.send(path, init, auth);
|
|
230
338
|
}
|
|
231
339
|
return this.parseResponse(res);
|
|
232
340
|
}
|
|
341
|
+
// FastAPI errors carry {"detail": "..."} (string) or {"detail": [...]} (422).
|
|
233
342
|
async parseResponse(res) {
|
|
234
343
|
const isJson = res.headers.get("content-type")?.includes("json") ?? false;
|
|
235
344
|
const text = await res.text();
|
|
@@ -244,33 +353,28 @@ var GrimoireClient = class {
|
|
|
244
353
|
}
|
|
245
354
|
}
|
|
246
355
|
if (!res.ok) {
|
|
247
|
-
const
|
|
248
|
-
throw new ApiError(res.status,
|
|
356
|
+
const detail = parsed && typeof body === "object" && body !== null ? body.detail : void 0;
|
|
357
|
+
throw new ApiError(res.status, CODES[res.status] ?? "error", detail ?? (parsed ? body : text));
|
|
249
358
|
}
|
|
250
359
|
if (isJson)
|
|
251
360
|
return parsed ? body : {};
|
|
252
361
|
return text;
|
|
253
362
|
}
|
|
254
363
|
search(input) {
|
|
255
|
-
return this.request("/v1/search", {
|
|
256
|
-
method: "POST",
|
|
257
|
-
body: JSON.stringify(input)
|
|
258
|
-
});
|
|
364
|
+
return this.request("/v1/search", { method: "POST", body: JSON.stringify(input) });
|
|
259
365
|
}
|
|
260
|
-
listSources(
|
|
261
|
-
return this.request(
|
|
366
|
+
listSources() {
|
|
367
|
+
return this.request("/v1/sources");
|
|
262
368
|
}
|
|
263
|
-
listVersions(
|
|
264
|
-
return this.request(`/v1/sources/${encodeURIComponent(
|
|
369
|
+
listVersions(product) {
|
|
370
|
+
return this.request(`/v1/sources/${encodeURIComponent(product)}/versions`);
|
|
265
371
|
}
|
|
266
|
-
|
|
267
|
-
return this.request(`/v1/
|
|
372
|
+
getDoc(pointId, window = 2) {
|
|
373
|
+
return this.request(`/v1/doc/${encodeURIComponent(pointId)}?window=${window}`);
|
|
268
374
|
}
|
|
269
|
-
reportResult(
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
body: JSON.stringify({ chunk_id: chunkId, verdict, note })
|
|
273
|
-
});
|
|
375
|
+
reportResult(pointId, verdict, note) {
|
|
376
|
+
const body = { point_id: pointId, verdict, ...note ? { note } : {} };
|
|
377
|
+
return this.request("/v1/report", { method: "POST", body: JSON.stringify(body) });
|
|
274
378
|
}
|
|
275
379
|
submitSource(body) {
|
|
276
380
|
return this.request("/v1/sources", { method: "POST", body: JSON.stringify(body) });
|
|
@@ -278,12 +382,49 @@ var GrimoireClient = class {
|
|
|
278
382
|
getJob(jobId) {
|
|
279
383
|
return this.request(`/v1/jobs/${encodeURIComponent(jobId)}`);
|
|
280
384
|
}
|
|
385
|
+
me() {
|
|
386
|
+
return this.request("/v1/me");
|
|
387
|
+
}
|
|
388
|
+
reviewQueue() {
|
|
389
|
+
return this.request("/v1/staff/review-queue");
|
|
390
|
+
}
|
|
391
|
+
approveJob(jobId) {
|
|
392
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/approve`, { method: "POST" });
|
|
393
|
+
}
|
|
394
|
+
listUsers() {
|
|
395
|
+
return this.request("/v1/staff/users");
|
|
396
|
+
}
|
|
397
|
+
grantUser(subject, name) {
|
|
398
|
+
return this.request("/v1/staff/users", {
|
|
399
|
+
method: "POST",
|
|
400
|
+
body: JSON.stringify({ subject, name })
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
revokeUser(subject) {
|
|
404
|
+
return this.request(`/v1/staff/users/${encodeURIComponent(subject)}`, { method: "DELETE" });
|
|
405
|
+
}
|
|
406
|
+
rejectJob(jobId, reason) {
|
|
407
|
+
return this.request(`/v1/staff/jobs/${encodeURIComponent(jobId)}/reject`, {
|
|
408
|
+
method: "POST",
|
|
409
|
+
body: JSON.stringify({ reason })
|
|
410
|
+
});
|
|
411
|
+
}
|
|
281
412
|
};
|
|
282
413
|
|
|
283
414
|
// src/args.ts
|
|
284
415
|
var UsageError = class extends Error {
|
|
285
416
|
};
|
|
286
|
-
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
417
|
+
var BOOL_FLAGS = /* @__PURE__ */ new Set([
|
|
418
|
+
"json",
|
|
419
|
+
"compact",
|
|
420
|
+
"watch",
|
|
421
|
+
"names",
|
|
422
|
+
"http",
|
|
423
|
+
"unset",
|
|
424
|
+
"rolling",
|
|
425
|
+
"debug",
|
|
426
|
+
"no-launch-browser"
|
|
427
|
+
]);
|
|
287
428
|
function parseArgs(argv, aliases = {}, allowed) {
|
|
288
429
|
const positionals = [];
|
|
289
430
|
const flags = {};
|
|
@@ -334,39 +475,39 @@ function requireFlagOneOf(parsed, name, allowed, usage) {
|
|
|
334
475
|
|
|
335
476
|
// src/output.ts
|
|
336
477
|
var EXIT = { ok: 0, apiError: 1, authRequired: 2, quota: 3, notFound: 4 };
|
|
337
|
-
function
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
process.stderr.write(`${label}
|
|
478
|
+
function preamble(res) {
|
|
479
|
+
const versions = Object.entries(res.resolved_versions).map(([product, version]) => `${product}@${version}`).join(", ");
|
|
480
|
+
process.stderr.write(`sources: ${versions} \xB7 retrievals remaining: ${res.retrievals_remaining}
|
|
341
481
|
`);
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
if (
|
|
345
|
-
process.stderr.write(
|
|
346
|
-
"note: low-confidence results \u2014 the docs may not cover this; tell the user rather than guessing.\n"
|
|
347
|
-
);
|
|
482
|
+
process.stderr.write(`note: ${res.untrusted_content_notice}
|
|
483
|
+
`);
|
|
484
|
+
if (res.results.length === 0) {
|
|
485
|
+
process.stderr.write("note: no result passed the relevance threshold \u2014 the docs may not cover this.\n");
|
|
348
486
|
}
|
|
349
487
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
488
|
+
function heading(r) {
|
|
489
|
+
return r.heading_path.join(" \u203A ");
|
|
490
|
+
}
|
|
491
|
+
function printResults(res) {
|
|
492
|
+
preamble(res);
|
|
493
|
+
for (const r of res.results) {
|
|
494
|
+
process.stdout.write(
|
|
495
|
+
`
|
|
496
|
+
${r.score.toFixed(3)} ${r.product}@${r.version} ${heading(r)}
|
|
497
|
+
${r.source_url}
|
|
498
|
+
point_id: ${r.point_id}
|
|
499
|
+
`
|
|
500
|
+
);
|
|
359
501
|
const preview = r.text.length > 500 ? `${r.text.slice(0, 500)}\u2026` : r.text;
|
|
360
502
|
process.stdout.write(`${preview}
|
|
361
503
|
`);
|
|
362
504
|
}
|
|
363
505
|
}
|
|
364
|
-
function printCompact(
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
for (const r of results) {
|
|
506
|
+
function printCompact(res) {
|
|
507
|
+
preamble(res);
|
|
508
|
+
for (const r of res.results) {
|
|
368
509
|
process.stdout.write(
|
|
369
|
-
`${r.score.toFixed(3)} | ${r.
|
|
510
|
+
`${r.score.toFixed(3)} | ${r.product}@${r.version} | ${heading(r)} | ${r.source_url} | ${r.point_id}
|
|
370
511
|
`
|
|
371
512
|
);
|
|
372
513
|
}
|
|
@@ -418,7 +559,7 @@ function setupCodex() {
|
|
|
418
559
|
function hasSession() {
|
|
419
560
|
if (process.env.GRIMOIRE_AUTH_TOKEN) return true;
|
|
420
561
|
try {
|
|
421
|
-
return
|
|
562
|
+
return readMachineToken() !== void 0 || readSession() !== void 0;
|
|
422
563
|
} catch {
|
|
423
564
|
return false;
|
|
424
565
|
}
|
|
@@ -518,7 +659,7 @@ var CONFIG_KEYS = {
|
|
|
518
659
|
describe: `API origin without a path (default ${DEFAULT_API_BASE}; env GRIMOIRE_API_URL overrides)`,
|
|
519
660
|
parse: (raw) => {
|
|
520
661
|
if (!/^https?:\/\/[^/]+$/.test(raw)) {
|
|
521
|
-
throw new UsageError("api-url must be a bare origin, e.g. https://
|
|
662
|
+
throw new UsageError("api-url must be a bare origin, e.g. https://api.example.com");
|
|
522
663
|
}
|
|
523
664
|
return raw;
|
|
524
665
|
}
|
|
@@ -527,22 +668,11 @@ var CONFIG_KEYS = {
|
|
|
527
668
|
prop: "updateCheckHours",
|
|
528
669
|
describe: "hours between CLI update checks (default 24, 0 disables)",
|
|
529
670
|
parse: (raw) => intInRange("update-check-hours", raw, 0)
|
|
530
|
-
},
|
|
531
|
-
language: {
|
|
532
|
-
prop: "defaultLanguage",
|
|
533
|
-
describe: "default search language filter",
|
|
534
|
-
parse: (raw) => {
|
|
535
|
-
if (!raw) throw new UsageError("language must be non-empty");
|
|
536
|
-
return raw;
|
|
537
|
-
}
|
|
538
|
-
},
|
|
539
|
-
"max-response-tokens": {
|
|
540
|
-
prop: "maxResponseTokens",
|
|
541
|
-
describe: "search response token budget",
|
|
542
|
-
parse: (raw) => intInRange("max-response-tokens", raw, 1)
|
|
543
671
|
}
|
|
544
672
|
};
|
|
545
|
-
var
|
|
673
|
+
var AUTH_TOKEN_KEY = "auth-token";
|
|
674
|
+
var MACHINE_TOKEN_RE = /^mt_[A-Za-z0-9_-]{32,}$/;
|
|
675
|
+
var USAGE = "Usage: grimoire config [<key>] [<value>] [--unset] (keys: " + [...Object.keys(CONFIG_KEYS), AUTH_TOKEN_KEY].join(", ") + ")";
|
|
546
676
|
function runConfig(args) {
|
|
547
677
|
const [key, value] = args.positionals;
|
|
548
678
|
if (key === void 0) {
|
|
@@ -554,6 +684,30 @@ function runConfig(args) {
|
|
|
554
684
|
process.stdout.write(`${name} = ${current === void 0 ? "(unset)" : JSON.stringify(current)} # ${spec2.describe}
|
|
555
685
|
`);
|
|
556
686
|
}
|
|
687
|
+
process.stdout.write(
|
|
688
|
+
`${AUTH_TOKEN_KEY} = ${readMachineToken() ? "(set)" : "(unset)"} # machine token, stored 0600 (env GRIMOIRE_AUTH_TOKEN overrides)
|
|
689
|
+
`
|
|
690
|
+
);
|
|
691
|
+
return EXIT.ok;
|
|
692
|
+
}
|
|
693
|
+
if (key === AUTH_TOKEN_KEY) {
|
|
694
|
+
if (args.bools.has("unset")) {
|
|
695
|
+
clearMachineToken();
|
|
696
|
+
process.stdout.write(`${AUTH_TOKEN_KEY} unset
|
|
697
|
+
`);
|
|
698
|
+
return EXIT.ok;
|
|
699
|
+
}
|
|
700
|
+
if (value === void 0) {
|
|
701
|
+
process.stdout.write(`${readMachineToken() ? "(set)" : "(unset)"}
|
|
702
|
+
`);
|
|
703
|
+
return EXIT.ok;
|
|
704
|
+
}
|
|
705
|
+
if (!MACHINE_TOKEN_RE.test(value)) {
|
|
706
|
+
throw new UsageError("auth-token must be a machine token starting with mt_");
|
|
707
|
+
}
|
|
708
|
+
storeMachineToken(value);
|
|
709
|
+
process.stdout.write(`${AUTH_TOKEN_KEY} set
|
|
710
|
+
`);
|
|
557
711
|
return EXIT.ok;
|
|
558
712
|
}
|
|
559
713
|
const spec = CONFIG_KEYS[key];
|
|
@@ -622,6 +776,33 @@ function runUpdate() {
|
|
|
622
776
|
return result.status === 0 ? EXIT.ok : EXIT.apiError;
|
|
623
777
|
}
|
|
624
778
|
|
|
779
|
+
// src/commands/ingest.ts
|
|
780
|
+
function submissionFromArgs(url, args) {
|
|
781
|
+
const usage = "Usage: grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)";
|
|
782
|
+
const product = args.flags.product?.[0];
|
|
783
|
+
if (!product) throw new UsageError(usage);
|
|
784
|
+
const fixed = args.flags.fixed?.[0];
|
|
785
|
+
const npm = args.flags.npm?.[0];
|
|
786
|
+
const pypi = args.flags.pypi?.[0];
|
|
787
|
+
const github = args.flags.github?.[0];
|
|
788
|
+
const probes = [npm, pypi, github].filter(Boolean).length;
|
|
789
|
+
if (probes > 1) throw new UsageError("Pass only one of --npm, --pypi, --github");
|
|
790
|
+
const body = {
|
|
791
|
+
url,
|
|
792
|
+
product,
|
|
793
|
+
version_rule: { kind: "rolling" },
|
|
794
|
+
include_patterns: args.flags.include ?? [],
|
|
795
|
+
exclude_patterns: args.flags.exclude ?? []
|
|
796
|
+
};
|
|
797
|
+
if (args.bools.has("rolling")) return body;
|
|
798
|
+
if (fixed) return { ...body, version_rule: { kind: "fixed", value: fixed } };
|
|
799
|
+
if (npm) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "npm", package: npm } };
|
|
800
|
+
if (pypi) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "pypi", package: pypi } };
|
|
801
|
+
if (github) return { ...body, version_rule: { kind: "fixed" }, probe: { kind: "github", repo: github } };
|
|
802
|
+
throw new UsageError(`${usage}
|
|
803
|
+
A version rule is mandatory: --rolling for unversioned docs, --fixed, or a release probe.`);
|
|
804
|
+
}
|
|
805
|
+
|
|
625
806
|
// src/updatecheck.ts
|
|
626
807
|
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync5 } from "node:fs";
|
|
627
808
|
import { homedir as homedir4 } from "node:os";
|
|
@@ -678,15 +859,21 @@ async function refreshUpdateState(current, configuredHours) {
|
|
|
678
859
|
}
|
|
679
860
|
|
|
680
861
|
// src/index.ts
|
|
681
|
-
function
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
685
|
-
} catch {
|
|
686
|
-
process.stdout.write(`Open this URL to log in:
|
|
687
|
-
${url}
|
|
862
|
+
function loginOpener(launchBrowser) {
|
|
863
|
+
return (url) => {
|
|
864
|
+
process.stdout.write(`${url}
|
|
688
865
|
`);
|
|
689
|
-
|
|
866
|
+
if (!launchBrowser) {
|
|
867
|
+
process.stderr.write("Open the URL above in a browser to log in; waiting for the callback\u2026\n");
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const cmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
871
|
+
try {
|
|
872
|
+
execFileSync(cmd, [url], { stdio: "ignore" });
|
|
873
|
+
} catch {
|
|
874
|
+
process.stderr.write("Could not open a browser; open the URL above manually.\n");
|
|
875
|
+
}
|
|
876
|
+
};
|
|
690
877
|
}
|
|
691
878
|
function parseSourceFlags(values) {
|
|
692
879
|
return (values ?? []).map((v) => {
|
|
@@ -694,33 +881,39 @@ function parseSourceFlags(values) {
|
|
|
694
881
|
return version ? { source, version } : { source };
|
|
695
882
|
});
|
|
696
883
|
}
|
|
697
|
-
|
|
884
|
+
function toSelectors(pins) {
|
|
885
|
+
return pins.map((p) => ({ product: p.source, ...p.version ? { version: p.version } : {} }));
|
|
886
|
+
}
|
|
887
|
+
var VERSION = true ? "0.4.0" : "dev";
|
|
698
888
|
var HELP = `grimoire ${VERSION} \u2014 documentation retrieval for AI agents
|
|
699
889
|
|
|
700
|
-
grimoire login | logout | whoami
|
|
890
|
+
grimoire login [--no-launch-browser] | logout | whoami
|
|
701
891
|
grimoire setup <claude-code|cursor|windsurf|codex>
|
|
702
892
|
grimoire init
|
|
703
|
-
grimoire search "<query>" [-s nextjs@15 -s react] [--
|
|
893
|
+
grimoire search "<query>" [-s nextjs@15 -s react] [--json|--compact] [--debug]
|
|
704
894
|
grimoire sources [--q <kw>] [--names|--json]
|
|
705
|
-
grimoire versions <
|
|
895
|
+
grimoire versions <product> [--json]
|
|
706
896
|
grimoire config [<key>] [<value>] [--unset]
|
|
707
897
|
grimoire update
|
|
708
|
-
grimoire doc <
|
|
709
|
-
grimoire report <
|
|
710
|
-
grimoire ingest <url>
|
|
898
|
+
grimoire doc <point_id> [--window 2] [--json]
|
|
899
|
+
grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]
|
|
900
|
+
grimoire ingest <url> --product <name> (--rolling | --fixed <version> | --npm <pkg> | --pypi <pkg> | --github <owner/repo>)
|
|
901
|
+
[--include <pattern>]... [--exclude <pattern>]... [--watch]
|
|
711
902
|
grimoire jobs <job_id> [--watch]
|
|
903
|
+
grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..."
|
|
904
|
+
grimoire staff users [--json] | grant <subject> --name "..." | revoke <subject>
|
|
712
905
|
grimoire mcp [--http]
|
|
713
906
|
grimoire help | --help | -h
|
|
714
907
|
grimoire version | --version | -v
|
|
715
908
|
|
|
716
909
|
env: GRIMOIRE_AUTH_TOKEN \u2014 machine token (CI, instead of login)
|
|
717
|
-
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
910
|
+
GRIMOIRE_API_URL \u2014 API origin without a path
|
|
718
911
|
`;
|
|
719
912
|
var COMMAND_FLAGS = {
|
|
720
913
|
version: [],
|
|
721
914
|
"--version": [],
|
|
722
915
|
"-v": [],
|
|
723
|
-
login: [],
|
|
916
|
+
login: ["no-launch-browser"],
|
|
724
917
|
logout: [],
|
|
725
918
|
setup: [],
|
|
726
919
|
init: [],
|
|
@@ -731,14 +924,23 @@ var COMMAND_FLAGS = {
|
|
|
731
924
|
mcp: ["http"],
|
|
732
925
|
config: ["unset"],
|
|
733
926
|
update: [],
|
|
734
|
-
search: ["source", "
|
|
927
|
+
search: ["source", "json", "compact", "debug"],
|
|
735
928
|
sources: ["q", "names", "json"],
|
|
736
929
|
versions: ["json"],
|
|
737
|
-
doc: ["window"],
|
|
930
|
+
doc: ["window", "json"],
|
|
738
931
|
report: ["verdict", "note"],
|
|
739
|
-
ingest: ["
|
|
740
|
-
jobs: ["watch"]
|
|
932
|
+
ingest: ["product", "rolling", "fixed", "npm", "pypi", "github", "include", "exclude", "watch"],
|
|
933
|
+
jobs: ["watch"],
|
|
934
|
+
staff: ["reason", "name", "json"]
|
|
741
935
|
};
|
|
936
|
+
function describeError(err) {
|
|
937
|
+
const detail = typeof err.body === "string" ? err.body : err.body !== void 0 && err.body !== null ? JSON.stringify(err.body) : "";
|
|
938
|
+
return `error: ${err.code}${detail ? ` \u2014 ${detail}` : ""}`;
|
|
939
|
+
}
|
|
940
|
+
function describeJob(job) {
|
|
941
|
+
const extra = [job.reason ? `reason: ${job.reason}` : "", job.source_id ? `source: ${job.source_id}` : ""].filter(Boolean).join(" ");
|
|
942
|
+
return `${job.state}${extra ? ` ${extra}` : ""}`;
|
|
943
|
+
}
|
|
742
944
|
async function main(argv) {
|
|
743
945
|
const [command, ...rest] = argv;
|
|
744
946
|
const args = parseArgs(rest, { "-s": "source", "-q": "q" }, command !== void 0 ? COMMAND_FLAGS[command] : []);
|
|
@@ -751,12 +953,14 @@ async function main(argv) {
|
|
|
751
953
|
`);
|
|
752
954
|
return EXIT.ok;
|
|
753
955
|
case "login": {
|
|
754
|
-
|
|
956
|
+
const launch = !args.bools.has("no-launch-browser");
|
|
957
|
+
process.stderr.write(launch ? "Opening your browser to log in\u2026\n" : "Login URL:\n");
|
|
958
|
+
await browserLogin(loadGlobalConfig().apiBaseUrl, loginOpener(launch));
|
|
755
959
|
process.stdout.write("Logged in.\n");
|
|
756
960
|
return EXIT.ok;
|
|
757
961
|
}
|
|
758
962
|
case "logout":
|
|
759
|
-
|
|
963
|
+
clearSession();
|
|
760
964
|
process.stdout.write("Logged out.\n");
|
|
761
965
|
return EXIT.ok;
|
|
762
966
|
case "setup":
|
|
@@ -781,29 +985,22 @@ async function main(argv) {
|
|
|
781
985
|
try {
|
|
782
986
|
switch (command) {
|
|
783
987
|
case "whoami": {
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
return EXIT.ok;
|
|
787
|
-
}
|
|
788
|
-
if (!readRefreshToken()) {
|
|
988
|
+
const via = process.env.GRIMOIRE_AUTH_TOKEN ? "GRIMOIRE_AUTH_TOKEN" : readMachineToken() ? "grimoire config auth-token" : readSession() ? "browser login" : void 0;
|
|
989
|
+
if (!via) {
|
|
789
990
|
process.stderr.write("not logged in \u2014 run `grimoire login`\n");
|
|
790
991
|
return EXIT.authRequired;
|
|
791
992
|
}
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
process.stderr.write(`not logged in (${reason}) \u2014 run \`grimoire login\`
|
|
799
|
-
`);
|
|
800
|
-
return EXIT.authRequired;
|
|
801
|
-
}
|
|
993
|
+
const me = await client.me();
|
|
994
|
+
process.stdout.write(
|
|
995
|
+
`${me.kind} ${me.subject}${me.is_staff ? " staff" : ""} quota ${me.quota_per_day}/day via ${via}
|
|
996
|
+
`
|
|
997
|
+
);
|
|
998
|
+
return EXIT.ok;
|
|
802
999
|
}
|
|
803
1000
|
case "search": {
|
|
804
1001
|
const query = args.positionals[0];
|
|
805
1002
|
if (!query) {
|
|
806
|
-
process.stderr.write('Usage: grimoire search "<query>" -s <
|
|
1003
|
+
process.stderr.write('Usage: grimoire search "<query>" -s <product>[@version]\n');
|
|
807
1004
|
return EXIT.apiError;
|
|
808
1005
|
}
|
|
809
1006
|
const explicit = parseSourceFlags(args.flags.source);
|
|
@@ -811,99 +1008,73 @@ async function main(argv) {
|
|
|
811
1008
|
if (sources.length === 0) {
|
|
812
1009
|
const initHelps = existsSync6(join6(process.cwd(), "package.json")) || existsSync6(join6(process.cwd(), "requirements.txt"));
|
|
813
1010
|
process.stderr.write(
|
|
814
|
-
'No sources selected. List what is indexed:\n grimoire sources\nthen scope the search:\n grimoire search "<query>" -s <
|
|
1011
|
+
'No sources selected. List what is indexed:\n grimoire sources\nthen scope the search:\n grimoire search "<query>" -s <product>[@version]\n' + (initHelps ? "or pin this project's sources from its dependencies:\n grimoire init\n" : "")
|
|
815
1012
|
);
|
|
816
1013
|
return EXIT.apiError;
|
|
817
1014
|
}
|
|
818
|
-
const res = await client.search({
|
|
819
|
-
query,
|
|
820
|
-
sources,
|
|
821
|
-
language: args.flags.lang?.[0],
|
|
822
|
-
top_k: intFlag(args, "top-k")
|
|
823
|
-
});
|
|
1015
|
+
const res = await client.search({ query, sources: toSelectors(sources), debug: args.bools.has("debug") });
|
|
824
1016
|
if (json) process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
825
|
-
else if (args.bools.has("compact")) printCompact(res
|
|
826
|
-
else printResults(res
|
|
1017
|
+
else if (args.bools.has("compact")) printCompact(res);
|
|
1018
|
+
else printResults(res);
|
|
827
1019
|
return EXIT.ok;
|
|
828
1020
|
}
|
|
829
1021
|
case "sources": {
|
|
830
|
-
const
|
|
831
|
-
const sources =
|
|
1022
|
+
const needle = args.flags.q?.[0]?.toLowerCase();
|
|
1023
|
+
const sources = (await client.listSources()).filter(
|
|
1024
|
+
(s) => !needle || s.product.toLowerCase().includes(needle) || s.base_url.toLowerCase().includes(needle)
|
|
1025
|
+
);
|
|
832
1026
|
if (json) {
|
|
833
1027
|
process.stdout.write(JSON.stringify(sources, null, 2) + "\n");
|
|
834
1028
|
} else if (args.bools.has("names")) {
|
|
835
|
-
for (const s of sources) process.stdout.write(`${s.
|
|
1029
|
+
for (const s of sources) process.stdout.write(`${s.product}
|
|
836
1030
|
`);
|
|
837
1031
|
} else {
|
|
838
1032
|
for (const s of sources) {
|
|
839
|
-
const
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
s.latest_chunks != null ? `${s.latest_chunks} chunks` : "",
|
|
843
|
-
s.latest_pages != null ? `${s.latest_pages} pages` : "",
|
|
844
|
-
s.latest_crawled_at ? `crawled ${s.latest_crawled_at.slice(0, 10)}` : ""
|
|
845
|
-
].filter(Boolean).join(" \xB7 ");
|
|
846
|
-
process.stdout.write(
|
|
847
|
-
[`${s.source_id}@${latest}${vis}`, meta, s.origin_url ?? ""].filter(Boolean).join(" ") + "\n"
|
|
848
|
-
);
|
|
1033
|
+
const versions = s.versions.length > 0 ? s.versions.join(", ") : "(not indexed yet)";
|
|
1034
|
+
process.stdout.write(`${s.product} ${versions} ${s.base_url}
|
|
1035
|
+
`);
|
|
849
1036
|
}
|
|
850
1037
|
}
|
|
851
1038
|
return EXIT.ok;
|
|
852
1039
|
}
|
|
853
1040
|
case "versions": {
|
|
854
|
-
const
|
|
855
|
-
const res = await client.listVersions(
|
|
856
|
-
const versions = res.versions ?? [];
|
|
1041
|
+
const product = requirePositional(args, 0, "Usage: grimoire versions <product> [--json]");
|
|
1042
|
+
const res = await client.listVersions(product);
|
|
857
1043
|
if (json) {
|
|
858
|
-
process.stdout.write(JSON.stringify(
|
|
1044
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
859
1045
|
return EXIT.ok;
|
|
860
1046
|
}
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
process.stdout.write(`no active versions for ${source}
|
|
1047
|
+
for (const v of res.versions) {
|
|
1048
|
+
process.stdout.write(`${v.version}${v.version === res.latest ? " latest" : ""} ${v.chunk_count} chunks
|
|
864
1049
|
`);
|
|
865
|
-
return EXIT.ok;
|
|
866
|
-
}
|
|
867
|
-
for (const v of available) {
|
|
868
|
-
const label = v.semver ?? v.version_id;
|
|
869
|
-
const parts = [
|
|
870
|
-
label,
|
|
871
|
-
v.is_latest ? "latest" : "",
|
|
872
|
-
`${v.chunk_count ?? 0} chunks`,
|
|
873
|
-
`crawled ${(v.ingested_at ?? "").slice(0, 10)}`,
|
|
874
|
-
v.semver ? `(${v.version_id})` : ""
|
|
875
|
-
].filter(Boolean);
|
|
876
|
-
process.stdout.write(parts.join(" ") + "\n");
|
|
877
1050
|
}
|
|
878
1051
|
return EXIT.ok;
|
|
879
1052
|
}
|
|
880
1053
|
case "doc": {
|
|
881
|
-
const
|
|
1054
|
+
const pointId = requirePositional(args, 0, "Usage: grimoire doc <point_id> [--window 2] [--json]");
|
|
882
1055
|
const window = intFlag(args, "window", { min: 0, max: 5 }) ?? 2;
|
|
883
|
-
const res = await client.
|
|
884
|
-
|
|
1056
|
+
const res = await client.getDoc(pointId, window);
|
|
1057
|
+
if (json) {
|
|
1058
|
+
process.stdout.write(JSON.stringify(res, null, 2) + "\n");
|
|
1059
|
+
return EXIT.ok;
|
|
1060
|
+
}
|
|
1061
|
+
process.stderr.write(`${res.product}@${res.version} ${res.heading_path.join(" \u203A ")}
|
|
1062
|
+
${res.source_url}
|
|
1063
|
+
`);
|
|
1064
|
+
process.stdout.write(res.text + "\n");
|
|
885
1065
|
return EXIT.ok;
|
|
886
1066
|
}
|
|
887
1067
|
case "report": {
|
|
888
|
-
const usage = 'Usage: grimoire report <
|
|
889
|
-
const
|
|
1068
|
+
const usage = 'Usage: grimoire report <point_id> --verdict helpful|incorrect|outdated [--note "..."]';
|
|
1069
|
+
const pointId = requirePositional(args, 0, usage);
|
|
890
1070
|
const verdict = requireFlagOneOf(args, "verdict", ["helpful", "incorrect", "outdated"], usage);
|
|
891
|
-
await client.reportResult(
|
|
1071
|
+
await client.reportResult(pointId, verdict, args.flags.note?.[0]);
|
|
892
1072
|
process.stdout.write("Reported.\n");
|
|
893
1073
|
return EXIT.ok;
|
|
894
1074
|
}
|
|
895
1075
|
case "ingest": {
|
|
896
|
-
const url = requirePositional(
|
|
897
|
-
|
|
898
|
-
0,
|
|
899
|
-
"Usage: grimoire ingest <url> [--version 15.2] [--private] [--webhook URL] [--watch]"
|
|
900
|
-
);
|
|
901
|
-
const res = await client.submitSource({
|
|
902
|
-
url,
|
|
903
|
-
version: args.flags.version?.[0],
|
|
904
|
-
visibility: args.bools.has("private") ? "private" : "public",
|
|
905
|
-
webhook_url: args.flags.webhook?.[0]
|
|
906
|
-
});
|
|
1076
|
+
const url = requirePositional(args, 0, "Usage: grimoire ingest <url> --product <name> --rolling|--fixed|--npm|--pypi|--github");
|
|
1077
|
+
const res = await client.submitSource(submissionFromArgs(url, args));
|
|
907
1078
|
process.stdout.write(`Job: ${res.job_id}
|
|
908
1079
|
`);
|
|
909
1080
|
if (args.bools.has("watch")) return watchJob(client, res.job_id);
|
|
@@ -913,6 +1084,54 @@ async function main(argv) {
|
|
|
913
1084
|
const jobId = requirePositional(args, 0, "Usage: grimoire jobs <job_id> [--watch]");
|
|
914
1085
|
return args.bools.has("watch") ? watchJob(client, jobId) : printJob(client, jobId);
|
|
915
1086
|
}
|
|
1087
|
+
case "staff": {
|
|
1088
|
+
const usage = 'Usage: grimoire staff queue [--json] | approve <job_id> | reject <job_id> --reason "..." | users [--json] | grant <subject> --name "..." | revoke <subject>';
|
|
1089
|
+
const [action, jobId] = args.positionals;
|
|
1090
|
+
if (action === "users") {
|
|
1091
|
+
const users = await client.listUsers();
|
|
1092
|
+
if (json) process.stdout.write(JSON.stringify(users, null, 2) + "\n");
|
|
1093
|
+
else for (const u of users) process.stdout.write(`${u.subject} ${u.status} ${u.name} by ${u.granted_by}
|
|
1094
|
+
`);
|
|
1095
|
+
return EXIT.ok;
|
|
1096
|
+
}
|
|
1097
|
+
if (action === "grant") {
|
|
1098
|
+
const name = args.flags.name?.[0];
|
|
1099
|
+
if (!jobId || !name) throw new UsageError(usage);
|
|
1100
|
+
const granted = await client.grantUser(jobId, name);
|
|
1101
|
+
process.stdout.write(`${granted.subject} ${granted.status} ${granted.name}
|
|
1102
|
+
`);
|
|
1103
|
+
return EXIT.ok;
|
|
1104
|
+
}
|
|
1105
|
+
if (action === "revoke") {
|
|
1106
|
+
if (!jobId) throw new UsageError(usage);
|
|
1107
|
+
const revoked = await client.revokeUser(jobId);
|
|
1108
|
+
process.stdout.write(`${revoked.subject} ${revoked.status}
|
|
1109
|
+
`);
|
|
1110
|
+
return EXIT.ok;
|
|
1111
|
+
}
|
|
1112
|
+
if (action === "queue") {
|
|
1113
|
+
const queue = await client.reviewQueue();
|
|
1114
|
+
if (json) process.stdout.write(JSON.stringify(queue, null, 2) + "\n");
|
|
1115
|
+
else if (queue.length === 0) process.stdout.write("review queue is empty\n");
|
|
1116
|
+
else for (const job of queue) process.stdout.write(`${job.id} ${describeJob(job)}
|
|
1117
|
+
`);
|
|
1118
|
+
return EXIT.ok;
|
|
1119
|
+
}
|
|
1120
|
+
if (!jobId) throw new UsageError(usage);
|
|
1121
|
+
if (action === "approve") {
|
|
1122
|
+
process.stdout.write(`${describeJob(await client.approveJob(jobId))}
|
|
1123
|
+
`);
|
|
1124
|
+
return EXIT.ok;
|
|
1125
|
+
}
|
|
1126
|
+
if (action === "reject") {
|
|
1127
|
+
const reason = args.flags.reason?.[0];
|
|
1128
|
+
if (!reason) throw new UsageError(usage);
|
|
1129
|
+
process.stdout.write(`${describeJob(await client.rejectJob(jobId, reason))}
|
|
1130
|
+
`);
|
|
1131
|
+
return EXIT.ok;
|
|
1132
|
+
}
|
|
1133
|
+
throw new UsageError(usage);
|
|
1134
|
+
}
|
|
916
1135
|
default:
|
|
917
1136
|
process.stderr.write(`Unknown command: ${command}
|
|
918
1137
|
${HELP}`);
|
|
@@ -920,14 +1139,10 @@ ${HELP}`);
|
|
|
920
1139
|
}
|
|
921
1140
|
} catch (err) {
|
|
922
1141
|
if (err instanceof ApiError) {
|
|
923
|
-
|
|
924
|
-
process.stderr.write(`error: ${err.code}${body.message ? ` \u2014 ${body.message}` : ""}
|
|
925
|
-
`);
|
|
926
|
-
if (body.available?.length) process.stderr.write(`available: ${body.available.join(", ")}
|
|
927
|
-
`);
|
|
928
|
-
if (body.did_you_mean?.length) process.stderr.write(`did you mean: ${body.did_you_mean.join(", ")}
|
|
1142
|
+
process.stderr.write(`${describeError(err)}
|
|
929
1143
|
`);
|
|
930
1144
|
if (err.status === 401) return EXIT.authRequired;
|
|
1145
|
+
if (err.status === 403) return EXIT.authRequired;
|
|
931
1146
|
if (err.status === 429) return EXIT.quota;
|
|
932
1147
|
if (err.status === 404) return EXIT.notFound;
|
|
933
1148
|
return EXIT.apiError;
|
|
@@ -935,14 +1150,13 @@ ${HELP}`);
|
|
|
935
1150
|
throw err;
|
|
936
1151
|
}
|
|
937
1152
|
}
|
|
938
|
-
var
|
|
939
|
-
var
|
|
1153
|
+
var TERMINAL_STATES = ["accepted", "done", "rejected", "failed", "pending_review"];
|
|
1154
|
+
var FAILED_STATES = ["rejected", "failed"];
|
|
940
1155
|
async function printJob(client, jobId) {
|
|
941
1156
|
const job = await client.getJob(jobId);
|
|
942
|
-
|
|
943
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1157
|
+
process.stdout.write(`${describeJob(job)}
|
|
944
1158
|
`);
|
|
945
|
-
return
|
|
1159
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
946
1160
|
}
|
|
947
1161
|
var WATCH_POLL_MS = 5e3;
|
|
948
1162
|
var WATCH_BACKOFF_CAP_MS = 3e4;
|
|
@@ -956,11 +1170,13 @@ async function watchJob(client, jobId) {
|
|
|
956
1170
|
while (Date.now() < deadline) {
|
|
957
1171
|
try {
|
|
958
1172
|
const job = await client.getJob(jobId);
|
|
959
|
-
|
|
960
|
-
process.stdout.write(`${status} ${JSON.stringify(job.counters ?? {})}
|
|
1173
|
+
process.stdout.write(`${describeJob(job)}
|
|
961
1174
|
`);
|
|
962
|
-
if (
|
|
963
|
-
|
|
1175
|
+
if (TERMINAL_STATES.includes(job.state)) {
|
|
1176
|
+
if (job.state === "pending_review") {
|
|
1177
|
+
process.stderr.write("Parked for staff review: grimoire staff queue\n");
|
|
1178
|
+
}
|
|
1179
|
+
return FAILED_STATES.includes(job.state) ? EXIT.apiError : EXIT.ok;
|
|
964
1180
|
}
|
|
965
1181
|
backoff = WATCH_POLL_MS;
|
|
966
1182
|
await sleep(WATCH_POLL_MS);
|