@qingflow-tech/qingflow-cli 1.1.34 → 1.1.35

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/README.md CHANGED
@@ -3,13 +3,13 @@
3
3
  Install:
4
4
 
5
5
  ```bash
6
- npm install @qingflow-tech/qingflow-cli@1.1.34
6
+ npm install @qingflow-tech/qingflow-cli@1.1.35
7
7
  ```
8
8
 
9
9
  Run:
10
10
 
11
11
  ```bash
12
- npx -y -p @qingflow-tech/qingflow-cli@1.1.34 qingflow
12
+ npx -y -p @qingflow-tech/qingflow-cli@1.1.35 qingflow
13
13
  ```
14
14
 
15
15
  Environment:
@@ -4,6 +4,7 @@ import { spawn, spawnSync } from "node:child_process";
4
4
  import { fileURLToPath } from "node:url";
5
5
 
6
6
  const WINDOWS = process.platform === "win32";
7
+ const MIN_PYTHON_VERSION = [3, 11, 0];
7
8
 
8
9
  const PKG_BY_BIN = {
9
10
  qingflow: "qingflow-cli",
@@ -26,11 +27,41 @@ function runChecked(command, args, options = {}) {
26
27
  }
27
28
  }
28
29
 
29
- function commandWorks(command, args) {
30
+ function commandResult(command, args) {
30
31
  const result = spawnSync(command, args, {
31
- stdio: "ignore",
32
+ encoding: "utf8",
33
+ stdio: ["ignore", "pipe", "pipe"],
32
34
  });
33
- return result.status === 0;
35
+ return {
36
+ status: result.status ?? 1,
37
+ error: result.error,
38
+ stdout: result.stdout ?? "",
39
+ stderr: result.stderr ?? "",
40
+ };
41
+ }
42
+
43
+ function parseVersion(text) {
44
+ const match = /(\d+)\.(\d+)\.(\d+)/.exec(text);
45
+ if (!match) {
46
+ return null;
47
+ }
48
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
49
+ }
50
+
51
+ function versionAtLeast(current, minimum) {
52
+ for (let index = 0; index < minimum.length; index += 1) {
53
+ if ((current[index] ?? 0) > minimum[index]) {
54
+ return true;
55
+ }
56
+ if ((current[index] ?? 0) < minimum[index]) {
57
+ return false;
58
+ }
59
+ }
60
+ return true;
61
+ }
62
+
63
+ function formatVersion(version) {
64
+ return version.join(".");
34
65
  }
35
66
 
36
67
  export function getPackageRoot(metaUrl) {
@@ -449,6 +480,40 @@ export function inspectPythonEnv(packageRoot, commandName = "qingflow-mcp") {
449
480
  };
450
481
  }
451
482
 
483
+ function getRuntimeProbeModule(commandName) {
484
+ if (commandName === "qingflow") {
485
+ return "qingflow_mcp.cli.main";
486
+ }
487
+ if (commandName === "qingflow-app-user-mcp") {
488
+ return "qingflow_mcp.server_app_user";
489
+ }
490
+ if (commandName === "qingflow-app-builder-mcp") {
491
+ return "qingflow_mcp.server_app_builder";
492
+ }
493
+ return "qingflow_mcp";
494
+ }
495
+
496
+ function probePythonRuntime(packageRoot, commandName) {
497
+ const python = getVenvPython(packageRoot);
498
+ if (!fs.existsSync(python)) {
499
+ return { ok: false, details: `missing Python executable: ${python}` };
500
+ }
501
+ const moduleName = getRuntimeProbeModule(commandName);
502
+ const result = spawnSync(python, ["-c", `import ${moduleName}`], {
503
+ cwd: packageRoot,
504
+ encoding: "utf8",
505
+ stdio: ["ignore", "pipe", "pipe"],
506
+ });
507
+ if (result.error) {
508
+ return { ok: false, details: result.error.message };
509
+ }
510
+ if (result.status !== 0) {
511
+ const details = [result.stderr, result.stdout].filter((value) => typeof value === "string" && value.trim()).join("\n");
512
+ return { ok: false, details: details || `import ${moduleName} failed` };
513
+ }
514
+ return { ok: true, details: "" };
515
+ }
516
+
452
517
  function formatRuntimeProblem(runtime, commandName, { allowRuntimeBootstrap = false } = {}) {
453
518
  const problemLines = [];
454
519
  for (const problem of runtime.problems) {
@@ -511,30 +576,38 @@ function findOfflineProjectWheel(findLinksDir) {
511
576
  export function findPython() {
512
577
  const preferred = process.env.QINGFLOW_MCP_PYTHON?.trim();
513
578
  const candidates = preferred
514
- ? [{ command: preferred, args: [], label: preferred }]
579
+ ? [{ command: preferred, checkArgs: ["-V"], runArgs: [], label: preferred }]
515
580
  : WINDOWS
516
581
  ? [
517
- { command: "py", args: ["-3", "-V"], label: "py -3" },
518
- { command: "python", args: ["-V"], label: "python" },
519
- { command: "python3", args: ["-V"], label: "python3" },
582
+ { command: "py", checkArgs: ["-3", "-V"], runArgs: ["-3"], label: "py -3" },
583
+ { command: "python", checkArgs: ["-V"], runArgs: [], label: "python" },
584
+ { command: "python3", checkArgs: ["-V"], runArgs: [], label: "python3" },
520
585
  ]
521
586
  : [
522
- { command: "python3", args: ["-V"], label: "python3" },
523
- { command: "python", args: ["-V"], label: "python" },
587
+ { command: "python3", checkArgs: ["-V"], runArgs: [], label: "python3" },
588
+ { command: "python", checkArgs: ["-V"], runArgs: [], label: "python" },
524
589
  ];
525
590
 
591
+ const rejected = [];
526
592
  for (const candidate of candidates) {
527
- if (commandWorks(candidate.command, candidate.args)) {
528
- if (candidate.command === "py") {
529
- return { command: "py", args: ["-3"], label: candidate.label };
530
- }
531
- return { command: candidate.command, args: [], label: candidate.label };
593
+ const result = commandResult(candidate.command, candidate.checkArgs);
594
+ if (result.status !== 0) {
595
+ continue;
596
+ }
597
+ const version = parseVersion(`${result.stdout}\n${result.stderr}`);
598
+ if (!version) {
599
+ rejected.push(`${candidate.label}: version could not be parsed`);
600
+ continue;
532
601
  }
602
+ if (!versionAtLeast(version, MIN_PYTHON_VERSION)) {
603
+ rejected.push(`${candidate.label}: ${formatVersion(version)}`);
604
+ continue;
605
+ }
606
+ return { command: candidate.command, args: candidate.runArgs, label: `${candidate.label} (${formatVersion(version)})` };
533
607
  }
534
608
 
535
- throw new Error(
536
- "Python 3.11+ was not found. Set QINGFLOW_MCP_PYTHON to a Python 3 executable before running npm install."
537
- );
609
+ const rejectedText = rejected.length ? ` Detected but rejected: ${rejected.join(", ")}.` : "";
610
+ throw new Error(`Python ${formatVersion(MIN_PYTHON_VERSION)}+ was not found.${rejectedText} Set QINGFLOW_MCP_PYTHON to a Python 3.11+ executable before running npm install.`);
538
611
  }
539
612
 
540
613
  export function ensurePythonEnv(packageRoot, { force = false, commandName = "qingflow-mcp" } = {}) {
@@ -544,7 +617,11 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
544
617
  const { packageVersion, serverCommand, stampPath, venvDir, stampVersion } = runtime;
545
618
 
546
619
  if (!force && fs.existsSync(serverCommand) && fs.existsSync(stampPath) && stampVersion && stampVersion === packageVersion) {
547
- return serverCommand;
620
+ const probe = probePythonRuntime(packageRoot, commandName);
621
+ if (probe.ok) {
622
+ return serverCommand;
623
+ }
624
+ fs.rmSync(venvDir, { recursive: true, force: true });
548
625
  }
549
626
 
550
627
  if (force && fs.existsSync(venvDir)) {
@@ -571,6 +648,11 @@ export function ensurePythonEnv(packageRoot, { force = false, commandName = "qin
571
648
  }
572
649
  runChecked(pip, pipArgs, { cwd: packageRoot });
573
650
 
651
+ const probe = probePythonRuntime(packageRoot, commandName);
652
+ if (!probe.ok) {
653
+ throw new Error(`Bootstrap finished but Python runtime import probe failed:\n${probe.details}`);
654
+ }
655
+
574
656
  fs.writeFileSync(
575
657
  stampPath,
576
658
  JSON.stringify(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@qingflow-tech/qingflow-cli",
3
- "version": "1.1.34",
3
+ "version": "1.1.35",
4
4
  "description": "Human-friendly Qingflow command line interface for auth, record operations, import, tasks, and stable builder flows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
package/pyproject.toml CHANGED
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "qingflow-mcp"
7
- version = "1.1.34"
7
+ version = "1.1.35"
8
8
  description = "User-authenticated MCP server for Qingflow"
9
9
  readme = "README.md"
10
10
  license = "MIT"