@emend-ai/utim 1.43.6 → 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,28 +1,55 @@
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
16
  const isWin = os.platform() === 'win32';
17
+ const isMac = os.platform() === 'darwin';
10
18
  const isTermux =
11
19
  (process.env.PREFIX && process.env.PREFIX.includes('com.termux')) ||
12
20
  fs.existsSync('/data/data/com.termux');
13
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
+ }
42
+
14
43
  // ── Find a working Python 3 interpreter ──────────────────────────────────────
15
44
  function findPython() {
16
- const candidates = isWin
17
- ? ['python', 'python3', 'py']
18
- : ['python3', 'python'];
19
-
45
+ const candidates = isWin ? ['python', 'python3', 'py'] : ['python3', 'python'];
20
46
  for (const candidate of candidates) {
21
47
  try {
22
48
  const r = spawnSync(candidate, ['--version'], {
23
49
  encoding: 'utf8',
24
50
  timeout: 4000,
25
51
  shell: isWin,
52
+ windowsHide: true,
26
53
  });
27
54
  const out = (r.stdout || '') + (r.stderr || '');
28
55
  if (out.includes('Python 3')) return candidate;
@@ -31,116 +58,110 @@ function findPython() {
31
58
  return null;
32
59
  }
33
60
 
34
- // ── Check if a binary is actually available and runnable ─────────────────────
35
- function commandExists(cmd) {
61
+ // ── Check if utim_cli Python package is importable ───────────────────────────
62
+ function isUtimInstalled(python) {
36
63
  try {
37
- // Use 'where' on Windows, 'which' on Unix to check existence
38
- const checker = isWin ? 'where' : 'which';
39
- const probe = spawnSync(checker, [cmd], {
40
- encoding: 'utf8',
41
- timeout: 3000,
42
- shell: false,
64
+ const r = spawnSync(python, ['-c', 'import utim_cli'], {
65
+ timeout: 5000,
66
+ shell: isWin,
67
+ windowsHide: true,
43
68
  });
44
- return probe.status === 0 && (probe.stdout || '').trim().length > 0;
69
+ return r.status === 0;
45
70
  } catch (_) {
46
71
  return false;
47
72
  }
48
73
  }
49
74
 
50
- // ── Launch a command, forwarding stdio ───────────────────────────────────────
51
- function launch(cmd, launchArgs) {
52
- const proc = spawn(cmd, launchArgs, {
53
- stdio: 'inherit',
75
+ // ── Install UTIM engine from PyPI with a spinner ──────────────────────────────
76
+ function installEngine(python) {
77
+ const spinner = createSpinner('Setting up UTIM (first run)...');
78
+
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',
54
95
  shell: isWin,
96
+ windowsHide: true,
97
+ timeout: 300000, // 5 min max
55
98
  });
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
99
 
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
- }
100
+ if (!isTermux) spinner.stop();
71
101
 
72
- // ── Try via `python -m utim_cli.utim` ───────────────────────────────────────
73
- function tryModule(python) {
74
- try {
75
- const probe = spawnSync(
76
- python,
77
- ['-c', 'import utim_cli'],
78
- { shell: isWin, timeout: 5000 }
79
- );
80
- if (probe.status === 0) {
81
- return launch(python, ['-m', 'utim_cli.utim', ...args]);
82
- }
83
- } catch (_) {}
84
- return false;
102
+ return result.status === 0;
85
103
  }
86
104
 
87
- // ── Install from PyPI then retry ─────────────────────────────────────────────
88
- function selfInstallAndRun(python) {
89
- console.error('\n⚙ First run: installing UTIM Python engine from PyPI...\n');
105
+ // ── Launch UTIM Python engine ─────────────────────────────────────────────────
106
+ function launch(python) {
107
+ const proc = spawn(python, ['-m', 'utim_cli.utim', ...args], {
108
+ stdio: 'inherit',
109
+ shell: false,
110
+ windowsHide: false,
111
+ });
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
+ }
90
118
 
91
- // On Termux, prebuilt packages must be installed first
119
+ // ── Error: Python not found ───────────────────────────────────────────────────
120
+ function noPythonError() {
121
+ process.stderr.write('\n❌ Python 3 was not found on this system.\n\n');
92
122
  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'
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'
98
140
  );
99
141
  }
100
-
101
- const install = spawnSync(
102
- python,
103
- ['-m', 'pip', 'install', '--upgrade', 'utim-cli'],
104
- { stdio: 'inherit', shell: isWin }
105
- );
106
-
107
- if (install.status !== 0) return false;
108
-
109
- // Retry both paths after install
110
- return tryEntryPoint() || tryModule(python);
142
+ process.stderr.write('\nThen run utim again — no extra steps required.\n\n');
143
+ process.exit(1);
111
144
  }
112
145
 
113
146
  // ── Main ─────────────────────────────────────────────────────────────────────
114
147
  const python = findPython();
115
148
 
116
- const ok =
117
- tryEntryPoint() ||
118
- (python && tryModule(python)) ||
119
- (python && selfInstallAndRun(python));
149
+ if (!python) {
150
+ noPythonError();
151
+ }
120
152
 
121
- if (!ok) {
122
- console.error('\n❌ UTIM Engine could not start.\n');
123
- 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'
129
- );
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'
135
- );
136
- } 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'
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'
143
161
  );
162
+ process.exit(1);
144
163
  }
145
- process.exit(1);
146
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.6",
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);