@qubiqlabs/mobiflow 0.3.0 → 0.5.0

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 +10 -0
  2. package/bin/mobiflow.js +124 -32
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -37,6 +37,16 @@ npm install -g @qubiqlabs/mobiflow
37
37
  npx @qubiqlabs/mobiflow --help
38
38
  ```
39
39
 
40
+ On Windows, the wrapper prefers `py -3.12` / `py -3` (not the Microsoft Store
41
+ `python` stub). If detection still fails:
42
+
43
+ ```bat
44
+ set MOBIFLOW_PYTHON=C:\Path\To\Python312\python.exe
45
+ mobiflow init
46
+ ```
47
+
48
+ Or skip npm and run: `py -3.12 -m pip install mobiflow` then `py -3.12 -m mobiflow init`.
49
+
40
50
  See [docs/PUBLISH.md](docs/PUBLISH.md) for maintainers.
41
51
 
42
52
  ## Quick start
package/bin/mobiflow.js CHANGED
@@ -2,6 +2,9 @@
2
2
  /**
3
3
  * npm bin wrapper for the Python MobiFlow CLI.
4
4
  * Resolves: PATH mobiflow → python -m mobiflow → pip install → retry.
5
+ *
6
+ * Windows note: never run ``python -c "…"`` through ``cmd.exe`` (shell:true) —
7
+ * quoting breaks and a real 3.12 install looks like “no Python 3.11+”.
5
8
  */
6
9
  "use strict";
7
10
 
@@ -12,50 +15,126 @@ const path = require("path");
12
15
  const PKG = require("../package.json");
13
16
  const VERSION = PKG.version || "0.1.0";
14
17
  const REPO = "https://github.com/javed0211/MobiFlow.git";
18
+ const IS_WIN = process.platform === "win32";
19
+
20
+ /** @typedef {{ cmd: string, prefixArgs?: string[], label?: string }} PyCandidate */
15
21
 
16
22
  function pyCandidates() {
17
- const fromEnv = process.env.MOBIFLOW_PYTHON;
23
+ /** @type {PyCandidate[]} */
18
24
  const list = [];
19
- if (fromEnv) list.push(fromEnv);
20
- if (process.platform === "win32") {
21
- list.push("py", "python3.12", "python3.11", "python", "python3");
25
+ const fromEnv = process.env.MOBIFLOW_PYTHON;
26
+ if (fromEnv) {
27
+ list.push({ cmd: fromEnv, label: "MOBIFLOW_PYTHON" });
28
+ }
29
+ if (IS_WIN) {
30
+ // Prefer the Python launcher so we skip the Windows Store stub.
31
+ list.push(
32
+ { cmd: "py", prefixArgs: ["-3.12"], label: "py -3.12" },
33
+ { cmd: "py", prefixArgs: ["-3.11"], label: "py -3.11" },
34
+ { cmd: "py", prefixArgs: ["-3"], label: "py -3" },
35
+ { cmd: "python3.12", label: "python3.12" },
36
+ { cmd: "python3.11", label: "python3.11" },
37
+ { cmd: "python", label: "python" },
38
+ { cmd: "python3", label: "python3" }
39
+ );
22
40
  } else {
23
- list.push("python3.12", "python3.11", "python3", "python");
41
+ list.push(
42
+ { cmd: "python3.12", label: "python3.12" },
43
+ { cmd: "python3.11", label: "python3.11" },
44
+ { cmd: "python3", label: "python3" },
45
+ { cmd: "python", label: "python" }
46
+ );
24
47
  }
25
- return [...new Set(list)];
48
+ return list;
26
49
  }
27
50
 
51
+ /**
52
+ * Spawn without a shell so argv (especially ``-c``) is not mangled by cmd.exe.
53
+ * @param {string} cmd
54
+ * @param {string[]} args
55
+ * @param {{ stdio?: any }} [opts]
56
+ */
28
57
  function run(cmd, args, opts = {}) {
29
58
  return spawnSync(cmd, args, {
30
59
  stdio: opts.stdio ?? "inherit",
31
60
  encoding: "utf8",
32
- shell: process.platform === "win32",
61
+ windowsHide: true,
33
62
  env: process.env,
34
63
  });
35
64
  }
36
65
 
37
- function pythonVersion(py) {
38
- const r = run(
39
- py,
40
- ["-c", "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}')"],
41
- { stdio: ["ignore", "pipe", "ignore"] }
66
+ /**
67
+ * @param {PyCandidate} cand
68
+ * @returns {{ major: number, minor: number } | null}
69
+ */
70
+ function pythonVersion(cand) {
71
+ // Avoid f-strings / nested quotes — Windows cmd historically mangled those
72
+ // when shell:true was used; keep the probe trivial either way.
73
+ const code =
74
+ "import sys; print(str(sys.version_info[0]) + '.' + str(sys.version_info[1]))";
75
+ const prefix = cand.prefixArgs || [];
76
+ const r = run(cand.cmd, [...prefix, "-c", code], {
77
+ stdio: ["ignore", "pipe", "pipe"],
78
+ });
79
+ if (r.error && r.error.code === "ENOENT") return null;
80
+ if (r.status !== 0 || !r.stdout) return null;
81
+ const text = r.stdout.toString().trim().split(/\r?\n/).pop() || "";
82
+ const m = text.match(/^(\d+)\.(\d+)/);
83
+ if (!m) return null;
84
+ return { major: Number(m[1]), minor: Number(m[2]) };
85
+ }
86
+
87
+ /**
88
+ * Reject the Microsoft Store alias that opens the Store instead of Python.
89
+ * @param {string} exe
90
+ */
91
+ function isWindowsStoreStub(exe) {
92
+ if (!IS_WIN || !exe) return false;
93
+ const n = exe.replace(/\//g, "\\").toLowerCase();
94
+ return (
95
+ n.includes("\\windowsapps\\") ||
96
+ n.includes("\\microsoft\\windowsapps\\") ||
97
+ n.endsWith("\\windowsapps\\python.exe") ||
98
+ n.endsWith("\\windowsapps\\python3.exe")
42
99
  );
100
+ }
101
+
102
+ /**
103
+ * @param {PyCandidate} cand
104
+ * @returns {string | null}
105
+ */
106
+ function resolveExecutable(cand) {
107
+ const prefix = cand.prefixArgs || [];
108
+ const r = run(cand.cmd, [...prefix, "-c", "import sys; print(sys.executable)"], {
109
+ stdio: ["ignore", "pipe", "pipe"],
110
+ });
43
111
  if (r.status !== 0 || !r.stdout) return null;
44
- const parts = r.stdout.toString().trim().split(".").map(Number);
45
- return { major: parts[0], minor: parts[1], exe: null };
112
+ const exe = r.stdout.toString().trim().split(/\r?\n/).pop() || "";
113
+ if (!exe || isWindowsStoreStub(exe)) return null;
114
+ return exe;
46
115
  }
47
116
 
48
117
  function whichPython() {
49
- for (const py of pyCandidates()) {
50
- const ver = pythonVersion(py);
51
- if (!ver || ver.major < 3 || ver.minor < 11) continue;
52
- const r = run(py, ["-c", "import sys; print(sys.executable)"], {
53
- stdio: ["ignore", "pipe", "ignore"],
54
- });
55
- if (r.status === 0 && r.stdout) {
56
- return r.stdout.toString().trim();
118
+ const tried = [];
119
+ for (const cand of pyCandidates()) {
120
+ const label = cand.label || cand.cmd;
121
+ const ver = pythonVersion(cand);
122
+ if (!ver) {
123
+ tried.push(`${label} (not found / failed)`);
124
+ continue;
57
125
  }
126
+ if (ver.major < 3 || ver.minor < 11) {
127
+ tried.push(`${label} (${ver.major}.${ver.minor} < 3.11)`);
128
+ continue;
129
+ }
130
+ const exe = resolveExecutable(cand);
131
+ if (!exe) {
132
+ tried.push(`${label} (${ver.major}.${ver.minor}, stub or unusable)`);
133
+ continue;
134
+ }
135
+ return exe;
58
136
  }
137
+ whichPython._tried = tried;
59
138
  return null;
60
139
  }
61
140
 
@@ -91,25 +170,32 @@ function ensureMobiflow(py) {
91
170
  return false;
92
171
  }
93
172
 
173
+ function pathLookup(binName) {
174
+ // Use where.exe explicitly — ``where`` with shell can behave oddly.
175
+ const cmd = IS_WIN ? "where.exe" : "which";
176
+ return run(cmd, [binName], { stdio: ["ignore", "pipe", "ignore"] });
177
+ }
178
+
94
179
  function main(argv) {
95
180
  const py = whichPython();
96
181
  if (!py) {
182
+ const tried = whichPython._tried || [];
97
183
  console.error(
98
184
  "[mobiflow] Python 3.11+ is required on PATH.\n" +
99
185
  " https://www.python.org/downloads/\n" +
100
- " Or set MOBIFLOW_PYTHON=/path/to/python3.12\n" +
101
- " Or: pip install mobiflow && mobiflow --help"
186
+ " Or set MOBIFLOW_PYTHON to your python.exe, e.g.\n" +
187
+ ' set MOBIFLOW_PYTHON=C:\\Users\\You\\AppData\\Local\\Programs\\Python\\Python312\\python.exe\n' +
188
+ " Or: py -3.12 -m pip install mobiflow && py -3.12 -m mobiflow --help"
102
189
  );
190
+ if (tried.length) {
191
+ console.error(" Tried: " + tried.join("; "));
192
+ }
103
193
  process.exit(1);
104
194
  }
105
195
 
106
196
  // Prefer an already-installed console script on PATH (avoid recursion).
107
197
  const self = path.resolve(__filename);
108
- const onPath = run(
109
- process.platform === "win32" ? "where" : "which",
110
- ["mobiflow"],
111
- { stdio: ["ignore", "pipe", "ignore"] }
112
- );
198
+ const onPath = pathLookup("mobiflow");
113
199
  if (onPath.status === 0 && onPath.stdout) {
114
200
  const candidates = onPath.stdout
115
201
  .toString()
@@ -131,7 +217,13 @@ function main(argv) {
131
217
  if (resolved.includes(`${path.sep}mobiflow${path.sep}bin${path.sep}`)) {
132
218
  continue;
133
219
  }
134
- const r = run(bin, argv, { stdio: "inherit" });
220
+ // Console scripts on Windows are often .cmd — shell helps those only.
221
+ const r = spawnSync(bin, argv, {
222
+ stdio: "inherit",
223
+ windowsHide: true,
224
+ env: process.env,
225
+ shell: IS_WIN && /\.(cmd|bat)$/i.test(bin),
226
+ });
135
227
  process.exit(r.status ?? 1);
136
228
  }
137
229
  }
@@ -139,8 +231,8 @@ function main(argv) {
139
231
  if (!ensureMobiflow(py)) {
140
232
  console.error(
141
233
  "[mobiflow] Could not install the Python package.\n" +
142
- ` Try: ${py} -m pip install "git+${REPO}@main"\n` +
143
- ` Or: ${py} -m pip install mobiflow`
234
+ ` Try: "${py}" -m pip install "git+${REPO}@main"\n` +
235
+ ` Or: "${py}" -m pip install mobiflow`
144
236
  );
145
237
  process.exit(1);
146
238
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qubiqlabs/mobiflow",
3
- "version": "0.3.0",
3
+ "version": "0.5.0",
4
4
  "description": "CLI: NL \u2192 Maestro mobile flows via LLM, run on device/emulator, self-heal. (npm wrapper for the Python package)",
5
5
  "license": "Apache-2.0",
6
6
  "author": "QubiQ Labs <labs@qubiq.ai>",