drift-ml 0.1.5 → 0.1.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 (3) hide show
  1. package/README.md +8 -2
  2. package/bin/drift.js +178 -47
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,13 +52,19 @@ drift
52
52
 
53
53
  The `drift` command installs or upgrades the chat CLI (Python) and runs it. You get the welcome and instructions every time.
54
54
 
55
- ### Alternative: pipx (Python only)
55
+ ### Alternative: pipx (Python only — macOS, Linux, Windows)
56
56
 
57
57
  ```bash
58
- pipx install drift
58
+ pipx install drift-ml
59
59
  drift
60
60
  ```
61
61
 
62
+ **Update (pipx):**
63
+ ```bash
64
+ pipx upgrade drift-ml
65
+ ```
66
+ (PowerShell on Windows: same command.)
67
+
62
68
  ---
63
69
 
64
70
  ## Example usage
package/bin/drift.js CHANGED
@@ -1,60 +1,190 @@
1
1
  #!/usr/bin/env node
2
+ //
3
+ // REMOVED: Any pip/pipx/python -m pip install or upgrade logic.
4
+ // WHY: PEP 668 and user envs; we must not modify Python. User installs CLI via pipx once.
5
+ //
6
+ // FINAL FLOW: (1) If no DRIFT_BACKEND_URL, ensure engine at 127.0.0.1:8000 (download + start if needed).
7
+ // (2) Locate Python drift ONLY in ~/.local/bin (macOS/Linux) or %USERPROFILE%\.local\bin (Windows).
8
+ // (3) Spawn that binary with DRIFT_BACKEND_URL set; never run drift.cmd/drift.ps1/drift.js (self).
9
+ //
2
10
 
3
- const { spawnSync } = require("child_process");
11
+ const { spawnSync, spawn } = require("child_process");
4
12
  const path = require("path");
5
13
  const fs = require("fs");
14
+ const https = require("https");
15
+ const http = require("http");
6
16
 
7
- // Known pipx install locations
8
- function getPipxBinPaths() {
17
+ const isWindows = process.platform === "win32";
18
+ const ENGINE_PORT = process.env.DRIFT_ENGINE_PORT || "8000";
19
+ // Pinned tag: draft releases are invisible to /releases/latest.
20
+ const ENGINE_BASE_URL = process.env.DRIFT_ENGINE_BASE_URL || "https://github.com/lakshitsachdeva/intent2model/releases/download/v0.1.0";
21
+ const HEALTH_URL = `http://127.0.0.1:${ENGINE_PORT}/health`;
22
+ const HEALTH_TIMEOUT_MS = 2000;
23
+ const HEALTH_POLL_MS = 500;
24
+ const HEALTH_POLL_MAX = 60; // 30 seconds total
25
+ const isMac = process.platform === "darwin";
26
+
27
+ function getPlatformKey() {
28
+ const p = process.platform;
29
+ const a = process.arch;
30
+ const plat = p === "darwin" ? "macos" : p === "win32" ? "windows" : "linux";
31
+ const arch = a === "arm64" || a === "aarch64" ? "arm64" : "x64";
32
+ return { plat, arch };
33
+ }
34
+
35
+ function getEngineDir() {
9
36
  const home = process.env.HOME || process.env.USERPROFILE || "";
10
- return [
11
- path.join(home, ".local", "bin", "drift"),
12
- "/usr/local/bin/drift",
13
- ];
37
+ if (!home) return null;
38
+ return path.join(home, ".drift", "bin");
14
39
  }
15
40
 
16
- // Find Python-based drift (not this Node script)
17
- function findPythonDrift() {
18
- // First check pipx standard locations
19
- for (const p of getPipxBinPaths()) {
20
- if (fs.existsSync(p)) {
21
- // Verify it's not a Node script (i.e., not us)
41
+ function getEnginePath() {
42
+ const dir = getEngineDir();
43
+ if (!dir) return null;
44
+ const { plat, arch } = getPlatformKey();
45
+ const ext = isWindows ? ".exe" : "";
46
+ return path.join(dir, `drift-engine-${plat}-${arch}${ext}`);
47
+ }
48
+
49
+ function fetchOk(url) {
50
+ return new Promise((resolve) => {
51
+ const client = url.startsWith("https") ? https : http;
52
+ const req = client.get(url, { timeout: HEALTH_TIMEOUT_MS }, (res) => {
53
+ const redirect = res.statusCode >= 301 && res.statusCode <= 302 && res.headers.location;
54
+ if (redirect) {
55
+ fetchOk(redirect).then(resolve).catch(() => resolve(false));
56
+ return;
57
+ }
58
+ resolve(res.statusCode === 200);
59
+ });
60
+ req.on("error", () => resolve(false));
61
+ req.on("timeout", () => { req.destroy(); resolve(false); });
62
+ });
63
+ }
64
+
65
+ function downloadFile(url, destPath) {
66
+ return new Promise((resolve, reject) => {
67
+ const client = url.startsWith("https") ? https : http;
68
+ const req = client.get(url, (res) => {
69
+ const redirect = res.statusCode >= 301 && res.statusCode <= 302 && res.headers.location;
70
+ if (redirect) {
71
+ downloadFile(redirect, destPath).then(resolve).catch(reject);
72
+ return;
73
+ }
74
+ if (res.statusCode !== 200) {
75
+ reject(new Error(`Download failed: ${res.statusCode}`));
76
+ return;
77
+ }
78
+ const dir = path.dirname(destPath);
79
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
80
+ const file = fs.createWriteStream(destPath);
81
+ res.pipe(file);
82
+ file.on("finish", () => { file.close(); resolve(); });
83
+ file.on("error", reject);
84
+ });
85
+ req.on("error", reject);
86
+ });
87
+ }
88
+
89
+ function engineRunning() {
90
+ return fetchOk(HEALTH_URL);
91
+ }
92
+
93
+ function waitForEngine() {
94
+ return new Promise((resolve) => {
95
+ let n = 0;
96
+ const t = setInterval(() => {
97
+ n++;
98
+ fetchOk(HEALTH_URL).then((ok) => {
99
+ if (ok) { clearInterval(t); resolve(true); }
100
+ else if (n >= HEALTH_POLL_MAX) { clearInterval(t); resolve(false); }
101
+ });
102
+ }, HEALTH_POLL_MS);
103
+ });
104
+ }
105
+
106
+ async function ensureEngine() {
107
+ const binPath = getEnginePath();
108
+ const binDir = getEngineDir();
109
+ if (!binPath || !binDir) {
110
+ console.error("drift: Could not resolve engine directory (~/.drift/bin). Set HOME or USERPROFILE.");
111
+ return false;
112
+ }
113
+ if (!fs.existsSync(binDir)) {
114
+ fs.mkdirSync(binDir, { recursive: true });
115
+ }
116
+ if (!fs.existsSync(binPath)) {
117
+ const { plat, arch } = getPlatformKey();
118
+ const ext = isWindows ? ".exe" : "";
119
+ const asset = `drift-engine-${plat}-${arch}${ext}`;
120
+ const url = `${ENGINE_BASE_URL.replace(/\/$/, "")}/${asset}`;
121
+ process.stderr.write(`drift: Downloading engine (${asset})...\n`);
122
+ try {
123
+ await downloadFile(url, binPath);
124
+ } catch (e) {
125
+ console.error("drift: Download failed.", e.message);
126
+ console.error("drift: Set DRIFT_ENGINE_BASE_URL or run the engine manually.");
127
+ return false;
128
+ }
129
+ if (!isWindows) {
130
+ try { fs.chmodSync(binPath, 0o755); } catch (_) {}
131
+ }
132
+ if (isMac) {
22
133
  try {
23
- const content = fs.readFileSync(p, "utf8").slice(0, 100);
24
- if (!content.includes("#!/usr/bin/env node")) {
25
- return p;
26
- }
134
+ spawnSync("xattr", ["-dr", "com.apple.quarantine", binPath], { stdio: "pipe" });
27
135
  } catch (_) {}
28
136
  }
29
137
  }
30
-
31
- // Fallback: search PATH but skip Node scripts
32
- const pathEnv = process.env.PATH || "";
33
- const dirs = pathEnv.split(path.delimiter);
34
-
35
- for (const dir of dirs) {
36
- const candidate = path.join(dir, "drift");
37
- if (fs.existsSync(candidate)) {
38
- try {
39
- const content = fs.readFileSync(candidate, "utf8").slice(0, 100);
40
- // Skip if it's a Node script (that's us or another Node launcher)
41
- if (content.includes("#!/usr/bin/env node")) {
42
- continue;
43
- }
44
- return candidate;
45
- } catch (_) {
46
- // Binary file or unreadable - might be the real one
47
- return candidate;
48
- }
49
- }
138
+ // On macOS, always ensure binary is executable and not quarantined before spawn (covers existing binaries).
139
+ if (isMac && binPath) {
140
+ try {
141
+ fs.chmodSync(binPath, 0o755);
142
+ spawnSync("xattr", ["-dr", "com.apple.quarantine", binPath], { stdio: "pipe" });
143
+ } catch (_) {}
144
+ }
145
+ const child = spawn(binPath, [], {
146
+ detached: true,
147
+ stdio: "ignore",
148
+ cwd: binDir,
149
+ env: { ...process.env, DRIFT_ENGINE_PORT: ENGINE_PORT },
150
+ });
151
+ child.unref();
152
+ return waitForEngine();
153
+ }
154
+
155
+ // Locate Python drift ONLY in pipx bin dir. Never search PATH (avoids running drift.cmd/drift.ps1/drift.js).
156
+ function findPythonDrift() {
157
+ const home = process.env.HOME || process.env.USERPROFILE || "";
158
+ if (!home) return null;
159
+ const localBin = path.join(home, ".local", "bin");
160
+ if (isWindows) {
161
+ const exe = path.join(localBin, "drift.exe");
162
+ if (fs.existsSync(exe)) return exe;
163
+ return null;
50
164
  }
51
-
165
+ const unix = path.join(localBin, "drift");
166
+ if (fs.existsSync(unix)) return unix;
52
167
  return null;
53
168
  }
54
169
 
55
- function main() {
170
+ async function main() {
171
+ const userBackend = process.env.DRIFT_BACKEND_URL;
172
+ if (!userBackend) {
173
+ const already = await engineRunning();
174
+ if (!already) {
175
+ const started = await ensureEngine().catch((e) => {
176
+ console.error("drift:", e.message);
177
+ return false;
178
+ });
179
+ if (!started) {
180
+ console.error("Failed to start drift engine.");
181
+ console.error("Please check permissions or download the engine manually.");
182
+ process.exit(1);
183
+ }
184
+ }
185
+ }
186
+
56
187
  const driftPath = findPythonDrift();
57
-
58
188
  if (!driftPath) {
59
189
  console.error(`
60
190
  drift is not installed.
@@ -62,21 +192,22 @@ drift is not installed.
62
192
  Install it with:
63
193
 
64
194
  pipx install drift-ml
65
-
66
- Then run:
67
-
68
- drift
69
195
  `);
70
196
  process.exit(1);
71
197
  }
72
198
 
73
- // Run the Python drift and forward stdin/stdout
199
+ const backendUrl = userBackend || `http://127.0.0.1:${ENGINE_PORT}`;
200
+ const env = { ...process.env, DRIFT_BACKEND_URL: backendUrl };
201
+
74
202
  const result = spawnSync(driftPath, process.argv.slice(2), {
75
203
  stdio: "inherit",
76
- env: process.env,
204
+ env,
77
205
  });
78
206
 
79
207
  process.exit(result.status === null ? (result.signal ? 128 + 9 : 1) : result.status);
80
208
  }
81
209
 
82
- main();
210
+ main().catch((e) => {
211
+ console.error("drift:", e.message);
212
+ process.exit(1);
213
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drift-ml",
3
- "version": "0.1.5",
3
+ "version": "0.1.10",
4
4
  "description": "Drift — terminal-first, chat-based AutoML. Same engine as the web app. On first run: downloads and starts the engine locally (never exposes engine source).",
5
5
  "bin": {
6
6
  "drift": "./bin/drift.js"