msdevflow 0.7.2 → 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 +190 -35
  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 {
@@ -792,8 +906,8 @@ function writeWrapper(details, platform) {
792
906
  }
793
907
  }
794
908
 
795
- function installGitcode(plan, run, platform) {
796
- run(plan.installInvocation);
909
+ async function installGitcode(plan, runLong, run, platform) {
910
+ await runLong(plan.installInvocation);
797
911
  if (plan.gitcodeInstall.mode === "coexist") {
798
912
  verifyFile(
799
913
  plan.gitcodeInstall.cliTarget,
@@ -885,10 +999,10 @@ function cliInvoker(cliExecutable, run, platform) {
885
999
  return (args, options) => run(executableCommand(cliExecutable, args, platform), options);
886
1000
  }
887
1001
 
888
- function validateCore(cliExecutable, run, platform) {
1002
+ async function validateCore(cliExecutable, run, platform) {
889
1003
  const invoke = cliInvoker(cliExecutable, run, platform);
890
- const version = invoke(["version"]).stdout.trim();
891
- 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 });
892
1006
  const missing = [];
893
1007
  try {
894
1008
  const metadata = JSON.parse(doctor.stdout);
@@ -899,7 +1013,7 @@ function validateCore(cliExecutable, run, platform) {
899
1013
  missing.push("doctor install (invalid JSON)");
900
1014
  }
901
1015
  for (const [schema, requiredFlags] of Object.entries(REQUIRED_SCHEMAS)) {
902
- const result = invoke(["schema", schema], { allowFailure: true });
1016
+ const result = await invoke(["schema", schema], { allowFailure: true });
903
1017
  if (result.status !== 0) {
904
1018
  missing.push(schema);
905
1019
  continue;
@@ -916,28 +1030,45 @@ function validateCore(cliExecutable, run, platform) {
916
1030
  missing.push(`${schema} (invalid schema JSON)`);
917
1031
  }
918
1032
  }
919
- if (invoke(["api", "--help"], { allowFailure: true }).status !== 0) {
1033
+ if ((await invoke(["api", "--help"], { allowFailure: true })).status !== 0) {
920
1034
  missing.push("api");
921
1035
  }
922
1036
  if (missing.length) {
923
1037
  throw new BootstrapError(`GitCode CLI is missing required capabilities: ${missing.join(", ")}.`);
924
1038
  }
925
- const authentication = safeAuthStatus(invoke(["auth", "status", "--json"], { allowFailure: true }).stdout);
1039
+ const authentication = safeAuthStatus(
1040
+ (await invoke(["auth", "status", "--json"], { allowFailure: true })).stdout,
1041
+ );
926
1042
  return { version, authentication, capabilities: "passed" };
927
1043
  }
928
1044
 
929
- 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
+ ) {
930
1055
  if (authentication.logged_in) {
931
1056
  return authentication;
932
1057
  }
933
- const invoke = cliInvoker(cliExecutable, run, platform);
1058
+ const invokeLong = cliInvoker(cliExecutable, runLong, platform);
934
1059
  write("GitCode authentication is required. Opening the official browser login.");
935
- write("Complete the GitCode page shown by the CLI; do not paste a token into this terminal or the conversation.");
936
- invoke(["auth", "login", "--web"], {
937
- inherit: true,
938
- outputToStderr: jsonOutput,
939
- });
940
- 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
+ ));
941
1072
  if (!authentication.logged_in) {
942
1073
  throw new BootstrapError("GitCode browser login did not produce an authenticated CLI session.", 2);
943
1074
  }
@@ -969,8 +1100,16 @@ export async function runSetup(options, dependencies = {}) {
969
1100
  const platform = dependencies.platform || process.platform;
970
1101
  const environment = dependencies.environment || process.env;
971
1102
  const run = dependencies.run || execute;
1103
+ const runLong = dependencies.runLong || dependencies.run || executeAsync;
1104
+ const runInteractive = dependencies.runInteractive || dependencies.run || executeAsync;
972
1105
  const write = dependencies.write || ((line) => console.log(line));
973
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
+ );
974
1113
  const targetSelection = await resolveClientTargets(
975
1114
  options,
976
1115
  run,
@@ -1003,11 +1142,14 @@ export async function runSetup(options, dependencies = {}) {
1003
1142
  run(namedCommand("npm", ["--version"], platform));
1004
1143
  const npmPrefix = run(namedCommand("npm", ["config", "get", "prefix"], platform)).stdout.trim();
1005
1144
  const gitVersion = run(namedCommand("git", ["--version"], platform)).stdout.trim();
1006
- const latestResult = runOptional(run, namedCommand(
1007
- "npm",
1008
- ["view", "@gitcode-cli/cli", "version", `--registry=${REGISTRY}`],
1009
- platform,
1010
- ));
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
+ );
1011
1153
  const diagnosis = diagnoseGitcode(run, platform);
1012
1154
  diagnosis.officialLatest = latestResult.status === 0 ? latestResult.stdout.trim() || null : null;
1013
1155
  const gitcodeInstall = gitcodeInstallDetails(diagnosis.classification, environment, platform, npmPrefix);
@@ -1052,27 +1194,40 @@ export async function runSetup(options, dependencies = {}) {
1052
1194
  validateWrapper(gitcodeInstall, platform);
1053
1195
  }
1054
1196
 
1055
- const installed = installGitcode(plan, run, platform);
1056
- 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
+ );
1057
1205
  gitcode.command = installed.command;
1058
1206
  gitcode.executable = installed.executable;
1059
1207
 
1060
- run(pipInvocation);
1061
- run({ command: python.executable, args: ["-c", "import playwright.sync_api"] });
1062
- 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(
1063
1214
  installed.executable,
1064
1215
  gitcode.authentication,
1065
- run,
1216
+ runInteractive,
1217
+ runLong,
1066
1218
  platform,
1067
- options.json ? writePlan : write,
1219
+ writeProgress,
1220
+ progress,
1068
1221
  options.json,
1069
1222
  );
1070
- const skills = skillInstalls.map(({ layout, details }) => ({
1071
- kind: layout.kind,
1072
- clients: layout.clients,
1073
- status: installBundledSkill(details),
1074
- target: layout.workflowDir,
1075
- }));
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
+ ));
1076
1231
  const result = {
1077
1232
  state: "ready",
1078
1233
  git: gitVersion,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "msdevflow",
3
- "version": "0.7.2",
3
+ "version": "0.7.3",
4
4
  "description": "Install the msdevflow GitCode skill and its runtime dependencies",
5
5
  "type": "module",
6
6
  "bin": {