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.
- package/lib/bootstrap.js +190 -35
- 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
|
-
|
|
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(
|
|
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(
|
|
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
|
|
1058
|
+
const invokeLong = cliInvoker(cliExecutable, runLong, platform);
|
|
934
1059
|
write("GitCode authentication is required. Opening the official browser login.");
|
|
935
|
-
write("
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
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 =
|
|
1007
|
-
"
|
|
1008
|
-
|
|
1009
|
-
|
|
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 =
|
|
1056
|
-
|
|
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(
|
|
1062
|
-
|
|
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
|
-
|
|
1216
|
+
runInteractive,
|
|
1217
|
+
runLong,
|
|
1066
1218
|
platform,
|
|
1067
|
-
|
|
1219
|
+
writeProgress,
|
|
1220
|
+
progress,
|
|
1068
1221
|
options.json,
|
|
1069
1222
|
);
|
|
1070
|
-
const skills =
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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,
|