@emend-ai/utim 1.43.7 → 1.43.10
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 +110 -66
- package/package.json +1 -1
package/bin/utim.js
CHANGED
|
@@ -2,39 +2,37 @@
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* UTIM CLI Launcher
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* with a spinner before launching — no extra commands needed.
|
|
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.
|
|
9
8
|
*/
|
|
10
9
|
|
|
11
|
-
const { spawnSync, spawn
|
|
12
|
-
const os
|
|
13
|
-
const fs
|
|
10
|
+
const { spawnSync, spawn } = require('child_process');
|
|
11
|
+
const os = require('os');
|
|
12
|
+
const fs = require('fs');
|
|
14
13
|
|
|
15
|
-
const args
|
|
16
|
-
const isWin
|
|
17
|
-
const isMac
|
|
14
|
+
const args = process.argv.slice(2);
|
|
15
|
+
const isWin = os.platform() === 'win32';
|
|
16
|
+
const isMac = os.platform() === 'darwin';
|
|
18
17
|
const isTermux =
|
|
19
18
|
(process.env.PREFIX && process.env.PREFIX.includes('com.termux')) ||
|
|
20
19
|
fs.existsSync('/data/data/com.termux');
|
|
21
20
|
|
|
22
|
-
// ── Spinner
|
|
21
|
+
// ── Spinner (no-op when not a TTY) ───────────────────────────────────────────
|
|
23
22
|
function createSpinner(text) {
|
|
24
23
|
const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
|
|
25
24
|
let i = 0;
|
|
26
|
-
|
|
27
|
-
if (!process.stdout.isTTY) {
|
|
25
|
+
if (!process.stderr.isTTY) {
|
|
28
26
|
process.stderr.write(text + '\n');
|
|
29
27
|
return { stop: (msg) => { if (msg) process.stderr.write(msg + '\n'); } };
|
|
30
28
|
}
|
|
31
29
|
const iv = setInterval(() => {
|
|
32
|
-
process.stderr.write(`\r${frames[i++ % frames.length]} ${text}`);
|
|
30
|
+
process.stderr.write(`\r${frames[i++ % frames.length]} ${text} `);
|
|
33
31
|
}, 80);
|
|
34
32
|
return {
|
|
35
33
|
stop: (msg) => {
|
|
36
34
|
clearInterval(iv);
|
|
37
|
-
process.stderr.write('\r' + ' '.repeat(text.length +
|
|
35
|
+
process.stderr.write('\r' + ' '.repeat(text.length + 6) + '\r');
|
|
38
36
|
if (msg) process.stderr.write(msg + '\n');
|
|
39
37
|
}
|
|
40
38
|
};
|
|
@@ -42,64 +40,97 @@ function createSpinner(text) {
|
|
|
42
40
|
|
|
43
41
|
// ── Find a working Python 3 interpreter ──────────────────────────────────────
|
|
44
42
|
function findPython() {
|
|
45
|
-
const candidates = isWin
|
|
46
|
-
|
|
43
|
+
const candidates = isWin
|
|
44
|
+
? ['python', 'python3', 'py']
|
|
45
|
+
: ['python3', 'python'];
|
|
46
|
+
|
|
47
|
+
for (const cmd of candidates) {
|
|
47
48
|
try {
|
|
48
|
-
const r = spawnSync(
|
|
49
|
+
const r = spawnSync(cmd, ['--version'], {
|
|
49
50
|
encoding: 'utf8',
|
|
50
|
-
timeout:
|
|
51
|
+
timeout: 5000,
|
|
51
52
|
shell: isWin,
|
|
52
53
|
windowsHide: true,
|
|
53
54
|
});
|
|
54
55
|
const out = (r.stdout || '') + (r.stderr || '');
|
|
55
|
-
if (out.includes('Python 3')) return
|
|
56
|
+
if (out.includes('Python 3')) return cmd;
|
|
56
57
|
} catch (_) {}
|
|
57
58
|
}
|
|
58
59
|
return null;
|
|
59
60
|
}
|
|
60
61
|
|
|
61
|
-
// ── Check if
|
|
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.
|
|
62
64
|
function isUtimInstalled(python) {
|
|
63
65
|
try {
|
|
64
|
-
const r = spawnSync(python, ['-
|
|
65
|
-
|
|
66
|
+
const r = spawnSync(python, ['-m', 'pip', 'show', 'utim-cli'], {
|
|
67
|
+
encoding: 'utf8',
|
|
68
|
+
timeout: 10000,
|
|
66
69
|
shell: isWin,
|
|
67
70
|
windowsHide: true,
|
|
68
71
|
});
|
|
72
|
+
return r.status === 0 && (r.stdout || '').includes('Name:');
|
|
73
|
+
} catch (_) {
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Run pip install with given extra flags — always show output ───────────────
|
|
79
|
+
function runPip(python, extraFlags) {
|
|
80
|
+
try {
|
|
81
|
+
const r = spawnSync(
|
|
82
|
+
python,
|
|
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
|
+
}
|
|
90
|
+
);
|
|
69
91
|
return r.status === 0;
|
|
70
92
|
} catch (_) {
|
|
71
93
|
return false;
|
|
72
94
|
}
|
|
73
95
|
}
|
|
74
96
|
|
|
75
|
-
// ── Install UTIM engine
|
|
97
|
+
// ── Install UTIM engine — tries multiple strategies ───────────────────────────
|
|
76
98
|
function installEngine(python) {
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
// On Termux: prebuilt packages must exist before pydantic-core compiles
|
|
99
|
+
// Termux: install prebuilt system packages first to skip pydantic-core compilation
|
|
80
100
|
if (isTermux) {
|
|
81
|
-
spinner.stop();
|
|
82
101
|
process.stderr.write(
|
|
83
|
-
'\n💡 Termux
|
|
84
|
-
' (This is needed to avoid pydantic-core compilation errors)\n\n'
|
|
102
|
+
'\n💡 Termux: installing prebuilt system dependencies first...\n\n'
|
|
85
103
|
);
|
|
86
104
|
spawnSync('pkg', ['install', '-y', 'python-cryptography', 'python-pydantic'], {
|
|
87
105
|
stdio: 'inherit',
|
|
88
106
|
shell: false,
|
|
89
107
|
});
|
|
108
|
+
spawnSync(python, ['-m', 'pip', 'install', '--upgrade', 'pip', 'setuptools', 'wheel'], {
|
|
109
|
+
stdio: 'pipe',
|
|
110
|
+
shell: false,
|
|
111
|
+
timeout: 60000,
|
|
112
|
+
});
|
|
90
113
|
}
|
|
91
114
|
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
stdio: isTermux ? 'inherit' : 'pipe',
|
|
95
|
-
shell: isWin,
|
|
96
|
-
windowsHide: true,
|
|
97
|
-
timeout: 300000, // 5 min max
|
|
98
|
-
});
|
|
115
|
+
const spinner = createSpinner('Setting up UTIM (first run)');
|
|
116
|
+
spinner.stop('\n⚙ Installing UTIM Python engine...\n');
|
|
99
117
|
|
|
100
|
-
|
|
118
|
+
// Strategy 1: standard install
|
|
119
|
+
if (runPip(python, [])) return true;
|
|
101
120
|
|
|
102
|
-
|
|
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;
|
|
124
|
+
|
|
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;
|
|
132
|
+
|
|
133
|
+
return false;
|
|
103
134
|
}
|
|
104
135
|
|
|
105
136
|
// ── Launch UTIM Python engine ─────────────────────────────────────────────────
|
|
@@ -111,57 +142,70 @@ function launch(python) {
|
|
|
111
142
|
});
|
|
112
143
|
proc.on('exit', (code) => process.exit(code == null ? 0 : code));
|
|
113
144
|
proc.on('error', (err) => {
|
|
114
|
-
process.stderr.write(
|
|
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
|
+
);
|
|
115
150
|
process.exit(1);
|
|
116
151
|
});
|
|
117
152
|
}
|
|
118
153
|
|
|
119
|
-
// ──
|
|
154
|
+
// ── Friendly "Python not found" message ──────────────────────────────────────
|
|
120
155
|
function noPythonError() {
|
|
121
|
-
process.stderr.write('\n❌ Python 3 was not found
|
|
156
|
+
process.stderr.write('\n❌ Python 3 is required but was not found.\n\n');
|
|
122
157
|
if (isTermux) {
|
|
123
|
-
process.stderr.write(
|
|
124
|
-
'💡 Termux: run pkg install python then try utim again.\n'
|
|
125
|
-
);
|
|
158
|
+
process.stderr.write('Run: pkg install python\nThen: utim\n\n');
|
|
126
159
|
} else if (isMac) {
|
|
127
160
|
process.stderr.write(
|
|
128
|
-
'Install
|
|
129
|
-
'
|
|
161
|
+
'Install: brew install python\n' +
|
|
162
|
+
'Or: https://python.org\n' +
|
|
163
|
+
'Then: utim\n\n'
|
|
130
164
|
);
|
|
131
165
|
} else if (isWin) {
|
|
132
166
|
process.stderr.write(
|
|
133
|
-
'Download
|
|
134
|
-
'
|
|
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
172
|
process.stderr.write(
|
|
138
|
-
'
|
|
139
|
-
'
|
|
173
|
+
'sudo apt install python3 python3-pip # Debian/Ubuntu\n' +
|
|
174
|
+
'sudo dnf install python3 # Fedora/RHEL\n' +
|
|
175
|
+
'Then: utim\n\n'
|
|
140
176
|
);
|
|
141
177
|
}
|
|
142
|
-
process.stderr.write('\nThen run utim again — no extra steps required.\n\n');
|
|
143
178
|
process.exit(1);
|
|
144
179
|
}
|
|
145
180
|
|
|
146
181
|
// ── Main ─────────────────────────────────────────────────────────────────────
|
|
147
182
|
const python = findPython();
|
|
148
|
-
|
|
149
|
-
if (!python) {
|
|
150
|
-
noPythonError();
|
|
151
|
-
}
|
|
183
|
+
if (!python) noPythonError();
|
|
152
184
|
|
|
153
185
|
if (!isUtimInstalled(python)) {
|
|
154
|
-
const
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
'
|
|
161
|
-
|
|
162
|
-
|
|
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
|
+
}
|
|
163
207
|
}
|
|
164
208
|
}
|
|
165
209
|
|
|
166
|
-
//
|
|
210
|
+
// Launch — even if verification was uncertain, let Python report a real error
|
|
167
211
|
launch(python);
|