airgen-cli 0.1.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/dist/client.d.ts +51 -0
- package/dist/client.js +263 -0
- package/dist/commands/activity.d.ts +3 -0
- package/dist/commands/activity.js +33 -0
- package/dist/commands/ai.d.ts +3 -0
- package/dist/commands/ai.js +86 -0
- package/dist/commands/baselines.d.ts +3 -0
- package/dist/commands/baselines.js +55 -0
- package/dist/commands/diagrams.d.ts +3 -0
- package/dist/commands/diagrams.js +196 -0
- package/dist/commands/documents.d.ts +3 -0
- package/dist/commands/documents.js +123 -0
- package/dist/commands/implementation.d.ts +3 -0
- package/dist/commands/implementation.js +76 -0
- package/dist/commands/import-export.d.ts +3 -0
- package/dist/commands/import-export.js +66 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +56 -0
- package/dist/commands/quality.d.ts +3 -0
- package/dist/commands/quality.js +61 -0
- package/dist/commands/reports.d.ts +3 -0
- package/dist/commands/reports.js +114 -0
- package/dist/commands/requirements.d.ts +3 -0
- package/dist/commands/requirements.js +231 -0
- package/dist/commands/tenants.d.ts +3 -0
- package/dist/commands/tenants.js +17 -0
- package/dist/commands/traceability.d.ts +3 -0
- package/dist/commands/traceability.js +106 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.js +30 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +75 -0
- package/dist/output.d.ts +13 -0
- package/dist/output.js +46 -0
- package/package.json +39 -0
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIRGen HTTP API client with JWT authentication.
|
|
3
|
+
*
|
|
4
|
+
* Handles login, token caching, and automatic refresh.
|
|
5
|
+
*/
|
|
6
|
+
export interface ClientConfig {
|
|
7
|
+
apiUrl: string;
|
|
8
|
+
email: string;
|
|
9
|
+
password: string;
|
|
10
|
+
}
|
|
11
|
+
export declare class AirgenClient {
|
|
12
|
+
private config;
|
|
13
|
+
private auth;
|
|
14
|
+
private loginPromise;
|
|
15
|
+
constructor(config: ClientConfig);
|
|
16
|
+
/** The base API URL this client is configured for. */
|
|
17
|
+
get apiUrl(): string;
|
|
18
|
+
get<T = unknown>(path: string, query?: Record<string, string | number | undefined>): Promise<T>;
|
|
19
|
+
post<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
20
|
+
patch<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
21
|
+
delete<T = unknown>(path: string, body?: unknown): Promise<T>;
|
|
22
|
+
/** POST multipart/form-data (for file uploads). */
|
|
23
|
+
postMultipart<T = unknown>(path: string, fields: Record<string, string>, file: {
|
|
24
|
+
fieldName?: string;
|
|
25
|
+
filename: string;
|
|
26
|
+
contentType: string;
|
|
27
|
+
data: Buffer;
|
|
28
|
+
}): Promise<T>;
|
|
29
|
+
/** Fetch binary content from an API path (e.g. document file download). */
|
|
30
|
+
fetchBinary(path: string): Promise<{
|
|
31
|
+
data: Buffer;
|
|
32
|
+
contentType: string;
|
|
33
|
+
}>;
|
|
34
|
+
/** Fetch a static file from the server root (not the /api prefix). */
|
|
35
|
+
fetchStaticFile(relativePath: string): Promise<{
|
|
36
|
+
data: Buffer;
|
|
37
|
+
contentType: string;
|
|
38
|
+
}>;
|
|
39
|
+
private request;
|
|
40
|
+
private fetch;
|
|
41
|
+
private handleResponse;
|
|
42
|
+
private ensureAuth;
|
|
43
|
+
private login;
|
|
44
|
+
private refresh;
|
|
45
|
+
}
|
|
46
|
+
export declare class AirgenApiError extends Error {
|
|
47
|
+
statusCode: number;
|
|
48
|
+
apiMessage: string;
|
|
49
|
+
endpoint: string;
|
|
50
|
+
constructor(statusCode: number, apiMessage: string, endpoint: string);
|
|
51
|
+
}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AIRGen HTTP API client with JWT authentication.
|
|
3
|
+
*
|
|
4
|
+
* Handles login, token caching, and automatic refresh.
|
|
5
|
+
*/
|
|
6
|
+
export class AirgenClient {
|
|
7
|
+
config;
|
|
8
|
+
auth = {
|
|
9
|
+
accessToken: null,
|
|
10
|
+
refreshToken: null,
|
|
11
|
+
tokenExpiresAt: 0,
|
|
12
|
+
};
|
|
13
|
+
loginPromise = null;
|
|
14
|
+
constructor(config) {
|
|
15
|
+
this.config = config;
|
|
16
|
+
}
|
|
17
|
+
/** The base API URL this client is configured for. */
|
|
18
|
+
get apiUrl() {
|
|
19
|
+
return this.config.apiUrl;
|
|
20
|
+
}
|
|
21
|
+
// ── HTTP methods ──────────────────────────────────────────────
|
|
22
|
+
async get(path, query) {
|
|
23
|
+
const qs = buildQueryString(query);
|
|
24
|
+
return this.request("GET", `${path}${qs}`);
|
|
25
|
+
}
|
|
26
|
+
async post(path, body) {
|
|
27
|
+
return this.request("POST", path, body);
|
|
28
|
+
}
|
|
29
|
+
async patch(path, body) {
|
|
30
|
+
return this.request("PATCH", path, body);
|
|
31
|
+
}
|
|
32
|
+
async delete(path, body) {
|
|
33
|
+
return this.request("DELETE", path, body);
|
|
34
|
+
}
|
|
35
|
+
/** POST multipart/form-data (for file uploads). */
|
|
36
|
+
async postMultipart(path, fields, file) {
|
|
37
|
+
await this.ensureAuth();
|
|
38
|
+
const boundary = `----AirgenMCP${Date.now()}${Math.random().toString(36).slice(2)}`;
|
|
39
|
+
const parts = [];
|
|
40
|
+
for (const [key, value] of Object.entries(fields)) {
|
|
41
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${key}"\r\n\r\n${value}\r\n`));
|
|
42
|
+
}
|
|
43
|
+
const fieldName = file.fieldName ?? "file";
|
|
44
|
+
parts.push(Buffer.from(`--${boundary}\r\nContent-Disposition: form-data; name="${fieldName}"; filename="${file.filename}"\r\nContent-Type: ${file.contentType}\r\n\r\n`));
|
|
45
|
+
parts.push(file.data);
|
|
46
|
+
parts.push(Buffer.from(`\r\n--${boundary}--\r\n`));
|
|
47
|
+
const body = Buffer.concat(parts);
|
|
48
|
+
const headers = {
|
|
49
|
+
"Content-Type": `multipart/form-data; boundary=${boundary}`,
|
|
50
|
+
"Content-Length": String(body.byteLength),
|
|
51
|
+
};
|
|
52
|
+
if (this.auth.accessToken) {
|
|
53
|
+
headers["Authorization"] = `Bearer ${this.auth.accessToken}`;
|
|
54
|
+
}
|
|
55
|
+
if (this.auth.refreshToken) {
|
|
56
|
+
headers["Cookie"] = `airgen_refresh=${this.auth.refreshToken}`;
|
|
57
|
+
}
|
|
58
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
59
|
+
const res = await globalThis.fetch(url, { method: "POST", headers, body });
|
|
60
|
+
return this.handleResponse(res, path);
|
|
61
|
+
}
|
|
62
|
+
/** Fetch binary content from an API path (e.g. document file download). */
|
|
63
|
+
async fetchBinary(path) {
|
|
64
|
+
await this.ensureAuth();
|
|
65
|
+
const res = await this.fetch("GET", path);
|
|
66
|
+
if (!res.ok) {
|
|
67
|
+
const text = await res.text();
|
|
68
|
+
const msg = text || `Request failed with status ${res.status}`;
|
|
69
|
+
throw new AirgenApiError(res.status, msg, path);
|
|
70
|
+
}
|
|
71
|
+
const contentType = res.headers.get("content-type") || "application/octet-stream";
|
|
72
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
73
|
+
return { data: buffer, contentType };
|
|
74
|
+
}
|
|
75
|
+
/** Fetch a static file from the server root (not the /api prefix). */
|
|
76
|
+
async fetchStaticFile(relativePath) {
|
|
77
|
+
const baseUrl = this.config.apiUrl.replace(/\/api\/?$/, "");
|
|
78
|
+
const url = `${baseUrl}${relativePath}`;
|
|
79
|
+
const res = await globalThis.fetch(url);
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
throw new AirgenApiError(res.status, `Failed to fetch ${relativePath}`, relativePath);
|
|
82
|
+
}
|
|
83
|
+
const contentType = res.headers.get("content-type") || "application/octet-stream";
|
|
84
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
85
|
+
return { data: buffer, contentType };
|
|
86
|
+
}
|
|
87
|
+
// ── Internal ──────────────────────────────────────────────────
|
|
88
|
+
async request(method, path, body) {
|
|
89
|
+
await this.ensureAuth();
|
|
90
|
+
const res = await this.fetch(method, path, body);
|
|
91
|
+
// If 401, try refreshing once then retry
|
|
92
|
+
if (res.status === 401 && this.auth.refreshToken) {
|
|
93
|
+
await this.refresh();
|
|
94
|
+
const retry = await this.fetch(method, path, body);
|
|
95
|
+
return this.handleResponse(retry, path);
|
|
96
|
+
}
|
|
97
|
+
return this.handleResponse(res, path);
|
|
98
|
+
}
|
|
99
|
+
async fetch(method, path, body) {
|
|
100
|
+
const headers = {};
|
|
101
|
+
if (this.auth.accessToken) {
|
|
102
|
+
headers["Authorization"] = `Bearer ${this.auth.accessToken}`;
|
|
103
|
+
}
|
|
104
|
+
if (body !== undefined) {
|
|
105
|
+
headers["Content-Type"] = "application/json";
|
|
106
|
+
}
|
|
107
|
+
if (this.auth.refreshToken) {
|
|
108
|
+
headers["Cookie"] = `airgen_refresh=${this.auth.refreshToken}`;
|
|
109
|
+
}
|
|
110
|
+
const url = `${this.config.apiUrl}${path}`;
|
|
111
|
+
try {
|
|
112
|
+
return await globalThis.fetch(url, {
|
|
113
|
+
method,
|
|
114
|
+
headers,
|
|
115
|
+
body: body !== undefined ? JSON.stringify(body) : undefined,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const e = err;
|
|
120
|
+
const cause = e.cause ? ` cause=${e.cause.message ?? e.cause}` : "";
|
|
121
|
+
console.error(`[fetch error] ${method} ${url}: ${e.message}${cause}`);
|
|
122
|
+
throw new Error(`API request failed: ${method} ${path} — ${e.message}`, { cause: err });
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
async handleResponse(res, path) {
|
|
126
|
+
if (res.status === 204) {
|
|
127
|
+
return undefined;
|
|
128
|
+
}
|
|
129
|
+
const text = await res.text();
|
|
130
|
+
let parsed;
|
|
131
|
+
try {
|
|
132
|
+
parsed = JSON.parse(text);
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
parsed = text;
|
|
136
|
+
}
|
|
137
|
+
if (!res.ok) {
|
|
138
|
+
const msg = typeof parsed === "object" && parsed !== null && "error" in parsed
|
|
139
|
+
? parsed.error
|
|
140
|
+
: typeof parsed === "string"
|
|
141
|
+
? parsed
|
|
142
|
+
: `Request failed with status ${res.status}`;
|
|
143
|
+
throw new AirgenApiError(res.status, msg, path);
|
|
144
|
+
}
|
|
145
|
+
return parsed;
|
|
146
|
+
}
|
|
147
|
+
// ── Auth ──────────────────────────────────────────────────────
|
|
148
|
+
async ensureAuth() {
|
|
149
|
+
// Already have a valid token (with 2min buffer)
|
|
150
|
+
if (this.auth.accessToken && Date.now() < this.auth.tokenExpiresAt - 120_000) {
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
// Token exists but is expiring — try refresh
|
|
154
|
+
if (this.auth.refreshToken) {
|
|
155
|
+
await this.refresh();
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
// No token at all — login
|
|
159
|
+
// Deduplicate concurrent login attempts
|
|
160
|
+
if (!this.loginPromise) {
|
|
161
|
+
this.loginPromise = this.login().finally(() => {
|
|
162
|
+
this.loginPromise = null;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
await this.loginPromise;
|
|
166
|
+
}
|
|
167
|
+
async login() {
|
|
168
|
+
const res = await globalThis.fetch(`${this.config.apiUrl}/auth/login`, {
|
|
169
|
+
method: "POST",
|
|
170
|
+
headers: { "Content-Type": "application/json" },
|
|
171
|
+
body: JSON.stringify({
|
|
172
|
+
email: this.config.email,
|
|
173
|
+
password: this.config.password,
|
|
174
|
+
}),
|
|
175
|
+
});
|
|
176
|
+
if (!res.ok) {
|
|
177
|
+
const text = await res.text();
|
|
178
|
+
let msg = `Login failed (${res.status})`;
|
|
179
|
+
try {
|
|
180
|
+
const json = JSON.parse(text);
|
|
181
|
+
msg = json.error ?? msg;
|
|
182
|
+
}
|
|
183
|
+
catch { /* keep default */ }
|
|
184
|
+
throw new AirgenApiError(res.status, msg, "/auth/login");
|
|
185
|
+
}
|
|
186
|
+
const data = await res.json();
|
|
187
|
+
if ("status" in data && data.status === "MFA_REQUIRED") {
|
|
188
|
+
throw new Error("AIRGen account has MFA enabled. MFA is not supported in headless MCP mode. " +
|
|
189
|
+
"Please disable MFA for the MCP service account, or use a dedicated account without MFA.");
|
|
190
|
+
}
|
|
191
|
+
if (!("token" in data)) {
|
|
192
|
+
throw new Error("Unexpected login response from AIRGen API");
|
|
193
|
+
}
|
|
194
|
+
this.auth.accessToken = data.token;
|
|
195
|
+
this.auth.tokenExpiresAt = decodeTokenExpiry(data.token);
|
|
196
|
+
this.auth.refreshToken = extractRefreshToken(res);
|
|
197
|
+
}
|
|
198
|
+
async refresh() {
|
|
199
|
+
const headers = {};
|
|
200
|
+
if (this.auth.refreshToken) {
|
|
201
|
+
headers["Cookie"] = `airgen_refresh=${this.auth.refreshToken}`;
|
|
202
|
+
}
|
|
203
|
+
const res = await globalThis.fetch(`${this.config.apiUrl}/auth/refresh`, {
|
|
204
|
+
method: "POST",
|
|
205
|
+
headers,
|
|
206
|
+
});
|
|
207
|
+
if (!res.ok) {
|
|
208
|
+
// Refresh failed — clear state and re-login
|
|
209
|
+
this.auth = { accessToken: null, refreshToken: null, tokenExpiresAt: 0 };
|
|
210
|
+
await this.login();
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
const data = await res.json();
|
|
214
|
+
this.auth.accessToken = data.token;
|
|
215
|
+
this.auth.tokenExpiresAt = decodeTokenExpiry(data.token);
|
|
216
|
+
const newRefresh = extractRefreshToken(res);
|
|
217
|
+
if (newRefresh) {
|
|
218
|
+
this.auth.refreshToken = newRefresh;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
// ── Error class ───────────────────────────────────────────────
|
|
223
|
+
export class AirgenApiError extends Error {
|
|
224
|
+
statusCode;
|
|
225
|
+
apiMessage;
|
|
226
|
+
endpoint;
|
|
227
|
+
constructor(statusCode, apiMessage, endpoint) {
|
|
228
|
+
super(`AIRGen API error (${statusCode}) on ${endpoint}: ${apiMessage}`);
|
|
229
|
+
this.statusCode = statusCode;
|
|
230
|
+
this.apiMessage = apiMessage;
|
|
231
|
+
this.endpoint = endpoint;
|
|
232
|
+
this.name = "AirgenApiError";
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
// ── Helpers ───────────────────────────────────────────────────
|
|
236
|
+
function decodeTokenExpiry(jwt) {
|
|
237
|
+
try {
|
|
238
|
+
const payload = jwt.split(".")[1];
|
|
239
|
+
const decoded = JSON.parse(Buffer.from(payload, "base64url").toString());
|
|
240
|
+
return (decoded.exp ?? 0) * 1000; // Convert seconds to ms
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
// If we can't decode, assume 10 minutes from now
|
|
244
|
+
return Date.now() + 10 * 60 * 1000;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function extractRefreshToken(res) {
|
|
248
|
+
const setCookie = res.headers.get("set-cookie");
|
|
249
|
+
if (!setCookie)
|
|
250
|
+
return null;
|
|
251
|
+
// Parse airgen_refresh cookie value
|
|
252
|
+
const match = setCookie.match(/airgen_refresh=([^;]+)/);
|
|
253
|
+
return match ? match[1] : null;
|
|
254
|
+
}
|
|
255
|
+
function buildQueryString(params) {
|
|
256
|
+
if (!params)
|
|
257
|
+
return "";
|
|
258
|
+
const entries = Object.entries(params).filter((entry) => entry[1] !== undefined);
|
|
259
|
+
if (entries.length === 0)
|
|
260
|
+
return "";
|
|
261
|
+
const qs = new URLSearchParams(entries.map(([k, v]) => [k, String(v)]));
|
|
262
|
+
return `?${qs.toString()}`;
|
|
263
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode, truncate } from "../output.js";
|
|
2
|
+
export function registerActivityCommands(program, client) {
|
|
3
|
+
const cmd = program.command("activity").description("Project activity timeline");
|
|
4
|
+
cmd
|
|
5
|
+
.command("list")
|
|
6
|
+
.description("Get activity timeline for a project")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project>", "Project slug")
|
|
9
|
+
.option("-l, --limit <n>", "Max entries")
|
|
10
|
+
.option("--type <type>", "Activity type filter")
|
|
11
|
+
.option("--action <action>", "Action type filter")
|
|
12
|
+
.action(async (tenant, project, opts) => {
|
|
13
|
+
const data = await client.get("/activity", {
|
|
14
|
+
tenantSlug: tenant,
|
|
15
|
+
projectSlug: project,
|
|
16
|
+
limit: opts.limit,
|
|
17
|
+
activityTypes: opts.type,
|
|
18
|
+
actionTypes: opts.action,
|
|
19
|
+
});
|
|
20
|
+
const activities = data.activities ?? [];
|
|
21
|
+
if (isJsonMode()) {
|
|
22
|
+
output(activities);
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
printTable(["Time", "Type", "Action", "Description"], activities.map(a => [
|
|
26
|
+
a.createdAt ?? "",
|
|
27
|
+
a.type ?? "",
|
|
28
|
+
a.action ?? "",
|
|
29
|
+
truncate(a.description ?? "", 60),
|
|
30
|
+
]));
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode, truncate } from "../output.js";
|
|
2
|
+
export function registerAiCommands(program, client) {
|
|
3
|
+
const cmd = program.command("ai").description("AI generation and candidates");
|
|
4
|
+
cmd
|
|
5
|
+
.command("generate")
|
|
6
|
+
.description("Generate requirement candidates using AI")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project-key>", "Project key")
|
|
9
|
+
.requiredOption("--input <text>", "Natural language input")
|
|
10
|
+
.option("--glossary <text>", "Domain glossary")
|
|
11
|
+
.option("--constraints <text>", "Constraints")
|
|
12
|
+
.option("-n, --count <n>", "Number of candidates")
|
|
13
|
+
.action(async (tenant, projectKey, opts) => {
|
|
14
|
+
const data = await client.post("/airgen/chat", {
|
|
15
|
+
tenant,
|
|
16
|
+
projectKey,
|
|
17
|
+
user_input: opts.input,
|
|
18
|
+
glossary: opts.glossary,
|
|
19
|
+
constraints: opts.constraints,
|
|
20
|
+
n: opts.count ? parseInt(opts.count, 10) : undefined,
|
|
21
|
+
mode: "generate",
|
|
22
|
+
});
|
|
23
|
+
output(data);
|
|
24
|
+
});
|
|
25
|
+
cmd
|
|
26
|
+
.command("candidates")
|
|
27
|
+
.description("List pending AI-generated candidates")
|
|
28
|
+
.argument("<tenant>", "Tenant slug")
|
|
29
|
+
.argument("<project>", "Project slug")
|
|
30
|
+
.action(async (tenant, project) => {
|
|
31
|
+
const data = await client.get(`/airgen/candidates/${tenant}/${project}`);
|
|
32
|
+
const candidates = data.candidates ?? [];
|
|
33
|
+
if (isJsonMode()) {
|
|
34
|
+
output(candidates);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
printTable(["ID", "Text", "Status", "Created"], candidates.map(c => [
|
|
38
|
+
c.id,
|
|
39
|
+
truncate(c.text ?? "", 60),
|
|
40
|
+
c.status ?? "",
|
|
41
|
+
c.createdAt ?? "",
|
|
42
|
+
]));
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
cmd
|
|
46
|
+
.command("accept")
|
|
47
|
+
.description("Accept a candidate and promote to requirement")
|
|
48
|
+
.argument("<candidate-id>", "Candidate ID")
|
|
49
|
+
.requiredOption("--tenant <slug>", "Tenant slug")
|
|
50
|
+
.requiredOption("--project-key <key>", "Project key")
|
|
51
|
+
.option("--pattern <p>", "Pattern")
|
|
52
|
+
.option("--verification <v>", "Verification method")
|
|
53
|
+
.option("--document <slug>", "Document slug")
|
|
54
|
+
.option("--section <id>", "Section ID")
|
|
55
|
+
.option("--tags <tags>", "Comma-separated tags")
|
|
56
|
+
.action(async (candidateId, opts) => {
|
|
57
|
+
const data = await client.post(`/airgen/candidates/${candidateId}/accept`, {
|
|
58
|
+
tenant: opts.tenant,
|
|
59
|
+
projectKey: opts.projectKey,
|
|
60
|
+
pattern: opts.pattern,
|
|
61
|
+
verification: opts.verification,
|
|
62
|
+
documentSlug: opts.document,
|
|
63
|
+
sectionId: opts.section,
|
|
64
|
+
tags: opts.tags?.split(",").map(t => t.trim()),
|
|
65
|
+
});
|
|
66
|
+
if (isJsonMode()) {
|
|
67
|
+
output(data);
|
|
68
|
+
}
|
|
69
|
+
else {
|
|
70
|
+
console.log("Candidate accepted.");
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
cmd
|
|
74
|
+
.command("reject")
|
|
75
|
+
.description("Reject a candidate")
|
|
76
|
+
.argument("<candidate-id>", "Candidate ID")
|
|
77
|
+
.requiredOption("--tenant <slug>", "Tenant slug")
|
|
78
|
+
.requiredOption("--project-key <key>", "Project key")
|
|
79
|
+
.action(async (candidateId, opts) => {
|
|
80
|
+
await client.post(`/airgen/candidates/${candidateId}/reject`, {
|
|
81
|
+
tenant: opts.tenant,
|
|
82
|
+
projectKey: opts.projectKey,
|
|
83
|
+
});
|
|
84
|
+
console.log("Candidate rejected.");
|
|
85
|
+
});
|
|
86
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { output, printTable, isJsonMode } from "../output.js";
|
|
2
|
+
export function registerBaselineCommands(program, client) {
|
|
3
|
+
const cmd = program.command("baselines").alias("bl").description("Baseline snapshots");
|
|
4
|
+
cmd
|
|
5
|
+
.command("list")
|
|
6
|
+
.description("List all baselines for a project")
|
|
7
|
+
.argument("<tenant>", "Tenant slug")
|
|
8
|
+
.argument("<project>", "Project slug")
|
|
9
|
+
.action(async (tenant, project) => {
|
|
10
|
+
const data = await client.get(`/baselines/${tenant}/${project}`);
|
|
11
|
+
const baselines = data.baselines ?? [];
|
|
12
|
+
if (isJsonMode()) {
|
|
13
|
+
output(baselines);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
printTable(["Ref", "Label", "Created", "Requirements"], baselines.map(b => [
|
|
17
|
+
b.ref ?? b.id,
|
|
18
|
+
b.label ?? "",
|
|
19
|
+
b.createdAt ?? "",
|
|
20
|
+
String(b.requirementCount ?? 0),
|
|
21
|
+
]));
|
|
22
|
+
}
|
|
23
|
+
});
|
|
24
|
+
cmd
|
|
25
|
+
.command("create")
|
|
26
|
+
.description("Create a new baseline snapshot")
|
|
27
|
+
.argument("<tenant>", "Tenant slug")
|
|
28
|
+
.argument("<project-key>", "Project key")
|
|
29
|
+
.option("--label <label>", "Baseline label")
|
|
30
|
+
.action(async (tenant, projectKey, opts) => {
|
|
31
|
+
const data = await client.post("/baseline", {
|
|
32
|
+
tenant,
|
|
33
|
+
projectKey,
|
|
34
|
+
label: opts.label,
|
|
35
|
+
});
|
|
36
|
+
if (isJsonMode()) {
|
|
37
|
+
output(data);
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
console.log("Baseline created.");
|
|
41
|
+
output(data);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
cmd
|
|
45
|
+
.command("compare")
|
|
46
|
+
.description("Compare two baselines")
|
|
47
|
+
.argument("<tenant>", "Tenant slug")
|
|
48
|
+
.argument("<project>", "Project slug")
|
|
49
|
+
.requiredOption("--from <ref>", "From baseline ref")
|
|
50
|
+
.requiredOption("--to <ref>", "To baseline ref")
|
|
51
|
+
.action(async (tenant, project, opts) => {
|
|
52
|
+
const data = await client.get(`/baselines/${tenant}/${project}/compare`, { from: opts.from, to: opts.to });
|
|
53
|
+
output(data);
|
|
54
|
+
});
|
|
55
|
+
}
|