@callstack/apex 0.2.1 → 0.3.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/package.json CHANGED
@@ -1,22 +1,39 @@
1
1
  {
2
2
  "name": "@callstack/apex",
3
- "version": "0.2.1",
3
+ "version": "0.3.0",
4
4
  "description": "Detects the AI coding assistants installed on your machine and configures them to use Callstack Apex.",
5
5
  "license": "MIT",
6
+ "keywords": [
7
+ "callstack",
8
+ "apex",
9
+ "ai"
10
+ ],
11
+ "type": "module",
6
12
  "bin": {
7
13
  "apex": "bin/apex.js"
8
14
  },
9
15
  "files": [
10
16
  "bin",
11
- "src",
17
+ "dist",
12
18
  "README.md"
13
19
  ],
14
20
  "engines": {
15
- "node": ">=18"
21
+ "node": ">=22"
16
22
  },
17
- "keywords": [
18
- "callstack",
19
- "apex",
20
- "ai"
21
- ]
23
+ "scripts": {
24
+ "build": "tsdown",
25
+ "pretest": "npm run build",
26
+ "prepack": "npm run build",
27
+ "test": "node --test",
28
+ "check": "for f in src/*.js bin/apex.js; do node --check \"$f\" || exit 1; done && npm run build"
29
+ },
30
+ "devDependencies": {
31
+ "@clack/prompts": "1.8.1",
32
+ "jsonc-parser": "3.3.1",
33
+ "smol-toml": "1.4.2",
34
+ "tsdown": "^0.22.14"
35
+ },
36
+ "publishConfig": {
37
+ "access": "public"
38
+ }
22
39
  }
@@ -1,54 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const fs = require('node:fs');
5
- const { readJsonIfExists, pathExists, backupFile } = require('../util/fsHelpers');
6
-
7
- const id = 'ai-sdk';
8
- const name = 'Vercel AI SDK / Eve (programmatic)';
9
-
10
- function projectDeps(cwd) {
11
- const pkg = readJsonIfExists(path.join(cwd, 'package.json'));
12
- if (!pkg) return null;
13
- return { ...pkg.dependencies, ...pkg.devDependencies };
14
- }
15
-
16
- function detect({ cwd }) {
17
- const deps = projectDeps(cwd);
18
- if (!deps) return false;
19
- return Boolean(deps['ai'] || deps['@ai-sdk/openai'] || deps['eve']);
20
- }
21
-
22
- function apply({ cwd, apiKey, dryRun, log }) {
23
- const deps = projectDeps(cwd) || {};
24
- const envPath = path.join(cwd, '.env');
25
- const existingEnv = pathExists(envPath) ? fs.readFileSync(envPath, 'utf8') : '';
26
-
27
- if (apiKey && !/^APEX_API_KEY=/m.test(existingEnv)) {
28
- const separator = existingEnv && !existingEnv.endsWith('\n') ? '\n' : '';
29
- const nextEnv = `${existingEnv}${separator}APEX_API_KEY=${apiKey}\n`;
30
- if (!dryRun) {
31
- backupFile(envPath);
32
- fs.writeFileSync(envPath, nextEnv);
33
- }
34
- log(`wrote APEX_API_KEY to ${envPath} (make sure .env is gitignored)`);
35
- } else if (!apiKey) {
36
- log(`no API key provided — add APEX_API_KEY manually to ${envPath}`);
37
- } else {
38
- log(`${envPath} already has APEX_API_KEY, left untouched`);
39
- }
40
-
41
- log('connector snippet (Vercel AI SDK):');
42
- log(" import { createOpenAI } from '@ai-sdk/openai';");
43
- log(' const apex = createOpenAI({ apiKey: process.env.APEX_API_KEY, baseURL: "https://api.callstack.ai/v1", name: "callstack" });');
44
- log(' // use apex(\'callstack/Apex\') with generateText / streamText');
45
- log(' // reasoning effort (none/low/medium/xhigh): pass providerOptions: { callstack: { reasoningEffort: "medium" } }');
46
-
47
- if (deps['eve']) {
48
- log('Eve note: set modelContextWindowTokens: 262_144 on defineAgent, or compaction will fail to compile.');
49
- }
50
-
51
- return { configured: Boolean(apiKey) };
52
- }
53
-
54
- module.exports = { id, name, detect, apply };
@@ -1,37 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const { commandExists, dirExists, homeDir } = require('../util/detect');
5
- const { readJsonIfExists, backupFile, writeJson } = require('../util/fsHelpers');
6
-
7
- const id = 'claude-code';
8
- const name = 'Claude Code';
9
-
10
- function detect() {
11
- return commandExists('claude') || dirExists(homeDir(), '.claude');
12
- }
13
-
14
- function apply({ apiKey, dryRun, log }) {
15
- const settingsPath = path.join(homeDir(), '.claude', 'settings.json');
16
- const settings = readJsonIfExists(settingsPath) || {};
17
- settings.env = settings.env || {};
18
- settings.env.CLAUDE_CODE_ATTRIBUTION_HEADER = '0';
19
- settings.env.ANTHROPIC_BASE_URL = 'https://api.callstack.ai';
20
- settings.env.ANTHROPIC_MODEL = 'callstack/Apex';
21
- if (apiKey) {
22
- settings.env.ANTHROPIC_AUTH_TOKEN = apiKey;
23
- }
24
-
25
- if (!dryRun) {
26
- backupFile(settingsPath);
27
- writeJson(settingsPath, settings);
28
- }
29
- log(`wrote ${settingsPath}`);
30
- if (!apiKey) {
31
- log('no API key provided — set ANTHROPIC_AUTH_TOKEN in that file manually');
32
- }
33
-
34
- return { configured: true };
35
- }
36
-
37
- module.exports = { id, name, detect, apply };
@@ -1,56 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const { commandExists, dirExists, homeDir } = require('../util/detect');
5
- const { readTextIfExists, backupFile, writeFileEnsuringDir } = require('../util/fsHelpers');
6
- const { upsertTomlSection } = require('../util/toml');
7
-
8
- const id = 'codex';
9
- const name = 'Codex';
10
-
11
- function detect() {
12
- return commandExists('codex') || dirExists(homeDir(), '.codex');
13
- }
14
-
15
- function apply({ dryRun, log }) {
16
- const home = homeDir();
17
- const configPath = path.join(home, '.codex', 'config.toml');
18
- const profilePath = path.join(home, '.codex', 'callstack_ai.config.toml');
19
-
20
- const providerBody = [
21
- 'name = "callstack.ai"',
22
- 'base_url = "https://api.callstack.ai/v1"',
23
- 'env_key = "CALLSTACK_AUTH_TOKEN"',
24
- 'wire_api = "responses"',
25
- 'requires_openai_auth = false',
26
- '',
27
- ].join('\n');
28
-
29
- const existingConfig = readTextIfExists(configPath);
30
- const nextConfig = upsertTomlSection(existingConfig, 'model_providers.callstack_ai', providerBody);
31
- if (!dryRun) {
32
- backupFile(configPath);
33
- writeFileEnsuringDir(configPath, nextConfig);
34
- }
35
- log(`wrote ${configPath}`);
36
-
37
- const profileContents = [
38
- 'model_provider = "callstack_ai"',
39
- 'model = "callstack/Apex"',
40
- 'model_context_window = 262144',
41
- 'model_max_output_tokens = 16384',
42
- 'model_reasoning_effort = "medium"',
43
- '',
44
- ].join('\n');
45
- if (!dryRun) {
46
- backupFile(profilePath);
47
- writeFileEnsuringDir(profilePath, profileContents);
48
- }
49
- log(`wrote ${profilePath}`);
50
-
51
- log('run with: export CALLSTACK_AUTH_TOKEN="<your Apex API key>" && codex -p callstack_ai');
52
-
53
- return { configured: true };
54
- }
55
-
56
- module.exports = { id, name, detect, apply };
@@ -1,30 +0,0 @@
1
- 'use strict';
2
-
3
- const { commandExists, dirExists, anyAppPathExists, homeDir } = require('../util/detect');
4
-
5
- const id = 'cursor';
6
- const name = 'Cursor';
7
-
8
- function detect() {
9
- return (
10
- commandExists('cursor') ||
11
- dirExists(homeDir(), '.cursor') ||
12
- anyAppPathExists(['/Applications/Cursor.app'])
13
- );
14
- }
15
-
16
- // Cursor's model configuration lives behind its GUI and isn't a documented file
17
- // format, so this can't be written automatically — print the manual steps instead.
18
- function apply({ log }) {
19
- log('Cursor requires manual setup (no config file to write automatically):');
20
- log(' 1. Cursor Settings -> Models');
21
- log(' 2. Enable API Keys -> OpenAI API Key, confirm the prompt');
22
- log(' 3. Paste your Apex API key, and set "Override OpenAI Base URL" to https://api.callstack.ai/v1');
23
- log(' 4. In Models, "Add Custom Model" -> enter callstack/Apex -> enable it');
24
- log(' 5. Cmd+L -> pick "Apex" from the model dropdown');
25
- log('Note: Cursor has no way to declare model capabilities (tool calling, vision, reasoning) for a custom model — it infers them, so the steps above are all there is to configure.');
26
-
27
- return { configured: false, manual: true };
28
- }
29
-
30
- module.exports = { id, name, detect, apply };
@@ -1,11 +0,0 @@
1
- 'use strict';
2
-
3
- module.exports = [
4
- require('./claudeCode'),
5
- require('./codex'),
6
- require('./opencode'),
7
- require('./pi'),
8
- require('./cursor'),
9
- require('./vscodeCopilot'),
10
- require('./aiSdk'),
11
- ];
@@ -1,103 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const { commandExists, commandVersion, dirExists, homeDir } = require('../util/detect');
5
- const { readJsonIfExists, backupFile, writeJson, pathExists } = require('../util/fsHelpers');
6
-
7
- const id = 'opencode';
8
- const name = 'OpenCode';
9
-
10
- function detect() {
11
- return commandExists('opencode') || dirExists(homeDir(), '.config', 'opencode');
12
- }
13
-
14
- // OpenCode 2 renamed the config's top-level `provider` map to `providers` and
15
- // changed the per-model capability field names (tool_call -> capabilities.tools,
16
- // etc). It's still invoked as `opencode`, so version is the only way to tell
17
- // v1 and v2 apart. If detection fails, default to the v1 shape: OpenCode 2
18
- // still reads and normalizes v1-style config, but v1 installs can't read v2-only fields.
19
- function isV2() {
20
- const version = commandVersion('opencode', ['--version']);
21
- return Boolean(version && version.major >= 2);
22
- }
23
-
24
- function modelConfigV1() {
25
- return {
26
- name: 'Apex',
27
- reasoning: true,
28
- tool_call: true,
29
- attachment: true,
30
- modalities: { input: ['text', 'image'], output: ['text'] },
31
- limit: { context: 262144, input: 240000, output: 16384 },
32
- options: { reasoningEffort: 'medium' },
33
- variants: {
34
- none: { reasoningEffort: 'none' },
35
- low: { reasoningEffort: 'low' },
36
- medium: { reasoningEffort: 'medium' },
37
- xhigh: { reasoningEffort: 'xhigh' },
38
- },
39
- };
40
- }
41
-
42
- function modelConfigV2() {
43
- return {
44
- name: 'Apex',
45
- capabilities: { tools: true, input: ['text', 'image'], output: ['text'] },
46
- limit: { context: 262144, input: 240000, output: 16384 },
47
- settings: { reasoningEffort: 'medium' },
48
- variants: [
49
- { id: 'none', settings: { reasoningEffort: 'none' } },
50
- { id: 'low', settings: { reasoningEffort: 'low' } },
51
- { id: 'medium', settings: { reasoningEffort: 'medium' } },
52
- { id: 'xhigh', settings: { reasoningEffort: 'xhigh' } },
53
- ],
54
- };
55
- }
56
-
57
- function apply({ apiKey, dryRun, log }) {
58
- const home = homeDir();
59
- const configPath = path.join(home, '.config', 'opencode', 'opencode.json');
60
- const authPath = path.join(home, '.local', 'share', 'opencode', 'auth.json');
61
- const v2 = isV2();
62
-
63
- const config = readJsonIfExists(configPath) || { $schema: 'https://opencode.ai/config.json' };
64
-
65
- if (v2) {
66
- config.providers = config.providers || {};
67
- config.providers['callstack.ai'] = {
68
- name: 'callstack.ai',
69
- package: '@opencode/ai/providers/openai-compatible',
70
- settings: { baseURL: 'https://api.callstack.ai/v1' },
71
- models: { 'callstack/Apex': modelConfigV2() },
72
- };
73
- } else {
74
- config.provider = config.provider || {};
75
- config.provider['callstack.ai'] = {
76
- npm: '@ai-sdk/openai-compatible',
77
- name: 'callstack.ai',
78
- options: { baseURL: 'https://api.callstack.ai/v1' },
79
- models: { 'callstack/Apex': modelConfigV1() },
80
- };
81
- }
82
-
83
- if (!dryRun) {
84
- backupFile(configPath);
85
- writeJson(configPath, config);
86
- }
87
- log(`wrote ${configPath} (OpenCode ${v2 ? 'v2' : 'v1'} config format)`);
88
-
89
- if (v2) {
90
- log('run `/connect`, select `callstack.ai` -> "Manually enter API Key", then paste your Apex API key');
91
- } else if (pathExists(authPath)) {
92
- log(`skipped ${authPath} (already exists — leaving your existing credentials untouched)`);
93
- } else if (apiKey) {
94
- if (!dryRun) writeJson(authPath, { 'callstack.ai': { type: 'api', key: apiKey } });
95
- log(`wrote ${authPath}`);
96
- } else {
97
- log(`skipped ${authPath} (no API key provided — create it manually, see docs)`);
98
- }
99
-
100
- return { configured: true };
101
- }
102
-
103
- module.exports = { id, name, detect, apply };
@@ -1,55 +0,0 @@
1
- 'use strict';
2
-
3
- const path = require('node:path');
4
- const { dirExists, homeDir } = require('../util/detect');
5
- const { readJsonIfExists, backupFile, writeJson } = require('../util/fsHelpers');
6
-
7
- const id = 'pi';
8
- const name = 'pi';
9
-
10
- function detect() {
11
- // ".pi" is checked instead of the "pi" binary, which collides with unrelated tools on PATH.
12
- return dirExists(homeDir(), '.pi');
13
- }
14
-
15
- function apply({ apiKey, dryRun, log }) {
16
- const configPath = path.join(homeDir(), '.pi', 'agent', 'models.json');
17
- const config = readJsonIfExists(configPath) || { providers: {} };
18
- config.providers = config.providers || {};
19
- config.providers.callstack = {
20
- baseUrl: 'https://api.callstack.ai/v1',
21
- api: 'openai-completions',
22
- apiKey: apiKey || 'XXX',
23
- models: [
24
- {
25
- id: 'callstack/Apex',
26
- reasoning: true,
27
- input: ['text', 'image'],
28
- thinkingLevelMap: {
29
- off: 'none',
30
- minimal: null,
31
- low: 'low',
32
- medium: 'medium',
33
- high: null,
34
- xhigh: 'xhigh',
35
- max: null,
36
- },
37
- contextWindow: 262144,
38
- },
39
- ],
40
- };
41
-
42
- if (!dryRun) {
43
- backupFile(configPath);
44
- writeJson(configPath, config);
45
- }
46
- log(`wrote ${configPath}`);
47
- if (!apiKey) {
48
- log('no API key provided — replace the placeholder "XXX" in that file manually');
49
- }
50
- log('known issue: some users have hit Cloudflare 403s with pi — ask in #apex if this happens to you');
51
-
52
- return { configured: true };
53
- }
54
-
55
- module.exports = { id, name, detect, apply };
@@ -1,34 +0,0 @@
1
- 'use strict';
2
-
3
- const { commandExists, dirExists, anyAppPathExists, homeDir } = require('../util/detect');
4
-
5
- const id = 'vscode-copilot';
6
- const name = 'VS Code (GitHub Copilot)';
7
-
8
- function detect() {
9
- return (
10
- commandExists('code') ||
11
- dirExists(homeDir(), '.vscode') ||
12
- anyAppPathExists(['/Applications/Visual Studio Code.app'])
13
- );
14
- }
15
-
16
- // Adding a custom Copilot model endpoint goes through VS Code's GUI and an
17
- // interactive secret input, so this can't be written automatically.
18
- function apply({ log }) {
19
- log('VS Code + GitHub Copilot requires manual setup (make sure VS Code is up to date):');
20
- log(' 1. Open the GitHub Copilot panel -> model selector -> "Manage Models..."');
21
- log(' 2. "Add Models" -> "Custom Endpoint"');
22
- log(' 3. Name it "callstack.ai", paste your Apex API key, choose "Chat Completions"');
23
- log(' 4. In the JSON editor that opens, set the "models" array to:');
24
- log(' [{');
25
- log(' "id": "callstack/Apex", "name": "Apex", "url": "https://api.callstack.ai/v1",');
26
- log(' "toolCalling": true, "vision": true, "thinking": true,');
27
- log(' "contextWindow": 262144, "maxOutputTokens": 16384,');
28
- log(' "supportsReasoningEffort": ["none", "low", "medium", "xhigh"], "reasoningEffortFormat": "chat-completions"');
29
- log(' }]');
30
-
31
- return { configured: false, manual: true };
32
- }
33
-
34
- module.exports = { id, name, detect, apply };
package/src/cli.js DELETED
@@ -1,89 +0,0 @@
1
- 'use strict';
2
-
3
- const { runInit, runList } = require('./init');
4
-
5
- const VERSION = require('../package.json').version;
6
-
7
- const HELP = `apex — configure your AI coding assistants to use Callstack Apex
8
-
9
- Usage:
10
- apex init [options] Detect installed assistants and configure them
11
- apex list Show which supported assistants are detected
12
- apex --help Show this help
13
- apex --version Show the CLI version
14
-
15
- Options for "init":
16
- --api-key <key> Apex API key (else reads APEX_API_KEY / CALLSTACK_AUTH_TOKEN, else prompts)
17
- --yes, -y Don't prompt; skip anything that needs interactive input
18
- --dry-run Show what would change without writing any files
19
- --only <ids> Comma-separated assistant ids to limit to (see "apex list")
20
- --skip <ids> Comma-separated assistant ids to skip
21
- `;
22
-
23
- function parseArgs(argv) {
24
- const opts = { _: [] };
25
- for (let i = 0; i < argv.length; i += 1) {
26
- const arg = argv[i];
27
- switch (arg) {
28
- case '--api-key':
29
- opts.apiKey = argv[++i];
30
- break;
31
- case '--only':
32
- opts.only = (argv[++i] || '').split(',').filter(Boolean);
33
- break;
34
- case '--skip':
35
- opts.skip = (argv[++i] || '').split(',').filter(Boolean);
36
- break;
37
- case '--yes':
38
- case '-y':
39
- opts.yes = true;
40
- break;
41
- case '--dry-run':
42
- opts.dryRun = true;
43
- break;
44
- case '--no-key':
45
- opts.noKey = true;
46
- break;
47
- case '--help':
48
- case '-h':
49
- opts.help = true;
50
- break;
51
- case '--version':
52
- case '-v':
53
- opts.version = true;
54
- break;
55
- default:
56
- opts._.push(arg);
57
- }
58
- }
59
- return opts;
60
- }
61
-
62
- async function main(argv) {
63
- const opts = parseArgs(argv);
64
-
65
- if (opts.version) {
66
- console.log(VERSION);
67
- return;
68
- }
69
- if (opts.help || opts._.length === 0) {
70
- console.log(HELP);
71
- return;
72
- }
73
-
74
- const command = opts._[0];
75
- switch (command) {
76
- case 'init':
77
- await runInit(opts);
78
- break;
79
- case 'list':
80
- await runList(opts);
81
- break;
82
- default:
83
- console.error(`Unknown command: ${command}\n`);
84
- console.log(HELP);
85
- process.exitCode = 1;
86
- }
87
- }
88
-
89
- module.exports = { main, parseArgs };
package/src/init.js DELETED
@@ -1,91 +0,0 @@
1
- 'use strict';
2
-
3
- const assistants = require('./assistants');
4
- const { askHidden } = require('./util/prompt');
5
-
6
- async function resolveApiKey(opts) {
7
- if (opts.apiKey) return opts.apiKey;
8
- if (process.env.APEX_API_KEY) return process.env.APEX_API_KEY;
9
- if (process.env.CALLSTACK_AUTH_TOKEN) return process.env.CALLSTACK_AUTH_TOKEN;
10
- if (opts.noKey || opts.yes) return null;
11
- return askHidden('Apex API key (leave blank to skip, add it manually later): ');
12
- }
13
-
14
- function selectAssistants(opts) {
15
- let list = assistants;
16
- if (opts.only && opts.only.length) {
17
- list = list.filter((a) => opts.only.includes(a.id));
18
- }
19
- if (opts.skip && opts.skip.length) {
20
- list = list.filter((a) => !opts.skip.includes(a.id));
21
- }
22
- return list;
23
- }
24
-
25
- async function runInit(opts) {
26
- const cwd = process.cwd();
27
- const apiKey = await resolveApiKey(opts);
28
- const ctx = { cwd, apiKey, dryRun: Boolean(opts.dryRun) };
29
-
30
- const candidates = selectAssistants(opts);
31
- const detected = candidates.filter((a) => {
32
- try {
33
- return a.detect(ctx);
34
- } catch {
35
- return false;
36
- }
37
- });
38
-
39
- if (!detected.length) {
40
- console.log('No supported AI assistants were detected on this machine.');
41
- console.log(`Supported: ${assistants.map((a) => a.name).join(', ')}`);
42
- return;
43
- }
44
-
45
- console.log(`Detected: ${detected.map((a) => a.name).join(', ')}`);
46
- if (opts.dryRun) console.log('(dry run — no files will be written)\n');
47
-
48
- const results = [];
49
- for (const assistant of detected) {
50
- console.log(`\n${assistant.name}`);
51
- const log = (line) => console.log(` ${line}`);
52
- try {
53
- const result = assistant.apply({ ...ctx, log });
54
- results.push({ assistant, ...result });
55
- } catch (err) {
56
- log(`error: ${err.message}`);
57
- results.push({ assistant, configured: false, error: err.message });
58
- }
59
- }
60
-
61
- const configured = results.filter((r) => r.configured);
62
- const manual = results.filter((r) => r.manual);
63
- console.log('\nSummary:');
64
- if (configured.length) {
65
- console.log(` Configured automatically: ${configured.map((r) => r.assistant.name).join(', ')}`);
66
- }
67
- if (manual.length) {
68
- console.log(` Needs manual steps: ${manual.map((r) => r.assistant.name).join(', ')}`);
69
- }
70
- if (!apiKey) {
71
- console.log(' No API key was provided — some config files were written with a placeholder.');
72
- console.log(' Never commit real API keys; keep them out of version control.');
73
- }
74
- console.log('\nDocs: see the internal "Apex - how to use it?" Notion page, or ask in #apex on Slack.');
75
- }
76
-
77
- async function runList(opts) {
78
- const cwd = process.cwd();
79
- const ctx = { cwd };
80
- for (const assistant of assistants) {
81
- let isDetected = false;
82
- try {
83
- isDetected = assistant.detect(ctx);
84
- } catch {
85
- isDetected = false;
86
- }
87
- console.log(`${isDetected ? '[x]' : '[ ]'} ${assistant.name}`);
88
- }
89
- }
90
-
91
- module.exports = { runInit, runList };
@@ -1,49 +0,0 @@
1
- 'use strict';
2
-
3
- const os = require('node:os');
4
- const path = require('node:path');
5
- const { execFileSync } = require('node:child_process');
6
- const { pathExists } = require('./fsHelpers');
7
-
8
- function commandExists(cmd) {
9
- const finder = process.platform === 'win32' ? 'where' : 'which';
10
- try {
11
- execFileSync(finder, [cmd], { stdio: 'ignore' });
12
- return true;
13
- } catch {
14
- return false;
15
- }
16
- }
17
-
18
- // Returns { major, minor, patch } parsed from `<cmd> <args>` output, or null
19
- // if the command fails or its output doesn't contain a semver-like version.
20
- function commandVersion(cmd, args = ['--version']) {
21
- try {
22
- const out = execFileSync(cmd, args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] });
23
- const match = out.match(/(\d+)\.(\d+)\.(\d+)/);
24
- if (!match) return null;
25
- return { major: Number(match[1]), minor: Number(match[2]), patch: Number(match[3]) };
26
- } catch {
27
- return null;
28
- }
29
- }
30
-
31
- function dirExists(...segments) {
32
- return pathExists(path.join(...segments));
33
- }
34
-
35
- function anyAppPathExists(paths) {
36
- return paths.some((p) => pathExists(p));
37
- }
38
-
39
- function homeDir() {
40
- return os.homedir();
41
- }
42
-
43
- module.exports = {
44
- commandExists,
45
- commandVersion,
46
- dirExists,
47
- anyAppPathExists,
48
- homeDir,
49
- };