@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.
package/dist/update.js ADDED
@@ -0,0 +1,462 @@
1
+ import { execFile as execFileCallback, spawn } from "node:child_process";
2
+ import { mkdir, readFile, stat, writeFile } from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ import { promisify } from "node:util";
7
+ import { isJsonMode, isNonInteractive } from "./args.js";
8
+ import { logLine, printJson } from "./output.js";
9
+ import { confirm } from "./prompt.js";
10
+ const CHECK_INTERVAL_MS = 6 * 60 * 60 * 1000;
11
+ const PROMPT_COOLDOWN_MS = 12 * 60 * 60 * 1000;
12
+ const PACKAGE_NAME = "@cantinasecurity/apex-cli";
13
+ const REPO_SPEC = PACKAGE_NAME;
14
+ const NPM_PACKAGE_URL = `https://registry.npmjs.org/${encodeURIComponent(PACKAGE_NAME)}/latest`;
15
+ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
16
+ const UPDATE_STATE_PATH = path.join(os.homedir(), ".config", "apex", "update-check.json");
17
+ const execFile = promisify(execFileCallback);
18
+ function quoteShellArg(value) {
19
+ return /[^A-Za-z0-9_./:-]/.test(value)
20
+ ? `'${value.replace(/'/g, `'\\''`)}'`
21
+ : value;
22
+ }
23
+ function shortSha(value) {
24
+ return value ? value.slice(0, 7) : null;
25
+ }
26
+ async function pathExists(targetPath) {
27
+ try {
28
+ await stat(targetPath);
29
+ return true;
30
+ }
31
+ catch {
32
+ return false;
33
+ }
34
+ }
35
+ async function readTextFile(filePath) {
36
+ try {
37
+ return await readFile(filePath, "utf8");
38
+ }
39
+ catch {
40
+ return null;
41
+ }
42
+ }
43
+ async function readPackageVersion(packageRoot) {
44
+ const raw = await readTextFile(path.join(packageRoot, "package.json"));
45
+ if (!raw) {
46
+ throw new Error("Unable to read Apex CLI package.json");
47
+ }
48
+ const parsed = JSON.parse(raw);
49
+ if (typeof parsed.version !== "string" || parsed.version.trim().length === 0) {
50
+ throw new Error("Apex CLI package.json does not include a version");
51
+ }
52
+ return parsed.version.trim();
53
+ }
54
+ async function execText(command, args, cwd) {
55
+ const result = await execFile(command, args, {
56
+ cwd,
57
+ encoding: "utf8",
58
+ });
59
+ return {
60
+ stdout: result.stdout.trim(),
61
+ stderr: result.stderr.trim(),
62
+ };
63
+ }
64
+ async function tryExecText(command, args, cwd) {
65
+ try {
66
+ return await execText(command, args, cwd);
67
+ }
68
+ catch {
69
+ return null;
70
+ }
71
+ }
72
+ async function detectProjectPackageManager(packageRoot) {
73
+ if (await pathExists(path.join(packageRoot, "pnpm-lock.yaml"))) {
74
+ return {
75
+ command: "pnpm",
76
+ args: ["install"],
77
+ label: "pnpm install",
78
+ };
79
+ }
80
+ if (await pathExists(path.join(packageRoot, "package-lock.json"))) {
81
+ return {
82
+ command: "npm",
83
+ args: ["install"],
84
+ label: "npm install",
85
+ };
86
+ }
87
+ if (await pathExists(path.join(packageRoot, "yarn.lock"))) {
88
+ return {
89
+ command: "yarn",
90
+ args: ["install"],
91
+ label: "yarn install",
92
+ };
93
+ }
94
+ return null;
95
+ }
96
+ async function detectGlobalInstaller() {
97
+ if (await tryExecText("pnpm", ["--version"])) {
98
+ return {
99
+ command: "pnpm",
100
+ args: ["add", "-g", REPO_SPEC],
101
+ label: `pnpm add -g ${REPO_SPEC}`,
102
+ };
103
+ }
104
+ if (await tryExecText("npm", ["--version"])) {
105
+ return {
106
+ command: "npm",
107
+ args: ["install", "-g", REPO_SPEC],
108
+ label: `npm install -g ${REPO_SPEC}`,
109
+ };
110
+ }
111
+ return {
112
+ command: "pnpm",
113
+ args: ["add", "-g", REPO_SPEC],
114
+ label: `pnpm add -g ${REPO_SPEC}`,
115
+ };
116
+ }
117
+ async function resolveInstallContext(packageRoot = PACKAGE_ROOT) {
118
+ const currentVersion = await readPackageVersion(packageRoot);
119
+ const gitDirExists = await pathExists(path.join(packageRoot, ".git"));
120
+ if (gitDirExists) {
121
+ const head = await tryExecText("git", ["rev-parse", "HEAD"], packageRoot);
122
+ if (head?.stdout) {
123
+ const upstream = await tryExecText("git", ["rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"], packageRoot);
124
+ const packageManager = await detectProjectPackageManager(packageRoot);
125
+ const updateCommands = [
126
+ {
127
+ command: "git",
128
+ args: ["pull", "--ff-only"],
129
+ cwd: packageRoot,
130
+ },
131
+ ];
132
+ const display = [`git -C ${quoteShellArg(packageRoot)} pull --ff-only`];
133
+ if (packageManager) {
134
+ updateCommands.push({
135
+ command: packageManager.command,
136
+ args: packageManager.args,
137
+ cwd: packageRoot,
138
+ });
139
+ display.push(packageManager.label);
140
+ }
141
+ return {
142
+ packageRoot,
143
+ currentVersion,
144
+ currentRevision: head.stdout,
145
+ installKind: "git-checkout",
146
+ upstreamRef: upstream?.stdout ?? null,
147
+ updateCommands,
148
+ updateCommand: display.join(" && "),
149
+ };
150
+ }
151
+ }
152
+ const installer = await detectGlobalInstaller();
153
+ return {
154
+ packageRoot,
155
+ currentVersion,
156
+ currentRevision: null,
157
+ installKind: "package-install",
158
+ upstreamRef: null,
159
+ updateCommands: [
160
+ {
161
+ command: installer.command,
162
+ args: installer.args,
163
+ },
164
+ ],
165
+ updateCommand: installer.label,
166
+ };
167
+ }
168
+ async function loadUpdateState() {
169
+ const raw = await readTextFile(UPDATE_STATE_PATH);
170
+ if (!raw) {
171
+ return { version: 1 };
172
+ }
173
+ try {
174
+ const parsed = JSON.parse(raw);
175
+ return {
176
+ version: 1,
177
+ installKind: parsed.installKind,
178
+ upstreamRef: parsed.upstreamRef ?? null,
179
+ lastCheckedAt: parsed.lastCheckedAt ?? null,
180
+ latestRevision: parsed.latestRevision ?? null,
181
+ latestVersion: parsed.latestVersion ?? null,
182
+ lastPromptedAt: parsed.lastPromptedAt ?? null,
183
+ lastPromptedRevision: parsed.lastPromptedRevision ?? null,
184
+ lastPromptedVersion: parsed.lastPromptedVersion ?? null,
185
+ };
186
+ }
187
+ catch {
188
+ return { version: 1 };
189
+ }
190
+ }
191
+ async function saveUpdateState(state) {
192
+ await mkdir(path.dirname(UPDATE_STATE_PATH), { recursive: true, mode: 0o700 });
193
+ await writeFile(UPDATE_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`, {
194
+ encoding: "utf8",
195
+ mode: 0o600,
196
+ });
197
+ }
198
+ function canReuseCachedRemote(state, context, now = Date.now()) {
199
+ if (!state.lastCheckedAt) {
200
+ return false;
201
+ }
202
+ const checkedAt = Date.parse(state.lastCheckedAt);
203
+ if (!Number.isFinite(checkedAt) || now - checkedAt > CHECK_INTERVAL_MS) {
204
+ return false;
205
+ }
206
+ return (state.installKind === context.installKind &&
207
+ (state.upstreamRef ?? null) === context.upstreamRef);
208
+ }
209
+ async function fetchLatestPublishedVersion() {
210
+ const response = await fetch(NPM_PACKAGE_URL, {
211
+ headers: {
212
+ "User-Agent": "apex-cli",
213
+ Accept: "application/json",
214
+ },
215
+ });
216
+ if (!response.ok) {
217
+ return null;
218
+ }
219
+ try {
220
+ const parsed = JSON.parse(await response.text());
221
+ return typeof parsed.version === "string" && parsed.version.trim().length > 0
222
+ ? parsed.version.trim()
223
+ : null;
224
+ }
225
+ catch {
226
+ return null;
227
+ }
228
+ }
229
+ async function fetchRemoteUpdateInfo(context) {
230
+ if (context.installKind === "git-checkout" && context.upstreamRef) {
231
+ const [remoteName, ...branchParts] = context.upstreamRef.split("/");
232
+ const branchName = branchParts.join("/");
233
+ if (!remoteName || !branchName) {
234
+ return null;
235
+ }
236
+ const remote = await execText("git", ["ls-remote", "--exit-code", remoteName, `refs/heads/${branchName}`], context.packageRoot);
237
+ const latestRevision = remote.stdout.split(/\s+/)[0] ?? null;
238
+ return {
239
+ latestRevision,
240
+ latestVersion: null,
241
+ checkedAt: new Date().toISOString(),
242
+ };
243
+ }
244
+ const latestVersion = await fetchLatestPublishedVersion();
245
+ if (!latestVersion) {
246
+ return null;
247
+ }
248
+ return {
249
+ latestVersion,
250
+ latestRevision: null,
251
+ checkedAt: new Date().toISOString(),
252
+ };
253
+ }
254
+ export function compareVersions(left, right) {
255
+ const leftParts = left.split(".").map((part) => Number.parseInt(part, 10) || 0);
256
+ const rightParts = right.split(".").map((part) => Number.parseInt(part, 10) || 0);
257
+ const maxLength = Math.max(leftParts.length, rightParts.length);
258
+ for (let index = 0; index < maxLength; index += 1) {
259
+ const leftValue = leftParts[index] ?? 0;
260
+ const rightValue = rightParts[index] ?? 0;
261
+ if (leftValue < rightValue) {
262
+ return -1;
263
+ }
264
+ if (leftValue > rightValue) {
265
+ return 1;
266
+ }
267
+ }
268
+ return 0;
269
+ }
270
+ function hasAvailableUpdate(context, remote) {
271
+ if (!remote) {
272
+ return false;
273
+ }
274
+ if (context.currentRevision && remote.latestRevision) {
275
+ return context.currentRevision !== remote.latestRevision;
276
+ }
277
+ if (remote.latestVersion) {
278
+ return compareVersions(context.currentVersion, remote.latestVersion) < 0;
279
+ }
280
+ return false;
281
+ }
282
+ async function getRemoteUpdateInfo(context, forceRefresh = false) {
283
+ const state = await loadUpdateState();
284
+ if (!forceRefresh && canReuseCachedRemote(state, context)) {
285
+ return {
286
+ latestRevision: state.latestRevision ?? null,
287
+ latestVersion: state.latestVersion ?? null,
288
+ checkedAt: state.lastCheckedAt ?? new Date().toISOString(),
289
+ };
290
+ }
291
+ const remote = await fetchRemoteUpdateInfo(context).catch(() => null);
292
+ if (!remote) {
293
+ return null;
294
+ }
295
+ await saveUpdateState({
296
+ ...state,
297
+ version: 1,
298
+ installKind: context.installKind,
299
+ upstreamRef: context.upstreamRef,
300
+ lastCheckedAt: remote.checkedAt,
301
+ latestRevision: remote.latestRevision,
302
+ latestVersion: remote.latestVersion,
303
+ });
304
+ return remote;
305
+ }
306
+ export async function getUpdateStatus(packageRoot = PACKAGE_ROOT, forceRefresh = false) {
307
+ const context = await resolveInstallContext(packageRoot);
308
+ const remote = await getRemoteUpdateInfo(context, forceRefresh);
309
+ return {
310
+ installKind: context.installKind,
311
+ currentVersion: context.currentVersion,
312
+ currentRevision: context.currentRevision,
313
+ latestVersion: remote?.latestVersion ?? null,
314
+ latestRevision: remote?.latestRevision ?? null,
315
+ available: hasAvailableUpdate(context, remote),
316
+ updateCommand: context.updateCommand,
317
+ updated: false,
318
+ };
319
+ }
320
+ async function ensureCleanGitCheckout(context) {
321
+ if (context.installKind !== "git-checkout") {
322
+ return;
323
+ }
324
+ if (!context.upstreamRef) {
325
+ throw new Error("This Apex CLI checkout does not track a remote branch yet. Configure an upstream before running `apex update`.");
326
+ }
327
+ const status = await execText("git", ["status", "--porcelain"], context.packageRoot);
328
+ if (status.stdout.length > 0) {
329
+ throw new Error("The Apex CLI checkout has uncommitted changes. Commit or stash them before running `apex update`.");
330
+ }
331
+ }
332
+ function runInteractiveCommand(command, args, cwd) {
333
+ return new Promise((resolve, reject) => {
334
+ const child = spawn(command, args, {
335
+ cwd,
336
+ stdio: "inherit",
337
+ });
338
+ child.on("error", reject);
339
+ child.on("close", (code) => {
340
+ if (code === 0) {
341
+ resolve();
342
+ return;
343
+ }
344
+ reject(new Error(`Command failed (${code ?? "unknown"}): ${command} ${args.join(" ")}`));
345
+ });
346
+ });
347
+ }
348
+ function shouldPromptForUpdate(state, status, now = Date.now()) {
349
+ if (!status.available) {
350
+ return false;
351
+ }
352
+ if (!state.lastPromptedAt) {
353
+ return true;
354
+ }
355
+ const lastPromptedAt = Date.parse(state.lastPromptedAt);
356
+ if (!Number.isFinite(lastPromptedAt) || now - lastPromptedAt > PROMPT_COOLDOWN_MS) {
357
+ return true;
358
+ }
359
+ if (status.latestRevision && state.lastPromptedRevision !== status.latestRevision) {
360
+ return true;
361
+ }
362
+ if (status.latestVersion && state.lastPromptedVersion !== status.latestVersion) {
363
+ return true;
364
+ }
365
+ return false;
366
+ }
367
+ function canPromptForUpdate(flags, command) {
368
+ if (isJsonMode(flags) || isNonInteractive(flags)) {
369
+ return false;
370
+ }
371
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
372
+ return false;
373
+ }
374
+ return !["help", "update"].includes(command ?? "");
375
+ }
376
+ function formatUpdateSummary(status) {
377
+ const currentRevision = shortSha(status.currentRevision);
378
+ const latestRevision = shortSha(status.latestRevision);
379
+ if (currentRevision && latestRevision) {
380
+ return `${currentRevision} -> ${latestRevision}`;
381
+ }
382
+ if (status.latestVersion) {
383
+ return `${status.currentVersion} -> ${status.latestVersion}`;
384
+ }
385
+ return status.currentVersion;
386
+ }
387
+ export async function maybePromptForUpdate(flags, command) {
388
+ if (!canPromptForUpdate(flags, command)) {
389
+ return false;
390
+ }
391
+ const status = await getUpdateStatus().catch(() => null);
392
+ if (!status?.available || !status.updateCommand) {
393
+ return false;
394
+ }
395
+ const state = await loadUpdateState();
396
+ if (!shouldPromptForUpdate(state, status)) {
397
+ return false;
398
+ }
399
+ const promptText = `Apex CLI update available (${formatUpdateSummary(status)}). Install now?`;
400
+ const approved = await confirm(promptText, true);
401
+ await saveUpdateState({
402
+ ...state,
403
+ version: 1,
404
+ installKind: status.installKind,
405
+ lastPromptedAt: new Date().toISOString(),
406
+ lastPromptedRevision: status.latestRevision,
407
+ lastPromptedVersion: status.latestVersion,
408
+ });
409
+ if (!approved) {
410
+ if (!isJsonMode(flags)) {
411
+ process.stderr.write(`Update command: ${status.updateCommand}\n`);
412
+ }
413
+ return false;
414
+ }
415
+ await commandUpdate(flags);
416
+ return true;
417
+ }
418
+ export async function commandUpdate(flags, packageRoot = PACKAGE_ROOT) {
419
+ const context = await resolveInstallContext(packageRoot);
420
+ if (context.installKind === "git-checkout" && !context.upstreamRef) {
421
+ throw new Error("This Apex CLI checkout does not track a remote branch yet. Configure an upstream before running `apex update`.");
422
+ }
423
+ const status = await getUpdateStatus(packageRoot, true);
424
+ if (!status.available) {
425
+ if (isJsonMode(flags)) {
426
+ printJson(status);
427
+ }
428
+ else {
429
+ logLine("Apex CLI is already up to date.", flags);
430
+ }
431
+ return status;
432
+ }
433
+ if (!context.updateCommand || context.updateCommands.length === 0) {
434
+ throw new Error("No update command is available for this Apex CLI installation.");
435
+ }
436
+ await ensureCleanGitCheckout(context);
437
+ if (!isJsonMode(flags)) {
438
+ logLine(`Updating Apex CLI with ${context.updateCommand}`, flags);
439
+ }
440
+ for (const step of context.updateCommands) {
441
+ await runInteractiveCommand(step.command, step.args, step.cwd);
442
+ }
443
+ const payload = context.installKind === "git-checkout"
444
+ ? {
445
+ ...(await getUpdateStatus(packageRoot, true)),
446
+ updated: true,
447
+ }
448
+ : {
449
+ ...status,
450
+ currentVersion: status.latestVersion ?? status.currentVersion,
451
+ currentRevision: status.latestRevision ?? status.currentRevision,
452
+ available: false,
453
+ updated: true,
454
+ };
455
+ if (isJsonMode(flags)) {
456
+ printJson(payload);
457
+ }
458
+ else {
459
+ logLine("Apex CLI updated. Re-run `apex` to use the latest version. Re-run `apex setup` to refresh copied Codex or Claude skill files.", flags);
460
+ }
461
+ return payload;
462
+ }
@@ -0,0 +1,7 @@
1
+ import { readFileSync } from "node:fs";
2
+ const packageJsonUrl = new URL("../package.json", import.meta.url);
3
+ const packageJson = JSON.parse(readFileSync(packageJsonUrl, "utf8"));
4
+ if (typeof packageJson.version !== "string" || packageJson.version.trim().length === 0) {
5
+ throw new Error("Apex CLI package.json does not include a version");
6
+ }
7
+ export const APEX_CLI_VERSION = packageJson.version.trim();
@@ -0,0 +1,30 @@
1
+ import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises";
2
+ import path from "node:path";
3
+ function getWorkspaceDir(cwd) {
4
+ return path.join(cwd, ".apex");
5
+ }
6
+ function getWorkspaceBindingPath(cwd) {
7
+ return path.join(getWorkspaceDir(cwd), "workspace.json");
8
+ }
9
+ export async function loadWorkspaceBinding(cwd) {
10
+ try {
11
+ const contents = await readFile(getWorkspaceBindingPath(cwd), "utf8");
12
+ return JSON.parse(contents);
13
+ }
14
+ catch {
15
+ return null;
16
+ }
17
+ }
18
+ export async function saveWorkspaceBinding(cwd, binding) {
19
+ const workspaceDir = getWorkspaceDir(cwd);
20
+ const bindingPath = getWorkspaceBindingPath(cwd);
21
+ await mkdir(workspaceDir, { recursive: true, mode: 0o700 });
22
+ await writeFile(bindingPath, `${JSON.stringify(binding, null, 2)}\n`, {
23
+ encoding: "utf8",
24
+ mode: 0o600,
25
+ });
26
+ await chmod(bindingPath, 0o600);
27
+ }
28
+ export async function clearWorkspaceBinding(cwd) {
29
+ await rm(getWorkspaceBindingPath(cwd), { force: true });
30
+ }
@@ -0,0 +1,50 @@
1
+ export async function fetchCompanyCredits(client, companyId) {
2
+ return client.request(`/api/cli/v1/companies/${encodeURIComponent(companyId)}/credits`);
3
+ }
4
+ export async function fetchCompanyWorkspaces(client, companyId) {
5
+ return client.request(`/api/cli/v1/companies/${encodeURIComponent(companyId)}/workspaces`);
6
+ }
7
+ function normalizeWorkspaceRef(ref) {
8
+ return ref.trim().toLowerCase();
9
+ }
10
+ export function findWorkspaceByRef(workspaces, ref) {
11
+ const normalizedRef = normalizeWorkspaceRef(ref);
12
+ if (!normalizedRef) {
13
+ return null;
14
+ }
15
+ const exactMatches = workspaces.filter((workspace) => {
16
+ const workspaceName = normalizeWorkspaceRef(workspace.name);
17
+ const workspacePrefix = normalizeWorkspaceRef(workspace.prefix ?? "");
18
+ return (normalizeWorkspaceRef(workspace.workspaceId) === normalizedRef ||
19
+ workspaceName === normalizedRef ||
20
+ (workspacePrefix.length > 0 && workspacePrefix === normalizedRef));
21
+ });
22
+ if (exactMatches.length === 1) {
23
+ return exactMatches[0];
24
+ }
25
+ if (exactMatches.length > 1) {
26
+ return null;
27
+ }
28
+ const prefixMatches = workspaces.filter((workspace) => {
29
+ const workspaceName = normalizeWorkspaceRef(workspace.name);
30
+ const workspacePrefix = normalizeWorkspaceRef(workspace.prefix ?? "");
31
+ return (workspaceName.startsWith(normalizedRef) ||
32
+ (workspacePrefix.length > 0 && workspacePrefix.startsWith(normalizedRef)));
33
+ });
34
+ return prefixMatches.length === 1 ? prefixMatches[0] : null;
35
+ }
36
+ export function createWorkspaceBindingFromSummary(params) {
37
+ const isSameWorkspace = params.currentBinding?.workspaceId === params.workspace.workspaceId;
38
+ return {
39
+ version: 1,
40
+ companyId: params.companyId,
41
+ workspaceId: params.workspace.workspaceId,
42
+ workspaceName: params.workspace.name,
43
+ createdAt: isSameWorkspace && params.currentBinding?.createdAt
44
+ ? params.currentBinding.createdAt
45
+ : new Date().toISOString(),
46
+ lastSyncedAt: new Date().toISOString(),
47
+ lastScanId: isSameWorkspace ? params.currentBinding?.lastScanId ?? null : null,
48
+ lastScanUrl: isSameWorkspace ? params.currentBinding?.lastScanUrl ?? null : null,
49
+ };
50
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@cantinasecurity/apex-cli",
3
+ "version": "0.1.0",
4
+ "description": "Standalone CLI and MCP server for Apex.",
5
+ "private": false,
6
+ "type": "module",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "bin": {
11
+ "apex": "./pkg-bin/apex.js",
12
+ "apex-mcp": "./pkg-bin/apex-mcp.js"
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "pkg-bin",
17
+ "skills",
18
+ ".claude/skills",
19
+ "README.md"
20
+ ],
21
+ "engines": {
22
+ "node": ">=20"
23
+ },
24
+ "dependencies": {
25
+ "@modelcontextprotocol/sdk": "^1.29.0",
26
+ "ignore": "^7.0.5",
27
+ "tar": "^7.4.3",
28
+ "zod": "^4.3.6"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "^22.18.6",
32
+ "tsx": "^4.21.0",
33
+ "typescript": "^5.9.3",
34
+ "vitest": "^2.1.9"
35
+ },
36
+ "scripts": {
37
+ "apex": "tsx src/apex.ts",
38
+ "build": "node -e \"require('node:fs').rmSync('dist', { recursive: true, force: true })\" && tsc -p tsconfig.build.json",
39
+ "mcp": "tsx src/mcp-main.ts",
40
+ "typecheck": "tsc --noEmit",
41
+ "test": "vitest run"
42
+ }
43
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/mcp-main.js";
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import "../dist/apex.js";
@@ -0,0 +1,29 @@
1
+ ---
2
+ name: apex-cli
3
+ description: Use when a user wants to start Apex scans, inspect findings, bind workspaces, check scan status, or troubleshoot Apex auth/provider setup through the Apex MCP server instead of shelling out manually.
4
+ ---
5
+
6
+ # Apex CLI
7
+
8
+ This skill is bundled with Apex CLI and can be installed into Codex with `apex setup codex`.
9
+
10
+ Prefer the Apex MCP tools over running `apex` in the shell when the server is available.
11
+
12
+ Workflow:
13
+
14
+ 1. Start with `apex-auth-status`.
15
+ 2. If Apex is unauthenticated, call `apex-auth-start`, ask the user to approve the device-login URL, then call `apex-auth-wait` with the returned `deviceCode`.
16
+ 3. For repository work, pass `cwd` explicitly.
17
+ 4. Call `apex-doctor` before starting a scan if workspace binding or source planning might be unclear.
18
+ 5. If the directory is not bound to the right workspace, call `apex-workspaces` and `apex-workspace-use`.
19
+ 6. Use `apex-scan`, `apex-status`, `apex-scans`, `apex-findings`, and `apex-export-findings` for the scan lifecycle.
20
+
21
+ Guidelines:
22
+
23
+ - Do not rely on interactive CLI prompts. The MCP tools are intentionally non-interactive.
24
+ - `apex-doctor` reports whether Apex will use remote materialization or a local snapshot upload for each selected source.
25
+ - Apex can scan plain local directories and dirty git worktrees without provider connections by using local snapshot uploads.
26
+ - `apex-workspace-use` accepts a workspace name, prefix, or ID.
27
+ - Use `sourceMode: "remote"` only when the user explicitly wants to forbid local snapshot fallbacks.
28
+ - Use `force: true` on `apex-scan` only when the user explicitly wants to replace or overlap an active scan.
29
+ - Prefer `apex-findings` for quick inspection and `apex-export-findings` when the user needs a file artifact.