relionhq 2.0.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.
Files changed (2) hide show
  1. package/dist/index.js +1057 -0
  2. package/package.json +24 -0
package/dist/index.js ADDED
@@ -0,0 +1,1057 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/commands/scan.ts
27
+ var path3 = __toESM(require("path"));
28
+
29
+ // src/config.ts
30
+ var fs = __toESM(require("fs"));
31
+ var path = __toESM(require("path"));
32
+ var os = __toESM(require("os"));
33
+ var GLOBAL_CONFIG_DIR = path.join(os.homedir(), ".relion");
34
+ var GLOBAL_CONFIG_PATH = path.join(GLOBAL_CONFIG_DIR, "config.json");
35
+ var DEFAULT_API_URL = "https://relion.dev";
36
+ function readGlobalConfig() {
37
+ try {
38
+ const raw = fs.readFileSync(GLOBAL_CONFIG_PATH, "utf8");
39
+ return JSON.parse(raw);
40
+ } catch {
41
+ return {};
42
+ }
43
+ }
44
+ function writeGlobalConfig(config) {
45
+ if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
46
+ fs.mkdirSync(GLOBAL_CONFIG_DIR, { recursive: true });
47
+ }
48
+ fs.writeFileSync(GLOBAL_CONFIG_PATH, JSON.stringify(config, null, 2), "utf8");
49
+ try {
50
+ fs.chmodSync(GLOBAL_CONFIG_PATH, 384);
51
+ } catch {
52
+ }
53
+ }
54
+ function readProjectConfig(cwd) {
55
+ const candidates = [
56
+ path.join(cwd, ".relionrc"),
57
+ path.join(cwd, ".relion", "config")
58
+ ];
59
+ for (const p of candidates) {
60
+ try {
61
+ const raw = fs.readFileSync(p, "utf8");
62
+ return JSON.parse(raw);
63
+ } catch {
64
+ }
65
+ }
66
+ return {};
67
+ }
68
+ function resolveConfig(flags, cwd = process.cwd()) {
69
+ const global = readGlobalConfig();
70
+ const project = readProjectConfig(cwd);
71
+ const token = flags.token ?? process.env.RELION_TOKEN ?? project.token ?? global.token ?? null;
72
+ const apiUrl = flags.apiUrl ?? process.env.RELION_API_URL ?? project.apiUrl ?? global.apiUrl ?? DEFAULT_API_URL;
73
+ const repoUrl = flags.repoUrl ?? process.env.RELION_REPO_URL ?? project.repoUrl ?? autoDetectRepoUrl() ?? null;
74
+ const commit = flags.commit ?? process.env.RELION_COMMIT ?? process.env.GITHUB_SHA ?? autoDetectCommit() ?? null;
75
+ const branch = flags.branch ?? process.env.RELION_BRANCH ?? process.env.GITHUB_REF_NAME ?? autoDetectBranch() ?? null;
76
+ return {
77
+ token,
78
+ apiUrl,
79
+ repoUrl,
80
+ commit,
81
+ branch,
82
+ workspace: flags.workspace ?? process.env.RELION_WORKSPACE ?? project.workspace ?? global.workspace ?? null,
83
+ email: global.email ?? null,
84
+ lastScanId: global.lastScanId ?? null,
85
+ lastDashboardUrl: global.lastDashboardUrl ?? null
86
+ };
87
+ }
88
+ function autoDetectRepoUrl() {
89
+ try {
90
+ const { execSync: execSync2 } = require("child_process");
91
+ const remote = execSync2("git remote get-url origin", { stdio: ["pipe", "pipe", "ignore"] }).toString().trim();
92
+ return remote.replace(/^git@github\.com:/, "https://github.com/").replace(/\.git$/, "");
93
+ } catch {
94
+ return null;
95
+ }
96
+ }
97
+ function autoDetectCommit() {
98
+ try {
99
+ const { execSync: execSync2 } = require("child_process");
100
+ return execSync2("git rev-parse HEAD", { stdio: ["pipe", "pipe", "ignore"] }).toString().trim();
101
+ } catch {
102
+ return null;
103
+ }
104
+ }
105
+ function autoDetectBranch() {
106
+ try {
107
+ const { execSync: execSync2 } = require("child_process");
108
+ return execSync2("git rev-parse --abbrev-ref HEAD", { stdio: ["pipe", "pipe", "ignore"] }).toString().trim();
109
+ } catch {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ // src/ui.ts
115
+ var NO_COLOR = !process.stdout.isTTY || process.env.NO_COLOR === "1" || process.env.RELION_NO_COLOR === "1";
116
+ var IS_CI = Boolean(process.env.CI);
117
+ var QUIET = process.env.RELION_QUIET === "1";
118
+ function c(code, text) {
119
+ if (NO_COLOR) return text;
120
+ return `\x1B[${code}m${text}\x1B[0m`;
121
+ }
122
+ var color = {
123
+ green: (t) => c("32", t),
124
+ red: (t) => c("31", t),
125
+ yellow: (t) => c("33", t),
126
+ cyan: (t) => c("36", t),
127
+ gray: (t) => c("90", t),
128
+ bold: (t) => c("1", t),
129
+ dim: (t) => c("2", t)
130
+ };
131
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
132
+ var Spinner = class {
133
+ constructor() {
134
+ this.frame = 0;
135
+ this.timer = null;
136
+ this.current = "";
137
+ }
138
+ start(msg) {
139
+ if (QUIET || IS_CI) return;
140
+ this.current = msg;
141
+ if (!NO_COLOR && process.stdout.isTTY) {
142
+ this.timer = setInterval(() => {
143
+ process.stdout.write(`\r${color.cyan(FRAMES[this.frame % FRAMES.length])} ${this.current} `);
144
+ this.frame++;
145
+ }, 80);
146
+ } else {
147
+ process.stdout.write(` ${msg}...
148
+ `);
149
+ }
150
+ }
151
+ update(msg) {
152
+ this.current = msg;
153
+ }
154
+ succeed(msg) {
155
+ this.stop();
156
+ if (!QUIET) console.log(`${color.green("\u2713")} ${msg}`);
157
+ }
158
+ fail(msg) {
159
+ this.stop();
160
+ console.error(`${color.red("\u2717")} ${msg}`);
161
+ }
162
+ warn(msg) {
163
+ this.stop();
164
+ if (!QUIET) console.log(`${color.yellow("\u26A0")} ${msg}`);
165
+ }
166
+ stop() {
167
+ if (this.timer) {
168
+ clearInterval(this.timer);
169
+ this.timer = null;
170
+ if (process.stdout.isTTY) process.stdout.write("\r\x1B[K");
171
+ }
172
+ }
173
+ };
174
+ function printReceipt(opts) {
175
+ if (QUIET) return;
176
+ const width = 60;
177
+ const line = "\u2500".repeat(width);
178
+ const pad = (label, value) => {
179
+ const gap = width - label.length - value.length - 2;
180
+ return `\u2502 ${label}${" ".repeat(Math.max(1, gap))}${value} \u2502`;
181
+ };
182
+ const gateLabel = opts.deployGateStatus === "clear" ? color.green("\u2713 CLEAR") : opts.deployGateStatus === "blocked" ? color.red("\u2717 BLOCKED") : color.yellow("\u26A0 PENDING REVIEW");
183
+ console.log("");
184
+ console.log(`\u250C${"\u2500".repeat(width + 2)}\u2510`);
185
+ console.log(`\u2502 ${color.bold(opts.dryRun ? "Relion Scan \u2014 Dry Run" : "Relion Scan Complete")}${" ".repeat(width - (opts.dryRun ? 20 : 19))} \u2502`);
186
+ console.log(`\u251C${line}\u2524`);
187
+ if (opts.scanId) console.log(pad("Scan ID:", color.dim(opts.scanId)));
188
+ if (opts.branch || opts.commit) {
189
+ const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join(" \xB7 Commit: ");
190
+ console.log(pad("Branch:", color.dim(meta)));
191
+ }
192
+ console.log(pad("Duration:", color.dim(`${(opts.durationMs / 1e3).toFixed(1)}s`)));
193
+ console.log(`\u251C${line}\u2524`);
194
+ console.log(pad("Files scanned:", String(opts.filesScanned)));
195
+ console.log(pad("Vendors detected:", String(opts.vendorsDetected)));
196
+ console.log(pad("API endpoints found:", String(opts.endpointsFound)));
197
+ console.log(pad("Data transmitted:", opts.dryRun ? "0 bytes (dry run)" : `${humanBytes(opts.bytesTransmitted)} (metadata only)`));
198
+ console.log(pad("Source code sent:", color.green("0 bytes (never)")));
199
+ if (opts.secretsRedacted > 0) {
200
+ console.log(pad("Secrets redacted:", color.yellow(String(opts.secretsRedacted))));
201
+ }
202
+ console.log(`\u251C${line}\u2524`);
203
+ console.log(pad("Deploy gate:", gateLabel));
204
+ if (opts.deployGateAlerts?.length) {
205
+ console.log(`\u2502${" ".repeat(width + 2)}\u2502`);
206
+ for (const alert of opts.deployGateAlerts.slice(0, 3)) {
207
+ const sev = alert.severity === "critical" ? color.red(`[${alert.severity.toUpperCase()}]`) : color.yellow(`[${alert.severity.toUpperCase()}]`);
208
+ const text = ` ${sev} ${alert.title}`.slice(0, width + 2);
209
+ console.log(`\u2502${text.padEnd(width + 2)}\u2502`);
210
+ }
211
+ }
212
+ if (opts.dashboardUrl && !opts.dryRun) {
213
+ console.log(`\u251C${line}\u2524`);
214
+ console.log(`\u2502 ${color.cyan("View on dashboard:")}${" ".repeat(width - 18)} \u2502`);
215
+ console.log(`\u2502 ${color.dim("\u2192")} ${opts.dashboardUrl.slice(0, width - 2).padEnd(width - 2)} \u2502`);
216
+ }
217
+ console.log(`\u2514${"\u2500".repeat(width + 2)}\u2518`);
218
+ console.log("");
219
+ }
220
+ function printPredeployReceipt(opts) {
221
+ if (QUIET) return;
222
+ const width = 60;
223
+ const line = "\u2500".repeat(width);
224
+ const pad = (label, value) => {
225
+ const stripped = value.replace(/\x1b\[[0-9;]*m/g, "");
226
+ const gap = width - label.length - stripped.length - 2;
227
+ return `\u2502 ${label}${" ".repeat(Math.max(1, gap))}${value} \u2502`;
228
+ };
229
+ const verdictLabel = opts.verdict === "safe" ? color.green("\u2713 SAFE") : opts.verdict === "caution" ? color.yellow("\u26A0 CAUTION") : opts.verdict === "high_risk" ? color.red("\u2717 HIGH RISK") : opts.verdict === "blocked" ? color.red("\u2717 BLOCKED") : color.dim("\u2014 OFFLINE");
230
+ console.log("");
231
+ console.log(`\u250C${"\u2500".repeat(width + 2)}\u2510`);
232
+ console.log(`\u2502 ${color.bold("Relion Pre-Deploy Check")}${" ".repeat(width - 22)} \u2502`);
233
+ console.log(`\u251C${line}\u2524`);
234
+ if (opts.branch || opts.commit) {
235
+ const meta = [opts.branch, opts.commit ? opts.commit.slice(0, 7) : ""].filter(Boolean).join(" \u2192 ");
236
+ console.log(pad("Branch:", color.dim(meta)));
237
+ }
238
+ if (opts.baseBranch) console.log(pad("Comparing against:", color.dim(opts.baseBranch)));
239
+ console.log(pad("Duration:", color.dim(`${(opts.durationMs / 1e3).toFixed(1)}s`)));
240
+ if (opts.offline) console.log(pad("Mode:", color.yellow("offline")));
241
+ console.log(`\u251C${line}\u2524`);
242
+ console.log(pad("Files in diff:", String(opts.filesChangedCount)));
243
+ console.log(pad("APIs involved:", String(opts.apisInvolvedCount)));
244
+ console.log(`\u251C${line}\u2524`);
245
+ console.log(pad("Verdict:", verdictLabel));
246
+ const nonSafe = opts.findings.filter((f) => f.riskLevel !== "safe" && f.riskLevel !== "info");
247
+ if (nonSafe.length > 0) {
248
+ console.log(`\u2502${" ".repeat(width + 2)}\u2502`);
249
+ for (const finding of nonSafe.slice(0, 5)) {
250
+ const icon = finding.riskLevel === "blocked" ? color.red("\u2717") : finding.riskLevel === "high_risk" ? color.red("!") : color.yellow("\u26A0");
251
+ const text = ` ${icon} ${finding.vendorName}: ${finding.description}`;
252
+ console.log(`\u2502${text.slice(0, width + 2).padEnd(width + 2)}\u2502`);
253
+ }
254
+ if (nonSafe.length > 5) {
255
+ console.log(`\u2502 ${color.dim(`...and ${nonSafe.length - 5} more findings`)}${" ".repeat(Math.max(0, width - 20 - String(nonSafe.length - 5).length))} \u2502`);
256
+ }
257
+ }
258
+ if (opts.dashboardUrl) {
259
+ console.log(`\u251C${line}\u2524`);
260
+ console.log(`\u2502 ${color.cyan("View on dashboard:")}${" ".repeat(width - 18)} \u2502`);
261
+ console.log(`\u2502 ${color.dim("\u2192")} ${opts.dashboardUrl.slice(0, width - 2).padEnd(width - 2)} \u2502`);
262
+ }
263
+ console.log(`\u2514${"\u2500".repeat(width + 2)}\u2518`);
264
+ console.log("");
265
+ }
266
+ function printError(msg, hint) {
267
+ console.error(`
268
+ ${color.red("\u2717")} ${color.bold(msg)}`);
269
+ if (hint) console.error(color.dim(` ${hint}`));
270
+ console.error("");
271
+ }
272
+ function printWarn(msg) {
273
+ if (!QUIET) console.warn(`${color.yellow("\u26A0")} ${msg}`);
274
+ }
275
+ function printInfo(msg) {
276
+ if (!QUIET) console.log(`${color.cyan("\u2139")} ${msg}`);
277
+ }
278
+ function humanBytes(bytes) {
279
+ if (bytes < 1024) return `${bytes} B`;
280
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
281
+ return `${(bytes / 1024 / 1024).toFixed(1)} MB`;
282
+ }
283
+
284
+ // src/engines/git.ts
285
+ var import_child_process = require("child_process");
286
+ function exec(cmd, cwd) {
287
+ try {
288
+ return (0, import_child_process.execSync)(cmd, { cwd, stdio: ["pipe", "pipe", "pipe"], timeout: 15e3 }).toString().trim();
289
+ } catch {
290
+ return "";
291
+ }
292
+ }
293
+ function getChangedFiles(root, mode, diffBase, commitSha) {
294
+ let out = "";
295
+ switch (mode) {
296
+ case "staged":
297
+ out = exec("git diff --cached --name-only", root);
298
+ break;
299
+ case "commit":
300
+ out = exec(`git diff-tree --no-commit-id -r --name-only ${commitSha ?? "HEAD"}`, root);
301
+ break;
302
+ case "diff":
303
+ out = exec(`git diff ${diffBase ?? "HEAD"} --name-only`, root);
304
+ break;
305
+ default:
306
+ out = exec("git diff HEAD --name-only", root);
307
+ if (!out) out = exec("git diff --cached --name-only", root);
308
+ }
309
+ return out ? out.split("\n").filter(Boolean) : [];
310
+ }
311
+ function gitMeta(root) {
312
+ const commit = exec("git rev-parse --short HEAD", root) || null;
313
+ const branch = exec("git rev-parse --abbrev-ref HEAD", root) || null;
314
+ const remote = exec("git remote get-url origin", root);
315
+ const repoUrl = remote ? remote.replace(/^git@github\.com:/, "https://github.com/").replace(/\.git$/, "") : null;
316
+ return { commit, branch, repoUrl };
317
+ }
318
+
319
+ // src/engines/detect.ts
320
+ var fs2 = __toESM(require("fs"));
321
+ var path2 = __toESM(require("path"));
322
+
323
+ // src/engines/vendors.ts
324
+ var VENDORS = [
325
+ { key: "stripe", name: "Stripe", packages: ["stripe", "@stripe/stripe-js", "@stripe/react-stripe-js"], envPrefixes: ["STRIPE_"], domains: ["api.stripe.com"] },
326
+ { key: "openai", name: "OpenAI", packages: ["openai", "@openai/openai"], envPrefixes: ["OPENAI_"], domains: ["api.openai.com"] },
327
+ { key: "anthropic", name: "Anthropic", packages: ["@anthropic-ai/sdk", "anthropic"], envPrefixes: ["ANTHROPIC_", "CLAUDE_"], domains: ["api.anthropic.com"] },
328
+ { key: "twilio", name: "Twilio", packages: ["twilio"], envPrefixes: ["TWILIO_"], domains: ["api.twilio.com"] },
329
+ { key: "sendgrid", name: "SendGrid", packages: ["@sendgrid/mail", "@sendgrid/client"], envPrefixes: ["SENDGRID_"], domains: ["api.sendgrid.com"] },
330
+ { key: "plaid", name: "Plaid", packages: ["plaid"], envPrefixes: ["PLAID_"], domains: ["production.plaid.com", "sandbox.plaid.com"] },
331
+ { key: "slack", name: "Slack", packages: ["@slack/web-api", "@slack/bolt"], envPrefixes: ["SLACK_"], domains: ["api.slack.com", "hooks.slack.com"] },
332
+ { key: "github", name: "GitHub", packages: ["@octokit/rest", "@octokit/core", "octokit", "@octokit/graphql"], envPrefixes: ["GITHUB_TOKEN", "GH_TOKEN"], domains: ["api.github.com"] },
333
+ { key: "shopify", name: "Shopify", packages: ["@shopify/shopify-api", "@shopify/app-bridge"], envPrefixes: ["SHOPIFY_"], domains: ["myshopify.com", "admin.shopify.com"] },
334
+ { key: "aws", name: "AWS", packages: ["aws-sdk", "@aws-sdk/client-s3", "@aws-sdk/client-dynamodb", "@aws-sdk/client-lambda", "@aws-sdk/client-sqs"], envPrefixes: ["AWS_"], domains: ["amazonaws.com"] },
335
+ { key: "google_cloud", name: "Google Cloud", packages: ["@google-cloud/storage", "@google-cloud/bigquery", "googleapis"], envPrefixes: ["GOOGLE_", "GCP_"], domains: ["googleapis.com"] },
336
+ { key: "datadog", name: "Datadog", packages: ["dd-trace", "datadog-lambda-js"], envPrefixes: ["DD_"], domains: ["api.datadoghq.com"] },
337
+ { key: "segment", name: "Segment", packages: ["@segment/analytics-node", "analytics-node"], envPrefixes: ["SEGMENT_"], domains: ["api.segment.io"] },
338
+ { key: "hubspot", name: "HubSpot", packages: ["@hubspot/api-client"], envPrefixes: ["HUBSPOT_", "HS_"], domains: ["api.hubapi.com"] },
339
+ { key: "salesforce", name: "Salesforce", packages: ["jsforce"], envPrefixes: ["SALESFORCE_", "SF_"], domains: ["salesforce.com"] },
340
+ { key: "pagerduty", name: "PagerDuty", packages: ["node-pagerduty", "@pagerduty/pdjs"], envPrefixes: ["PAGERDUTY_", "PD_"], domains: ["api.pagerduty.com"] },
341
+ { key: "resend", name: "Resend", packages: ["resend"], envPrefixes: ["RESEND_"], domains: ["api.resend.com"] },
342
+ { key: "clerk", name: "Clerk", packages: ["@clerk/nextjs", "@clerk/clerk-sdk-node", "@clerk/backend"], envPrefixes: ["CLERK_"], domains: ["api.clerk.dev"] },
343
+ { key: "supabase", name: "Supabase", packages: ["@supabase/supabase-js"], envPrefixes: ["SUPABASE_"], domains: ["supabase.co"] },
344
+ { key: "firebase", name: "Firebase", packages: ["firebase", "firebase-admin"], envPrefixes: ["FIREBASE_"], domains: ["firebaseapp.com"] },
345
+ { key: "linear", name: "Linear", packages: ["@linear/sdk"], envPrefixes: ["LINEAR_"], domains: ["api.linear.app"] },
346
+ { key: "notion", name: "Notion", packages: ["@notionhq/client"], envPrefixes: ["NOTION_"], domains: ["api.notion.com"] },
347
+ { key: "airtable", name: "Airtable", packages: ["airtable"], envPrefixes: ["AIRTABLE_"], domains: ["api.airtable.com"] },
348
+ { key: "vercel", name: "Vercel", packages: ["@vercel/edge", "@vercel/kv", "@vercel/blob"], envPrefixes: ["VERCEL_"], domains: ["api.vercel.com"] }
349
+ ];
350
+ var PACKAGE_TO_VENDOR = /* @__PURE__ */ new Map();
351
+ var PREFIX_TO_VENDOR = /* @__PURE__ */ new Map();
352
+ var DOMAIN_TO_VENDOR = /* @__PURE__ */ new Map();
353
+ for (const v of VENDORS) {
354
+ for (const pkg of v.packages) PACKAGE_TO_VENDOR.set(pkg, v);
355
+ for (const pfx of v.envPrefixes) PREFIX_TO_VENDOR.set(pfx, v);
356
+ for (const dom of v.domains) DOMAIN_TO_VENDOR.set(dom, v);
357
+ }
358
+
359
+ // src/engines/detect.ts
360
+ var MAX_FILE_BYTES = 256e3;
361
+ var TEXT_EXTS = /* @__PURE__ */ new Set([
362
+ ".ts",
363
+ ".tsx",
364
+ ".js",
365
+ ".jsx",
366
+ ".mjs",
367
+ ".cjs",
368
+ ".json",
369
+ ".env",
370
+ ".yaml",
371
+ ".yml",
372
+ ".toml",
373
+ ".py",
374
+ ".rb",
375
+ ".go",
376
+ ".java",
377
+ ".cs",
378
+ ".php",
379
+ ".rs",
380
+ ".swift",
381
+ ".kt",
382
+ ".scala"
383
+ ]);
384
+ function readSafe(filePath) {
385
+ try {
386
+ const stat = fs2.statSync(filePath);
387
+ if (!stat.isFile() || stat.size > MAX_FILE_BYTES) return null;
388
+ return fs2.readFileSync(filePath, "utf8");
389
+ } catch {
390
+ return null;
391
+ }
392
+ }
393
+ function matchPackageJson(content, detectedMap, filePath) {
394
+ let parsed;
395
+ try {
396
+ parsed = JSON.parse(content);
397
+ } catch {
398
+ return;
399
+ }
400
+ const deps = {
401
+ ...parsed.dependencies ?? {},
402
+ ...parsed.devDependencies ?? {}
403
+ };
404
+ for (const pkgName of Object.keys(deps)) {
405
+ const vendor = PACKAGE_TO_VENDOR.get(pkgName);
406
+ if (!vendor) continue;
407
+ upsert(detectedMap, vendor, "strong", `package.json dependency: ${pkgName}`, filePath);
408
+ }
409
+ }
410
+ function matchSourceFile(content, detectedMap, filePath) {
411
+ const importRe = /(?:from|require)\s*\(?['"`](@?[a-z0-9_\-./]+)['"`]\)?/g;
412
+ let m;
413
+ while ((m = importRe.exec(content)) !== null) {
414
+ const pkg = m[1];
415
+ const vendor = PACKAGE_TO_VENDOR.get(pkg) ?? PACKAGE_TO_VENDOR.get(pkg.split("/").slice(0, 2).join("/"));
416
+ if (vendor) upsert(detectedMap, vendor, "strong", `import: ${pkg}`, filePath);
417
+ }
418
+ const envRe = /\bprocess\.env\.([A-Z][A-Z0-9_]+)\b/g;
419
+ while ((m = envRe.exec(content)) !== null) {
420
+ const envKey = m[1];
421
+ for (const [prefix, vendor] of PREFIX_TO_VENDOR) {
422
+ if (envKey.startsWith(prefix)) {
423
+ upsert(detectedMap, vendor, "partial", `env var: ${envKey}`, filePath);
424
+ break;
425
+ }
426
+ }
427
+ }
428
+ const urlRe = /['"`](https?:\/\/([a-z0-9.\-]+)[^\s'"`]*)/g;
429
+ while ((m = urlRe.exec(content)) !== null) {
430
+ const host = m[2];
431
+ for (const [domain, vendor] of DOMAIN_TO_VENDOR) {
432
+ if (host.includes(domain)) {
433
+ upsert(detectedMap, vendor, "partial", `URL reference: ${host}`, filePath);
434
+ break;
435
+ }
436
+ }
437
+ }
438
+ }
439
+ function matchEnvFile(content, detectedMap, filePath) {
440
+ for (const line of content.split("\n")) {
441
+ const key = line.split("=")[0]?.trim().replace(/^export\s+/, "");
442
+ if (!key) continue;
443
+ for (const [prefix, vendor] of PREFIX_TO_VENDOR) {
444
+ if (key.startsWith(prefix)) {
445
+ upsert(detectedMap, vendor, "partial", `env key: ${key}`, filePath);
446
+ break;
447
+ }
448
+ }
449
+ }
450
+ }
451
+ function upsert(map, vendor, confidence, signal, filePath) {
452
+ const existing = map.get(vendor.key);
453
+ if (existing) {
454
+ if (confidence === "strong") existing.confidence = "strong";
455
+ else if (confidence === "partial" && existing.confidence === "weak") existing.confidence = "partial";
456
+ if (!existing.signals.includes(signal)) existing.signals.push(signal);
457
+ if (!existing.files.includes(filePath)) existing.files.push(filePath);
458
+ } else {
459
+ map.set(vendor.key, { key: vendor.key, name: vendor.name, confidence, signals: [signal], files: [filePath] });
460
+ }
461
+ }
462
+ function detectVendorsInFiles(root, relPaths) {
463
+ const detectedMap = /* @__PURE__ */ new Map();
464
+ for (const rel of relPaths) {
465
+ const absPath = path2.join(root, rel);
466
+ const ext = path2.extname(rel).toLowerCase();
467
+ const base = path2.basename(rel).toLowerCase();
468
+ if (!TEXT_EXTS.has(ext) && !base.startsWith(".env")) continue;
469
+ const content = readSafe(absPath);
470
+ if (!content) continue;
471
+ if (base === "package.json") {
472
+ matchPackageJson(content, detectedMap, rel);
473
+ } else if (base.startsWith(".env") || ext === ".env") {
474
+ matchEnvFile(content, detectedMap, rel);
475
+ } else {
476
+ matchSourceFile(content, detectedMap, rel);
477
+ }
478
+ }
479
+ return [...detectedMap.values()];
480
+ }
481
+ function detectVendorsInRoot(root) {
482
+ const pkgPath = path2.join(root, "package.json");
483
+ const content = readSafe(pkgPath);
484
+ if (!content) return [];
485
+ const map = /* @__PURE__ */ new Map();
486
+ matchPackageJson(content, map, "package.json");
487
+ return [...map.values()];
488
+ }
489
+
490
+ // src/commands/scan.ts
491
+ async function scanCommand(targetPath, flags) {
492
+ const root = targetPath ? path3.resolve(targetPath) : process.cwd();
493
+ const startedAt = Date.now();
494
+ const config = resolveConfig({ token: flags.token, apiUrl: flags.url, repoUrl: flags.repoUrl, commit: flags.commit, branch: flags.branch }, root);
495
+ if (!config.token && !flags.dryRun) {
496
+ printError("No API token found.", "Set RELION_TOKEN env var or run: relion login --token <token>");
497
+ process.exit(1);
498
+ }
499
+ const meta = gitMeta(root);
500
+ const branch = flags.branch ?? config.branch ?? meta.branch ?? void 0;
501
+ const commit = flags.commit ?? config.commit ?? meta.commit ?? void 0;
502
+ const repoUrl = flags.repoUrl ?? config.repoUrl ?? meta.repoUrl ?? void 0;
503
+ if (flags.output !== "json") console.log(`
504
+ Relion v2.0.0${repoUrl ? ` \xB7 ${repoUrl.replace("https://github.com/", "")}` : ""}
505
+ `);
506
+ const spinner = new Spinner();
507
+ try {
508
+ spinner.start("Detecting API vendors");
509
+ if (!flags.dryRun && config.token) {
510
+ await verifyToken(config.token, config.apiUrl);
511
+ }
512
+ spinner.succeed("Authenticated");
513
+ spinner.start("Scanning for API dependencies");
514
+ const detected = detectVendorsInRoot(root);
515
+ spinner.succeed(`${detected.length} vendor API${detected.length === 1 ? "" : "s"} detected`);
516
+ const durationMs = Date.now() - startedAt;
517
+ if (flags.dryRun || !config.token) {
518
+ if (flags.output !== "json") {
519
+ console.log("\nDry run \u2014 results not uploaded:\n");
520
+ for (const v of detected) {
521
+ console.log(` ${v.name} (${v.confidence}) \u2014 ${v.signals[0] ?? ""}`);
522
+ }
523
+ console.log("");
524
+ } else {
525
+ process.stdout.write(JSON.stringify({ vendors: detected, dryRun: true }, null, 2) + "\n");
526
+ }
527
+ process.exit(detected.length === 0 ? 4 : 0);
528
+ }
529
+ spinner.start("Uploading API metadata");
530
+ const payload = {
531
+ schemaVersion: "2",
532
+ idempotencyKey: `${commit ?? "none"}-${Date.now()}`,
533
+ sessionId: `cli-${Date.now()}`,
534
+ agentVersion: "2.0.0",
535
+ cliVersion: "2.0.0",
536
+ repository: { repoUrl, commit, branch },
537
+ scanMeta: { startedAt: new Date(startedAt).toISOString(), completedAt: (/* @__PURE__ */ new Date()).toISOString(), durationMs, filesScanned: 0, bytesScanned: 0, triggerSource: "cli" },
538
+ stats: { filesScanned: 0, bytesScanned: 0, vendorsDetected: detected.length, endpointsFound: 0, specFilesFound: 0 },
539
+ integrations: detected.map((v) => ({
540
+ vendorKey: v.key,
541
+ vendorName: v.name,
542
+ integrationType: "api",
543
+ description: `Detected via ${v.signals[0] ?? "pattern matching"}`,
544
+ confidence: v.confidence === "strong" ? "strong" : v.confidence === "partial" ? "partial" : "weak",
545
+ confidenceScore: v.confidence === "strong" ? 0.9 : v.confidence === "partial" ? 0.6 : 0.3,
546
+ signals: v.signals.map((s) => ({ signalType: "package_dependency", filePath: v.files[0] ?? "unknown", lineStart: 0, signalValue: s, confidenceWeight: 0.8 })),
547
+ surfaces: []
548
+ }))
549
+ };
550
+ const res = await fetch(`${config.apiUrl.replace(/\/$/, "")}/api/ingest/scan/v2`, {
551
+ method: "POST",
552
+ headers: { Authorization: `Bearer ${config.token}`, "Content-Type": "application/json" },
553
+ body: JSON.stringify(payload)
554
+ });
555
+ if (!res.ok) {
556
+ const err = await res.json().catch(() => ({ error: "Upload failed" }));
557
+ throw new Error(err.error ?? `Upload failed (${res.status})`);
558
+ }
559
+ const receipt = await res.json();
560
+ spinner.succeed("Upload complete");
561
+ if (flags.output === "json") {
562
+ process.stdout.write(JSON.stringify(receipt, null, 2) + "\n");
563
+ } else {
564
+ printReceipt({
565
+ scanId: receipt.scanId,
566
+ branch,
567
+ commit,
568
+ durationMs,
569
+ filesScanned: 0,
570
+ vendorsDetected: detected.length,
571
+ endpointsFound: 0,
572
+ bytesTransmitted: JSON.stringify(payload).length,
573
+ secretsRedacted: 0,
574
+ deployGateStatus: receipt.deployGate?.status ?? "clear",
575
+ deployGateAlerts: receipt.deployGate?.alerts ?? [],
576
+ dashboardUrl: receipt.dashboardUrl,
577
+ dryRun: false
578
+ });
579
+ const global = readGlobalConfig();
580
+ writeGlobalConfig({ ...global, lastScanId: receipt.scanId, lastScanAt: (/* @__PURE__ */ new Date()).toISOString(), lastDashboardUrl: receipt.dashboardUrl });
581
+ }
582
+ if (receipt.deployGate?.status === "blocked") process.exit(2);
583
+ if (receipt.deployGate?.status === "pending") process.exit(3);
584
+ if (detected.length === 0) process.exit(4);
585
+ } catch (err) {
586
+ spinner.fail("Scan failed");
587
+ const msg = err instanceof Error ? err.message : String(err);
588
+ if (msg.includes("401") || msg.includes("Invalid or missing")) {
589
+ printError("Authentication failed.", "Set RELION_TOKEN env var.");
590
+ } else {
591
+ printError(msg);
592
+ }
593
+ if (flags.verbose) console.error(err);
594
+ process.exit(1);
595
+ }
596
+ }
597
+ async function verifyToken(token, apiUrl) {
598
+ const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/cli/whoami`, { headers: { Authorization: `Bearer ${token}` } });
599
+ if (res.status === 401 || res.status === 403) throw new Error("401 Invalid or missing API token");
600
+ }
601
+
602
+ // src/commands/login.ts
603
+ async function loginCommand(flags) {
604
+ const apiUrl = flags.url ?? process.env.RELION_API_URL ?? "https://relion.dev";
605
+ if (flags.token) {
606
+ await saveToken(flags.token, apiUrl);
607
+ return;
608
+ }
609
+ const loginUrl = `${apiUrl}/settings/tokens`;
610
+ console.log(`
611
+ ${color.bold("Relion Login")}
612
+ `);
613
+ console.log(`Create an API token at:
614
+ ${color.cyan(loginUrl)}
615
+ `);
616
+ console.log(`Then run:
617
+ ${color.dim("relion login --token <your-token>")}
618
+ `);
619
+ }
620
+ async function saveToken(token, apiUrl) {
621
+ console.log(`
622
+ ${color.bold("Relion Login")}
623
+ `);
624
+ printInfo("Verifying token...");
625
+ let email;
626
+ try {
627
+ const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/cli/whoami`, {
628
+ headers: { Authorization: `Bearer ${token}` }
629
+ });
630
+ if (res.status === 401 || res.status === 403) {
631
+ printError("Token is invalid or revoked.", "Create a new token at " + apiUrl + "/settings/tokens");
632
+ process.exit(1);
633
+ }
634
+ if (res.ok) {
635
+ const data = await res.json();
636
+ email = data.email;
637
+ }
638
+ } catch {
639
+ console.warn(color.yellow(" Could not verify token (network issue). Saving anyway."));
640
+ }
641
+ const existing = readGlobalConfig();
642
+ writeGlobalConfig({ ...existing, token, apiUrl: apiUrl !== "https://relion.dev" ? apiUrl : void 0, email });
643
+ console.log(`${color.green("\u2713")} ${color.bold("Authenticated")}${email ? ` as ${email}` : ""}`);
644
+ console.log(`${color.dim(" Token saved to ~/.relion/config.json")}
645
+ `);
646
+ console.log(`Run: ${color.cyan("relion scan .")}
647
+ `);
648
+ }
649
+ async function logoutCommand() {
650
+ writeGlobalConfig({});
651
+ console.log(`${color.green("\u2713")} Logged out. Credentials removed.
652
+ `);
653
+ }
654
+ async function whoamiCommand(flags) {
655
+ const config = readGlobalConfig();
656
+ const token = process.env.RELION_TOKEN ?? config.token;
657
+ const apiUrl = flags.url ?? config.apiUrl ?? "https://relion.dev";
658
+ if (!token) {
659
+ printError("Not logged in.", "Run: relion login");
660
+ process.exit(1);
661
+ }
662
+ try {
663
+ const res = await fetch(`${apiUrl.replace(/\/$/, "")}/api/cli/whoami`, {
664
+ headers: { Authorization: `Bearer ${token}` }
665
+ });
666
+ if (!res.ok) {
667
+ printError("Token is invalid or expired.", "Run: relion login");
668
+ process.exit(1);
669
+ }
670
+ const data = await res.json();
671
+ console.log(`
672
+ ${color.bold("Relion identity")}`);
673
+ if (data.email) console.log(` Email: ${data.email}`);
674
+ if (data.workspace) console.log(` Workspace: ${data.workspace}`);
675
+ console.log(` API: ${apiUrl}
676
+ `);
677
+ } catch {
678
+ printError("Could not reach Relion API.", `URL: ${apiUrl}`);
679
+ process.exit(1);
680
+ }
681
+ }
682
+
683
+ // src/commands/status.ts
684
+ async function statusCommand(flags) {
685
+ const config = readGlobalConfig();
686
+ const token = process.env.RELION_TOKEN ?? config.token;
687
+ const apiUrl = flags.url ?? config.apiUrl ?? "https://relion.dev";
688
+ if (!token) {
689
+ printError("Not logged in.", "Run: relion login");
690
+ process.exit(1);
691
+ }
692
+ if (!config.lastScanId) {
693
+ console.log(`
694
+ ${color.dim("No scans recorded yet.")}
695
+ `);
696
+ console.log(`Run: ${color.cyan("relion scan .")}
697
+ `);
698
+ return;
699
+ }
700
+ try {
701
+ const res = await fetch(
702
+ `${apiUrl.replace(/\/$/, "")}/api/cli/status/${config.lastScanId}`,
703
+ { headers: { Authorization: `Bearer ${token}` } }
704
+ );
705
+ if (!res.ok) {
706
+ console.log(`
707
+ ${color.dim(`Last scan: ${config.lastScanId}`)}`);
708
+ if (config.lastDashboardUrl) {
709
+ console.log(`Dashboard: ${color.cyan(config.lastDashboardUrl)}
710
+ `);
711
+ }
712
+ return;
713
+ }
714
+ const data = await res.json();
715
+ if (flags.json) {
716
+ process.stdout.write(JSON.stringify(data, null, 2) + "\n");
717
+ return;
718
+ }
719
+ const gateIcon = data.deployGate.status === "clear" ? color.green("\u2713 clear") : data.deployGate.status === "blocked" ? color.red("\u2717 blocked") : color.yellow("\u26A0 pending");
720
+ console.log(`
721
+ ${color.bold("Last scan")}`);
722
+ console.log(` Scan ID: ${color.dim(data.scanId)}`);
723
+ if (data.scannedAt) console.log(` Scanned: ${new Date(data.scannedAt).toLocaleString()}`);
724
+ console.log(` Findings: ${data.findingsCount} vendor APIs`);
725
+ console.log(` Gate: ${gateIcon}`);
726
+ if (data.dashboardUrl) console.log(` Dashboard: ${color.cyan(data.dashboardUrl)}`);
727
+ console.log("");
728
+ } catch {
729
+ console.log(`
730
+ ${color.bold("Last scan")} ${color.dim("(cached)")}`);
731
+ console.log(` Scan ID: ${color.dim(config.lastScanId)}`);
732
+ if (config.lastScanAt) console.log(` At: ${new Date(config.lastScanAt).toLocaleString()}`);
733
+ if (config.lastDashboardUrl) console.log(` Dashboard: ${color.cyan(config.lastDashboardUrl)}`);
734
+ console.log("");
735
+ }
736
+ }
737
+
738
+ // src/commands/predeploy.ts
739
+ var path4 = __toESM(require("path"));
740
+
741
+ // src/engines/api.ts
742
+ async function apiFetch(url, token, method = "GET", body) {
743
+ const opts = {
744
+ method,
745
+ headers: {
746
+ Authorization: `Bearer ${token}`,
747
+ "Content-Type": "application/json"
748
+ }
749
+ };
750
+ if (body !== void 0) opts.body = JSON.stringify(body);
751
+ return fetch(url, opts);
752
+ }
753
+ async function lookupRisk(vendorKeys, token, apiUrl) {
754
+ if (vendorKeys.length === 0) return [];
755
+ const qs = vendorKeys.map((k) => `vendorKeys=${encodeURIComponent(k)}`).join("&");
756
+ const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/risk?${qs}`;
757
+ try {
758
+ const res = await apiFetch(url, token);
759
+ if (!res.ok) return [];
760
+ const data = await res.json();
761
+ return data.findings ?? [];
762
+ } catch {
763
+ return [];
764
+ }
765
+ }
766
+ async function submitCheck(payload, token, apiUrl) {
767
+ const url = `${apiUrl.replace(/\/$/, "")}/api/predeploy/check`;
768
+ try {
769
+ const res = await apiFetch(url, token, "POST", payload);
770
+ if (!res.ok) return null;
771
+ return await res.json();
772
+ } catch {
773
+ return null;
774
+ }
775
+ }
776
+ function scoreVerdict(findings) {
777
+ if (findings.some((f) => f.riskLevel === "blocked")) return { verdict: "blocked", exitCode: 2 };
778
+ if (findings.some((f) => f.riskLevel === "high_risk")) return { verdict: "high_risk", exitCode: 3 };
779
+ if (findings.some((f) => f.riskLevel === "caution")) return { verdict: "caution", exitCode: 3 };
780
+ return { verdict: "safe", exitCode: 0 };
781
+ }
782
+
783
+ // src/commands/predeploy.ts
784
+ async function predeployCommand(targetPath, flags) {
785
+ const root = targetPath ? path4.resolve(targetPath) : process.cwd();
786
+ const startedAt = Date.now();
787
+ const config = resolveConfig({ token: flags.token, apiUrl: flags.url, repoUrl: flags.repoUrl, commit: flags.commit, branch: flags.branch }, root);
788
+ const offline = flags.offline ?? !config.token;
789
+ if (!config.token && !offline) {
790
+ printError("No API token found.", "Set RELION_TOKEN env var or run: relion login --token <token>\nUse --offline to run without cloud lookup.");
791
+ process.exit(1);
792
+ }
793
+ let scopeMode = "default";
794
+ let diffBase;
795
+ let commitSha;
796
+ if (flags.staged) {
797
+ scopeMode = "staged";
798
+ } else if (flags.diff) {
799
+ scopeMode = "diff";
800
+ diffBase = flags.diff;
801
+ } else if (flags.commitFlag) {
802
+ scopeMode = "commit";
803
+ commitSha = flags.commitFlag;
804
+ }
805
+ const scopeLabel = scopeMode === "staged" ? "staged changes" : scopeMode === "diff" ? `diff from ${diffBase ?? "HEAD"}` : scopeMode === "commit" ? `commit ${(commitSha ?? "HEAD").slice(0, 7)}` : "uncommitted changes";
806
+ if (!flags.json) console.log(`
807
+ Relion pre-deploy check \u2014 ${scopeLabel}
808
+ `);
809
+ const spinner = new Spinner();
810
+ try {
811
+ spinner.start("Resolving changed files");
812
+ const changedFiles = getChangedFiles(root, scopeMode, diffBase, commitSha);
813
+ if (changedFiles.length === 0) {
814
+ spinner.succeed("No changed files found");
815
+ if (!flags.json) console.log("\nNothing to check \u2014 no changed files detected.\n");
816
+ process.exit(0);
817
+ }
818
+ spinner.succeed(`${changedFiles.length} changed file${changedFiles.length === 1 ? "" : "s"}`);
819
+ spinner.start("Detecting API dependencies");
820
+ const detected = detectVendorsInFiles(root, changedFiles);
821
+ spinner.succeed(detected.length > 0 ? `${detected.length} API vendor${detected.length === 1 ? "" : "s"} detected` : "No vendor APIs detected in changed files");
822
+ let findings = [];
823
+ if (!offline && config.token && detected.length > 0) {
824
+ spinner.start("Checking risk against monitored APIs");
825
+ findings = await lookupRisk(detected.map((d) => d.key), config.token, config.apiUrl);
826
+ spinner.succeed(`Risk check complete \u2014 ${findings.filter((f) => f.riskLevel !== "safe").length} finding${findings.length === 1 ? "" : "s"}`);
827
+ } else if (offline) {
828
+ if (!flags.json) printWarn("Offline mode \u2014 cloud risk lookup skipped.");
829
+ }
830
+ const { verdict, exitCode } = offline ? { verdict: "offline", exitCode: 0 } : scoreVerdict(findings);
831
+ const durationMs = Date.now() - startedAt;
832
+ const meta = gitMeta(root);
833
+ const branch = flags.branch ?? config.branch ?? meta.branch ?? void 0;
834
+ const commit = flags.commit ?? config.commit ?? meta.commit ?? void 0;
835
+ const repoUrl = flags.repoUrl ?? config.repoUrl ?? meta.repoUrl ?? void 0;
836
+ let checkId;
837
+ let dashboardUrl;
838
+ if (!offline && config.token) {
839
+ spinner.start("Submitting check");
840
+ const receipt = await submitCheck(
841
+ {
842
+ verdict,
843
+ exitCode,
844
+ branch,
845
+ baseBranch: flags.baseBranch,
846
+ commit,
847
+ repoUrl,
848
+ scopeMode,
849
+ offline: false,
850
+ filesChangedCount: changedFiles.length,
851
+ apisInvolvedCount: detected.length,
852
+ changedFiles,
853
+ findings,
854
+ durationMs,
855
+ cliVersion: "2.0.0"
856
+ },
857
+ config.token,
858
+ config.apiUrl
859
+ );
860
+ if (receipt) {
861
+ checkId = receipt.checkId;
862
+ dashboardUrl = receipt.dashboardUrl;
863
+ spinner.succeed("Check recorded");
864
+ } else {
865
+ spinner.succeed("Check complete (could not reach dashboard)");
866
+ }
867
+ }
868
+ if (flags.json) {
869
+ process.stdout.write(JSON.stringify({ verdict, exitCode, branch, commit, filesChangedCount: changedFiles.length, apisInvolvedCount: detected.length, findings, durationMs, checkId, dashboardUrl }, null, 2) + "\n");
870
+ } else {
871
+ printPredeployReceipt({
872
+ verdict,
873
+ branch,
874
+ commit,
875
+ baseBranch: flags.baseBranch,
876
+ filesChangedCount: changedFiles.length,
877
+ apisInvolvedCount: detected.length,
878
+ durationMs,
879
+ offline,
880
+ findings,
881
+ dashboardUrl,
882
+ checkId
883
+ });
884
+ if (flags.verbose && findings.length > 0) {
885
+ console.log("Findings detail:");
886
+ for (const f of findings) {
887
+ if (f.riskLevel === "safe") continue;
888
+ const tag = f.riskLevel.toUpperCase().replace("_", " ");
889
+ console.log(` [${tag}] ${f.vendorName}: ${f.description}`);
890
+ if (f.recommendation) console.log(` \u2192 ${f.recommendation}`);
891
+ console.log("");
892
+ }
893
+ }
894
+ }
895
+ process.exit(exitCode);
896
+ } catch (err) {
897
+ spinner.fail("Pre-deploy check failed");
898
+ const msg = err instanceof Error ? err.message : String(err);
899
+ if (msg.includes("401") || msg.includes("Invalid or missing")) {
900
+ printError("Authentication failed.", "Set RELION_TOKEN env var.");
901
+ } else if (msg.includes("ECONNREFUSED") || msg.includes("fetch failed")) {
902
+ printError(`Could not reach ${config.apiUrl}`, "Use --offline to skip cloud lookup.");
903
+ } else {
904
+ printError(msg);
905
+ }
906
+ if (flags.verbose) console.error(err);
907
+ process.exit(1);
908
+ }
909
+ }
910
+
911
+ // src/index.ts
912
+ var VERSION = "2.0.0";
913
+ function parseArgs(argv) {
914
+ const args = argv.slice(2);
915
+ const positional = [];
916
+ const flags = {};
917
+ for (let i = 0; i < args.length; i++) {
918
+ const arg = args[i];
919
+ if (!arg.startsWith("-")) {
920
+ positional.push(arg);
921
+ continue;
922
+ }
923
+ const key = arg.replace(/^--?/, "");
924
+ const next = args[i + 1];
925
+ if (!next || next.startsWith("-")) {
926
+ flags[key] = true;
927
+ continue;
928
+ }
929
+ if (key === "ignore") {
930
+ const existing = flags[key];
931
+ flags[key] = Array.isArray(existing) ? [...existing, next] : [next];
932
+ i++;
933
+ continue;
934
+ }
935
+ flags[key] = next;
936
+ i++;
937
+ }
938
+ return { command: positional[0] ?? "help", positional: positional.slice(1), flags };
939
+ }
940
+ function printHelp() {
941
+ console.log(`
942
+ ${color.bold("relion")} \u2014 MCP-powered API contract scanner and monitoring CLI
943
+
944
+ ${color.bold("Usage:")}
945
+ relion <command> [flags]
946
+
947
+ ${color.bold("Commands:")}
948
+ scan [path] Scan a directory for API vendor integrations
949
+ predeploy Check API risk before deploying (analyzes git diff)
950
+ login Authenticate with Relion
951
+ logout Remove stored credentials
952
+ whoami Show current identity
953
+ status Show last scan result for this directory
954
+ version Show CLI version
955
+
956
+ ${color.bold("Scan flags:")}
957
+ --token <token> API token (or set RELION_TOKEN)
958
+ --url <url> API base URL (default: https://relion.dev)
959
+ --repo-url <url> Repository URL for cloud matching
960
+ --commit <sha> Git commit SHA (auto-detected from git)
961
+ --branch <name> Branch name (auto-detected from git)
962
+ --dry-run Run locally, print findings, skip upload
963
+ --verbose Show per-file detail
964
+ --ignore <glob> Additional patterns to ignore (repeatable)
965
+ --output json Machine-readable JSON output
966
+
967
+ ${color.bold("Predeploy flags:")}
968
+ --staged Check staged changes (git diff --cached)
969
+ --diff <base> Check diff from a base branch/SHA
970
+ --commit <sha> Check files changed in a specific commit
971
+ --base-branch <b> Base branch label for display
972
+ --offline Skip cloud risk lookup (local analysis only)
973
+ --json Machine-readable JSON output
974
+ --verbose Show per-finding detail
975
+
976
+ ${color.bold("Exit codes:")}
977
+ 0 Clear 1 Error
978
+ 2 Gate blocked 3 Gate pending 4 No findings
979
+
980
+ ${color.bold("Environment variables:")}
981
+ RELION_TOKEN API token
982
+ RELION_API_URL API base URL
983
+ RELION_REPO_URL Repository URL
984
+ RELION_COMMIT Git commit SHA
985
+ RELION_BRANCH Branch name
986
+
987
+ ${color.dim("https://relion.dev/docs/cli")}
988
+ `);
989
+ }
990
+ async function main() {
991
+ const { command, positional, flags } = parseArgs(process.argv);
992
+ const f = flags;
993
+ switch (command) {
994
+ case "scan":
995
+ await scanCommand(positional[0], {
996
+ token: f.token,
997
+ url: f.url,
998
+ repoUrl: f["repo-url"],
999
+ commit: f.commit,
1000
+ branch: f.branch,
1001
+ dryRun: Boolean(f["dry-run"]),
1002
+ verbose: Boolean(f.verbose),
1003
+ ignore: f.ignore,
1004
+ output: f.output || "text"
1005
+ });
1006
+ break;
1007
+ case "predeploy":
1008
+ await predeployCommand(positional[0], {
1009
+ token: f.token,
1010
+ url: f.url,
1011
+ repoUrl: f["repo-url"],
1012
+ commit: f.commit,
1013
+ branch: f.branch,
1014
+ baseBranch: f["base-branch"],
1015
+ staged: Boolean(f.staged),
1016
+ diff: f.diff,
1017
+ commitFlag: f.commit,
1018
+ offline: Boolean(f.offline),
1019
+ json: Boolean(f.json),
1020
+ verbose: Boolean(f.verbose)
1021
+ });
1022
+ break;
1023
+ case "login":
1024
+ await loginCommand({
1025
+ token: f.token,
1026
+ url: f.url
1027
+ });
1028
+ break;
1029
+ case "logout":
1030
+ await logoutCommand();
1031
+ break;
1032
+ case "whoami":
1033
+ await whoamiCommand({ url: f.url });
1034
+ break;
1035
+ case "status":
1036
+ await statusCommand({
1037
+ json: Boolean(f.json),
1038
+ url: f.url
1039
+ });
1040
+ break;
1041
+ case "version":
1042
+ case "--version":
1043
+ case "-v":
1044
+ console.log(`relion v${VERSION}`);
1045
+ break;
1046
+ case "help":
1047
+ case "--help":
1048
+ case "-h":
1049
+ default:
1050
+ printHelp();
1051
+ break;
1052
+ }
1053
+ }
1054
+ main().catch((err) => {
1055
+ printError(err instanceof Error ? err.message : String(err));
1056
+ process.exit(1);
1057
+ });
package/package.json ADDED
@@ -0,0 +1,24 @@
1
+ {
2
+ "name": "relionhq",
3
+ "version": "2.0.0",
4
+ "description": "Relion CLI — pre-deploy API risk detection and monitoring client.",
5
+ "license": "MIT",
6
+ "bin": {
7
+ "relion": "dist/index.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "files": ["dist"],
11
+ "scripts": {
12
+ "build": "esbuild src/index.ts --bundle --platform=node --target=node18 --outfile=dist/index.js --external:fsevents",
13
+ "dev": "node --watch dist/index.js",
14
+ "prepublishOnly": "npm run build"
15
+ },
16
+ "dependencies": {},
17
+ "devDependencies": {
18
+ "@types/node": "^22.0.0",
19
+ "esbuild": "^0.25.0",
20
+ "typescript": "^5.8.0"
21
+ },
22
+ "engines": { "node": ">=18" },
23
+ "keywords": ["api", "monitoring", "relion", "predeploy", "cli", "deploy", "risk"]
24
+ }