drift-ml 0.1.6 → 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 +174 -62
  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,101 +1,213 @@
1
1
  #!/usr/bin/env node
2
-
3
- const { spawnSync } = require("child_process");
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
+ //
10
+
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
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
+ }
8
34
 
9
- // Known pipx install locations (Unix and Windows)
10
- function getPipxBinPaths() {
35
+ function getEngineDir() {
11
36
  const home = process.env.HOME || process.env.USERPROFILE || "";
12
- if (!home) return [];
13
- const localBin = path.join(home, ".local", "bin");
14
- if (isWindows) {
15
- return [
16
- path.join(localBin, "drift.exe"),
17
- path.join(localBin, "drift"),
18
- ];
19
- }
20
- return [
21
- path.join(localBin, "drift"),
22
- "/usr/local/bin/drift",
23
- ];
37
+ if (!home) return null;
38
+ return path.join(home, ".drift", "bin");
24
39
  }
25
40
 
26
- function isLikelyUsOrNpmWrapper(filePath, content) {
27
- if (!content || typeof content !== "string") return false;
28
- const s = content.slice(0, 200);
29
- // Node launcher
30
- if (s.includes("#!/usr/bin/env node")) return true;
31
- // Windows npm .cmd wrapper (invokes node)
32
- if (s.includes("node") && (s.startsWith("@") || s.includes("cmd.exe"))) return true;
33
- return false;
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}`);
34
47
  }
35
48
 
36
- // Find Python-based drift (not this Node script or npm's drift.cmd)
37
- function findPythonDrift() {
38
- for (const p of getPipxBinPaths()) {
39
- if (fs.existsSync(p)) {
40
- try {
41
- const content = fs.readFileSync(p, "utf8").slice(0, 200);
42
- if (!isLikelyUsOrNpmWrapper(p, content)) return p;
43
- } catch (_) {
44
- return p;
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;
45
57
  }
46
- }
47
- }
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
+ }
48
92
 
49
- const pathEnv = process.env.PATH || "";
50
- const dirs = pathEnv.split(path.delimiter);
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
+ }
51
105
 
52
- for (const dir of dirs) {
53
- const base = path.join(dir, "drift");
54
- const candidates = isWindows ? [base + ".exe", base + ".cmd", base] : [base];
55
- for (const candidate of candidates) {
56
- if (!fs.existsSync(candidate)) continue;
57
- const ext = path.extname(candidate).toLowerCase();
58
- if (isWindows && (ext === ".cmd" || ext === ".bat" || ext === ".ps1")) continue;
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) {
59
133
  try {
60
- const content = fs.readFileSync(candidate, "utf8").slice(0, 200);
61
- if (isLikelyUsOrNpmWrapper(candidate, content)) continue;
62
- return candidate;
63
- } catch (_) {
64
- return candidate;
65
- }
134
+ spawnSync("xattr", ["-dr", "com.apple.quarantine", binPath], { stdio: "pipe" });
135
+ } catch (_) {}
66
136
  }
67
137
  }
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
+ }
68
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;
164
+ }
165
+ const unix = path.join(localBin, "drift");
166
+ if (fs.existsSync(unix)) return unix;
69
167
  return null;
70
168
  }
71
169
 
72
- 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
+
73
187
  const driftPath = findPythonDrift();
74
-
75
188
  if (!driftPath) {
76
189
  console.error(`
77
190
  drift is not installed.
78
191
 
79
- Install the Python CLI first:
192
+ Install it with:
80
193
 
81
- pip install --user pipx
82
- pipx ensurepath
83
194
  pipx install drift-ml
84
-
85
- Then run:
86
-
87
- drift
88
195
  `);
89
196
  process.exit(1);
90
197
  }
91
198
 
199
+ const backendUrl = userBackend || `http://127.0.0.1:${ENGINE_PORT}`;
200
+ const env = { ...process.env, DRIFT_BACKEND_URL: backendUrl };
201
+
92
202
  const result = spawnSync(driftPath, process.argv.slice(2), {
93
203
  stdio: "inherit",
94
- env: process.env,
95
- shell: isWindows,
204
+ env,
96
205
  });
97
206
 
98
207
  process.exit(result.status === null ? (result.signal ? 128 + 9 : 1) : result.status);
99
208
  }
100
209
 
101
- 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.6",
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"