@emend-ai/utim 1.43.6 → 1.43.9
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/bin/utim.js +157 -92
- package/package.json +1 -1
- package/scripts/postinstall.js +49 -68
package/bin/utim.js
CHANGED
|
@@ -1,146 +1,211 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* UTIM CLI Launcher
|
|
5
|
+
*
|
|
6
|
+
* Zero-friction UX: `npm install -g @emend-ai/utim` then just run `utim`.
|
|
7
|
+
* Automatically installs the Python engine on first run if needed.
|
|
8
|
+
*/
|
|
9
|
+
|
|
3
10
|
const { spawnSync, spawn } = require('child_process');
|
|
4
|
-
const
|
|
5
|
-
const
|
|
6
|
-
const fs = require('fs');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const fs = require('fs');
|
|
7
13
|
|
|
8
|
-
const args
|
|
9
|
-
const isWin
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const isWin = os.platform() === 'win32';
|
|
16
|
+
const isMac = os.platform() === 'darwin';
|
|
10
17
|
const isTermux =
|
|
11
18
|
(process.env.PREFIX && process.env.PREFIX.includes('com.termux')) ||
|
|
12
19
|
fs.existsSync('/data/data/com.termux');
|
|
13
20
|
|
|
21
|
+
// ── Spinner (no-op when not a TTY) ───────────────────────────────────────────
|
|
22
|
+
function createSpinner(text) {
|
|
23
|
+
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
24
|
+
let i = 0;
|
|
25
|
+
if (!process.stderr.isTTY) {
|
|
26
|
+
process.stderr.write(text + '\n');
|
|
27
|
+
return { stop: (msg) => { if (msg) process.stderr.write(msg + '\n'); } };
|
|
28
|
+
}
|
|
29
|
+
const iv = setInterval(() => {
|
|
30
|
+
process.stderr.write(`\r${frames[i++ % frames.length]} ${text} `);
|
|
31
|
+
}, 80);
|
|
32
|
+
return {
|
|
33
|
+
stop: (msg) => {
|
|
34
|
+
clearInterval(iv);
|
|
35
|
+
process.stderr.write('\r' + ' '.repeat(text.length + 6) + '\r');
|
|
36
|
+
if (msg) process.stderr.write(msg + '\n');
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
|
|
14
41
|
// ── Find a working Python 3 interpreter ──────────────────────────────────────
|
|
15
42
|
function findPython() {
|
|
16
43
|
const candidates = isWin
|
|
17
44
|
? ['python', 'python3', 'py']
|
|
18
45
|
: ['python3', 'python'];
|
|
19
46
|
|
|
20
|
-
for (const
|
|
47
|
+
for (const cmd of candidates) {
|
|
21
48
|
try {
|
|
22
|
-
const r = spawnSync(
|
|
49
|
+
const r = spawnSync(cmd, ['--version'], {
|
|
23
50
|
encoding: 'utf8',
|
|
24
|
-
timeout:
|
|
51
|
+
timeout: 5000,
|
|
25
52
|
shell: isWin,
|
|
53
|
+
windowsHide: true,
|
|
26
54
|
});
|
|
27
55
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
28
|
-
if (out.includes('Python 3')) return
|
|
56
|
+
if (out.includes('Python 3')) return cmd;
|
|
29
57
|
} catch (_) {}
|
|
30
58
|
}
|
|
31
59
|
return null;
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
// ── Check if
|
|
35
|
-
|
|
62
|
+
// ── Check if utim-cli is installed via pip show (does NOT import the module) ─
|
|
63
|
+
// Using pip show avoids false negatives caused by broken/missing transitive deps.
|
|
64
|
+
function isUtimInstalled(python) {
|
|
36
65
|
try {
|
|
37
|
-
|
|
38
|
-
const checker = isWin ? 'where' : 'which';
|
|
39
|
-
const probe = spawnSync(checker, [cmd], {
|
|
66
|
+
const r = spawnSync(python, ['-m', 'pip', 'show', 'utim-cli'], {
|
|
40
67
|
encoding: 'utf8',
|
|
41
|
-
timeout:
|
|
42
|
-
shell:
|
|
68
|
+
timeout: 10000,
|
|
69
|
+
shell: isWin,
|
|
70
|
+
windowsHide: true,
|
|
43
71
|
});
|
|
44
|
-
return
|
|
72
|
+
return r.status === 0 && (r.stdout || '').includes('Name:');
|
|
45
73
|
} catch (_) {
|
|
46
74
|
return false;
|
|
47
75
|
}
|
|
48
76
|
}
|
|
49
77
|
|
|
50
|
-
// ──
|
|
51
|
-
function
|
|
52
|
-
const proc = spawn(cmd, launchArgs, {
|
|
53
|
-
stdio: 'inherit',
|
|
54
|
-
shell: isWin,
|
|
55
|
-
});
|
|
56
|
-
proc.on('exit', (code) => process.exit(code == null ? 0 : code));
|
|
57
|
-
proc.on('error', () => {
|
|
58
|
-
// If spawning fails at OS level, fall through (caller handles)
|
|
59
|
-
});
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
// ── Try the installed console_script entry point ──────────────────────────────
|
|
64
|
-
function tryEntryPoint() {
|
|
65
|
-
// Explicitly verify the binary exists before attempting to spawn it.
|
|
66
|
-
// This prevents the "not recognised as internal or external command" error
|
|
67
|
-
// from surfacing to the user when pip install was skipped.
|
|
68
|
-
if (!commandExists('utim-agent-core')) return false;
|
|
69
|
-
return launch('utim-agent-core', args);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
// ── Try via `python -m utim_cli.utim` ───────────────────────────────────────
|
|
73
|
-
function tryModule(python) {
|
|
78
|
+
// ── Run pip install with given extra flags — always show output ───────────────
|
|
79
|
+
function runPip(python, extraFlags) {
|
|
74
80
|
try {
|
|
75
|
-
const
|
|
81
|
+
const r = spawnSync(
|
|
76
82
|
python,
|
|
77
|
-
['-
|
|
78
|
-
{
|
|
83
|
+
['-m', 'pip', 'install', '--upgrade', ...extraFlags, 'utim-cli'],
|
|
84
|
+
{
|
|
85
|
+
stdio: 'inherit', // Always show output so errors are visible
|
|
86
|
+
shell: isWin,
|
|
87
|
+
windowsHide: false,
|
|
88
|
+
timeout: 300000,
|
|
89
|
+
}
|
|
79
90
|
);
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
84
|
-
return false;
|
|
91
|
+
return r.status === 0;
|
|
92
|
+
} catch (_) {
|
|
93
|
+
return false;
|
|
94
|
+
}
|
|
85
95
|
}
|
|
86
96
|
|
|
87
|
-
// ── Install
|
|
88
|
-
function
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
// On Termux, prebuilt packages must be installed first
|
|
97
|
+
// ── Install UTIM engine — tries multiple strategies ───────────────────────────
|
|
98
|
+
function installEngine(python) {
|
|
99
|
+
// Termux: install prebuilt system packages first to skip pydantic-core compilation
|
|
92
100
|
if (isTermux) {
|
|
93
|
-
|
|
94
|
-
'💡 Termux
|
|
95
|
-
' Run these commands if install fails:\n' +
|
|
96
|
-
' pkg install python-cryptography python-pydantic\n' +
|
|
97
|
-
' pip install utim-cli\n'
|
|
101
|
+
process.stderr.write(
|
|
102
|
+
'\n💡 Termux: installing prebuilt system dependencies first...\n\n'
|
|
98
103
|
);
|
|
104
|
+
spawnSync('pkg', ['install', '-y', 'python-cryptography', 'python-pydantic'], {
|
|
105
|
+
stdio: 'inherit',
|
|
106
|
+
shell: false,
|
|
107
|
+
});
|
|
108
|
+
spawnSync(python, ['-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools', 'wheel'], {
|
|
109
|
+
stdio: 'pipe',
|
|
110
|
+
shell: false,
|
|
111
|
+
timeout: 60000,
|
|
112
|
+
});
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
['-m', 'pip', 'install', '--upgrade', 'utim-cli'],
|
|
104
|
-
{ stdio: 'inherit', shell: isWin }
|
|
105
|
-
);
|
|
115
|
+
const spinner = createSpinner('Setting up UTIM (first run)');
|
|
116
|
+
spinner.stop('\n⚙ Installing UTIM Python engine...\n');
|
|
106
117
|
|
|
107
|
-
|
|
118
|
+
// Strategy 1: standard install
|
|
119
|
+
if (runPip(python, [])) return true;
|
|
108
120
|
|
|
109
|
-
//
|
|
110
|
-
|
|
111
|
-
|
|
121
|
+
// Strategy 2: --user (needed on systems where global site-packages is read-only)
|
|
122
|
+
process.stderr.write('\n Retrying with --user flag...\n\n');
|
|
123
|
+
if (runPip(python, ['--user'])) return true;
|
|
112
124
|
|
|
113
|
-
//
|
|
114
|
-
|
|
125
|
+
// Strategy 3: upgrade pip first, then retry both
|
|
126
|
+
process.stderr.write('\n Upgrading pip and retrying...\n\n');
|
|
127
|
+
spawnSync(python, ['-m', 'pip', 'install', '--upgrade', 'pip'], {
|
|
128
|
+
stdio: 'pipe', shell: isWin, windowsHide: true, timeout: 60000,
|
|
129
|
+
});
|
|
130
|
+
if (runPip(python, [])) return true;
|
|
131
|
+
if (runPip(python, ['--user'])) return true;
|
|
115
132
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
133
|
+
return false;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// ── Launch UTIM Python engine ─────────────────────────────────────────────────
|
|
137
|
+
function launch(python) {
|
|
138
|
+
const proc = spawn(python, ['-m', 'utim_cli.utim', ...args], {
|
|
139
|
+
stdio: 'inherit',
|
|
140
|
+
shell: false,
|
|
141
|
+
windowsHide: false,
|
|
142
|
+
});
|
|
143
|
+
proc.on('exit', (code) => process.exit(code == null ? 0 : code));
|
|
144
|
+
proc.on('error', (err) => {
|
|
145
|
+
process.stderr.write(
|
|
146
|
+
`\n❌ Could not start UTIM: ${err.message}\n\n` +
|
|
147
|
+
'Try reinstalling:\n' +
|
|
148
|
+
` ${python} -m pip install --upgrade utim-cli\n\n`
|
|
149
|
+
);
|
|
150
|
+
process.exit(1);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
|
|
154
|
+
// ── Friendly "Python not found" message ──────────────────────────────────────
|
|
155
|
+
function noPythonError() {
|
|
156
|
+
process.stderr.write('\n❌ Python 3 is required but was not found.\n\n');
|
|
123
157
|
if (isTermux) {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
'
|
|
128
|
-
'
|
|
158
|
+
process.stderr.write('Run: pkg install python\nThen: utim\n\n');
|
|
159
|
+
} else if (isMac) {
|
|
160
|
+
process.stderr.write(
|
|
161
|
+
'Install: brew install python\n' +
|
|
162
|
+
'Or: https://python.org\n' +
|
|
163
|
+
'Then: utim\n\n'
|
|
129
164
|
);
|
|
130
|
-
} else if (
|
|
131
|
-
|
|
132
|
-
'
|
|
133
|
-
'
|
|
134
|
-
'Then
|
|
165
|
+
} else if (isWin) {
|
|
166
|
+
process.stderr.write(
|
|
167
|
+
'Download from https://python.org\n' +
|
|
168
|
+
'✔ Check "Add Python to PATH" during setup.\n' +
|
|
169
|
+
'Then: utim\n\n'
|
|
135
170
|
);
|
|
136
171
|
} else {
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
'
|
|
140
|
-
'
|
|
141
|
-
'or:\n' +
|
|
142
|
-
' pip3 install --upgrade utim-cli\n'
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
'sudo apt install python3 python3-pip # Debian/Ubuntu\n' +
|
|
174
|
+
'sudo dnf install python3 # Fedora/RHEL\n' +
|
|
175
|
+
'Then: utim\n\n'
|
|
143
176
|
);
|
|
144
177
|
}
|
|
145
178
|
process.exit(1);
|
|
146
179
|
}
|
|
180
|
+
|
|
181
|
+
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
182
|
+
const python = findPython();
|
|
183
|
+
if (!python) noPythonError();
|
|
184
|
+
|
|
185
|
+
if (!isUtimInstalled(python)) {
|
|
186
|
+
const pipOk = installEngine(python);
|
|
187
|
+
|
|
188
|
+
if (!isUtimInstalled(python)) {
|
|
189
|
+
if (pipOk) {
|
|
190
|
+
// pip said success but `pip show` still doesn't see it —
|
|
191
|
+
// environment mismatch (e.g. multiple Python installs).
|
|
192
|
+
// Attempt launch anyway; Python's own error will be informative.
|
|
193
|
+
process.stderr.write(
|
|
194
|
+
'\n⚠ Installed but could not verify — launching anyway...\n\n'
|
|
195
|
+
);
|
|
196
|
+
} else {
|
|
197
|
+
// pip failed — output was already shown above by inherit stdio
|
|
198
|
+
process.stderr.write(
|
|
199
|
+
'\n❌ Automatic installation failed (see pip errors above).\n\n' +
|
|
200
|
+
'Common fixes:\n' +
|
|
201
|
+
` ${python} -m pip install --user utim-cli\n` +
|
|
202
|
+
' python -m pip install --upgrade pip && pip install utim-cli\n\n' +
|
|
203
|
+
'Then run: utim\n\n'
|
|
204
|
+
);
|
|
205
|
+
process.exit(1);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Launch — even if verification was uncertain, let Python report a real error
|
|
211
|
+
launch(python);
|
package/package.json
CHANGED
package/scripts/postinstall.js
CHANGED
|
@@ -1,30 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Optional postinstall pre-warm for @emend-ai/utim.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* 3. Set the execute bit on bin/utim.js (Unix / Termux only).
|
|
4
|
+
* This is a best-effort pre-installation of the UTIM Python engine.
|
|
5
|
+
* If it fails for ANY reason, we exit 0 — the main launcher (bin/utim.js)
|
|
6
|
+
* will handle installation automatically on first run with a clean spinner.
|
|
8
7
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
8
|
+
* This script exists purely to make the first `utim` launch faster
|
|
9
|
+
* by pre-installing the Python engine during `npm install`.
|
|
11
10
|
*/
|
|
12
11
|
const { spawnSync } = require('child_process');
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const fs = require('fs');
|
|
12
|
+
const os = require('os');
|
|
13
|
+
const fs = require('fs');
|
|
16
14
|
|
|
17
|
-
const
|
|
18
|
-
const
|
|
15
|
+
const isWin = os.platform() === 'win32';
|
|
16
|
+
const isTermux =
|
|
17
|
+
(process.env.PREFIX && process.env.PREFIX.includes('com.termux')) ||
|
|
18
|
+
fs.existsSync('/data/data/com.termux');
|
|
19
19
|
|
|
20
|
-
// ── Find a usable Python 3 interpreter ────────────────────────────────────────
|
|
21
20
|
function findPython() {
|
|
22
|
-
|
|
21
|
+
const candidates = isWin ? ['python', 'python3', 'py'] : ['python3', 'python'];
|
|
22
|
+
for (const candidate of candidates) {
|
|
23
23
|
try {
|
|
24
24
|
const r = spawnSync(candidate, ['--version'], {
|
|
25
25
|
encoding: 'utf8',
|
|
26
26
|
timeout: 4000,
|
|
27
|
-
shell:
|
|
27
|
+
shell: isWin,
|
|
28
|
+
windowsHide: true,
|
|
28
29
|
});
|
|
29
30
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
30
31
|
if (out.includes('Python 3')) return candidate;
|
|
@@ -33,68 +34,48 @@ function findPython() {
|
|
|
33
34
|
return null;
|
|
34
35
|
}
|
|
35
36
|
|
|
36
|
-
//
|
|
37
|
-
function installFromPyPI(python) {
|
|
38
|
-
console.log('\n⚙ Installing UTIM Python engine from PyPI (pip install utim-cli)...\n');
|
|
39
|
-
try {
|
|
40
|
-
const r = spawnSync(
|
|
41
|
-
python,
|
|
42
|
-
['-m', 'pip', 'install', '--upgrade', 'utim-cli'],
|
|
43
|
-
{ stdio: 'inherit', shell: useShell }
|
|
44
|
-
);
|
|
45
|
-
return r.status === 0;
|
|
46
|
-
} catch (_) {
|
|
47
|
-
return false;
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// ── Set execute bit on Unix / Termux ─────────────────────────────────────────
|
|
37
|
+
// Set execute bit on Unix / macOS / Termux
|
|
52
38
|
function setExecuteBit() {
|
|
53
|
-
if (
|
|
54
|
-
const binFile = path.join(pkgDir, 'bin', 'utim.js');
|
|
39
|
+
if (!isWin) {
|
|
55
40
|
try {
|
|
41
|
+
const binFile = require('path').join(__dirname, '..', 'bin', 'utim.js');
|
|
56
42
|
fs.chmodSync(binFile, 0o755);
|
|
57
|
-
} catch (_) {
|
|
58
|
-
// non-fatal — npm usually sets the bit itself via package.json "bin"
|
|
59
|
-
}
|
|
43
|
+
} catch (_) {}
|
|
60
44
|
}
|
|
61
45
|
}
|
|
62
46
|
|
|
63
|
-
|
|
64
|
-
const python = findPython();
|
|
47
|
+
setExecuteBit();
|
|
65
48
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
' Please install Python 3.9+ from https://python.org and then run:\n' +
|
|
70
|
-
' pip install utim-cli\n' +
|
|
71
|
-
' to finish setting up UTIM.\n'
|
|
72
|
-
);
|
|
73
|
-
// Exit 0 so npm does not roll back the install — user can pip install manually
|
|
49
|
+
// Skip pre-warm on Termux — needs prebuilt pkg packages first,
|
|
50
|
+
// which the main launcher handles properly with user guidance.
|
|
51
|
+
if (isTermux) {
|
|
74
52
|
process.exit(0);
|
|
75
53
|
}
|
|
76
54
|
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
if (ok) {
|
|
81
|
-
console.log('\n✅ UTIM installed successfully. Run utim to get started.\n');
|
|
82
|
-
} else {
|
|
83
|
-
const isTermux = (process.env.PREFIX && process.env.PREFIX.includes('com.termux')) || fs.existsSync('/data/data/com.termux');
|
|
84
|
-
if (isTermux) {
|
|
85
|
-
console.warn(
|
|
86
|
-
'\n💡 Note for Termux Users: Precompiled binaries are required to avoid Rust compiler errors (pydantic-core).\n' +
|
|
87
|
-
' Please run: pkg install python-cryptography python-pydantic\n' +
|
|
88
|
-
' And then: pip install utim-cli\n'
|
|
89
|
-
);
|
|
90
|
-
} else {
|
|
91
|
-
console.warn(
|
|
92
|
-
'\n⚠ pip install failed. Try running manually:\n' +
|
|
93
|
-
' pip install utim-cli\n' +
|
|
94
|
-
' or:\n' +
|
|
95
|
-
' pip3 install utim-cli\n'
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
// Exit 0 — don't break the npm install; user can manually pip install
|
|
55
|
+
const python = findPython();
|
|
56
|
+
if (!python) {
|
|
57
|
+
// No Python — launcher will guide the user on first run
|
|
99
58
|
process.exit(0);
|
|
100
59
|
}
|
|
60
|
+
|
|
61
|
+
// Silently pre-install in the background (pipe output, not inherit)
|
|
62
|
+
// so the npm install output stays clean. Any failure is fine —
|
|
63
|
+
// the launcher self-heals on first `utim` run.
|
|
64
|
+
try {
|
|
65
|
+
const r = spawnSync(
|
|
66
|
+
python,
|
|
67
|
+
['-m', 'pip', 'install', '--upgrade', '--quiet', 'utim-cli'],
|
|
68
|
+
{
|
|
69
|
+
stdio: 'pipe',
|
|
70
|
+
shell: isWin,
|
|
71
|
+
windowsHide: true,
|
|
72
|
+
timeout: 300000,
|
|
73
|
+
}
|
|
74
|
+
);
|
|
75
|
+
if (r.status === 0) {
|
|
76
|
+
console.log('\n✅ UTIM ready. Run utim to get started.\n');
|
|
77
|
+
}
|
|
78
|
+
// If it fails, stay silent — launcher will handle it
|
|
79
|
+
} catch (_) {}
|
|
80
|
+
|
|
81
|
+
process.exit(0);
|