@cantinasecurity/apex-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.
@@ -0,0 +1,629 @@
1
+ import { getFlagString, isJsonMode, isNonInteractive } from "./args.js";
2
+ import { logout } from "./auth.js";
3
+ import { ApiError } from "./api-client.js";
4
+ import { openInBrowser } from "./browser.js";
5
+ import { loadConfig, saveConfig } from "./config.js";
6
+ import { fetchScanExport, fetchScanFindings, resolveScanSelection } from "./findings.js";
7
+ import { prepareExplicitScanSources, supportsLegacyRemoteFlow, } from "./local-source-scan.js";
8
+ import { logLine, printJson, withLoadingIndicator } from "./output.js";
9
+ import { confirm } from "./prompt.js";
10
+ import { cancelScan, fetchWorkspaceScans, findMostRelevantActiveScan, getScanDisplayLabel, getScanDisplayId, getTrackedScanId, isActiveScanStatus, matchesScanId, selectDefaultScanToCancel, } from "./scan.js";
11
+ import { chooseCompany, createWorkspaceBinding, ensureAuthenticated, resolveWorkspaceSelection, resolveWorkspaceSelectionLegacy, resolveWorkspaceAllowingMissingConnections, selectWorkspaceTarget, } from "./session.js";
12
+ import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWorkspaces, findWorkspaceByRef, } from "./workspaces.js";
13
+ import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
14
+ import { commandSetup as runCliSetup } from "./setup.js";
15
+ import { commandUpdate as runCliUpdate } from "./update.js";
16
+ import { mkdir, writeFile } from "node:fs/promises";
17
+ import path from "node:path";
18
+ const WORKSPACE_USE_PATTERN = "<workspace-name|workspace-prefix|workspace-id>";
19
+ const WORKSPACE_BINDING_GUIDANCE = "Run `apex workspaces` to list workspace names, prefixes, and IDs, then bind one with `apex workspace use \"<workspace name>\"`, or run `apex scan` to create or resolve one automatically.";
20
+ const WORKSPACE_SELECTION_GUIDANCE = "Run `apex workspaces` to list available workspace names, prefixes, and IDs, then retry `apex workspace use \"<workspace name>\"`.";
21
+ async function saveCompanyDefault(companyId) {
22
+ const config = await loadConfig();
23
+ if (config.defaultCompanyId !== companyId) {
24
+ await saveConfig({
25
+ ...config,
26
+ defaultCompanyId: companyId,
27
+ });
28
+ }
29
+ }
30
+ function formatCommandArgument(value) {
31
+ return /\s/.test(value) ? `"${value}"` : value;
32
+ }
33
+ function getSuggestedWorkspaceRef(workspace) {
34
+ return workspace.prefix?.trim().length ? workspace.prefix : workspace.name;
35
+ }
36
+ function formatWorkspaceUseCommand(workspace) {
37
+ return `apex workspace use ${formatCommandArgument(getSuggestedWorkspaceRef(workspace))}`;
38
+ }
39
+ async function requireWorkspaceBinding(cwd) {
40
+ const binding = await loadWorkspaceBinding(cwd);
41
+ if (binding) {
42
+ return binding;
43
+ }
44
+ throw new Error(`This directory is not bound to an Apex workspace yet. ${WORKSPACE_BINDING_GUIDANCE}`);
45
+ }
46
+ function canPromptForConfirmation(flags) {
47
+ return !isNonInteractive(flags) && process.stdin.isTTY && process.stdout.isTTY;
48
+ }
49
+ async function findFallbackActiveScan(client, binding) {
50
+ if (!binding?.lastScanId) {
51
+ return null;
52
+ }
53
+ try {
54
+ const progress = await client.request(`/api/cli/v1/scans/${encodeURIComponent(binding.lastScanId)}/progress`);
55
+ if (!isActiveScanStatus(progress.progress?.status)) {
56
+ return null;
57
+ }
58
+ return {
59
+ scanId: progress.scanId,
60
+ kernelScanId: progress.kernelScanId,
61
+ apexScanId: binding.lastScanId,
62
+ bedrockScanId: null,
63
+ displayName: null,
64
+ sequenceNumber: null,
65
+ status: progress.progress?.status ?? "unknown",
66
+ mode: null,
67
+ scanUrl: binding.lastScanUrl ?? null,
68
+ createdAt: null,
69
+ startedAt: progress.progress?.startedAt ?? null,
70
+ updatedAt: null,
71
+ finishedAt: null,
72
+ };
73
+ }
74
+ catch (error) {
75
+ if (error instanceof ApiError && error.status === 404) {
76
+ return null;
77
+ }
78
+ throw error;
79
+ }
80
+ }
81
+ async function findActiveWorkspaceScan(client, workspaceId, binding) {
82
+ try {
83
+ const scans = await fetchWorkspaceScans(client, workspaceId);
84
+ return findMostRelevantActiveScan(scans);
85
+ }
86
+ catch (error) {
87
+ if (error instanceof ApiError && [404, 405].includes(error.status)) {
88
+ return findFallbackActiveScan(client, binding);
89
+ }
90
+ throw error;
91
+ }
92
+ }
93
+ async function ensureScanRestartConfirmed(flags, activeScan) {
94
+ if (!activeScan) {
95
+ return false;
96
+ }
97
+ if (flags.force === true) {
98
+ return true;
99
+ }
100
+ const scanId = getScanDisplayId(activeScan);
101
+ if (!canPromptForConfirmation(flags)) {
102
+ throw new Error(`A scan is already ${activeScan.status} for this workspace (${scanId}). Re-run with --force to start another scan.`);
103
+ }
104
+ const approved = await confirm(`Scan ${scanId} is already ${activeScan.status} for this workspace. Start another scan anyway?`, false);
105
+ if (!approved) {
106
+ throw new Error("Scan start cancelled.");
107
+ }
108
+ return true;
109
+ }
110
+ export async function initializeInteractiveSession(client, cwd, flags) {
111
+ const me = await ensureAuthenticated(client, flags);
112
+ const selection = await selectWorkspaceTarget(cwd, me, flags);
113
+ return withLoadingIndicator("Setting up Apex for this directory...", flags, async () => {
114
+ const result = await resolveWorkspaceSelection(client, selection);
115
+ const binding = createWorkspaceBinding(result, result.binding);
116
+ await saveWorkspaceBinding(cwd, binding);
117
+ await saveCompanyDefault(result.company.id);
118
+ return {
119
+ baseUrl: await client.getBaseUrl(),
120
+ me,
121
+ company: result.company,
122
+ workspaceName: result.workspaceName,
123
+ sources: result.sources,
124
+ binding,
125
+ };
126
+ });
127
+ }
128
+ export async function commandLogin(client, flags) {
129
+ const me = await ensureAuthenticated(client, flags);
130
+ if (isJsonMode(flags)) {
131
+ printJson(me);
132
+ return me;
133
+ }
134
+ logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
135
+ return me;
136
+ }
137
+ export async function commandCredits(client, cwd, flags) {
138
+ const me = await ensureAuthenticated(client, flags);
139
+ const binding = await loadWorkspaceBinding(cwd);
140
+ const company = await chooseCompany(me, flags, binding);
141
+ const payload = await withLoadingIndicator("Loading company credits...", flags, () => fetchCompanyCredits(client, company.id));
142
+ if (isJsonMode(flags)) {
143
+ printJson(payload);
144
+ return payload;
145
+ }
146
+ logLine(`Company: ${payload.company.handle ?? payload.company.id}`, flags);
147
+ logLine(`Credits: ${payload.scanBalance.remaining} remaining (${payload.scanBalance.purchased} purchased, ${payload.scanBalance.used} used)`, flags);
148
+ logLine(`Scans enabled: ${payload.scansEnabled ? "yes" : "no"}`, flags);
149
+ return payload;
150
+ }
151
+ export async function commandLogout(client, flags) {
152
+ await logout(client);
153
+ if (!isJsonMode(flags)) {
154
+ logLine("Logged out", flags);
155
+ }
156
+ }
157
+ export async function commandUpdate(flags) {
158
+ return runCliUpdate(flags);
159
+ }
160
+ export async function commandSetup(cwd, flags, target) {
161
+ return runCliSetup(cwd, flags, target);
162
+ }
163
+ export async function commandDoctor(client, cwd, flags) {
164
+ const me = await ensureAuthenticated(client, flags);
165
+ try {
166
+ const result = await withLoadingIndicator("Inspecting this directory and checking Apex setup...", flags, () => resolveWorkspaceAllowingMissingConnections(client, cwd, me, flags));
167
+ const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
168
+ const payload = {
169
+ authenticatedAs: me.user.username ?? me.user.id,
170
+ company: result.company,
171
+ workspaceName: result.workspaceName,
172
+ binding: result.binding,
173
+ sources: result.sources,
174
+ plannedSources: result.resolve.plannedSources,
175
+ };
176
+ if (isJsonMode(flags)) {
177
+ printJson(payload);
178
+ return payload;
179
+ }
180
+ logLine(`Authenticated as ${payload.authenticatedAs}`, flags);
181
+ logLine(`Company: ${payload.company.handle ?? payload.company.id}`, flags);
182
+ logLine(`Workspace name Apex will use: ${payload.workspaceName}`, flags);
183
+ logLine(`Sources detected locally: ${payload.sources.length}`, flags);
184
+ logLine(`Plan: ${payload.plannedSources.length - localSnapshotCount} remote, ${localSnapshotCount} local snapshot`, flags);
185
+ return payload;
186
+ }
187
+ catch (error) {
188
+ if (isJsonMode(flags)) {
189
+ const payload = {
190
+ ok: false,
191
+ error: error instanceof Error ? error.message : String(error),
192
+ };
193
+ printJson(payload);
194
+ if (error instanceof Error) {
195
+ error.reported = true;
196
+ }
197
+ throw error;
198
+ }
199
+ throw error;
200
+ }
201
+ }
202
+ export async function commandStatus(client, cwd, flags) {
203
+ await ensureAuthenticated(client, flags);
204
+ const binding = await requireWorkspaceBinding(cwd);
205
+ if (!binding.lastScanId) {
206
+ throw new Error("No scan has been started from this directory yet. Run `apex scan` first.");
207
+ }
208
+ const progress = await withLoadingIndicator("Loading scan status...", flags, () => client.request(`/api/cli/v1/scans/${binding.lastScanId}/progress`));
209
+ if (isJsonMode(flags)) {
210
+ printJson({ binding, progress });
211
+ return { binding, progress };
212
+ }
213
+ logLine(`Workspace: ${binding.workspaceName}`, flags);
214
+ logLine(`Scan: ${binding.lastScanId}`, flags);
215
+ logLine(`Status: ${progress.progress?.status ?? "unknown"}`, flags);
216
+ if (typeof progress.progress?.progressPct === "number") {
217
+ logLine(`Progress: ${progress.progress.progressPct}%`, flags);
218
+ }
219
+ if (binding.lastScanUrl) {
220
+ logLine(`View: ${binding.lastScanUrl}`, flags);
221
+ }
222
+ return { binding, progress };
223
+ }
224
+ export async function commandConnect(client, cwd, flags, provider) {
225
+ const me = await ensureAuthenticated(client, flags);
226
+ const binding = await loadWorkspaceBinding(cwd);
227
+ const company = await chooseCompany(me, flags, binding);
228
+ const route = provider === "github"
229
+ ? `/api/cli/v1/providers/github/connect?companyId=${encodeURIComponent(company.id)}`
230
+ : `/api/cli/v1/providers/gitlab/connect?companyId=${encodeURIComponent(company.id)}`;
231
+ const response = await withLoadingIndicator(`Preparing ${provider} connection link...`, flags, () => client.request(route));
232
+ if (!isJsonMode(flags)) {
233
+ logLine(`Connection URL for ${provider}: ${response.url}`, flags);
234
+ }
235
+ if (flags["no-open"] !== true) {
236
+ await openInBrowser(response.url);
237
+ }
238
+ if (isJsonMode(flags)) {
239
+ printJson({ provider, company, url: response.url });
240
+ }
241
+ return { provider, company, url: response.url };
242
+ }
243
+ export async function commandScan(client, cwd, flags) {
244
+ const me = await ensureAuthenticated(client, flags);
245
+ const selection = await selectWorkspaceTarget(cwd, me, flags);
246
+ const result = await withLoadingIndicator("Resolving workspace and scan plan...", flags, () => resolveWorkspaceSelection(client, selection));
247
+ const requestedMode = getFlagString(flags, "mode") === "ultra" ? "ultra" : "standard";
248
+ if (!result.resolve.workspaceId) {
249
+ throw new Error("Workspace resolution did not return a workspaceId.");
250
+ }
251
+ const resolvedWorkspaceId = result.resolve.workspaceId;
252
+ const activeScan = await withLoadingIndicator("Checking for existing scans...", flags, () => findActiveWorkspaceScan(client, resolvedWorkspaceId, result.binding));
253
+ const forceRestart = await ensureScanRestartConfirmed(flags, activeScan);
254
+ let workspaceId = resolvedWorkspaceId;
255
+ let scan;
256
+ if (requestedMode === "ultra") {
257
+ if (!supportsLegacyRemoteFlow(result.resolve.plannedSources)) {
258
+ throw new Error("Ultra scans currently require provider-backed GitHub or GitLab repositories without local snapshot fallbacks.");
259
+ }
260
+ const legacyResolve = await withLoadingIndicator("Preparing ultra scan...", flags, () => resolveWorkspaceSelectionLegacy(client, selection, resolvedWorkspaceId));
261
+ if (!legacyResolve.workspaceId) {
262
+ throw new Error("Workspace resolution did not return a workspaceId.");
263
+ }
264
+ workspaceId = legacyResolve.workspaceId;
265
+ scan = await withLoadingIndicator("Starting ultra scan...", flags, () => client.request(`/api/cli/v1/local-workspaces/${encodeURIComponent(workspaceId)}/scan`, {
266
+ method: "POST",
267
+ json: {
268
+ mode: requestedMode,
269
+ force: forceRestart || flags.force === true,
270
+ },
271
+ }));
272
+ }
273
+ else {
274
+ scan = await withLoadingIndicator("Starting scan...", flags, async () => {
275
+ const scanSources = await prepareExplicitScanSources({
276
+ client,
277
+ workspaceId: resolvedWorkspaceId,
278
+ plannedSources: result.resolve.plannedSources,
279
+ detectedSources: result.sources,
280
+ });
281
+ return client.request(`/api/cli/v2/local-workspaces/${encodeURIComponent(resolvedWorkspaceId)}/scan`, {
282
+ method: "POST",
283
+ json: {
284
+ mode: requestedMode,
285
+ force: forceRestart || flags.force === true,
286
+ scanSources,
287
+ },
288
+ });
289
+ });
290
+ }
291
+ const binding = {
292
+ ...createWorkspaceBinding(result, result.binding),
293
+ workspaceId,
294
+ lastScanId: getTrackedScanId(scan),
295
+ lastScanUrl: scan.scanUrl ?? null,
296
+ };
297
+ await saveWorkspaceBinding(cwd, binding);
298
+ await saveCompanyDefault(result.company.id);
299
+ const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
300
+ const payload = {
301
+ company: result.company,
302
+ workspaceId,
303
+ workspaceName: result.workspaceName,
304
+ sources: result.sources,
305
+ plannedSources: result.resolve.plannedSources,
306
+ scan: {
307
+ ...scan,
308
+ apexScanId: scan.apexScanId ?? scan.bedrockScanId ?? null,
309
+ },
310
+ binding,
311
+ };
312
+ if (isJsonMode(flags)) {
313
+ printJson(payload);
314
+ return payload;
315
+ }
316
+ logLine(`Authenticated as ${me.user.username ?? me.user.id}`, flags);
317
+ logLine(`Company: ${result.company.handle ?? result.company.id}`, flags);
318
+ logLine(`Workspace: ${result.workspaceName}`, flags);
319
+ logLine(`Sources selected: ${result.sources.length}`, flags);
320
+ logLine(`Plan: ${result.resolve.plannedSources.length - localSnapshotCount} remote, ${localSnapshotCount} local snapshot`, flags);
321
+ logLine(`Scan: ${scan.status}${getTrackedScanId(scan) ? ` (${getTrackedScanId(scan)})` : ""}`, flags);
322
+ if (scan.scanUrl) {
323
+ logLine(`View: ${scan.scanUrl}`, flags);
324
+ }
325
+ logLine("Next: run `apex status` to watch progress, then `apex findings` when the scan finishes.", flags);
326
+ return payload;
327
+ }
328
+ export async function commandWorkspaces(client, cwd, flags) {
329
+ const me = await ensureAuthenticated(client, flags);
330
+ const binding = await loadWorkspaceBinding(cwd);
331
+ const company = await chooseCompany(me, flags, binding);
332
+ const response = await withLoadingIndicator("Loading workspaces...", flags, () => fetchCompanyWorkspaces(client, company.id));
333
+ const payload = {
334
+ ...response,
335
+ currentWorkspaceId: binding?.workspaceId ?? null,
336
+ };
337
+ if (isJsonMode(flags)) {
338
+ printJson(payload);
339
+ return payload;
340
+ }
341
+ logLine(`Company: ${response.company.handle ?? response.company.id}`, flags);
342
+ logLine("Bind this directory with `apex workspace use <workspace-name|workspace-prefix|workspace-id>`.", flags);
343
+ if (response.workspaces.length === 0) {
344
+ logLine("Workspaces: none", flags);
345
+ return payload;
346
+ }
347
+ for (const [index, workspace] of response.workspaces.entries()) {
348
+ const currentMarker = binding?.workspaceId === workspace.workspaceId ? " [current]" : "";
349
+ logLine(`Workspace: ${workspace.name}${currentMarker}`, flags);
350
+ if (workspace.prefix) {
351
+ logLine(`Prefix: ${workspace.prefix}`, flags);
352
+ }
353
+ logLine(`ID: ${workspace.workspaceId}`, flags);
354
+ logLine(`Use here: ${formatWorkspaceUseCommand(workspace)}`, flags);
355
+ logLine(`Stats: ${workspace.scanCount} scans, ${workspace.findingsCount} findings`, flags);
356
+ if (workspace.latestScan) {
357
+ logLine(`Latest: ${workspace.latestScan.displayName} (${workspace.latestScan.status}, ${workspace.latestScan.createdAt})`, flags);
358
+ }
359
+ if (index < response.workspaces.length - 1) {
360
+ logLine("", flags);
361
+ }
362
+ }
363
+ return payload;
364
+ }
365
+ export async function commandWorkspace(cwd, flags) {
366
+ const binding = await loadWorkspaceBinding(cwd);
367
+ const payload = { binding };
368
+ if (isJsonMode(flags)) {
369
+ printJson(payload);
370
+ return payload;
371
+ }
372
+ if (!binding) {
373
+ logLine("This directory is not bound to an Apex workspace yet.", flags);
374
+ logLine(WORKSPACE_BINDING_GUIDANCE, flags);
375
+ return payload;
376
+ }
377
+ logLine(`This directory is bound to workspace: ${binding.workspaceName}`, flags);
378
+ logLine(`Workspace ID: ${binding.workspaceId}`, flags);
379
+ logLine(`Company ID: ${binding.companyId}`, flags);
380
+ if (binding.lastScanId) {
381
+ logLine(`Last scan: ${binding.lastScanId}`, flags);
382
+ }
383
+ return payload;
384
+ }
385
+ export async function commandWorkspaceUse(client, cwd, flags, workspaceRef) {
386
+ const trimmedRef = workspaceRef.trim();
387
+ if (!trimmedRef) {
388
+ throw new Error(`Usage: apex workspace use ${WORKSPACE_USE_PATTERN}`);
389
+ }
390
+ const me = await ensureAuthenticated(client, flags);
391
+ const currentBinding = await loadWorkspaceBinding(cwd);
392
+ const company = await chooseCompany(me, flags, currentBinding);
393
+ const response = await withLoadingIndicator("Loading workspaces...", flags, () => fetchCompanyWorkspaces(client, company.id));
394
+ const workspace = findWorkspaceByRef(response.workspaces, trimmedRef);
395
+ if (!workspace) {
396
+ throw new Error(`Could not find a workspace matching "${trimmedRef}". ${WORKSPACE_SELECTION_GUIDANCE}`);
397
+ }
398
+ const binding = createWorkspaceBindingFromSummary({
399
+ companyId: company.id,
400
+ workspace,
401
+ currentBinding,
402
+ });
403
+ await saveWorkspaceBinding(cwd, binding);
404
+ await saveCompanyDefault(company.id);
405
+ const payload = {
406
+ company,
407
+ workspace,
408
+ binding,
409
+ };
410
+ if (isJsonMode(flags)) {
411
+ printJson(payload);
412
+ return payload;
413
+ }
414
+ logLine(`Company: ${company.handle ?? company.id}`, flags);
415
+ logLine(`Bound this directory to workspace: ${workspace.name}`, flags);
416
+ if (workspace.prefix) {
417
+ logLine(`Workspace prefix: ${workspace.prefix}`, flags);
418
+ }
419
+ logLine(`Workspace ID: ${workspace.workspaceId}`, flags);
420
+ logLine("Next: run `apex scan` to start a scan in this workspace.", flags);
421
+ return payload;
422
+ }
423
+ export async function commandScans(client, cwd, flags) {
424
+ await ensureAuthenticated(client, flags);
425
+ const binding = await requireWorkspaceBinding(cwd);
426
+ const scans = await withLoadingIndicator("Loading scans...", flags, async () => {
427
+ try {
428
+ return await fetchWorkspaceScans(client, binding.workspaceId);
429
+ }
430
+ catch (error) {
431
+ if (error instanceof ApiError &&
432
+ [404, 405].includes(error.status) &&
433
+ binding.lastScanId) {
434
+ const fallback = await findFallbackActiveScan(client, binding);
435
+ return fallback ? [fallback] : [];
436
+ }
437
+ throw error;
438
+ }
439
+ });
440
+ const payload = {
441
+ binding,
442
+ scans,
443
+ };
444
+ if (isJsonMode(flags)) {
445
+ printJson(payload);
446
+ return payload;
447
+ }
448
+ logLine(`Workspace: ${binding.workspaceName}`, flags);
449
+ if (scans.length === 0) {
450
+ logLine("Scans: none yet. Run `apex scan` to start one.", flags);
451
+ return payload;
452
+ }
453
+ for (const scan of scans) {
454
+ const timestamp = scan.startedAt ?? scan.createdAt ?? scan.finishedAt ?? null;
455
+ const modeSuffix = scan.mode ? `, ${scan.mode}` : "";
456
+ const timeSuffix = timestamp ? `, ${timestamp}` : "";
457
+ logLine(`Scan: ${getScanDisplayLabel(scan)} (${scan.status}${modeSuffix}${timeSuffix})`, flags);
458
+ if (scan.scanUrl) {
459
+ logLine(`View: ${scan.scanUrl}`, flags);
460
+ }
461
+ }
462
+ return payload;
463
+ }
464
+ export async function commandCancelScan(client, cwd, flags, requestedScanId) {
465
+ await ensureAuthenticated(client, flags);
466
+ const binding = await requireWorkspaceBinding(cwd);
467
+ let scanId = requestedScanId?.trim() || null;
468
+ if (!scanId) {
469
+ const scans = await withLoadingIndicator("Finding a scan to cancel...", flags, async () => {
470
+ try {
471
+ return await fetchWorkspaceScans(client, binding.workspaceId);
472
+ }
473
+ catch (error) {
474
+ if (error instanceof ApiError &&
475
+ [404, 405].includes(error.status) &&
476
+ binding.lastScanId) {
477
+ scanId = binding.lastScanId;
478
+ return [];
479
+ }
480
+ throw error;
481
+ }
482
+ });
483
+ if (!scanId && scans.length === 0) {
484
+ throw new Error("No scans are available to cancel for this workspace.");
485
+ }
486
+ if (!scanId) {
487
+ const selected = selectDefaultScanToCancel(scans, binding.lastScanId ?? null);
488
+ if (!selected) {
489
+ throw new Error("Multiple active scans are running. Run `apex scans` to list scan IDs, then re-run `apex cancel-scan <scan-id>`.");
490
+ }
491
+ scanId = getScanDisplayId(selected);
492
+ }
493
+ }
494
+ if (!scanId) {
495
+ throw new Error("No scans are available to cancel for this workspace.");
496
+ }
497
+ const targetScanId = scanId;
498
+ const scan = await withLoadingIndicator("Cancelling scan...", flags, () => cancelScan(client, targetScanId));
499
+ const nextBinding = binding.lastScanId && matchesScanId(scan, binding.lastScanId)
500
+ ? {
501
+ ...binding,
502
+ lastScanUrl: scan.scanUrl ?? binding.lastScanUrl ?? null,
503
+ }
504
+ : binding;
505
+ await saveWorkspaceBinding(cwd, nextBinding);
506
+ const payload = {
507
+ binding: nextBinding,
508
+ scan,
509
+ };
510
+ if (isJsonMode(flags)) {
511
+ printJson(payload);
512
+ return payload;
513
+ }
514
+ logLine(`Workspace: ${nextBinding.workspaceName}`, flags);
515
+ logLine(`Scan: ${getScanDisplayId(scan)}`, flags);
516
+ logLine(`Status: ${scan.status}`, flags);
517
+ if (scan.scanUrl) {
518
+ logLine(`View: ${scan.scanUrl}`, flags);
519
+ }
520
+ return payload;
521
+ }
522
+ export async function commandFindings(client, cwd, flags) {
523
+ await ensureAuthenticated(client, flags);
524
+ const binding = await requireWorkspaceBinding(cwd);
525
+ const { scan, response } = await withLoadingIndicator("Loading findings...", flags, async () => {
526
+ const scan = await resolveScanSelection({
527
+ client,
528
+ binding,
529
+ requestedScanId: getFlagString(flags, "scan"),
530
+ });
531
+ const response = await fetchScanFindings(client, scan.scanId, getFlagString(flags, "limit"));
532
+ return { scan, response };
533
+ });
534
+ const nextBinding = {
535
+ ...binding,
536
+ lastScanId: getScanDisplayId(scan),
537
+ lastScanUrl: response.scan.scanUrl ?? binding.lastScanUrl ?? null,
538
+ };
539
+ await saveWorkspaceBinding(cwd, nextBinding);
540
+ const payload = {
541
+ ...response,
542
+ binding: nextBinding,
543
+ };
544
+ if (isJsonMode(flags)) {
545
+ printJson(payload);
546
+ return payload;
547
+ }
548
+ logLine(`Workspace: ${response.workspace.name}`, flags);
549
+ logLine(`Scan: ${response.scan.displayName} (${response.scan.status})`, flags);
550
+ if (response.findings.length === 0) {
551
+ logLine("Findings: none for this scan.", flags);
552
+ return payload;
553
+ }
554
+ for (const finding of response.findings) {
555
+ const identifier = finding.findingIdentifier ?? finding.id;
556
+ logLine(`${finding.severity.toUpperCase()} ${identifier} ${finding.title} (${finding.status}, ${finding.confidence})`, flags);
557
+ if (finding.findingUrl) {
558
+ logLine(`View: ${finding.findingUrl}`, flags);
559
+ }
560
+ }
561
+ return payload;
562
+ }
563
+ export async function commandExportFindings(client, cwd, flags) {
564
+ await ensureAuthenticated(client, flags);
565
+ const binding = await requireWorkspaceBinding(cwd);
566
+ const format = getFlagString(flags, "format") ?? "markdown";
567
+ const { scan, response, outputPath } = await withLoadingIndicator("Exporting findings...", flags, async () => {
568
+ const scan = await resolveScanSelection({
569
+ client,
570
+ binding,
571
+ requestedScanId: getFlagString(flags, "scan"),
572
+ });
573
+ const response = await fetchScanExport(client, scan.scanId, format);
574
+ const outputPath = path.resolve(cwd, getFlagString(flags, "output") ?? response.fileName);
575
+ await mkdir(path.dirname(outputPath), { recursive: true });
576
+ const fileContents = typeof response.content === "string"
577
+ ? response.content.endsWith("\n")
578
+ ? response.content
579
+ : `${response.content}\n`
580
+ : `${JSON.stringify(response.content, null, 2)}\n`;
581
+ await writeFile(outputPath, fileContents, "utf8");
582
+ return { scan, response, outputPath };
583
+ });
584
+ const nextBinding = {
585
+ ...binding,
586
+ lastScanId: getScanDisplayId(scan),
587
+ lastScanUrl: response.scan.scanUrl ?? binding.lastScanUrl ?? null,
588
+ };
589
+ await saveWorkspaceBinding(cwd, nextBinding);
590
+ const payload = {
591
+ ...response,
592
+ binding: nextBinding,
593
+ outputPath,
594
+ };
595
+ if (isJsonMode(flags)) {
596
+ printJson({
597
+ format: payload.format,
598
+ fileName: payload.fileName,
599
+ contentType: payload.contentType,
600
+ findingCount: payload.findingCount,
601
+ workspace: payload.workspace,
602
+ scan: payload.scan,
603
+ outputPath: payload.outputPath,
604
+ });
605
+ return payload;
606
+ }
607
+ logLine(`Workspace: ${response.workspace.name}`, flags);
608
+ logLine(`Scan: ${response.scan.displayName}`, flags);
609
+ logLine(`Export: ${response.format} (${response.findingCount} findings)`, flags);
610
+ logLine(`Saved: ${outputPath}`, flags);
611
+ return payload;
612
+ }
613
+ export async function openCurrentApexView(client, cwd, flags) {
614
+ const binding = await loadWorkspaceBinding(cwd);
615
+ const url = binding?.lastScanUrl ?? (await client.getBaseUrl());
616
+ if (!isJsonMode(flags)) {
617
+ logLine(`Open: ${url}`, flags);
618
+ }
619
+ if (flags["no-open"] !== true) {
620
+ await openInBrowser(url);
621
+ }
622
+ if (isJsonMode(flags)) {
623
+ printJson({
624
+ url,
625
+ source: binding?.lastScanUrl ? "last_scan" : "home",
626
+ });
627
+ }
628
+ return url;
629
+ }
package/dist/config.js ADDED
@@ -0,0 +1,54 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ const CONFIG_DIR = path.join(os.homedir(), ".config", "apex");
5
+ const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
6
+ const CREDENTIALS_PATH = path.join(CONFIG_DIR, "credentials.json");
7
+ const DEFAULT_BASE_URL = process.env.APEX_BASE_URL ?? "https://ai.cantina.xyz/";
8
+ async function ensureConfigDir() {
9
+ await mkdir(CONFIG_DIR, { recursive: true, mode: 0o700 });
10
+ }
11
+ async function readJsonFile(filePath) {
12
+ try {
13
+ const contents = await readFile(filePath, "utf8");
14
+ return JSON.parse(contents);
15
+ }
16
+ catch {
17
+ return null;
18
+ }
19
+ }
20
+ async function writeJsonFile(filePath, value, mode) {
21
+ await ensureConfigDir();
22
+ await writeFile(filePath, `${JSON.stringify(value, null, 2)}\n`, {
23
+ encoding: "utf8",
24
+ mode,
25
+ });
26
+ await chmod(filePath, mode);
27
+ }
28
+ export async function loadConfig() {
29
+ const config = await readJsonFile(CONFIG_PATH);
30
+ if (!config?.baseUrl) {
31
+ return {
32
+ version: 1,
33
+ baseUrl: DEFAULT_BASE_URL,
34
+ defaultCompanyId: null,
35
+ };
36
+ }
37
+ return {
38
+ version: 1,
39
+ baseUrl: config.baseUrl,
40
+ defaultCompanyId: config.defaultCompanyId ?? null,
41
+ };
42
+ }
43
+ export async function saveConfig(config) {
44
+ await writeJsonFile(CONFIG_PATH, config, 0o600);
45
+ }
46
+ export async function loadCredentials() {
47
+ return readJsonFile(CREDENTIALS_PATH);
48
+ }
49
+ export async function saveCredentials(credentials) {
50
+ await writeJsonFile(CREDENTIALS_PATH, credentials, 0o600);
51
+ }
52
+ export async function clearCredentials() {
53
+ await rm(CREDENTIALS_PATH, { force: true });
54
+ }