msdevflow 0.7.6 → 0.7.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.
Files changed (2) hide show
  1. package/lib/bootstrap.js +212 -89
  2. package/package.json +1 -1
package/lib/bootstrap.js CHANGED
@@ -764,11 +764,13 @@ function outputLines(output) {
764
764
  return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
765
765
  }
766
766
 
767
- export function resolveExecutable(name, run, platform) {
768
- const invocation = platform === "win32"
767
+ function executableLookup(name, platform) {
768
+ return platform === "win32"
769
769
  ? { command: "where.exe", args: [name] }
770
770
  : { command: "sh", args: ["-c", "command -v -- \"$1\"", "sh", name] };
771
- const result = runOptional(run, invocation);
771
+ }
772
+
773
+ function executableFromLookup(result, platform) {
772
774
  if (result.status !== 0) {
773
775
  return "";
774
776
  }
@@ -779,6 +781,20 @@ export function resolveExecutable(name, run, platform) {
779
781
  return candidates[0] || "";
780
782
  }
781
783
 
784
+ export function resolveExecutable(name, run, platform) {
785
+ return executableFromLookup(
786
+ runOptional(run, executableLookup(name, platform)),
787
+ platform,
788
+ );
789
+ }
790
+
791
+ async function resolveExecutableAsync(name, run, platform) {
792
+ return executableFromLookup(
793
+ await runOptionalAsync(run, executableLookup(name, platform)),
794
+ platform,
795
+ );
796
+ }
797
+
782
798
  export function detectClients(run, platform) {
783
799
  return SUPPORTED_CLIENTS.filter((client) => Boolean(resolveExecutable(client, run, platform)));
784
800
  }
@@ -849,60 +865,95 @@ function hasPythonShebang(file) {
849
865
  }
850
866
  }
851
867
 
852
- function pythonScriptDirectories(run, platform) {
853
- const candidates = platform === "win32"
868
+ function pythonCandidates(platform) {
869
+ return platform === "win32"
854
870
  ? [{ command: "py", prefix: ["-3"] }, { command: "python", prefix: [] }, { command: "python3", prefix: [] }]
855
871
  : [{ command: "python3", prefix: [] }, { command: "python", prefix: [] }];
872
+ }
873
+
874
+ function parsePythonScriptDirectory(result) {
875
+ if (result.status !== 0) {
876
+ return "";
877
+ }
878
+ try {
879
+ const directory = JSON.parse(result.stdout.trim());
880
+ return typeof directory === "string" ? directory : "";
881
+ } catch {
882
+ return "";
883
+ }
884
+ }
885
+
886
+ function pythonScriptDirectories(run, platform) {
887
+ const probe = "import json,sysconfig; print(json.dumps(sysconfig.get_path('scripts')))";
888
+ return pythonCandidates(platform)
889
+ .map((candidate) => parsePythonScriptDirectory(runOptional(run, {
890
+ command: candidate.command,
891
+ args: [...candidate.prefix, "-c", probe],
892
+ })))
893
+ .filter(Boolean);
894
+ }
895
+
896
+ async function pythonScriptDirectoriesAsync(run, platform) {
856
897
  const probe = "import json,sysconfig; print(json.dumps(sysconfig.get_path('scripts')))";
857
898
  const directories = [];
858
- for (const candidate of candidates) {
859
- const result = runOptional(run, {
899
+ for (const candidate of pythonCandidates(platform)) {
900
+ const directory = parsePythonScriptDirectory(await runOptionalAsync(run, {
860
901
  command: candidate.command,
861
902
  args: [...candidate.prefix, "-c", probe],
862
- });
863
- if (result.status !== 0) {
864
- continue;
865
- }
866
- try {
867
- const directory = JSON.parse(result.stdout.trim());
868
- if (typeof directory === "string" && directory) {
869
- directories.push(directory);
870
- }
871
- } catch {
872
- continue;
903
+ }));
904
+ if (directory) {
905
+ directories.push(directory);
873
906
  }
874
907
  }
875
908
  return directories;
876
909
  }
877
910
 
878
- function isPythonGitcode(executable, run, platform) {
879
- if (/[\\/]Python[^\\/]*[\\/]Scripts[\\/]gitcode(?:\.exe)?$/i.test(executable)
880
- || /[\\/]pipx[\\/]/i.test(executable)
881
- || hasPythonShebang(executable)) {
882
- return true;
883
- }
911
+ function isKnownPythonGitcodePath(executable) {
912
+ return /[\\/]Python[^\\/]*[\\/]Scripts[\\/]gitcode(?:\.exe)?$/i.test(executable)
913
+ || /[\\/]pipx[\\/]/i.test(executable)
914
+ || hasPythonShebang(executable);
915
+ }
916
+
917
+ function executableMatchesPythonScripts(executable, directories, platform) {
884
918
  const directory = normalizedPath(executableDirectory(executable, platform), platform);
885
- return pythonScriptDirectories(run, platform)
886
- .some((candidate) => normalizedPath(candidate, platform) === directory);
919
+ return directories.some((candidate) => normalizedPath(candidate, platform) === directory);
887
920
  }
888
921
 
889
- export function diagnoseGitcode(run, platform) {
890
- const executable = resolveExecutable("gitcode", run, platform);
891
- if (!executable) {
892
- return {
893
- classification: "absent",
894
- existingExecutable: null,
895
- workflowCommand: "gitcode",
896
- };
897
- }
898
- if (isPythonGitcode(executable, run, platform)) {
899
- return {
900
- classification: "python",
901
- existingExecutable: executable,
902
- workflowCommand: "gitcode-npm",
903
- };
904
- }
905
- const doctor = runOptional(run, executableCommand(executable, ["doctor", "install", "--json"], platform));
922
+ function isPythonGitcode(executable, run, platform) {
923
+ return isKnownPythonGitcodePath(executable)
924
+ || executableMatchesPythonScripts(
925
+ executable,
926
+ pythonScriptDirectories(run, platform),
927
+ platform,
928
+ );
929
+ }
930
+
931
+ async function isPythonGitcodeAsync(executable, run, platform) {
932
+ return isKnownPythonGitcodePath(executable)
933
+ || executableMatchesPythonScripts(
934
+ executable,
935
+ await pythonScriptDirectoriesAsync(run, platform),
936
+ platform,
937
+ );
938
+ }
939
+
940
+ function absentGitcodeDiagnosis() {
941
+ return {
942
+ classification: "absent",
943
+ existingExecutable: null,
944
+ workflowCommand: "gitcode",
945
+ };
946
+ }
947
+
948
+ function pythonGitcodeDiagnosis(executable) {
949
+ return {
950
+ classification: "python",
951
+ existingExecutable: executable,
952
+ workflowCommand: "gitcode-npm",
953
+ };
954
+ }
955
+
956
+ function npmGitcodeDiagnosis(executable, doctor) {
906
957
  if (doctor.status === 0) {
907
958
  try {
908
959
  const metadata = JSON.parse(doctor.stdout);
@@ -920,23 +971,74 @@ export function diagnoseGitcode(run, platform) {
920
971
  throw new BootstrapError(`Existing gitcode has unknown ownership: ${executable}. Refusing to overwrite it.`, 3);
921
972
  }
922
973
 
923
- function detectPython(run, platform) {
924
- const candidates = platform === "win32"
925
- ? [{ command: "py", prefix: ["-3"] }, { command: "python", prefix: [] }, { command: "python3", prefix: [] }]
926
- : [{ command: "python3", prefix: [] }, { command: "python", prefix: [] }];
974
+ export function diagnoseGitcode(run, platform) {
975
+ const executable = resolveExecutable("gitcode", run, platform);
976
+ if (!executable) {
977
+ return absentGitcodeDiagnosis();
978
+ }
979
+ if (isPythonGitcode(executable, run, platform)) {
980
+ return pythonGitcodeDiagnosis(executable);
981
+ }
982
+ return npmGitcodeDiagnosis(
983
+ executable,
984
+ runOptional(run, executableCommand(executable, ["doctor", "install", "--json"], platform)),
985
+ );
986
+ }
987
+
988
+ async function diagnoseGitcodeAsync(run, platform) {
989
+ const executable = await resolveExecutableAsync("gitcode", run, platform);
990
+ if (!executable) {
991
+ return absentGitcodeDiagnosis();
992
+ }
993
+ if (await isPythonGitcodeAsync(executable, run, platform)) {
994
+ return pythonGitcodeDiagnosis(executable);
995
+ }
996
+ return npmGitcodeDiagnosis(
997
+ executable,
998
+ await runOptionalAsync(
999
+ run,
1000
+ executableCommand(executable, ["doctor", "install", "--json"], platform),
1001
+ ),
1002
+ );
1003
+ }
1004
+
1005
+ function parsePythonProbe(result) {
1006
+ if (result.status !== 0) {
1007
+ return null;
1008
+ }
1009
+ try {
1010
+ const data = JSON.parse(result.stdout.trim());
1011
+ if (data.version[0] > 3 || (data.version[0] === 3 && data.version[1] >= 10)) {
1012
+ return data;
1013
+ }
1014
+ } catch {
1015
+ return null;
1016
+ }
1017
+ return null;
1018
+ }
1019
+
1020
+ function pythonProbe(candidate) {
927
1021
  const probe = "import json,sys; print(json.dumps({'executable':sys.executable,'version':list(sys.version_info[:3])}))";
928
- for (const candidate of candidates) {
929
- const result = runOptional(run, { command: candidate.command, args: [...candidate.prefix, "-c", probe] });
930
- if (result.status !== 0) {
931
- continue;
1022
+ return { command: candidate.command, args: [...candidate.prefix, "-c", probe] };
1023
+ }
1024
+
1025
+ function detectPython(run, platform) {
1026
+ for (const candidate of pythonCandidates(platform)) {
1027
+ const data = parsePythonProbe(runOptional(run, pythonProbe(candidate)));
1028
+ if (data) {
1029
+ return data;
932
1030
  }
933
- try {
934
- const data = JSON.parse(result.stdout.trim());
935
- if (data.version[0] > 3 || (data.version[0] === 3 && data.version[1] >= 10)) {
936
- return data;
937
- }
938
- } catch {
939
- continue;
1031
+ }
1032
+ throw new BootstrapError("Python >=3.10 is required for workflow scripts.", 2);
1033
+ }
1034
+
1035
+ async function detectPythonAsync(run, platform) {
1036
+ for (const candidate of pythonCandidates(platform)) {
1037
+ const python = parsePythonProbe(
1038
+ await runOptionalAsync(run, pythonProbe(candidate)),
1039
+ );
1040
+ if (python) {
1041
+ return python;
940
1042
  }
941
1043
  }
942
1044
  throw new BootstrapError("Python >=3.10 is required for workflow scripts.", 2);
@@ -1004,7 +1106,7 @@ export function pythonRuntimeDetails(
1004
1106
  return pythonRuntimeAt(directory, python, platform);
1005
1107
  }
1006
1108
 
1007
- function validatePythonRuntime(runtime, run) {
1109
+ async function validatePythonRuntime(runtime, run) {
1008
1110
  let marker;
1009
1111
  try {
1010
1112
  const metadata = lstatSync(runtime.directory);
@@ -1043,10 +1145,10 @@ function validatePythonRuntime(runtime, run) {
1043
1145
  closeSync(descriptor);
1044
1146
  }
1045
1147
  }
1046
- if (runOptional(run, {
1148
+ if ((await runOptionalAsync(run, {
1047
1149
  command: runtime.executable,
1048
1150
  args: ["-m", "pip", "--version"],
1049
- }).status !== 0) {
1151
+ })).status !== 0) {
1050
1152
  return "repair";
1051
1153
  }
1052
1154
  return "current";
@@ -1069,8 +1171,8 @@ function writePythonRuntimeMarker(runtime) {
1069
1171
  writeFileSync(runtime.marker, PYTHON_RUNTIME_MARKER_CONTENT, { encoding: "utf8", flag: "wx" });
1070
1172
  }
1071
1173
 
1072
- async function ensurePythonRuntime(runtime, status, runLong, run) {
1073
- if (validatePythonRuntime(runtime, run) !== status) {
1174
+ async function ensurePythonRuntime(runtime, status, runLong) {
1175
+ if ((await validatePythonRuntime(runtime, runLong)) !== status) {
1074
1176
  throw new BootstrapError(
1075
1177
  `Managed Python runtime changed after confirmation: ${runtime.directory}`,
1076
1178
  3,
@@ -1499,7 +1601,42 @@ export async function runSetup(options, dependencies = {}) {
1499
1601
  platform,
1500
1602
  )),
1501
1603
  );
1502
- const diagnosis = diagnoseGitcode(run, platform);
1604
+ const { diagnosis, python, pythonRuntime } = await progress.run(
1605
+ "Inspecting local GitCode and Python environment",
1606
+ async () => {
1607
+ const [diagnosisResult, pythonResult] = await Promise.all([
1608
+ diagnoseGitcodeAsync(runLong, platform),
1609
+ detectPythonAsync(runLong, platform),
1610
+ ]);
1611
+ const configuredPythonRuntime = pythonRuntimeDetails(
1612
+ pythonResult,
1613
+ environment,
1614
+ platform,
1615
+ );
1616
+ const runtime = dependencies.pythonRuntimeDir
1617
+ ? pythonRuntimeAt(dependencies.pythonRuntimeDir, pythonResult, platform)
1618
+ : configuredPythonRuntime;
1619
+ const [venvCapability, runtimeStatus] = await Promise.all([
1620
+ runOptionalAsync(runLong, {
1621
+ command: pythonResult.executable,
1622
+ args: ["-m", "venv", "--help"],
1623
+ }),
1624
+ validatePythonRuntime(runtime, runLong),
1625
+ ]);
1626
+ if (venvCapability.status !== 0) {
1627
+ throw new BootstrapError(
1628
+ `Python venv is unavailable for ${pythonResult.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
1629
+ 2,
1630
+ );
1631
+ }
1632
+ runtime.status = runtimeStatus;
1633
+ return {
1634
+ diagnosis: diagnosisResult,
1635
+ python: pythonResult,
1636
+ pythonRuntime: runtime,
1637
+ };
1638
+ },
1639
+ );
1503
1640
  diagnosis.officialLatest = latestResult.status === 0 ? latestResult.stdout.trim() || null : null;
1504
1641
  const gitcodeInstall = gitcodeInstallDetails(diagnosis.classification, environment, platform, npmPrefix);
1505
1642
  if (gitcodeInstall.wrapper) {
@@ -1511,22 +1648,6 @@ export async function runSetup(options, dependencies = {}) {
1511
1648
  platform,
1512
1649
  );
1513
1650
 
1514
- const python = detectPython(run, platform);
1515
- const venvCapability = runOptional(run, {
1516
- command: python.executable,
1517
- args: ["-m", "venv", "--help"],
1518
- });
1519
- if (venvCapability.status !== 0) {
1520
- throw new BootstrapError(
1521
- `Python venv is unavailable for ${python.executable}. Install the venv component for this Python (for example python3-venv on Debian/Ubuntu) and rerun setup.`,
1522
- 2,
1523
- );
1524
- }
1525
- const configuredPythonRuntime = pythonRuntimeDetails(python, environment, platform);
1526
- const pythonRuntime = dependencies.pythonRuntimeDir
1527
- ? pythonRuntimeAt(dependencies.pythonRuntimeDir, python, platform)
1528
- : configuredPythonRuntime;
1529
- pythonRuntime.status = validatePythonRuntime(pythonRuntime, run);
1530
1651
  const venvInvocation = {
1531
1652
  command: python.executable,
1532
1653
  args: ["-m", "venv", pythonRuntime.directory],
@@ -1554,14 +1675,16 @@ export async function runSetup(options, dependencies = {}) {
1554
1675
  if (!options.yes && !(await (dependencies.confirm || confirmPlan)(process.stdin, process.stdout))) {
1555
1676
  throw new BootstrapError("Setup cancelled; no changes were applied.", 2);
1556
1677
  }
1557
- const currentDiagnosis = diagnoseGitcode(run, platform);
1558
- if (currentDiagnosis.classification !== diagnosis.classification
1559
- || currentDiagnosis.existingExecutable !== diagnosis.existingExecutable) {
1560
- throw new BootstrapError("GitCode command ownership changed after confirmation; refusing to install.", 3);
1561
- }
1562
- if (gitcodeInstall.wrapper) {
1563
- validateWrapper(gitcodeInstall, platform);
1564
- }
1678
+ await progress.run("Revalidating command ownership", async () => {
1679
+ const currentDiagnosis = await diagnoseGitcodeAsync(runLong, platform);
1680
+ if (currentDiagnosis.classification !== diagnosis.classification
1681
+ || currentDiagnosis.existingExecutable !== diagnosis.existingExecutable) {
1682
+ throw new BootstrapError("GitCode command ownership changed after confirmation; refusing to install.", 3);
1683
+ }
1684
+ if (gitcodeInstall.wrapper) {
1685
+ validateWrapper(gitcodeInstall, platform);
1686
+ }
1687
+ });
1565
1688
 
1566
1689
  await progress.run(
1567
1690
  pythonRuntime.status === "absent"
@@ -1569,7 +1692,7 @@ export async function runSetup(options, dependencies = {}) {
1569
1692
  : pythonRuntime.status === "repair"
1570
1693
  ? "Repairing the managed Python runtime"
1571
1694
  : "Validating the managed Python runtime",
1572
- () => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong, run),
1695
+ () => ensurePythonRuntime(pythonRuntime, pythonRuntime.status, runLong),
1573
1696
  );
1574
1697
  await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
1575
1698
  await progress.run(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.7.6",
3
+ "version": "0.7.7",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {