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.
- package/lib/bootstrap.js +200 -38
- 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
|
|
485
|
-
return output.split(/\r?\n/).map((line) => line.trim()).
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1058
|
+
const invokeLong = cliInvoker(cliExecutable, runLong, platform);
|
|
927
1059
|
write("GitCode authentication is required. Opening the official browser login.");
|
|
928
|
-
write("
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
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 =
|
|
1000
|
-
"
|
|
1001
|
-
|
|
1002
|
-
|
|
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 =
|
|
1049
|
-
|
|
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(
|
|
1055
|
-
|
|
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
|
-
|
|
1216
|
+
runInteractive,
|
|
1217
|
+
runLong,
|
|
1059
1218
|
platform,
|
|
1060
|
-
|
|
1219
|
+
writeProgress,
|
|
1220
|
+
progress,
|
|
1061
1221
|
options.json,
|
|
1062
1222
|
);
|
|
1063
|
-
const skills =
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
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,
|