scalemax 0.1.43 → 0.1.44

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.
@@ -115,6 +115,7 @@ function scopeHint(scope) {
115
115
  'glm-5.2': 'GLM 5.2 only',
116
116
  'deepseek-v4-pro': 'DeepSeek V4 Pro only',
117
117
  'auto-api': 'Auto only',
118
+ voice: 'Voice only',
118
119
  };
119
120
  return labels[scope];
120
121
  }
@@ -1,5 +1,8 @@
1
+ export declare function parseCodexSemver(text?: string): string | undefined;
2
+ export declare function compareCodexSemver(a: string, b: string): number;
1
3
  export declare function isCodexExecutableAvailable(): Promise<boolean>;
2
4
  export declare function getCodexVersion(): Promise<string | undefined>;
5
+ export declare function resolveLatestCodexVersion(): Promise<string | undefined>;
3
6
  export declare function installCodexWithNpm(): Promise<{
4
7
  ok: boolean;
5
8
  message: string;
@@ -1,6 +1,27 @@
1
1
  import { confirm, isCancel } from '@clack/prompts';
2
2
  import { execa } from 'execa';
3
+ import fs from 'node:fs';
4
+ import path from 'node:path';
3
5
  import { commandExists } from '../utils/exec.js';
6
+ const PKG = '@openai/codex';
7
+ const NPMJS = 'https://registry.npmjs.org/';
8
+ function npmCmd() { return process.platform === 'win32' ? 'npm.cmd' : 'npm'; }
9
+ function specFor(ver) { return ver ? `${PKG}@${ver}` : `${PKG}@latest`; }
10
+ function installHint(ver) { return `npm install -g ${specFor(ver)} --prefer-online`; }
11
+ export function parseCodexSemver(text) {
12
+ const m = String(text ?? '').match(/(?<![\d.])(\d+\.\d+\.\d+)(?![\d.])/);
13
+ return m?.[1];
14
+ }
15
+ export function compareCodexSemver(a, b) {
16
+ const pa = a.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
17
+ const pb = b.replace(/^v/, '').split('.').map(n => parseInt(n, 10) || 0);
18
+ for (let i = 0; i < 3; i++) {
19
+ const d = (pa[i] || 0) - (pb[i] || 0);
20
+ if (d)
21
+ return d;
22
+ }
23
+ return 0;
24
+ }
4
25
  export async function isCodexExecutableAvailable() { return commandExists('codex'); }
5
26
  export async function getCodexVersion() { try {
6
27
  return (await execa('codex', ['--version'], { reject: false })).stdout.trim();
@@ -8,32 +29,140 @@ export async function getCodexVersion() { try {
8
29
  catch {
9
30
  return undefined;
10
31
  } }
32
+ async function viewLatest(useNpmjs) {
33
+ const args = ['view', `${PKG}@latest`, 'version', '--prefer-online'];
34
+ if (useNpmjs)
35
+ args.push(`--registry=${NPMJS}`);
36
+ try {
37
+ const res = await execa(npmCmd(), args, { reject: false, timeout: 15000 });
38
+ if (res.exitCode !== 0)
39
+ return undefined;
40
+ return parseCodexSemver(res.stdout);
41
+ }
42
+ catch {
43
+ return undefined;
44
+ }
45
+ }
46
+ export async function resolveLatestCodexVersion() {
47
+ return (await viewLatest(true)) ?? (await viewLatest(false));
48
+ }
49
+ async function npmInstall(spec, registry) {
50
+ const args = ['install', '-g', spec, '--prefer-online'];
51
+ if (registry)
52
+ args.push(`--registry=${registry}`);
53
+ return execa(npmCmd(), args, { all: true, reject: false, stdio: 'pipe' });
54
+ }
55
+ async function npmGlobalBinDir() {
56
+ try {
57
+ const prefix = (await execa(npmCmd(), ['prefix', '-g'], { reject: false })).stdout.trim();
58
+ if (!prefix)
59
+ return undefined;
60
+ return process.platform === 'win32' ? prefix : path.join(prefix, 'bin');
61
+ }
62
+ catch {
63
+ return undefined;
64
+ }
65
+ }
66
+ function npmGlobalCodexPath(binDir) {
67
+ return path.join(binDir, process.platform === 'win32' ? 'codex.cmd' : 'codex');
68
+ }
69
+ async function whichCodex() {
70
+ try {
71
+ if (process.platform === 'win32') {
72
+ const res = await execa('where', ['codex'], { reject: false });
73
+ return res.stdout.trim().split(/\r?\n/)[0] || undefined;
74
+ }
75
+ const res = await execa('which', ['codex'], { reject: false });
76
+ return res.stdout.trim() || undefined;
77
+ }
78
+ catch {
79
+ return undefined;
80
+ }
81
+ }
82
+ async function versionAt(bin) {
83
+ try {
84
+ return parseCodexSemver((await execa(bin, ['--version'], { reject: false })).stdout);
85
+ }
86
+ catch {
87
+ return undefined;
88
+ }
89
+ }
90
+ async function inspectNpmCodex() {
91
+ const binDir = await npmGlobalBinDir();
92
+ if (!binDir)
93
+ return {};
94
+ const bin = npmGlobalCodexPath(binDir);
95
+ if (!fs.existsSync(bin))
96
+ return { binDir };
97
+ return { binDir, bin, version: await versionAt(bin) };
98
+ }
11
99
  export async function installCodexWithNpm() {
12
- const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm';
13
- const args = ['install', '-g', '@openai/codex@latest'];
100
+ const target = await resolveLatestCodexVersion();
101
+ const spec = specFor(target);
102
+ const hint = installHint(target);
14
103
  try {
15
- const res = await execa(npm, args, { all: true, reject: false, stdio: 'pipe' });
16
- if (res.exitCode === 0)
17
- return { ok: true, message: 'Codex installed with npm.' };
18
- return { ok: false, message: `npm install exited with ${res.exitCode}.`, output: res.all };
104
+ let res = await npmInstall(spec);
105
+ let inspected = await inspectNpmCodex();
106
+ let pathVer = parseCodexSemver(await getCodexVersion());
107
+ const yielded = inspected.version ?? pathVer;
108
+ const older = Boolean(target && yielded && compareCodexSemver(yielded, target) < 0);
109
+ if (res.exitCode !== 0 || older) {
110
+ const retry = await npmInstall(spec, NPMJS);
111
+ if (retry.exitCode === 0) {
112
+ res = retry;
113
+ inspected = await inspectNpmCodex();
114
+ pathVer = parseCodexSemver(await getCodexVersion());
115
+ }
116
+ else if (res.exitCode !== 0) {
117
+ return { ok: false, message: `npm install exited with ${retry.exitCode}.`, output: retry.all };
118
+ }
119
+ }
120
+ pathVer = parseCodexSemver(await getCodexVersion());
121
+ if (target && pathVer && compareCodexSemver(pathVer, target) < 0) {
122
+ const npmVer = inspected.version;
123
+ if (inspected.bin && inspected.binDir && npmVer && compareCodexSemver(npmVer, target) >= 0) {
124
+ const shadowed = (await whichCodex()) || 'codex';
125
+ return { ok: true, message: `Codex ${npmVer} installed at ${inspected.bin}, but an older Codex (${pathVer}) at ${shadowed} is shadowing it on PATH. Remove the older binary or put ${inspected.binDir} first on PATH.`, output: res.all };
126
+ }
127
+ }
128
+ if (res.exitCode !== 0)
129
+ return { ok: false, message: `npm install exited with ${res.exitCode}.`, output: res.all };
130
+ return { ok: true, message: 'Codex installed with npm.', output: res.all };
19
131
  }
20
132
  catch (err) {
21
- return { ok: false, message: `Could not run npm install -g @openai/codex@latest: ${err instanceof Error ? err.message : String(err)}` };
133
+ return { ok: false, message: `Could not run ${hint}: ${err instanceof Error ? err.message : String(err)}` };
22
134
  }
23
135
  }
24
136
  export async function ensureCodexInstalled(interactive = true) {
25
- if (await isCodexExecutableAvailable())
26
- return { ok: true, installed: false, message: `Codex detected${(await getCodexVersion()) ? ` (${await getCodexVersion()})` : ''}.` };
137
+ if (await isCodexExecutableAvailable()) {
138
+ const raw = await getCodexVersion();
139
+ const current = parseCodexSemver(raw);
140
+ const latest = await resolveLatestCodexVersion().catch(() => undefined);
141
+ if (!latest || !current || compareCodexSemver(current, latest) >= 0) {
142
+ return { ok: true, installed: false, message: `Codex detected${raw ? ` (${raw})` : ''}.` };
143
+ }
144
+ if (!interactive) {
145
+ return { ok: true, installed: false, message: `Codex detected (${raw ?? current}). Update available: ${latest}. Run ${installHint(latest)} to update.` };
146
+ }
147
+ const ans = await confirm({ message: `Codex ${current} is installed; update to latest ${latest}?`, initialValue: true });
148
+ if (isCancel(ans) || ans === false)
149
+ return { ok: true, installed: false, message: `Codex detected (${raw ?? current}).` };
150
+ const res = await installCodexWithNpm();
151
+ if (!res.ok)
152
+ return { ok: false, installed: false, message: `${res.message}\nIf this is a permission issue, install Node with nvm/fnm/volta or run: ${installHint(latest)}` };
153
+ return { ok: true, installed: true, message: res.message.includes('shadowing') ? res.message : 'Codex installed and detected.' };
154
+ }
27
155
  if (!interactive)
28
156
  return { ok: false, installed: false, message: 'Codex executable was not found on PATH.' };
29
- const ans = await confirm({ message: 'Codex CLI is not installed. Install it now with npm install -g @openai/codex@latest?', initialValue: true });
157
+ const latest = await resolveLatestCodexVersion().catch(() => undefined);
158
+ const hint = installHint(latest);
159
+ const ans = await confirm({ message: `Codex CLI is not installed. Install it now with ${hint}?`, initialValue: true });
30
160
  if (isCancel(ans) || ans === false)
31
161
  return { ok: false, installed: false, message: 'Codex installation skipped.' };
32
162
  const res = await installCodexWithNpm();
33
163
  if (!res.ok)
34
- return { ok: false, installed: false, message: `${res.message}
35
- If this is a permission issue, install Node with nvm/fnm/volta or run: npm install -g @openai/codex@latest` };
164
+ return { ok: false, installed: false, message: `${res.message}\nIf this is a permission issue, install Node with nvm/fnm/volta or run: ${hint}` };
36
165
  if (!(await isCodexExecutableAvailable()))
37
166
  return { ok: false, installed: true, message: 'Codex install finished but codex is not on PATH. Open a new terminal or refresh PATH, then rerun npx scalemax.' };
38
- return { ok: true, installed: true, message: 'Codex installed and detected.' };
167
+ return { ok: true, installed: true, message: res.message.includes('shadowing') ? res.message : 'Codex installed and detected.' };
39
168
  }
@@ -1,6 +1,6 @@
1
1
  export type ClientId = 'codex' | 'cursor' | 'roo-code' | 'cline' | 'continue' | 'trae-solo' | 'claude' | 'opencode' | 'openclaw' | 'hermes' | 'cherry-studio' | 'api-code';
2
2
  /** Keep this mirror aligned with shared/constants/api-key-scopes.ts. */
3
- export declare const API_KEY_SCOPE_VALUES: readonly ["openai", "anthropic", "both", "deepseek-v4-flash", "glm-5.2", "deepseek-v4-pro", "auto-api", "reseller", "gemini", "grok", "qwen", "deepseek", "mimo", "local"];
3
+ export declare const API_KEY_SCOPE_VALUES: readonly ["openai", "anthropic", "both", "deepseek-v4-flash", "glm-5.2", "deepseek-v4-pro", "auto-api", "reseller", "gemini", "grok", "qwen", "deepseek", "mimo", "local", "voice"];
4
4
  export type ProviderScope = (typeof API_KEY_SCOPE_VALUES)[number];
5
5
  export declare function isProviderScope(value: unknown): value is ProviderScope;
6
6
  export interface ClientDefinition {
@@ -14,6 +14,7 @@ export const API_KEY_SCOPE_VALUES = [
14
14
  'deepseek',
15
15
  'mimo',
16
16
  'local',
17
+ 'voice',
17
18
  ];
18
19
  export function isProviderScope(value) {
19
20
  return typeof value === 'string'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "scalemax",
3
- "version": "0.1.43",
3
+ "version": "0.1.44",
4
4
  "description": "Universal ScaleMax installer for AI coding clients",
5
5
  "type": "module",
6
6
  "bin": {