msdevflow 0.7.1 → 0.7.3

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 +200 -38
  2. package/package.json +1 -1
package/lib/bootstrap.js CHANGED
@@ -1,4 +1,4 @@
1
- import { spawnSync } from "node:child_process";
1
+ import { spawn, spawnSync } from "node:child_process";
2
2
  import { createHash, randomUUID } from "node:crypto";
3
3
  import {
4
4
  closeSync,
@@ -265,6 +265,22 @@ function executableCommand(executable, args, platform) {
265
265
  };
266
266
  }
267
267
 
268
+ export function interactiveExecutableCommand(executable, args, platform) {
269
+ if (platform !== "win32" || !/\.(?:cmd|bat)$/i.test(executable)) {
270
+ return executableCommand(executable, args, platform);
271
+ }
272
+ const values = [executable, ...args];
273
+ if (values.some((value) => /["\r\n]/.test(value))) {
274
+ throw new BootstrapError("Interactive Windows command contains unsupported characters.", 3);
275
+ }
276
+ const commandLine = `"${values.map((value) => `"${value}"`).join(" ")}"`;
277
+ return {
278
+ command: "cmd.exe",
279
+ args: ["/d", "/s", "/c", commandLine],
280
+ windowsVerbatimArguments: true,
281
+ };
282
+ }
283
+
268
284
  export function formatCommand(invocation) {
269
285
  return [invocation.command, ...invocation.args]
270
286
  .map((value) => (/^[A-Za-z0-9_./:@=\\-]+$/.test(value) ? value : JSON.stringify(value)))
@@ -279,6 +295,7 @@ function execute(invocation, { allowFailure = false, inherit = false, outputToSt
279
295
  : "pipe",
280
296
  maxBuffer: 20 * 1024 * 1024,
281
297
  windowsHide: false,
298
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments === true,
282
299
  });
283
300
  if (result.error) {
284
301
  throw new BootstrapError(`Failed to run ${invocation.command}: ${result.error.message}`);
@@ -295,6 +312,54 @@ function execute(invocation, { allowFailure = false, inherit = false, outputToSt
295
312
  };
296
313
  }
297
314
 
315
+ function executeAsync(invocation, { allowFailure = false, inherit = false, outputToStderr = false } = {}) {
316
+ return new Promise((resolve, reject) => {
317
+ const child = spawn(invocation.command, invocation.args, {
318
+ stdio: inherit
319
+ ? ["inherit", outputToStderr ? process.stderr : "inherit", "inherit"]
320
+ : ["ignore", "pipe", "pipe"],
321
+ windowsHide: false,
322
+ windowsVerbatimArguments: invocation.windowsVerbatimArguments === true,
323
+ });
324
+ const stdout = [];
325
+ const stderr = [];
326
+ let outputBytes = 0;
327
+ let outputError = null;
328
+ const capture = (chunks) => (chunk) => {
329
+ outputBytes += chunk.length;
330
+ if (outputBytes > 20 * 1024 * 1024) {
331
+ outputError = new BootstrapError(`Command output exceeded 20 MiB: ${formatCommand(invocation)}`);
332
+ child.kill();
333
+ return;
334
+ }
335
+ chunks.push(chunk);
336
+ };
337
+ child.stdout?.on("data", capture(stdout));
338
+ child.stderr?.on("data", capture(stderr));
339
+ child.once("error", (error) => {
340
+ reject(new BootstrapError(`Failed to run ${invocation.command}: ${error.message}`));
341
+ });
342
+ child.once("close", (code) => {
343
+ if (outputError) {
344
+ reject(outputError);
345
+ return;
346
+ }
347
+ const status = code ?? 1;
348
+ const result = {
349
+ status,
350
+ stdout: Buffer.concat(stdout).toString("utf8"),
351
+ stderr: Buffer.concat(stderr).toString("utf8"),
352
+ };
353
+ if (status !== 0 && !allowFailure) {
354
+ const detail = inherit ? "" : `\n${(result.stderr || result.stdout).trim()}`;
355
+ reject(new BootstrapError(`Command failed (${status}): ${formatCommand(invocation)}${detail}`));
356
+ return;
357
+ }
358
+ resolve(result);
359
+ });
360
+ });
361
+ }
362
+
298
363
  function runOptional(run, invocation) {
299
364
  try {
300
365
  return run(invocation, { allowFailure: true });
@@ -306,6 +371,55 @@ function runOptional(run, invocation) {
306
371
  }
307
372
  }
308
373
 
374
+ async function runOptionalAsync(run, invocation) {
375
+ try {
376
+ return await run(invocation, { allowFailure: true });
377
+ } catch (error) {
378
+ if (error instanceof BootstrapError && error.message.startsWith("Failed to run ")) {
379
+ return { status: 1, stdout: "", stderr: error.message };
380
+ }
381
+ throw error;
382
+ }
383
+ }
384
+
385
+ export function createProgress(output, writeLine, timers = { setInterval, clearInterval }) {
386
+ const interactive = Boolean(output?.isTTY && typeof output.write === "function");
387
+ const frames = ["|", "/", "-", "\\"];
388
+ return {
389
+ async run(label, operation) {
390
+ if (!interactive) {
391
+ writeLine(`${label}...`);
392
+ try {
393
+ const result = await operation();
394
+ writeLine(`${label}: done`);
395
+ return result;
396
+ } catch (error) {
397
+ writeLine(`${label}: failed`);
398
+ throw error;
399
+ }
400
+ }
401
+
402
+ let frame = 0;
403
+ const render = () => {
404
+ output.write(`\r${frames[frame]} ${label}`);
405
+ frame = (frame + 1) % frames.length;
406
+ };
407
+ render();
408
+ const timer = timers.setInterval(render, 80);
409
+ try {
410
+ const result = await operation();
411
+ timers.clearInterval(timer);
412
+ output.write(`\r[done] ${label}\n`);
413
+ return result;
414
+ } catch (error) {
415
+ timers.clearInterval(timer);
416
+ output.write(`\r[failed] ${label}\n`);
417
+ throw error;
418
+ }
419
+ },
420
+ };
421
+ }
422
+
309
423
  function verifyFile(file, missingMessage) {
310
424
  let descriptor;
311
425
  try {
@@ -481,8 +595,8 @@ function validatePythonRequirements(file) {
481
595
  }
482
596
  }
483
597
 
484
- function firstOutputLine(output) {
485
- return output.split(/\r?\n/).map((line) => line.trim()).find(Boolean) || "";
598
+ function outputLines(output) {
599
+ return output.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
486
600
  }
487
601
 
488
602
  export function resolveExecutable(name, run, platform) {
@@ -490,7 +604,14 @@ export function resolveExecutable(name, run, platform) {
490
604
  ? { command: "where.exe", args: [name] }
491
605
  : { command: "sh", args: ["-c", "command -v -- \"$1\"", "sh", name] };
492
606
  const result = runOptional(run, invocation);
493
- return result.status === 0 ? firstOutputLine(result.stdout) : "";
607
+ if (result.status !== 0) {
608
+ return "";
609
+ }
610
+ const candidates = outputLines(result.stdout);
611
+ if (platform === "win32") {
612
+ return candidates.find((candidate) => /\.(?:com|exe|cmd|bat|ps1)$/i.test(candidate)) || "";
613
+ }
614
+ return candidates[0] || "";
494
615
  }
495
616
 
496
617
  export function detectClients(run, platform) {
@@ -785,8 +906,8 @@ function writeWrapper(details, platform) {
785
906
  }
786
907
  }
787
908
 
788
- function installGitcode(plan, run, platform) {
789
- run(plan.installInvocation);
909
+ async function installGitcode(plan, runLong, run, platform) {
910
+ await runLong(plan.installInvocation);
790
911
  if (plan.gitcodeInstall.mode === "coexist") {
791
912
  verifyFile(
792
913
  plan.gitcodeInstall.cliTarget,
@@ -878,10 +999,10 @@ function cliInvoker(cliExecutable, run, platform) {
878
999
  return (args, options) => run(executableCommand(cliExecutable, args, platform), options);
879
1000
  }
880
1001
 
881
- function validateCore(cliExecutable, run, platform) {
1002
+ async function validateCore(cliExecutable, run, platform) {
882
1003
  const invoke = cliInvoker(cliExecutable, run, platform);
883
- const version = invoke(["version"]).stdout.trim();
884
- const doctor = invoke(["doctor", "install", "--json"], { allowFailure: true });
1004
+ const version = (await invoke(["version"])).stdout.trim();
1005
+ const doctor = await invoke(["doctor", "install", "--json"], { allowFailure: true });
885
1006
  const missing = [];
886
1007
  try {
887
1008
  const metadata = JSON.parse(doctor.stdout);
@@ -892,7 +1013,7 @@ function validateCore(cliExecutable, run, platform) {
892
1013
  missing.push("doctor install (invalid JSON)");
893
1014
  }
894
1015
  for (const [schema, requiredFlags] of Object.entries(REQUIRED_SCHEMAS)) {
895
- const result = invoke(["schema", schema], { allowFailure: true });
1016
+ const result = await invoke(["schema", schema], { allowFailure: true });
896
1017
  if (result.status !== 0) {
897
1018
  missing.push(schema);
898
1019
  continue;
@@ -909,28 +1030,45 @@ function validateCore(cliExecutable, run, platform) {
909
1030
  missing.push(`${schema} (invalid schema JSON)`);
910
1031
  }
911
1032
  }
912
- if (invoke(["api", "--help"], { allowFailure: true }).status !== 0) {
1033
+ if ((await invoke(["api", "--help"], { allowFailure: true })).status !== 0) {
913
1034
  missing.push("api");
914
1035
  }
915
1036
  if (missing.length) {
916
1037
  throw new BootstrapError(`GitCode CLI is missing required capabilities: ${missing.join(", ")}.`);
917
1038
  }
918
- const authentication = safeAuthStatus(invoke(["auth", "status", "--json"], { allowFailure: true }).stdout);
1039
+ const authentication = safeAuthStatus(
1040
+ (await invoke(["auth", "status", "--json"], { allowFailure: true })).stdout,
1041
+ );
919
1042
  return { version, authentication, capabilities: "passed" };
920
1043
  }
921
1044
 
922
- function authenticateGitcode(cliExecutable, authentication, run, platform, write, jsonOutput) {
1045
+ async function authenticateGitcode(
1046
+ cliExecutable,
1047
+ authentication,
1048
+ runInteractive,
1049
+ runLong,
1050
+ platform,
1051
+ write,
1052
+ progress,
1053
+ jsonOutput,
1054
+ ) {
923
1055
  if (authentication.logged_in) {
924
1056
  return authentication;
925
1057
  }
926
- const invoke = cliInvoker(cliExecutable, run, platform);
1058
+ const invokeLong = cliInvoker(cliExecutable, runLong, platform);
927
1059
  write("GitCode authentication is required. Opening the official browser login.");
928
- write("Complete the GitCode page shown by the CLI; do not paste a token into this terminal or the conversation.");
929
- invoke(["auth", "login", "--web"], {
930
- inherit: true,
931
- outputToStderr: jsonOutput,
932
- });
933
- authentication = safeAuthStatus(invoke(["auth", "status", "--json"], { allowFailure: true }).stdout);
1060
+ write("Create the token in the browser, paste it at the CLI prompt, then press Enter.");
1061
+ write("Waiting for the GitCode CLI login process to finish; token verification can take up to 30 seconds.");
1062
+ await runInteractive(
1063
+ interactiveExecutableCommand(cliExecutable, ["auth", "login", "--web"], platform),
1064
+ {
1065
+ inherit: true,
1066
+ outputToStderr: jsonOutput,
1067
+ },
1068
+ );
1069
+ authentication = await progress.run("Verifying GitCode authentication", async () => safeAuthStatus(
1070
+ (await invokeLong(["auth", "status", "--json"], { allowFailure: true })).stdout,
1071
+ ));
934
1072
  if (!authentication.logged_in) {
935
1073
  throw new BootstrapError("GitCode browser login did not produce an authenticated CLI session.", 2);
936
1074
  }
@@ -962,8 +1100,16 @@ export async function runSetup(options, dependencies = {}) {
962
1100
  const platform = dependencies.platform || process.platform;
963
1101
  const environment = dependencies.environment || process.env;
964
1102
  const run = dependencies.run || execute;
1103
+ const runLong = dependencies.runLong || dependencies.run || executeAsync;
1104
+ const runInteractive = dependencies.runInteractive || dependencies.run || executeAsync;
965
1105
  const write = dependencies.write || ((line) => console.log(line));
966
1106
  const writePlan = dependencies.writePlan || write;
1107
+ const writeProgress = options.json ? writePlan : write;
1108
+ const progress = dependencies.progress || createProgress(
1109
+ dependencies.progressOutput || (options.json ? process.stderr : process.stdout),
1110
+ writeProgress,
1111
+ dependencies.timers,
1112
+ );
967
1113
  const targetSelection = await resolveClientTargets(
968
1114
  options,
969
1115
  run,
@@ -996,11 +1142,14 @@ export async function runSetup(options, dependencies = {}) {
996
1142
  run(namedCommand("npm", ["--version"], platform));
997
1143
  const npmPrefix = run(namedCommand("npm", ["config", "get", "prefix"], platform)).stdout.trim();
998
1144
  const gitVersion = run(namedCommand("git", ["--version"], platform)).stdout.trim();
999
- const latestResult = runOptional(run, namedCommand(
1000
- "npm",
1001
- ["view", "@gitcode-cli/cli", "version", `--registry=${REGISTRY}`],
1002
- platform,
1003
- ));
1145
+ const latestResult = await progress.run(
1146
+ "Checking the latest GitCode CLI version",
1147
+ () => runOptionalAsync(runLong, namedCommand(
1148
+ "npm",
1149
+ ["view", "@gitcode-cli/cli", "version", `--registry=${REGISTRY}`],
1150
+ platform,
1151
+ )),
1152
+ );
1004
1153
  const diagnosis = diagnoseGitcode(run, platform);
1005
1154
  diagnosis.officialLatest = latestResult.status === 0 ? latestResult.stdout.trim() || null : null;
1006
1155
  const gitcodeInstall = gitcodeInstallDetails(diagnosis.classification, environment, platform, npmPrefix);
@@ -1045,27 +1194,40 @@ export async function runSetup(options, dependencies = {}) {
1045
1194
  validateWrapper(gitcodeInstall, platform);
1046
1195
  }
1047
1196
 
1048
- const installed = installGitcode(plan, run, platform);
1049
- const gitcode = validateCore(installed.executable, run, platform);
1197
+ const installed = await progress.run(
1198
+ "Installing the official GitCode CLI",
1199
+ () => installGitcode(plan, runLong, run, platform),
1200
+ );
1201
+ const gitcode = await progress.run(
1202
+ "Validating GitCode CLI capabilities",
1203
+ () => validateCore(installed.executable, runLong, platform),
1204
+ );
1050
1205
  gitcode.command = installed.command;
1051
1206
  gitcode.executable = installed.executable;
1052
1207
 
1053
- run(pipInvocation);
1054
- run({ command: python.executable, args: ["-c", "import playwright.sync_api"] });
1055
- gitcode.authentication = authenticateGitcode(
1208
+ await progress.run("Installing reviewed Python dependencies", () => runLong(pipInvocation));
1209
+ await progress.run(
1210
+ "Verifying the Playwright Python package",
1211
+ () => runLong({ command: python.executable, args: ["-c", "import playwright.sync_api"] }),
1212
+ );
1213
+ gitcode.authentication = await authenticateGitcode(
1056
1214
  installed.executable,
1057
1215
  gitcode.authentication,
1058
- run,
1216
+ runInteractive,
1217
+ runLong,
1059
1218
  platform,
1060
- options.json ? writePlan : write,
1219
+ writeProgress,
1220
+ progress,
1061
1221
  options.json,
1062
1222
  );
1063
- const skills = skillInstalls.map(({ layout, details }) => ({
1064
- kind: layout.kind,
1065
- clients: layout.clients,
1066
- status: installBundledSkill(details),
1067
- target: layout.workflowDir,
1068
- }));
1223
+ const skills = await progress.run("Installing msdevflow skill targets", async () => (
1224
+ skillInstalls.map(({ layout, details }) => ({
1225
+ kind: layout.kind,
1226
+ clients: layout.clients,
1227
+ status: installBundledSkill(details),
1228
+ target: layout.workflowDir,
1229
+ }))
1230
+ ));
1069
1231
  const result = {
1070
1232
  state: "ready",
1071
1233
  git: gitVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.7.1",
3
+ "version": "0.7.3",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {