@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.
Files changed (2) hide show
  1. package/bin/utim.js +110 -66
  2. 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
- * Designed for zero-friction UX: the user runs `utim` and it just works.
7
- * If the Python engine isn't installed yet, this script installs it silently
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, execFileSync } = require('child_process');
12
- const os = require('os');
13
- const fs = require('fs');
10
+ const { spawnSync, spawn } = require('child_process');
11
+ const os = require('os');
12
+ const fs = require('fs');
14
13
 
15
- const args = process.argv.slice(2);
16
- const isWin = os.platform() === 'win32';
17
- const isMac = os.platform() === 'darwin';
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
- // Only show spinner if stdout is a TTY (not piped)
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 + 4) + '\r');
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 ? ['python', 'python3', 'py'] : ['python3', 'python'];
46
- for (const candidate of candidates) {
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(candidate, ['--version'], {
49
+ const r = spawnSync(cmd, ['--version'], {
49
50
  encoding: 'utf8',
50
- timeout: 4000,
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 candidate;
56
+ if (out.includes('Python 3')) return cmd;
56
57
  } catch (_) {}
57
58
  }
58
59
  return null;
59
60
  }
60
61
 
61
- // ── Check if utim_cli Python package is importable ───────────────────────────
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, ['-c', 'import utim_cli'], {
65
- timeout: 5000,
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 from PyPI with a spinner ──────────────────────────────
97
+ // ── Install UTIM engine tries multiple strategies ───────────────────────────
76
98
  function installEngine(python) {
77
- const spinner = createSpinner('Setting up UTIM (first run)...');
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 detected — installing prebuilt dependencies first...\n' +
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 pipArgs = ['-m', 'pip', 'install', '--upgrade', '--quiet', 'utim-cli'];
93
- const result = spawnSync(python, pipArgs, {
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
- if (!isTermux) spinner.stop();
118
+ // Strategy 1: standard install
119
+ if (runPip(python, [])) return true;
101
120
 
102
- return result.status === 0;
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(`\nFailed to start UTIM: ${err.message}\n`);
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
- // ── Error: Python not found ───────────────────────────────────────────────────
154
+ // ── Friendly "Python not found" message ──────────────────────────────────────
120
155
  function noPythonError() {
121
- process.stderr.write('\n❌ Python 3 was not found on this system.\n\n');
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 Python via Homebrew: brew install python\n' +
129
- 'or download from: https://python.org\n'
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 Python from https://python.org\n' +
134
- 'Make sure to check "Add Python to PATH" during installation.\n'
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
- 'Install Python 3.9+ via your package manager, e.g.:\n' +
139
- ' sudo apt install python3 python3-pip\n'
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 ok = installEngine(python);
155
- if (!ok || !isUtimInstalled(python)) {
156
- process.stderr.write(
157
- '\n❌ Failed to install UTIM Python engine automatically.\n\n' +
158
- 'Please run manually:\n' +
159
- ` ${python} -m pip install utim-cli\n\n` +
160
- 'Then run utim again.\n\n'
161
- );
162
- process.exit(1);
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
- // Engine is confirmed installed launch it
210
+ // Launch even if verification was uncertain, let Python report a real error
167
211
  launch(python);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emend-ai/utim",
3
- "version": "1.43.7",
3
+ "version": "1.43.10",
4
4
  "description": "UTIM – Universal Terminal Intelligence Manager. An agentic AI coding assistant for your terminal.",
5
5
  "keywords": [
6
6
  "ai",