@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 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 path = require('path');
5
- const os = require('os');
6
- const fs = require('fs');
11
+ const os = require('os');
12
+ const fs = require('fs');
7
13
 
8
- const args = process.argv.slice(2);
9
- const isWin = os.platform() === 'win32';
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 candidate of candidates) {
47
+ for (const cmd of candidates) {
21
48
  try {
22
- const r = spawnSync(candidate, ['--version'], {
49
+ const r = spawnSync(cmd, ['--version'], {
23
50
  encoding: 'utf8',
24
- timeout: 4000,
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 candidate;
56
+ if (out.includes('Python 3')) return cmd;
29
57
  } catch (_) {}
30
58
  }
31
59
  return null;
32
60
  }
33
61
 
34
- // ── Check if a binary is actually available and runnable ─────────────────────
35
- function commandExists(cmd) {
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
- // Use 'where' on Windows, 'which' on Unix to check existence
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: 3000,
42
- shell: false,
68
+ timeout: 10000,
69
+ shell: isWin,
70
+ windowsHide: true,
43
71
  });
44
- return probe.status === 0 && (probe.stdout || '').trim().length > 0;
72
+ return r.status === 0 && (r.stdout || '').includes('Name:');
45
73
  } catch (_) {
46
74
  return false;
47
75
  }
48
76
  }
49
77
 
50
- // ── Launch a command, forwarding stdio ───────────────────────────────────────
51
- function launch(cmd, launchArgs) {
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 probe = spawnSync(
81
+ const r = spawnSync(
76
82
  python,
77
- ['-c', 'import utim_cli'],
78
- { shell: isWin, timeout: 5000 }
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
- if (probe.status === 0) {
81
- return launch(python, ['-m', 'utim_cli.utim', ...args]);
82
- }
83
- } catch (_) {}
84
- return false;
91
+ return r.status === 0;
92
+ } catch (_) {
93
+ return false;
94
+ }
85
95
  }
86
96
 
87
- // ── Install from PyPI then retry ─────────────────────────────────────────────
88
- function selfInstallAndRun(python) {
89
- console.error('\n⚙ First run: installing UTIM Python engine from PyPI...\n');
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
- console.error(
94
- '💡 Termux detected. Installing prebuilt dependencies first...\n' +
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 install = spawnSync(
102
- python,
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
- if (install.status !== 0) return false;
118
+ // Strategy 1: standard install
119
+ if (runPip(python, [])) return true;
108
120
 
109
- // Retry both paths after install
110
- return tryEntryPoint() || tryModule(python);
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
- // ── Main ─────────────────────────────────────────────────────────────────────
114
- const python = findPython();
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
- const ok =
117
- tryEntryPoint() ||
118
- (python && tryModule(python)) ||
119
- (python && selfInstallAndRun(python));
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
- if (!ok) {
122
- console.error('\n❌ UTIM Engine could not start.\n');
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
- console.error(
125
- '💡 Termux: Prebuilt packages are required to avoid compilation errors.\n' +
126
- ' Run:\n' +
127
- ' pkg install python-cryptography python-pydantic\n' +
128
- ' pip install utim-cli\n'
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 (!python) {
131
- console.error(
132
- 'Python 3 was not found on this system.\n' +
133
- 'Please install Python 3.9+ from https://python.org\n' +
134
- 'Then run: pip install utim-cli\n'
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
- console.error(
138
- `Python found at: ${python}\n` +
139
- 'The UTIM Python engine is not installed. Please run:\n' +
140
- ' pip install utim-cli\n' +
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emend-ai/utim",
3
- "version": "1.43.6",
3
+ "version": "1.43.9",
4
4
  "description": "UTIM – Universal Terminal Intelligence Manager. An agentic AI coding assistant for your terminal.",
5
5
  "keywords": [
6
6
  "ai",
@@ -1,30 +1,31 @@
1
1
  /**
2
- * Cross-platform postinstall script for @emend-ai/utim.
2
+ * Optional postinstall pre-warm for @emend-ai/utim.
3
3
  *
4
- * After `npm install -g @emend-ai/utim`, this script runs automatically to:
5
- * 1. Locate a Python 3 interpreter on the user's machine.
6
- * 2. Install the UTIM Python engine from PyPI: pip install utim-cli
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
- * The Python source code is NOT bundled inside this npm package.
10
- * It is fetched from PyPI at install time.
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 path = require('path');
14
- const os = require('os');
15
- const fs = require('fs');
12
+ const os = require('os');
13
+ const fs = require('fs');
16
14
 
17
- const useShell = os.platform() === 'win32';
18
- const pkgDir = path.resolve(__dirname, '..');
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
- for (const candidate of ['python3', 'python']) {
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: useShell,
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
- // ── Install UTIM Python engine from PyPI ──────────────────────────────────────
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 (os.platform() !== 'win32') {
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
- // ── Main ──────────────────────────────────────────────────────────────────────
64
- const python = findPython();
47
+ setExecuteBit();
65
48
 
66
- if (!python) {
67
- console.error(
68
- '\n⚠ Python 3 not found on this machine.\n' +
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 ok = installFromPyPI(python);
78
- setExecuteBit();
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);