@emend-ai/utim 1.43.5 → 1.43.7

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,18 +1,56 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- const { spawnSync, spawn } = require('child_process');
4
- const path = require('path');
3
+ /**
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.
9
+ */
10
+
11
+ const { spawnSync, spawn, execFileSync } = require('child_process');
5
12
  const os = require('os');
6
13
  const fs = require('fs');
7
14
 
8
15
  const args = process.argv.slice(2);
9
- const useShell = os.platform() === 'win32';
16
+ const isWin = os.platform() === 'win32';
17
+ const isMac = os.platform() === 'darwin';
18
+ const isTermux =
19
+ (process.env.PREFIX && process.env.PREFIX.includes('com.termux')) ||
20
+ fs.existsSync('/data/data/com.termux');
21
+
22
+ // ── Spinner ───────────────────────────────────────────────────────────────────
23
+ function createSpinner(text) {
24
+ const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
25
+ let i = 0;
26
+ // Only show spinner if stdout is a TTY (not piped)
27
+ if (!process.stdout.isTTY) {
28
+ process.stderr.write(text + '\n');
29
+ return { stop: (msg) => { if (msg) process.stderr.write(msg + '\n'); } };
30
+ }
31
+ const iv = setInterval(() => {
32
+ process.stderr.write(`\r${frames[i++ % frames.length]} ${text}`);
33
+ }, 80);
34
+ return {
35
+ stop: (msg) => {
36
+ clearInterval(iv);
37
+ process.stderr.write('\r' + ' '.repeat(text.length + 4) + '\r');
38
+ if (msg) process.stderr.write(msg + '\n');
39
+ }
40
+ };
41
+ }
10
42
 
11
43
  // ── Find a working Python 3 interpreter ──────────────────────────────────────
12
44
  function findPython() {
13
- for (const candidate of ['python3', 'python']) {
45
+ const candidates = isWin ? ['python', 'python3', 'py'] : ['python3', 'python'];
46
+ for (const candidate of candidates) {
14
47
  try {
15
- const r = spawnSync(candidate, ['--version'], { encoding: 'utf8', timeout: 3000, shell: useShell });
48
+ const r = spawnSync(candidate, ['--version'], {
49
+ encoding: 'utf8',
50
+ timeout: 4000,
51
+ shell: isWin,
52
+ windowsHide: true,
53
+ });
16
54
  const out = (r.stdout || '') + (r.stderr || '');
17
55
  if (out.includes('Python 3')) return candidate;
18
56
  } catch (_) {}
@@ -20,73 +58,110 @@ function findPython() {
20
58
  return null;
21
59
  }
22
60
 
23
- // ── Attempt to launch a command, return true if successful ───────────────────
24
- function tryLaunch(cmd, launchArgs) {
61
+ // ── Check if utim_cli Python package is importable ───────────────────────────
62
+ function isUtimInstalled(python) {
25
63
  try {
26
- const probe = spawnSync(cmd, ['--version'], { shell: useShell, timeout: 3000, encoding: 'utf8' });
27
- const out = (probe.stdout || '') + (probe.stderr || '');
28
- // accept if exit 0 OR if it printed something (some tools exit non-0 for --version)
29
- if (probe.status === 0 || out.length > 0) {
30
- const proc = spawn(cmd, launchArgs, { stdio: 'inherit', shell: useShell });
31
- proc.on('exit', (code) => process.exit(code == null ? 0 : code));
32
- return true;
33
- }
34
- } catch (_) {}
35
- return false;
64
+ const r = spawnSync(python, ['-c', 'import utim_cli'], {
65
+ timeout: 5000,
66
+ shell: isWin,
67
+ windowsHide: true,
68
+ });
69
+ return r.status === 0;
70
+ } catch (_) {
71
+ return false;
72
+ }
36
73
  }
37
74
 
38
- // ── Try the installed console_script entry point ──────────────────────────────
39
- function tryEntryPoint() {
40
- return tryLaunch('utim-agent-core', args);
41
- }
75
+ // ── Install UTIM engine from PyPI with a spinner ──────────────────────────────
76
+ function installEngine(python) {
77
+ const spinner = createSpinner('Setting up UTIM (first run)...');
42
78
 
43
- // ── Try via `python -m utim_cli.utim` ───────────────────────────────────────
44
- function tryModule(python) {
45
- try {
46
- const probe = spawnSync(python, ['-m', 'utim_cli', '--help'], { shell: useShell, timeout: 5000 });
47
- if (probe.status === 0) {
48
- const proc = spawn(python, ['-m', 'utim_cli.utim', ...args], { stdio: 'inherit', shell: useShell });
49
- proc.on('exit', (code) => process.exit(code == null ? 0 : code));
50
- return true;
51
- }
52
- } catch (_) {}
53
- return false;
79
+ // On Termux: prebuilt packages must exist before pydantic-core compiles
80
+ if (isTermux) {
81
+ spinner.stop();
82
+ process.stderr.write(
83
+ '\n💡 Termux detected installing prebuilt dependencies first...\n' +
84
+ ' (This is needed to avoid pydantic-core compilation errors)\n\n'
85
+ );
86
+ spawnSync('pkg', ['install', '-y', 'python-cryptography', 'python-pydantic'], {
87
+ stdio: 'inherit',
88
+ shell: false,
89
+ });
90
+ }
91
+
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
+ });
99
+
100
+ if (!isTermux) spinner.stop();
101
+
102
+ return result.status === 0;
54
103
  }
55
104
 
56
- // ── Last resort: install from PyPI and retry ─────────────────────────────────
57
- function selfInstallAndRun(python) {
58
- console.error('\n⚙ First run: installing UTIM Python engine from PyPI...');
59
- const install = spawnSync(python, ['-m', 'pip', 'install', '--upgrade', 'utim-cli'], {
105
+ // ── Launch UTIM Python engine ─────────────────────────────────────────────────
106
+ function launch(python) {
107
+ const proc = spawn(python, ['-m', 'utim_cli.utim', ...args], {
60
108
  stdio: 'inherit',
61
- shell: useShell,
109
+ shell: false,
110
+ windowsHide: false,
62
111
  });
63
- if (install.status === 0) {
64
- // Retry the entry point after install
65
- return tryEntryPoint() || tryModule(python);
112
+ proc.on('exit', (code) => process.exit(code == null ? 0 : code));
113
+ proc.on('error', (err) => {
114
+ process.stderr.write(`\nFailed to start UTIM: ${err.message}\n`);
115
+ process.exit(1);
116
+ });
117
+ }
118
+
119
+ // ── Error: Python not found ───────────────────────────────────────────────────
120
+ function noPythonError() {
121
+ process.stderr.write('\n❌ Python 3 was not found on this system.\n\n');
122
+ if (isTermux) {
123
+ process.stderr.write(
124
+ '💡 Termux: run pkg install python then try utim again.\n'
125
+ );
126
+ } else if (isMac) {
127
+ process.stderr.write(
128
+ 'Install Python via Homebrew: brew install python\n' +
129
+ 'or download from: https://python.org\n'
130
+ );
131
+ } else if (isWin) {
132
+ process.stderr.write(
133
+ 'Download Python from https://python.org\n' +
134
+ 'Make sure to check "Add Python to PATH" during installation.\n'
135
+ );
136
+ } else {
137
+ process.stderr.write(
138
+ 'Install Python 3.9+ via your package manager, e.g.:\n' +
139
+ ' sudo apt install python3 python3-pip\n'
140
+ );
66
141
  }
67
- return false;
142
+ process.stderr.write('\nThen run utim again — no extra steps required.\n\n');
143
+ process.exit(1);
68
144
  }
69
145
 
70
146
  // ── Main ─────────────────────────────────────────────────────────────────────
71
147
  const python = findPython();
72
148
 
73
- const ok =
74
- tryEntryPoint() ||
75
- (python && tryModule(python)) ||
76
- (python && selfInstallAndRun(python));
149
+ if (!python) {
150
+ noPythonError();
151
+ }
77
152
 
78
- if (!ok) {
79
- console.error('\nError: UTIM Engine could not start.');
80
- const isTermux = (process.env.PREFIX && process.env.PREFIX.includes('com.termux')) || fs.existsSync('/data/data/com.termux');
81
- if (isTermux) {
82
- console.error('\n💡 Note for Termux Users: Precompiled binaries are required to avoid Rust compiler errors (pydantic-core).');
83
- console.error(' Please run: pkg install python-cryptography python-pydantic');
84
- console.error(' And then: pip install utim-cli');
85
- } else if (!python) {
86
- console.error('Python 3 was not found. On Termux run: pkg install python');
87
- } else {
88
- console.error(`Tried Python at: ${python}`);
89
- console.error('Run manually: pip install utim-cli or pip3 install --upgrade utim-cli');
153
+ 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);
90
163
  }
91
- process.exit(1);
92
164
  }
165
+
166
+ // Engine is confirmed installed — launch it
167
+ launch(python);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emend-ai/utim",
3
- "version": "1.43.5",
3
+ "version": "1.43.7",
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);