@cantinasecurity/apex-cli 0.1.42 → 0.1.43
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/.claude/skills/apex-cli/SKILL.md +1 -1
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/.mcp.claude.json +1 -1
- package/.mcp.codex.json +1 -1
- package/MARKETPLACE.md +1 -1
- package/README.md +17 -9
- package/dist/commands.js +24 -3
- package/dist/help.js +2 -2
- package/dist/mcp.js +25 -5
- package/dist/update.js +146 -31
- package/package.json +1 -1
- package/skills/apex-cli/SKILL.md +1 -1
|
@@ -74,7 +74,7 @@ If the Apex MCP server is not configured, fall back to the local CLI:
|
|
|
74
74
|
- `apex credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
|
|
75
75
|
- Work from the target repository directory so Apex can resolve `.apex/workspace.json`.
|
|
76
76
|
- `apex scan` scans the current working directory by default; pass `--repo` only when the user asks to scan explicit alternate roots.
|
|
77
|
-
- `apex-doctor` is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
77
|
+
- `apex-doctor` reports the running CLI version and detects conflicting npm/pnpm global installations. It is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
78
78
|
- Plain local directories and dirty git worktrees can scan through local snapshot uploads without provider access.
|
|
79
79
|
- Audit scans use `--mode audit` in user-facing CLI calls and request payloads. The legacy `ultra` mode remains accepted as an alias, but audit scans still require provider-backed GitHub, GitLab or company-enabled Forgejo sources.
|
|
80
80
|
- Lite scans use `--mode lite` in CLI calls or `mode: "lite"` in MCP, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
},
|
|
7
7
|
"metadata": {
|
|
8
8
|
"description": "Cantina agent plugins for security review workflows.",
|
|
9
|
-
"version": "0.1.
|
|
9
|
+
"version": "0.1.43"
|
|
10
10
|
},
|
|
11
11
|
"plugins": [
|
|
12
12
|
{
|
|
@@ -14,10 +14,10 @@
|
|
|
14
14
|
"source": {
|
|
15
15
|
"source": "npm",
|
|
16
16
|
"package": "@cantinasecurity/apex-cli",
|
|
17
|
-
"version": "0.1.
|
|
17
|
+
"version": "0.1.43"
|
|
18
18
|
},
|
|
19
19
|
"description": "Run Apex security scans and review findings from Claude Code.",
|
|
20
|
-
"version": "0.1.
|
|
20
|
+
"version": "0.1.43",
|
|
21
21
|
"author": {
|
|
22
22
|
"name": "Cantina",
|
|
23
23
|
"email": "support@cantina.xyz"
|
package/.mcp.claude.json
CHANGED
package/.mcp.codex.json
CHANGED
package/MARKETPLACE.md
CHANGED
|
@@ -13,7 +13,7 @@ This package is prepared as both a Claude Code plugin and a Codex plugin. The pu
|
|
|
13
13
|
The plugin MCP configs launch the pinned npm CLI package with:
|
|
14
14
|
|
|
15
15
|
```bash
|
|
16
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
16
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex-mcp
|
|
17
17
|
```
|
|
18
18
|
|
|
19
19
|
That keeps marketplace installs independent of a user's global `apex` install.
|
package/README.md
CHANGED
|
@@ -66,6 +66,14 @@ cannot adopt the new merge and generation-safety rules until it restarts.
|
|
|
66
66
|
|
|
67
67
|
When `apex` is run in an interactive terminal, it checks for updates periodically and offers to install them.
|
|
68
68
|
|
|
69
|
+
`apex update` updates the package-manager installation that owns the running
|
|
70
|
+
executable and verifies that installation actually changed before reporting
|
|
71
|
+
success. `apex doctor` reports the running version and package manager. If npm
|
|
72
|
+
and pnpm contain different global Apex versions, doctor identifies the stale
|
|
73
|
+
active copy and prints the command that repairs it. The same installation
|
|
74
|
+
diagnostic is included in `apex doctor --json`, including when another doctor
|
|
75
|
+
check fails.
|
|
76
|
+
|
|
69
77
|
## Local Development
|
|
70
78
|
|
|
71
79
|
1. Install dependencies:
|
|
@@ -158,30 +166,30 @@ unclaimed local trial proof so `apex claim` can still use it.
|
|
|
158
166
|
Agent-safe copy/paste prompt (substitute the issued code):
|
|
159
167
|
|
|
160
168
|
```text
|
|
161
|
-
Install and run Cantina Apex CLI 0.1.
|
|
169
|
+
Install and run Cantina Apex CLI 0.1.43 to start one free standard security scan of this repository immediately.
|
|
162
170
|
|
|
163
171
|
Use this trial code only where <TRIAL_CODE> appears below: SPBT-DEVREL-XXXXXXXX
|
|
164
172
|
|
|
165
173
|
Run the commands yourself from the repository root; do not stop after setup or only explain the steps. Use terminal CLI commands only. Do not use an Apex MCP tool or run `apex setup`, `apex mcp`, `apex login`, `apex-auth-start`, or `apex-auth-wait`. Never use `--replace` or `--force`.
|
|
166
174
|
|
|
167
175
|
Use this exact command prefix for every Apex command:
|
|
168
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
176
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex
|
|
169
177
|
|
|
170
178
|
1. Before redeeming, confirm every command will use the same persistent, user-accessible machine or container, HOME, filesystem, and repository root. Confirm Node.js 20+ and npx are available. If `APEX_BASE_URL` is set, continue only when its origin is `https://ai.cantina.xyz`. Run the exact prefix with `--help` and continue only if help lists both `redeem` and `claim`. If any check fails, STOP before sending the code; do not install system packages.
|
|
171
179
|
2. Redeem, with at most one response-loss recovery retry:
|
|
172
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
180
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex redeem <TRIAL_CODE> --json
|
|
173
181
|
This terminal command is the only allowed tool call containing the code. Never echo or restate the code, store it in a file or environment variable, put it in a URL or MCP call, or include it in your response. Parse the JSON from stdout only; do not merge stderr into stdout, because npm may write warnings to stderr. Continue when the command exits 0 and stdout is JSON with `redeemed: true` and `outcome` equal to `redeemed` or `idempotent`. `idempotent` means the existing guest session resumed and it may already own a workspace. If and only if the first attempt ends with an ambiguous transport/connection failure or an interrupted/missing response and no typed server error code, retry this exact same command once on the same machine, HOME, and repository root. Never retry a typed server rejection and never make more than one recovery retry. If the allowed retry does not return a successful JSON outcome, STOP.
|
|
174
182
|
3. Immediately start exactly one scan; do not wait for another instruction:
|
|
175
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
183
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex scan --mode standard --non-interactive --no-open --json
|
|
176
184
|
The CLI validates the complete guest local archive before it reserves a workspace. An unbound renamed checkout may recover only the sole empty guest workspace when no explicit workspace name was supplied. On any error or nonzero exit, STOP. After a successful scan JSON response, inspect `localPersistence.workspaceBindingSaved`. If it is `false`, the server scan already started: capture and report `binding.lastScanId`, then STOP; do not run status or findings until local repository permissions are repaired. Otherwise capture `binding.lastScanId` from the JSON as <SCAN_ID>; if it is absent, STOP.
|
|
177
185
|
4. About every 60 seconds, poll that exact scan:
|
|
178
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
186
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex status --scan <SCAN_ID> --json
|
|
179
187
|
Read `progress.progress.status`. Wait while it is `created`, `pending`, `queued`, `starting`, `running`, `in_progress`, or `processing`. Continue on `completed`. STOP on `failed`, `cancelled`, a missing or unknown status, or a command failure.
|
|
180
188
|
5. Fetch that exact scan's findings:
|
|
181
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
189
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex findings --scan <SCAN_ID> --json
|
|
182
190
|
On success, summarize findings by severity, most severe first, including identifier, title, status, and confidence. This command does not return file locations, so do not invent them. On failure, STOP.
|
|
183
191
|
6. Do not claim automatically. Tell me the claim deadline from step 2 and remind me to run this exact command myself from the same repository root, machine, and HOME:
|
|
184
|
-
npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
192
|
+
npx -y -p @cantinasecurity/apex-cli@0.1.43 apex claim
|
|
185
193
|
It may open a browser so I can create or sign in to a Cantina account and keep the results. Claim moves the trial into the currently selected Cantina workspace when I can run scans there, or into my existing personal workspace. If the selected workspace cannot run scans, tell me to switch to one that can and retry before the claim deadline.
|
|
186
194
|
|
|
187
195
|
Never paste raw command transcripts. For any failure, report only the step, exit code, and sanitized error. Redact the trial code and every `apex_at_`, `apex_rt_`, or `apex_ct_` value from all reports.
|
|
@@ -687,7 +695,7 @@ Telemetry event posts do not include bearer tokens. The telemetry endpoint is `P
|
|
|
687
695
|
{
|
|
688
696
|
"schemaVersion": 1,
|
|
689
697
|
"event": "apex.invocation.completed",
|
|
690
|
-
"cliVersion": "0.1.
|
|
698
|
+
"cliVersion": "0.1.43",
|
|
691
699
|
"invocation": {
|
|
692
700
|
"surface": "mcp",
|
|
693
701
|
"command": "apex-scan",
|
|
@@ -718,7 +726,7 @@ The npm package also includes marketplace-ready plugin artifacts:
|
|
|
718
726
|
- `.claude-plugin/plugin.json` and `.mcp.claude.json` for Claude Code plugin installs
|
|
719
727
|
- `.claude-plugin/marketplace.json` for a Claude marketplace entry backed by the public npm package
|
|
720
728
|
|
|
721
|
-
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.
|
|
729
|
+
These plugin installs launch the pinned npm package with `npx -y -p @cantinasecurity/apex-cli@0.1.43 apex-mcp`, so users do not need to install `apex` globally before enabling the plugin.
|
|
722
730
|
|
|
723
731
|
The repository also includes `.agents/plugins/marketplace.json` for local Codex marketplace testing from a checkout.
|
|
724
732
|
|
package/dist/commands.js
CHANGED
|
@@ -19,11 +19,11 @@ import { createWorkspaceBindingFromSummary, fetchCompanyCredits, fetchCompanyWor
|
|
|
19
19
|
import { loadWorkspaceBinding, saveWorkspaceBinding } from "./workspace-binding.js";
|
|
20
20
|
import { commandSetup as runCliSetup } from "./setup.js";
|
|
21
21
|
import { getTelemetryStatus, setTelemetryAuthKind, setTelemetryEnabled, setTelemetryOnboardingOutcome, } from "./telemetry.js";
|
|
22
|
-
import { commandUpdate as runCliUpdate } from "./update.js";
|
|
22
|
+
import { commandUpdate as runCliUpdate, getCliInstallDiagnostics, } from "./update.js";
|
|
23
23
|
import { mkdir, writeFile } from "node:fs/promises";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { z } from "zod";
|
|
26
|
-
function getRequestedWorkspaceSrDoctorPayload(me, flags) {
|
|
26
|
+
function getRequestedWorkspaceSrDoctorPayload(me, flags, cliInstallation) {
|
|
27
27
|
const requested = getFlagString(flags, "company")?.trim().toLowerCase();
|
|
28
28
|
if (!requested)
|
|
29
29
|
return null;
|
|
@@ -48,6 +48,7 @@ function getRequestedWorkspaceSrDoctorPayload(me, flags) {
|
|
|
48
48
|
const company = scopes[0];
|
|
49
49
|
const workspaceIds = [...new Set(scopes.map((scope) => scope.workspaceId))];
|
|
50
50
|
return {
|
|
51
|
+
cliInstallation,
|
|
51
52
|
authenticatedAs: me.user.username ?? me.user.id,
|
|
52
53
|
accessMode: "workspace_sr",
|
|
53
54
|
company: {
|
|
@@ -1035,6 +1036,21 @@ export async function commandTelemetry(flags, target) {
|
|
|
1035
1036
|
return payload;
|
|
1036
1037
|
}
|
|
1037
1038
|
export async function commandDoctor(client, cwd, flags) {
|
|
1039
|
+
const cliInstallation = await getCliInstallDiagnostics().catch(() => null);
|
|
1040
|
+
if (!isJsonMode(flags) && cliInstallation) {
|
|
1041
|
+
const manager = cliInstallation.packageManager
|
|
1042
|
+
? ` via ${cliInstallation.packageManager}`
|
|
1043
|
+
: cliInstallation.installKind === "git-checkout"
|
|
1044
|
+
? " from a Git checkout"
|
|
1045
|
+
: "";
|
|
1046
|
+
logLine(`CLI: ${escapeTerminalControlCharacters(cliInstallation.currentVersion)}${manager}`, flags);
|
|
1047
|
+
if (cliInstallation.warning) {
|
|
1048
|
+
logLine(`CLI installation warning: ${escapeTerminalControlCharacters(cliInstallation.warning)}`, flags);
|
|
1049
|
+
}
|
|
1050
|
+
if (cliInstallation.repairCommand) {
|
|
1051
|
+
logLine(`Repair: ${escapeTerminalControlCharacters(cliInstallation.repairCommand)}`, flags);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1038
1054
|
try {
|
|
1039
1055
|
const serviceKeyGuidance = getServiceKeyCliAuthGuidance();
|
|
1040
1056
|
const existingMe = serviceKeyGuidance ? await getMe(client) : null;
|
|
@@ -1044,7 +1060,7 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
1044
1060
|
const me = existingMe ?? (await ensureAuthenticated(client, flags));
|
|
1045
1061
|
const authState = await getCommandAuthState(client, me);
|
|
1046
1062
|
setTelemetryAuthKind(authState.authKind);
|
|
1047
|
-
const workspaceSrPayload = getRequestedWorkspaceSrDoctorPayload(me, flags);
|
|
1063
|
+
const workspaceSrPayload = getRequestedWorkspaceSrDoctorPayload(me, flags, cliInstallation);
|
|
1048
1064
|
if (workspaceSrPayload) {
|
|
1049
1065
|
if (isJsonMode(flags)) {
|
|
1050
1066
|
printJson(workspaceSrPayload);
|
|
@@ -1069,6 +1085,7 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
1069
1085
|
});
|
|
1070
1086
|
const localSnapshotCount = result.resolve.plannedSources.filter((source) => source.sourceKind === "local_archive").length;
|
|
1071
1087
|
const payload = {
|
|
1088
|
+
cliInstallation,
|
|
1072
1089
|
authenticatedAs: me.user.username ?? me.user.id,
|
|
1073
1090
|
accessMode: "company",
|
|
1074
1091
|
company: result.company,
|
|
@@ -1089,10 +1106,14 @@ export async function commandDoctor(client, cwd, flags) {
|
|
|
1089
1106
|
return payload;
|
|
1090
1107
|
}
|
|
1091
1108
|
catch (error) {
|
|
1109
|
+
if (error instanceof Error) {
|
|
1110
|
+
error.cliInstallation = cliInstallation;
|
|
1111
|
+
}
|
|
1092
1112
|
if (isJsonMode(flags)) {
|
|
1093
1113
|
const payload = {
|
|
1094
1114
|
ok: false,
|
|
1095
1115
|
error: formatApiError(error),
|
|
1116
|
+
cliInstallation,
|
|
1096
1117
|
};
|
|
1097
1118
|
printJson(payload);
|
|
1098
1119
|
if (error instanceof Error) {
|
package/dist/help.js
CHANGED
|
@@ -27,7 +27,7 @@ export const CLI_HELP_TEXT = `Usage:
|
|
|
27
27
|
Bind this directory to an existing Apex workspace
|
|
28
28
|
apex cancel-scan [scan-id] Cancel a running scan
|
|
29
29
|
apex status Show progress for the latest or selected scan
|
|
30
|
-
apex doctor Validate auth, repos, connections, and workspace binding
|
|
30
|
+
apex doctor Validate CLI installation, auth, repos, connections, and workspace binding
|
|
31
31
|
apex login Sign in to Apex
|
|
32
32
|
apex redeem <code> Redeem a Standard-scan code without device login
|
|
33
33
|
apex claim Link a guest workspace and findings to a Cantina account
|
|
@@ -178,7 +178,7 @@ Commands:
|
|
|
178
178
|
/workspaces List accessible workspaces for the active company
|
|
179
179
|
/cancel-scan [scan-id] Cancel a running or most recent scan
|
|
180
180
|
/status [scan-id] Show progress for the most recent or selected scan
|
|
181
|
-
/doctor Validate auth, repos, connections, and workspace binding
|
|
181
|
+
/doctor Validate CLI installation, auth, repos, connections, and workspace binding
|
|
182
182
|
/telemetry [status|enable|disable]
|
|
183
183
|
Show or change local anonymous usage telemetry settings
|
|
184
184
|
/update Update the local Apex CLI install and exit the shell
|
package/dist/mcp.js
CHANGED
|
@@ -56,6 +56,11 @@ const PR_MISSING_PROVIDER_CONNECTIONS_ERROR_PREFIX = "PR scans require connected
|
|
|
56
56
|
const MULTIPLE_ACTIVE_SCANS_ERROR = "Multiple active scans are running. Run `apex scans` to list scan IDs, then re-run `apex cancel-scan <scan-id>`.";
|
|
57
57
|
const DUPLICATE_SCAN_FORCE_GUIDANCE = "Re-run with --force to start another scan.";
|
|
58
58
|
const MULTIPLE_COMPANIES_ERROR = "Multiple companies available. Re-run with --company <id-or-handle>.";
|
|
59
|
+
const LOCAL_NOT_AUTHENTICATED_ERROR = "Apex is not authenticated. Run `apex login` explicitly, then retry this command.";
|
|
60
|
+
const LOCAL_CLAIM_PROOF_ERROR = "Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` to keep the trial results.";
|
|
61
|
+
const SERVICE_KEY_LOCAL_AUTH_GUIDANCE = "service keys authenticate the hosted MCP and REST endpoints, not the local Apex CLI";
|
|
62
|
+
const MCP_NOT_AUTHENTICATED_ERROR = "Not authenticated. If the user supplied a Cantina trial code, run `apex redeem <code>` in a terminal instead of starting device login; never put the code in an MCP tool call. Otherwise call `apex-auth-start`, ask the user to approve the device login, then call `apex-auth-wait`.";
|
|
63
|
+
const MCP_CLAIM_PROOF_ERROR = "Your guest trial session is no longer available, but its claim proof is stored on this machine. Run `apex claim` in a terminal to keep the trial results before starting another login; claim is intentionally not exposed through MCP.";
|
|
59
64
|
function resolveCwd(cwd) {
|
|
60
65
|
return path.resolve(cwd && cwd.trim().length > 0 ? cwd : process.cwd());
|
|
61
66
|
}
|
|
@@ -143,6 +148,15 @@ function successResult(summary, value) {
|
|
|
143
148
|
}
|
|
144
149
|
function formatMcpErrorMessage(toolName, error) {
|
|
145
150
|
const message = formatApiError(error);
|
|
151
|
+
if (message === LOCAL_NOT_AUTHENTICATED_ERROR) {
|
|
152
|
+
return MCP_NOT_AUTHENTICATED_ERROR;
|
|
153
|
+
}
|
|
154
|
+
if (message === LOCAL_CLAIM_PROOF_ERROR) {
|
|
155
|
+
return MCP_CLAIM_PROOF_ERROR;
|
|
156
|
+
}
|
|
157
|
+
if (message.includes(SERVICE_KEY_LOCAL_AUTH_GUIDANCE)) {
|
|
158
|
+
return `${message} ${MCP_NOT_AUTHENTICATED_ERROR}`;
|
|
159
|
+
}
|
|
146
160
|
if (message === WORKSPACE_BINDING_ERROR) {
|
|
147
161
|
return "No workspace is bound to this directory. Call `apex-workspace-use` to bind an existing workspace, or call `apex-scan` to create or resolve one first.";
|
|
148
162
|
}
|
|
@@ -165,16 +179,23 @@ function formatMcpErrorMessage(toolName, error) {
|
|
|
165
179
|
}
|
|
166
180
|
function errorResult(toolName, error) {
|
|
167
181
|
const message = formatMcpErrorMessage(toolName, error);
|
|
182
|
+
const cliInstallation = error instanceof Error && "cliInstallation" in error
|
|
183
|
+
? error.cliInstallation
|
|
184
|
+
: undefined;
|
|
185
|
+
const diagnosis = cliInstallation
|
|
186
|
+
? `\n\nCLI installation diagnosis:\n${JSON.stringify(cliInstallation, null, 2)}`
|
|
187
|
+
: "";
|
|
168
188
|
return {
|
|
169
189
|
content: [
|
|
170
190
|
{
|
|
171
191
|
type: "text",
|
|
172
|
-
text: message
|
|
192
|
+
text: `${message}${diagnosis}`,
|
|
173
193
|
},
|
|
174
194
|
],
|
|
175
195
|
structuredContent: {
|
|
176
196
|
ok: false,
|
|
177
197
|
error: message,
|
|
198
|
+
...(cliInstallation ? { cliInstallation } : {}),
|
|
178
199
|
},
|
|
179
200
|
isError: true,
|
|
180
201
|
};
|
|
@@ -199,9 +220,9 @@ async function requireAuthenticated(client) {
|
|
|
199
220
|
if (!me) {
|
|
200
221
|
const credentials = await client.getCredentials();
|
|
201
222
|
if (isClaimProofAvailable(credentials)) {
|
|
202
|
-
throw new Error(
|
|
223
|
+
throw new Error(MCP_CLAIM_PROOF_ERROR);
|
|
203
224
|
}
|
|
204
|
-
throw new Error(
|
|
225
|
+
throw new Error(MCP_NOT_AUTHENTICATED_ERROR);
|
|
205
226
|
}
|
|
206
227
|
return me;
|
|
207
228
|
}
|
|
@@ -396,7 +417,7 @@ function registerTools(server, auditSetupEnabled = false) {
|
|
|
396
417
|
});
|
|
397
418
|
server.registerTool("apex-doctor", {
|
|
398
419
|
title: "Run Apex Doctor",
|
|
399
|
-
description: "Validate auth, local source detection, workspace binding, and the planned remote-vs-local materialization strategy.",
|
|
420
|
+
description: "Validate the CLI installation, auth, local source detection, workspace binding, and the planned remote-vs-local materialization strategy.",
|
|
400
421
|
inputSchema: {
|
|
401
422
|
cwd: z.string().optional(),
|
|
402
423
|
company: z.string().optional(),
|
|
@@ -406,7 +427,6 @@ function registerTools(server, auditSetupEnabled = false) {
|
|
|
406
427
|
},
|
|
407
428
|
}, async ({ cwd, company, workspaceName, repoPaths, sourceMode }) => runTool("apex-doctor", async () => {
|
|
408
429
|
const client = new ApexApiClient();
|
|
409
|
-
await requireAuthenticated(client);
|
|
410
430
|
const targetCwd = resolveCwd(cwd);
|
|
411
431
|
const payload = await commandDoctor(client, targetCwd, buildFlags({
|
|
412
432
|
company,
|
package/dist/update.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile as execFileCallback, spawn } from "node:child_process";
|
|
2
|
-
import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, realpath, stat, writeFile } from "node:fs/promises";
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
@@ -51,19 +51,23 @@ async function readPackageVersion(packageRoot) {
|
|
|
51
51
|
}
|
|
52
52
|
return parsed.version.trim();
|
|
53
53
|
}
|
|
54
|
-
async function
|
|
54
|
+
async function canonicalPath(targetPath) {
|
|
55
|
+
return realpath(targetPath).catch(() => path.resolve(targetPath));
|
|
56
|
+
}
|
|
57
|
+
async function execText(command, args, cwd, timeout) {
|
|
55
58
|
const result = await execFile(command, args, {
|
|
56
59
|
cwd,
|
|
57
60
|
encoding: "utf8",
|
|
61
|
+
timeout,
|
|
58
62
|
});
|
|
59
63
|
return {
|
|
60
64
|
stdout: result.stdout.trim(),
|
|
61
65
|
stderr: result.stderr.trim(),
|
|
62
66
|
};
|
|
63
67
|
}
|
|
64
|
-
async function tryExecText(command, args, cwd) {
|
|
68
|
+
async function tryExecText(command, args, cwd, timeout) {
|
|
65
69
|
try {
|
|
66
|
-
return await execText(command, args, cwd);
|
|
70
|
+
return await execText(command, args, cwd, timeout);
|
|
67
71
|
}
|
|
68
72
|
catch {
|
|
69
73
|
return null;
|
|
@@ -93,25 +97,121 @@ async function detectProjectPackageManager(packageRoot) {
|
|
|
93
97
|
}
|
|
94
98
|
return null;
|
|
95
99
|
}
|
|
96
|
-
|
|
97
|
-
if (
|
|
100
|
+
function globalInstallCommand(packageManager, packageSpec = REPO_SPEC) {
|
|
101
|
+
if (packageManager === "pnpm") {
|
|
98
102
|
return {
|
|
99
103
|
command: "pnpm",
|
|
100
|
-
args: ["add", "-g",
|
|
101
|
-
label: `pnpm add -g ${
|
|
104
|
+
args: ["add", "-g", packageSpec],
|
|
105
|
+
label: `pnpm add -g ${packageSpec}`,
|
|
102
106
|
};
|
|
103
107
|
}
|
|
104
|
-
|
|
108
|
+
return {
|
|
109
|
+
command: "npm",
|
|
110
|
+
args: ["install", "-g", packageSpec],
|
|
111
|
+
label: `npm install -g ${packageSpec}`,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function globalRemoveCommand(packageManager) {
|
|
115
|
+
if (packageManager === "pnpm") {
|
|
105
116
|
return {
|
|
106
|
-
command: "
|
|
107
|
-
args: ["
|
|
108
|
-
label: `
|
|
117
|
+
command: "pnpm",
|
|
118
|
+
args: ["remove", "-g", REPO_SPEC],
|
|
119
|
+
label: `pnpm remove -g ${REPO_SPEC}`,
|
|
109
120
|
};
|
|
110
121
|
}
|
|
111
122
|
return {
|
|
112
|
-
command: "
|
|
113
|
-
args: ["
|
|
114
|
-
label: `
|
|
123
|
+
command: "npm",
|
|
124
|
+
args: ["uninstall", "-g", REPO_SPEC],
|
|
125
|
+
label: `npm uninstall -g ${REPO_SPEC}`,
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
async function getGlobalPackageManagerRoots() {
|
|
129
|
+
const candidates = await Promise.all(["pnpm", "npm"].map(async (packageManager) => {
|
|
130
|
+
const result = await tryExecText(packageManager, ["root", "-g"], undefined, 2_000);
|
|
131
|
+
if (!result?.stdout)
|
|
132
|
+
return null;
|
|
133
|
+
return { packageManager, root: path.resolve(result.stdout) };
|
|
134
|
+
}));
|
|
135
|
+
return candidates.filter((candidate) => candidate !== null);
|
|
136
|
+
}
|
|
137
|
+
async function inspectGlobalInstallations(packageRoot) {
|
|
138
|
+
const [activeRoot, roots] = await Promise.all([
|
|
139
|
+
canonicalPath(packageRoot),
|
|
140
|
+
getGlobalPackageManagerRoots(),
|
|
141
|
+
]);
|
|
142
|
+
const installations = (await Promise.all(roots.map(async ({ packageManager, root }) => {
|
|
143
|
+
const candidateRoot = path.join(root, ...PACKAGE_NAME.split("/"));
|
|
144
|
+
const version = await readPackageVersion(candidateRoot).catch(() => null);
|
|
145
|
+
if (!version)
|
|
146
|
+
return null;
|
|
147
|
+
const resolvedCandidateRoot = await canonicalPath(candidateRoot);
|
|
148
|
+
return {
|
|
149
|
+
packageManager,
|
|
150
|
+
version,
|
|
151
|
+
packageRoot: candidateRoot,
|
|
152
|
+
active: resolvedCandidateRoot === activeRoot,
|
|
153
|
+
};
|
|
154
|
+
}))).filter((installation) => installation !== null);
|
|
155
|
+
return { roots, installations };
|
|
156
|
+
}
|
|
157
|
+
async function detectGlobalInstaller(packageRoot) {
|
|
158
|
+
const { installations } = await inspectGlobalInstallations(packageRoot);
|
|
159
|
+
const owner = installations.find((installation) => installation.active);
|
|
160
|
+
if (owner) {
|
|
161
|
+
return {
|
|
162
|
+
...globalInstallCommand(owner.packageManager),
|
|
163
|
+
packageRoot: owner.packageRoot,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
throw new Error("Apex could not determine which package manager owns this installation. Run `npm install -g @cantinasecurity/apex-cli` or `pnpm add -g @cantinasecurity/apex-cli` explicitly.");
|
|
167
|
+
}
|
|
168
|
+
export async function getCliInstallDiagnostics(packageRoot = PACKAGE_ROOT) {
|
|
169
|
+
const currentVersion = await readPackageVersion(packageRoot);
|
|
170
|
+
const gitCheckout = await pathExists(path.join(packageRoot, ".git"));
|
|
171
|
+
if (gitCheckout) {
|
|
172
|
+
return {
|
|
173
|
+
currentVersion,
|
|
174
|
+
installKind: "git-checkout",
|
|
175
|
+
packageManager: null,
|
|
176
|
+
healthy: true,
|
|
177
|
+
installations: [],
|
|
178
|
+
warning: null,
|
|
179
|
+
repairCommand: null,
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
const { installations } = await inspectGlobalInstallations(packageRoot);
|
|
183
|
+
const active = installations.find((installation) => installation.active) ?? null;
|
|
184
|
+
const duplicates = installations.filter((installation) => !installation.active);
|
|
185
|
+
const newerDuplicate = duplicates.find((installation) => compareVersions(currentVersion, installation.version) < 0);
|
|
186
|
+
const mismatchedTransientCopies = active
|
|
187
|
+
? []
|
|
188
|
+
: installations.filter((installation) => installation.version !== currentVersion);
|
|
189
|
+
const warning = active && newerDuplicate
|
|
190
|
+
? `This process is Apex CLI ${currentVersion}, but ${newerDuplicate.packageManager} also has newer Apex CLI ${newerDuplicate.version}. Your shell is resolving the older installation.`
|
|
191
|
+
: active && duplicates.length > 0
|
|
192
|
+
? `Multiple global Apex CLI installations were found (${installations.map((installation) => `${installation.packageManager} ${installation.version}`).join(", ")}). They can drift and cause the shell and MCP clients to run different versions.`
|
|
193
|
+
: mismatchedTransientCopies.length > 0
|
|
194
|
+
? `This process is Apex CLI ${currentVersion} from a temporary package, but global Apex installations use ${installations.map((installation) => `${installation.packageManager} ${installation.version}`).join(", ")}. Terminal commands may resolve a different version.`
|
|
195
|
+
: null;
|
|
196
|
+
const repairCommand = active && duplicates.length > 0
|
|
197
|
+
? [
|
|
198
|
+
globalInstallCommand(active.packageManager, `${REPO_SPEC}@latest`).label,
|
|
199
|
+
...duplicates.map((installation) => globalRemoveCommand(installation.packageManager).label),
|
|
200
|
+
].join(" && ")
|
|
201
|
+
: mismatchedTransientCopies.length > 0
|
|
202
|
+
? installations
|
|
203
|
+
.map((installation) => globalInstallCommand(installation.packageManager, `${REPO_SPEC}@latest`).label)
|
|
204
|
+
.join(" && ")
|
|
205
|
+
: null;
|
|
206
|
+
const publicInstallations = installations.map(({ packageRoot: _packageRoot, ...installation }) => installation);
|
|
207
|
+
return {
|
|
208
|
+
currentVersion,
|
|
209
|
+
installKind: "package-install",
|
|
210
|
+
packageManager: active?.packageManager ?? null,
|
|
211
|
+
healthy: warning === null,
|
|
212
|
+
installations: publicInstallations,
|
|
213
|
+
warning,
|
|
214
|
+
repairCommand,
|
|
115
215
|
};
|
|
116
216
|
}
|
|
117
217
|
async function resolveInstallContext(packageRoot = PACKAGE_ROOT) {
|
|
@@ -146,23 +246,27 @@ async function resolveInstallContext(packageRoot = PACKAGE_ROOT) {
|
|
|
146
246
|
upstreamRef: upstream?.stdout ?? null,
|
|
147
247
|
updateCommands,
|
|
148
248
|
updateCommand: display.join(" && "),
|
|
249
|
+
verificationPackageRoot: null,
|
|
149
250
|
};
|
|
150
251
|
}
|
|
151
252
|
}
|
|
152
|
-
const installer = await detectGlobalInstaller();
|
|
253
|
+
const installer = await detectGlobalInstaller(packageRoot).catch(() => null);
|
|
153
254
|
return {
|
|
154
255
|
packageRoot,
|
|
155
256
|
currentVersion,
|
|
156
257
|
currentRevision: null,
|
|
157
258
|
installKind: "package-install",
|
|
158
259
|
upstreamRef: null,
|
|
159
|
-
updateCommands:
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
260
|
+
updateCommands: installer
|
|
261
|
+
? [
|
|
262
|
+
{
|
|
263
|
+
command: installer.command,
|
|
264
|
+
args: installer.args,
|
|
265
|
+
},
|
|
266
|
+
]
|
|
267
|
+
: [],
|
|
268
|
+
updateCommand: installer?.label ?? null,
|
|
269
|
+
verificationPackageRoot: installer?.packageRoot ?? null,
|
|
166
270
|
};
|
|
167
271
|
}
|
|
168
272
|
async function loadUpdateState() {
|
|
@@ -431,7 +535,7 @@ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
|
|
|
431
535
|
return status;
|
|
432
536
|
}
|
|
433
537
|
if (!context.updateCommand || context.updateCommands.length === 0) {
|
|
434
|
-
throw new Error("
|
|
538
|
+
throw new Error("Apex could not determine which package manager owns this installation. Run `npm install -g @cantinasecurity/apex-cli` or `pnpm add -g @cantinasecurity/apex-cli` explicitly.");
|
|
435
539
|
}
|
|
436
540
|
await ensureCleanGitCheckout(context);
|
|
437
541
|
if (!isJsonMode(flags)) {
|
|
@@ -445,13 +549,24 @@ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
|
|
|
445
549
|
...(await getUpdateStatus(packageRoot, true)),
|
|
446
550
|
updated: true,
|
|
447
551
|
}
|
|
448
|
-
: {
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
552
|
+
: await (async () => {
|
|
553
|
+
const installedVersion = await readPackageVersion(context.verificationPackageRoot ?? packageRoot);
|
|
554
|
+
if (status.latestVersion &&
|
|
555
|
+
compareVersions(installedVersion, status.latestVersion) < 0) {
|
|
556
|
+
const diagnostics = await getCliInstallDiagnostics(packageRoot).catch(() => null);
|
|
557
|
+
const repair = diagnostics?.repairCommand
|
|
558
|
+
? ` Run \`${diagnostics.repairCommand}\` to update the installation used by this shell.`
|
|
559
|
+
: " Update the package-manager installation selected by `which apex`.";
|
|
560
|
+
throw new Error(`The installer finished, but this Apex executable is still ${installedVersion}; expected ${status.latestVersion}.${repair}`);
|
|
561
|
+
}
|
|
562
|
+
return {
|
|
563
|
+
...status,
|
|
564
|
+
currentVersion: installedVersion,
|
|
565
|
+
currentRevision: status.latestRevision ?? status.currentRevision,
|
|
566
|
+
available: false,
|
|
567
|
+
updated: true,
|
|
568
|
+
};
|
|
569
|
+
})();
|
|
455
570
|
if (isJsonMode(flags)) {
|
|
456
571
|
printJson(payload);
|
|
457
572
|
}
|
package/package.json
CHANGED
package/skills/apex-cli/SKILL.md
CHANGED
|
@@ -76,7 +76,7 @@ Guidelines:
|
|
|
76
76
|
- Hosted repository scan triggers require `Idempotency-Key`. Reuse one stable key only for an identical transport/timeout/5xx retry. After correcting a definitive 4xx response, or for any new intended repository scan, use a new key. Apex Black stored start failures follow the support escalation rule above; never change keys to bypass them.
|
|
77
77
|
- `apex-credits` reports standard credits plus audit, fix review, and Lite scan entitlements when Bedrock returns those balances.
|
|
78
78
|
- `apex-scan` scans the provided `cwd` by default; pass `repoPaths` only when the user asks to scan explicit alternate roots.
|
|
79
|
-
- `apex-doctor` is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
79
|
+
- `apex-doctor` reports the running CLI version and detects conflicting npm/pnpm global installations. It is read-only for guest sessions and reports the local snapshot plan without reserving a workspace. For account sessions it reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
|
|
80
80
|
- Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
|
|
81
81
|
- Audit scans use `mode: "audit"` in user-facing instructions and request payloads. The legacy `ultra` mode remains accepted as an alias, but audit scans still require provider-backed GitHub, GitLab or company-enabled Forgejo sources.
|
|
82
82
|
- Lite scans use `mode: "lite"`, require Lite Scan beta access, and require provider-backed GitHub or GitLab sources without local snapshot fallback.
|