hacksh 0.6.0 → 0.7.0

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/dist/index.js CHANGED
@@ -643,6 +643,395 @@ async function parseArgs(command, argv, options) {
643
643
  return (await parseCommandArgs(command, argv, tokenizeCommandArgs(command, argv), options?.env, envPath)).args;
644
644
  }
645
645
  //#endregion
646
+ //#region src/terminal.ts
647
+ var TerminalLock = class {
648
+ #owner;
649
+ get owner() {
650
+ return this.#owner;
651
+ }
652
+ acquire(owner) {
653
+ if (this.#owner !== void 0) return false;
654
+ this.#owner = owner;
655
+ return true;
656
+ }
657
+ release() {
658
+ this.#owner = void 0;
659
+ }
660
+ };
661
+ //#endregion
662
+ //#region src/exec.ts
663
+ const OPTION_KEYS = /* @__PURE__ */ new Set([
664
+ "cwd",
665
+ "env",
666
+ "input",
667
+ "reject",
668
+ "timeout",
669
+ "signal",
670
+ "maxBuffer",
671
+ "forceKillAfterDelay",
672
+ "stdin",
673
+ "stdout",
674
+ "stderr",
675
+ "interactive",
676
+ "display",
677
+ "redact"
678
+ ]);
679
+ const MAX_DELAY = 2147483647;
680
+ const REFUSAL_MESSAGE = {
681
+ "json-output": "Command cannot use the terminal in JSON output mode",
682
+ "non-interactive": "Command requires an interactive terminal",
683
+ overlapping: "Command cannot use the terminal while it is in use"
684
+ };
685
+ function isRecord(value) {
686
+ return typeof value === "object" && value !== null && !Array.isArray(value);
687
+ }
688
+ function assertNoNul(value, label) {
689
+ if (value.includes("\0")) throw new TypeError(`${label} must not contain NUL characters`);
690
+ }
691
+ function readInteger(value, label, minimum, maximum) {
692
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}`);
693
+ return value;
694
+ }
695
+ function readEnum(value, label, allowed) {
696
+ if (typeof value === "string" && allowed.includes(value)) return value;
697
+ throw new TypeError(`Exec option \`${label}\` must be ${allowed.map((item) => JSON.stringify(item)).join(" or ")}`);
698
+ }
699
+ function readBoolean(value, label) {
700
+ if (typeof value !== "boolean") throw new TypeError(`Exec option \`${label}\` must be a boolean`);
701
+ return value;
702
+ }
703
+ function readRedact(value) {
704
+ if (!Array.isArray(value)) throw new TypeError("Exec option `redact` must be an array");
705
+ for (const item of value) {
706
+ if (typeof item !== "string" || item === "") throw new TypeError("Exec option `redact` must contain only non-empty strings");
707
+ assertNoNul(item, "Exec option `redact`");
708
+ }
709
+ return value;
710
+ }
711
+ function mergeEnvironment(base, overrides) {
712
+ if (overrides === void 0 && base === void 0) return void 0;
713
+ if (overrides !== void 0 && !isRecord(overrides)) throw new TypeError("Exec option `env` must be an object");
714
+ const merged = {};
715
+ for (const [key, value] of Object.entries(base ?? process.env)) if (value !== void 0) merged[key] = value;
716
+ for (const [key, value] of Object.entries(overrides ?? {})) {
717
+ if (key === "" || key.includes("=") || key.includes("\0")) throw new TypeError(`Invalid environment variable name ${JSON.stringify(key)}`);
718
+ if (value === void 0) {
719
+ delete merged[key];
720
+ continue;
721
+ }
722
+ if (typeof value !== "string") throw new TypeError(`Environment variable ${key} must be a string or undefined`);
723
+ assertNoNul(value, `Environment variable ${key}`);
724
+ merged[key] = value;
725
+ }
726
+ return merged;
727
+ }
728
+ function redactValue(value, secrets) {
729
+ let redacted = value;
730
+ for (const secret of [...secrets].sort((left, right) => right.length - left.length)) redacted = redacted.split(secret).join("***");
731
+ return redacted;
732
+ }
733
+ function redactCommand(command, secrets) {
734
+ if (secrets.length === 0) return command;
735
+ return {
736
+ file: redactValue(command.file, secrets),
737
+ args: command.args.map((arg) => redactValue(arg, secrets)),
738
+ cwd: command.cwd === void 0 ? void 0 : redactValue(command.cwd, secrets)
739
+ };
740
+ }
741
+ function redactError(error, secrets) {
742
+ const source = error;
743
+ const redacted = new Error(redactValue(error.message, secrets));
744
+ if (typeof error.stack === "string") redacted.stack = redactValue(error.stack, secrets);
745
+ if (source.code !== void 0) redacted.code = source.code;
746
+ if (source.errno !== void 0) redacted.errno = source.errno;
747
+ if (typeof source.syscall === "string") redacted.syscall = redactValue(source.syscall, secrets);
748
+ if (typeof source.path === "string") redacted.path = redactValue(source.path, secrets);
749
+ if (Array.isArray(source.spawnargs)) redacted.spawnargs = source.spawnargs.map((arg) => typeof arg === "string" ? redactValue(arg, secrets) : arg);
750
+ return redacted;
751
+ }
752
+ function observedError(plan, error) {
753
+ return plan.redact.length === 0 ? error : redactError(error, plan.redact);
754
+ }
755
+ function parsePlan(file, args, options, baseEnv) {
756
+ if (typeof file !== "string" || file === "") throw new TypeError("Exec file must be a non-empty string");
757
+ assertNoNul(file, "Exec file");
758
+ const argv = args === void 0 ? [] : args;
759
+ if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) throw new TypeError("Exec args must be an array of strings");
760
+ for (const arg of argv) assertNoNul(arg, "Exec args");
761
+ const raw = options === void 0 ? {} : options;
762
+ if (!isRecord(raw)) throw new TypeError("Exec options must be an object");
763
+ for (const key of Object.keys(raw)) if (!OPTION_KEYS.has(key)) throw new TypeError(`Unknown exec option \`${key}\``);
764
+ const { cwd, input, reject, timeout, signal, maxBuffer, forceKillAfterDelay } = raw;
765
+ if (cwd !== void 0) {
766
+ if (typeof cwd !== "string" || cwd === "") throw new TypeError("Exec option `cwd` must be a non-empty string");
767
+ assertNoNul(cwd, "Exec option `cwd`");
768
+ }
769
+ if (input !== void 0 && typeof input !== "string") throw new TypeError("Exec option `input` must be a string");
770
+ if (reject !== void 0 && typeof reject !== "boolean") throw new TypeError("Exec option `reject` must be a boolean");
771
+ if (signal !== void 0 && !(signal instanceof AbortSignal)) throw new TypeError("Exec option `signal` must be an AbortSignal");
772
+ const stdin = raw.stdin === void 0 ? "pipe" : readEnum(raw.stdin, "stdin", ["pipe", "inherit"]);
773
+ const stdout = raw.stdout === void 0 ? "pipe" : readEnum(raw.stdout, "stdout", ["pipe", "inherit"]);
774
+ const stderr = raw.stderr === void 0 ? "pipe" : readEnum(raw.stderr, "stderr", ["pipe", "inherit"]);
775
+ if (input !== void 0 && stdin === "inherit") throw new TypeError("Exec option `input` cannot be combined with stdin inherit");
776
+ const command = {
777
+ file,
778
+ args: [...argv],
779
+ cwd
780
+ };
781
+ const redact = raw.redact === void 0 ? [] : readRedact(raw.redact);
782
+ return {
783
+ command,
784
+ reported: redactCommand(command, redact),
785
+ env: mergeEnvironment(baseEnv, raw.env),
786
+ input,
787
+ reject: reject ?? true,
788
+ timeout: timeout === void 0 ? void 0 : readInteger(timeout, "Exec option `timeout`", 1, MAX_DELAY),
789
+ signal,
790
+ maxBuffer: maxBuffer === void 0 ? 1e8 : readInteger(maxBuffer, "Exec option `maxBuffer`", 1, Number.MAX_SAFE_INTEGER),
791
+ forceKillAfterDelay: forceKillAfterDelay === void 0 ? 5e3 : readInteger(forceKillAfterDelay, "Exec option `forceKillAfterDelay`", 0, MAX_DELAY),
792
+ stdin,
793
+ stdout,
794
+ stderr,
795
+ interactive: raw.interactive === void 0 ? false : readBoolean(raw.interactive, "interactive"),
796
+ display: raw.display === void 0 ? true : readBoolean(raw.display, "display"),
797
+ redact
798
+ };
799
+ }
800
+ function stripFinalNewline(text) {
801
+ if (text.endsWith("\r\n")) return text.slice(0, -2);
802
+ if (text.endsWith("\n")) return text.slice(0, -1);
803
+ return text;
804
+ }
805
+ function collect(stream, limit, onLimit) {
806
+ const chunks = [];
807
+ let length = 0;
808
+ let exceeded = false;
809
+ stream.on("data", (chunk) => {
810
+ if (exceeded) return;
811
+ const remaining = limit - length;
812
+ if (chunk.length <= remaining) {
813
+ chunks.push(chunk);
814
+ length += chunk.length;
815
+ return;
816
+ }
817
+ chunks.push(chunk.subarray(0, remaining));
818
+ length = limit;
819
+ exceeded = true;
820
+ onLimit();
821
+ });
822
+ return () => stripFinalNewline(Buffer.concat(chunks).toString("utf8"));
823
+ }
824
+ function toResult(command, outcome, stdout, stderr) {
825
+ const failed = {
826
+ status: "failed",
827
+ command,
828
+ stdout,
829
+ stderr
830
+ };
831
+ if (outcome.cause) return {
832
+ ...failed,
833
+ reason: outcome.cause
834
+ };
835
+ if (outcome.spawnError) return {
836
+ ...failed,
837
+ reason: "spawn",
838
+ error: outcome.spawnError
839
+ };
840
+ const { code, signal } = outcome.exit ?? {
841
+ code: null,
842
+ signal: null
843
+ };
844
+ if (signal !== null) return {
845
+ ...failed,
846
+ reason: "signal",
847
+ signal
848
+ };
849
+ if (code !== 0) return {
850
+ ...failed,
851
+ reason: "exit-code",
852
+ exitCode: code ?? 1
853
+ };
854
+ return {
855
+ status: "succeeded",
856
+ command,
857
+ stdout,
858
+ stderr,
859
+ exitCode: 0
860
+ };
861
+ }
862
+ function isSpawnErrno(error) {
863
+ return error instanceof Error && "syscall" in error;
864
+ }
865
+ function commandStdio(plan) {
866
+ return {
867
+ stdin: plan.stdin === "inherit" ? "inherit" : plan.input === void 0 ? "ignore" : "input",
868
+ stdout: plan.stdout,
869
+ stderr: plan.stderr
870
+ };
871
+ }
872
+ function usesTerminal(plan, stdio) {
873
+ return plan.interactive || stdio.stdin === "inherit" || stdio.stdout === "inherit" || stdio.stderr === "inherit";
874
+ }
875
+ function startChild(plan) {
876
+ const stdio = commandStdio(plan);
877
+ try {
878
+ return spawn(plan.command.file, plan.command.args, {
879
+ cwd: plan.command.cwd,
880
+ env: plan.env,
881
+ shell: false,
882
+ windowsHide: true,
883
+ stdio: [
884
+ stdio.stdin === "inherit" ? "inherit" : stdio.stdin === "input" ? "pipe" : "ignore",
885
+ stdio.stdout,
886
+ stdio.stderr
887
+ ]
888
+ });
889
+ } catch (error) {
890
+ if (isSpawnErrno(error)) return error;
891
+ throw error;
892
+ }
893
+ }
894
+ function runPlan(plan) {
895
+ const outcome = {
896
+ cause: void 0,
897
+ spawnError: void 0,
898
+ exit: void 0
899
+ };
900
+ if (plan.signal?.aborted) {
901
+ outcome.cause = "aborted";
902
+ return Promise.resolve(toResult(plan.reported, outcome, "", ""));
903
+ }
904
+ const child = startChild(plan);
905
+ if (child instanceof Error) {
906
+ outcome.spawnError = observedError(plan, child);
907
+ return Promise.resolve(toResult(plan.reported, outcome, "", ""));
908
+ }
909
+ return new Promise((resolve) => {
910
+ const stdout = child.stdout;
911
+ const stderr = child.stderr;
912
+ let settled = false;
913
+ let killTimer;
914
+ const settle = () => {
915
+ if (settled) return;
916
+ settled = true;
917
+ clearTimeout(timeoutTimer);
918
+ clearTimeout(killTimer);
919
+ plan.signal?.removeEventListener("abort", onAbort);
920
+ resolve(toResult(plan.reported, outcome, readStdout(), readStderr()));
921
+ };
922
+ const release = () => {
923
+ stdout?.destroy();
924
+ stderr?.destroy();
925
+ settle();
926
+ };
927
+ const terminate = (cause) => {
928
+ if (settled || outcome.cause) return;
929
+ outcome.cause = cause;
930
+ if (outcome.exit) {
931
+ release();
932
+ return;
933
+ }
934
+ child.kill("SIGTERM");
935
+ killTimer = setTimeout(() => child.kill("SIGKILL"), plan.forceKillAfterDelay);
936
+ };
937
+ const onAbort = () => terminate("aborted");
938
+ const readStdout = stdout ? collect(stdout, plan.maxBuffer, () => terminate("output-limit")) : () => "";
939
+ const readStderr = stderr ? collect(stderr, plan.maxBuffer, () => terminate("output-limit")) : () => "";
940
+ const timeoutTimer = plan.timeout === void 0 ? void 0 : setTimeout(() => terminate("timeout"), plan.timeout);
941
+ plan.signal?.addEventListener("abort", onAbort, { once: true });
942
+ child.on("error", (error) => {
943
+ if (child.pid !== void 0) return;
944
+ outcome.spawnError = observedError(plan, error);
945
+ release();
946
+ });
947
+ child.on("exit", (code, signal) => {
948
+ outcome.exit = {
949
+ code,
950
+ signal
951
+ };
952
+ clearTimeout(killTimer);
953
+ if (outcome.cause) release();
954
+ });
955
+ child.on("close", settle);
956
+ if (child.stdin) {
957
+ child.stdin.on("error", () => void 0);
958
+ child.stdin.end(plan.input);
959
+ }
960
+ });
961
+ }
962
+ function formatArgument(value) {
963
+ return value === "" || /[\s"'\\$`;|&<>*?()[\]{}~#!]/.test(value) ? JSON.stringify(value) : value;
964
+ }
965
+ function formatExecCommand(command) {
966
+ return [command.file, ...command.args].map(formatArgument).join(" ");
967
+ }
968
+ function failureMessage(plan, result) {
969
+ const display = formatExecCommand(plan.reported);
970
+ switch (result.reason) {
971
+ case "exit-code": return `Command failed with exit code ${result.exitCode}: ${display}`;
972
+ case "signal": return `Command was terminated by ${result.signal}: ${display}`;
973
+ case "spawn": return `Command could not be started (${result.error.code ?? result.error.message}): ${display}`;
974
+ case "timeout": return `Command timed out after ${plan.timeout} ms: ${display}`;
975
+ case "aborted": return `Command was aborted: ${display}`;
976
+ case "output-limit": return `Command output exceeded ${plan.maxBuffer} bytes: ${display}`;
977
+ case "json-output":
978
+ case "non-interactive":
979
+ case "overlapping": return `${REFUSAL_MESSAGE[result.reason]}: ${display}`;
980
+ }
981
+ }
982
+ function emptyFailure(plan, reason) {
983
+ return {
984
+ status: "failed",
985
+ command: plan.reported,
986
+ stdout: "",
987
+ stderr: "",
988
+ reason
989
+ };
990
+ }
991
+ function complete(plan, result) {
992
+ if (result.status === "failed" && plan.reject) throw new HackshExecError(failureMessage(plan, result), result, result.reason === "spawn" ? { cause: result.error } : void 0);
993
+ return result;
994
+ }
995
+ function blockedReason(plan, context, stdio) {
996
+ const inherited = stdio.stdin === "inherit" || stdio.stdout === "inherit" || stdio.stderr === "inherit";
997
+ if (context.mode.format === "json" && inherited) return "json-output";
998
+ if (plan.interactive && !context.mode.interactive) return "non-interactive";
999
+ }
1000
+ async function execute(file, args, options, context) {
1001
+ const plan = parsePlan(file, args, options, context.env);
1002
+ if (plan.signal?.aborted) return complete(plan, emptyFailure(plan, "aborted"));
1003
+ const stdio = commandStdio(plan);
1004
+ const blocked = blockedReason(plan, context, stdio);
1005
+ if (blocked) return complete(plan, emptyFailure(plan, blocked));
1006
+ const holdTerminal = usesTerminal(plan, stdio);
1007
+ if (holdTerminal && !context.terminal.acquire("exec")) return complete(plan, emptyFailure(plan, "overlapping"));
1008
+ try {
1009
+ if (plan.display) {
1010
+ const { file, args, cwd } = plan.reported;
1011
+ await context.print({
1012
+ type: "exec",
1013
+ phase: "start",
1014
+ command: cwd === void 0 ? {
1015
+ file,
1016
+ args
1017
+ } : {
1018
+ file,
1019
+ args,
1020
+ cwd
1021
+ },
1022
+ stdio,
1023
+ interactive: plan.interactive
1024
+ });
1025
+ }
1026
+ return complete(plan, await runPlan(plan));
1027
+ } finally {
1028
+ if (holdTerminal) context.terminal.release();
1029
+ }
1030
+ }
1031
+ function createExec(context) {
1032
+ return ((file, args, options) => execute(file, args, options, context));
1033
+ }
1034
+ //#endregion
646
1035
  //#region src/serialization.ts
647
1036
  function safeString(value) {
648
1037
  try {
@@ -771,7 +1160,7 @@ function isErrorLevel(level) {
771
1160
  return level === "warning" || level === "error" || level === "fatal";
772
1161
  }
773
1162
  function eventStream(event) {
774
- if (event.type === "error") return "stderr";
1163
+ if (event.type === "error" || event.type === "exec") return "stderr";
775
1164
  if (event.type === "usage") return event.stream;
776
1165
  if (event.type === "log" && isErrorLevel(event.record.level)) return "stderr";
777
1166
  return "stdout";
@@ -783,6 +1172,7 @@ function formatTextEvent(event) {
783
1172
  case "error": return event.error.kind === "usage" || event.error.kind === "validation" ? event.error.message : `Error: ${event.error.message}`;
784
1173
  case "log": return event.record.message;
785
1174
  case "prompt": return event.prompt.message;
1175
+ case "exec": return `$ ${formatExecCommand(event.command)}`;
786
1176
  }
787
1177
  }
788
1178
  /** Undecorated console compatibility adapter; automatic output uses the selected format printer. */
@@ -872,7 +1262,8 @@ const unicodeSymbols = {
872
1262
  usage: "◆",
873
1263
  version: "●",
874
1264
  errorEvent: "■",
875
- prompt: "?"
1265
+ prompt: "?",
1266
+ exec: "$"
876
1267
  }
877
1268
  };
878
1269
  const asciiSymbols = {
@@ -889,7 +1280,8 @@ const asciiSymbols = {
889
1280
  usage: "?",
890
1281
  version: "i",
891
1282
  errorEvent: "x",
892
- prompt: "?"
1283
+ prompt: "?",
1284
+ exec: "$"
893
1285
  }
894
1286
  };
895
1287
  const markStyles = {
@@ -908,7 +1300,8 @@ const markStyles = {
908
1300
  usage: "cyan",
909
1301
  version: "blue",
910
1302
  errorEvent: "red",
911
- prompt: "cyan"
1303
+ prompt: "cyan",
1304
+ exec: "gray"
912
1305
  };
913
1306
  function safelyInspect(value) {
914
1307
  if (typeof value === "string") return value;
@@ -955,6 +1348,7 @@ function eventMark(event) {
955
1348
  if (event.type === "version") return "version";
956
1349
  if (event.type === "error") return "errorEvent";
957
1350
  if (event.type === "prompt") return event.prompt.status === "submitted" ? "success" : "prompt";
1351
+ if (event.type === "exec") return "exec";
958
1352
  return event.record.kind === "message" ? event.record.level : event.record.kind;
959
1353
  }
960
1354
  function eventLines(event) {
@@ -976,6 +1370,11 @@ function eventLines(event) {
976
1370
  if (prompt.error) lines.push(prompt.error);
977
1371
  return lines;
978
1372
  }
1373
+ case "exec": {
1374
+ const lines = [formatExecCommand(event.command)];
1375
+ if (event.command.cwd !== void 0) lines.push(...propertyLines({ cwd: event.command.cwd }));
1376
+ return lines;
1377
+ }
979
1378
  }
980
1379
  }
981
1380
  function colorize(enabled, style, text) {
@@ -1059,253 +1458,6 @@ function createPrettyPrinter(options) {
1059
1458
  } };
1060
1459
  }
1061
1460
  //#endregion
1062
- //#region src/exec.ts
1063
- const OPTION_KEYS = /* @__PURE__ */ new Set([
1064
- "cwd",
1065
- "env",
1066
- "input",
1067
- "reject",
1068
- "timeout",
1069
- "signal",
1070
- "maxBuffer",
1071
- "forceKillAfterDelay"
1072
- ]);
1073
- const MAX_DELAY = 2147483647;
1074
- function isRecord(value) {
1075
- return typeof value === "object" && value !== null && !Array.isArray(value);
1076
- }
1077
- function assertNoNul(value, label) {
1078
- if (value.includes("\0")) throw new TypeError(`${label} must not contain NUL characters`);
1079
- }
1080
- function readInteger(value, label, minimum, maximum) {
1081
- if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}`);
1082
- return value;
1083
- }
1084
- function mergeEnvironment(base, overrides) {
1085
- if (overrides === void 0 && base === void 0) return void 0;
1086
- if (overrides !== void 0 && !isRecord(overrides)) throw new TypeError("Exec option `env` must be an object");
1087
- const merged = {};
1088
- for (const [key, value] of Object.entries(base ?? process.env)) if (value !== void 0) merged[key] = value;
1089
- for (const [key, value] of Object.entries(overrides ?? {})) {
1090
- if (key === "" || key.includes("=") || key.includes("\0")) throw new TypeError(`Invalid environment variable name ${JSON.stringify(key)}`);
1091
- if (value === void 0) {
1092
- delete merged[key];
1093
- continue;
1094
- }
1095
- if (typeof value !== "string") throw new TypeError(`Environment variable ${key} must be a string or undefined`);
1096
- assertNoNul(value, `Environment variable ${key}`);
1097
- merged[key] = value;
1098
- }
1099
- return merged;
1100
- }
1101
- function parsePlan(file, args, options, baseEnv) {
1102
- if (typeof file !== "string" || file === "") throw new TypeError("Exec file must be a non-empty string");
1103
- assertNoNul(file, "Exec file");
1104
- const argv = args === void 0 ? [] : args;
1105
- if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) throw new TypeError("Exec args must be an array of strings");
1106
- for (const arg of argv) assertNoNul(arg, "Exec args");
1107
- const raw = options === void 0 ? {} : options;
1108
- if (!isRecord(raw)) throw new TypeError("Exec options must be an object");
1109
- for (const key of Object.keys(raw)) if (!OPTION_KEYS.has(key)) throw new TypeError(`Unknown exec option \`${key}\``);
1110
- const { cwd, input, reject, timeout, signal, maxBuffer, forceKillAfterDelay } = raw;
1111
- if (cwd !== void 0) {
1112
- if (typeof cwd !== "string" || cwd === "") throw new TypeError("Exec option `cwd` must be a non-empty string");
1113
- assertNoNul(cwd, "Exec option `cwd`");
1114
- }
1115
- if (input !== void 0 && typeof input !== "string") throw new TypeError("Exec option `input` must be a string");
1116
- if (reject !== void 0 && typeof reject !== "boolean") throw new TypeError("Exec option `reject` must be a boolean");
1117
- if (signal !== void 0 && !(signal instanceof AbortSignal)) throw new TypeError("Exec option `signal` must be an AbortSignal");
1118
- return {
1119
- command: {
1120
- file,
1121
- args: [...argv],
1122
- cwd
1123
- },
1124
- env: mergeEnvironment(baseEnv, raw.env),
1125
- input,
1126
- reject: reject ?? true,
1127
- timeout: timeout === void 0 ? void 0 : readInteger(timeout, "Exec option `timeout`", 1, MAX_DELAY),
1128
- signal,
1129
- maxBuffer: maxBuffer === void 0 ? 1e8 : readInteger(maxBuffer, "Exec option `maxBuffer`", 1, Number.MAX_SAFE_INTEGER),
1130
- forceKillAfterDelay: forceKillAfterDelay === void 0 ? 5e3 : readInteger(forceKillAfterDelay, "Exec option `forceKillAfterDelay`", 0, MAX_DELAY)
1131
- };
1132
- }
1133
- function stripFinalNewline(text) {
1134
- if (text.endsWith("\r\n")) return text.slice(0, -2);
1135
- if (text.endsWith("\n")) return text.slice(0, -1);
1136
- return text;
1137
- }
1138
- function collect(stream, limit, onLimit) {
1139
- const chunks = [];
1140
- let length = 0;
1141
- let exceeded = false;
1142
- stream.on("data", (chunk) => {
1143
- if (exceeded) return;
1144
- const remaining = limit - length;
1145
- if (chunk.length <= remaining) {
1146
- chunks.push(chunk);
1147
- length += chunk.length;
1148
- return;
1149
- }
1150
- chunks.push(chunk.subarray(0, remaining));
1151
- length = limit;
1152
- exceeded = true;
1153
- onLimit();
1154
- });
1155
- return () => stripFinalNewline(Buffer.concat(chunks).toString("utf8"));
1156
- }
1157
- function toResult(command, outcome, stdout, stderr) {
1158
- const failed = {
1159
- status: "failed",
1160
- command,
1161
- stdout,
1162
- stderr
1163
- };
1164
- if (outcome.cause) return {
1165
- ...failed,
1166
- reason: outcome.cause
1167
- };
1168
- if (outcome.spawnError) return {
1169
- ...failed,
1170
- reason: "spawn",
1171
- error: outcome.spawnError
1172
- };
1173
- const { code, signal } = outcome.exit ?? {
1174
- code: null,
1175
- signal: null
1176
- };
1177
- if (signal !== null) return {
1178
- ...failed,
1179
- reason: "signal",
1180
- signal
1181
- };
1182
- if (code !== 0) return {
1183
- ...failed,
1184
- reason: "exit-code",
1185
- exitCode: code ?? 1
1186
- };
1187
- return {
1188
- status: "succeeded",
1189
- command,
1190
- stdout,
1191
- stderr,
1192
- exitCode: 0
1193
- };
1194
- }
1195
- function isSpawnErrno(error) {
1196
- return error instanceof Error && "syscall" in error;
1197
- }
1198
- function startChild(plan) {
1199
- try {
1200
- return spawn(plan.command.file, plan.command.args, {
1201
- cwd: plan.command.cwd,
1202
- env: plan.env,
1203
- shell: false,
1204
- windowsHide: true,
1205
- stdio: [
1206
- plan.input === void 0 ? "ignore" : "pipe",
1207
- "pipe",
1208
- "pipe"
1209
- ]
1210
- });
1211
- } catch (error) {
1212
- if (isSpawnErrno(error)) return error;
1213
- throw error;
1214
- }
1215
- }
1216
- function runPlan(plan) {
1217
- const outcome = {
1218
- cause: void 0,
1219
- spawnError: void 0,
1220
- exit: void 0
1221
- };
1222
- if (plan.signal?.aborted) {
1223
- outcome.cause = "aborted";
1224
- return Promise.resolve(toResult(plan.command, outcome, "", ""));
1225
- }
1226
- const child = startChild(plan);
1227
- if (child instanceof Error) {
1228
- outcome.spawnError = child;
1229
- return Promise.resolve(toResult(plan.command, outcome, "", ""));
1230
- }
1231
- return new Promise((resolve) => {
1232
- const stdout = child.stdout;
1233
- const stderr = child.stderr;
1234
- let settled = false;
1235
- let killTimer;
1236
- const settle = () => {
1237
- if (settled) return;
1238
- settled = true;
1239
- clearTimeout(timeoutTimer);
1240
- clearTimeout(killTimer);
1241
- plan.signal?.removeEventListener("abort", onAbort);
1242
- resolve(toResult(plan.command, outcome, readStdout(), readStderr()));
1243
- };
1244
- const release = () => {
1245
- stdout.destroy();
1246
- stderr.destroy();
1247
- settle();
1248
- };
1249
- const terminate = (cause) => {
1250
- if (settled || outcome.cause) return;
1251
- outcome.cause = cause;
1252
- if (outcome.exit) {
1253
- release();
1254
- return;
1255
- }
1256
- child.kill("SIGTERM");
1257
- killTimer = setTimeout(() => child.kill("SIGKILL"), plan.forceKillAfterDelay);
1258
- };
1259
- const onAbort = () => terminate("aborted");
1260
- const readStdout = collect(stdout, plan.maxBuffer, () => terminate("output-limit"));
1261
- const readStderr = collect(stderr, plan.maxBuffer, () => terminate("output-limit"));
1262
- const timeoutTimer = plan.timeout === void 0 ? void 0 : setTimeout(() => terminate("timeout"), plan.timeout);
1263
- plan.signal?.addEventListener("abort", onAbort, { once: true });
1264
- child.on("error", (error) => {
1265
- if (child.pid !== void 0) return;
1266
- outcome.spawnError = error;
1267
- release();
1268
- });
1269
- child.on("exit", (code, signal) => {
1270
- outcome.exit = {
1271
- code,
1272
- signal
1273
- };
1274
- clearTimeout(killTimer);
1275
- if (outcome.cause) release();
1276
- });
1277
- child.on("close", settle);
1278
- if (child.stdin) {
1279
- child.stdin.on("error", () => void 0);
1280
- child.stdin.end(plan.input);
1281
- }
1282
- });
1283
- }
1284
- function formatArgument(value) {
1285
- return value === "" || /[\s"'\\$`;|&<>*?()[\]{}~#!]/.test(value) ? JSON.stringify(value) : value;
1286
- }
1287
- function failureMessage(plan, result) {
1288
- const display = [plan.command.file, ...plan.command.args].map(formatArgument).join(" ");
1289
- switch (result.reason) {
1290
- case "exit-code": return `Command failed with exit code ${result.exitCode}: ${display}`;
1291
- case "signal": return `Command was terminated by ${result.signal}: ${display}`;
1292
- case "spawn": return `Command could not be started (${result.error.code ?? result.error.message}): ${display}`;
1293
- case "timeout": return `Command timed out after ${plan.timeout} ms: ${display}`;
1294
- case "aborted": return `Command was aborted: ${display}`;
1295
- case "output-limit": return `Command output exceeded ${plan.maxBuffer} bytes: ${display}`;
1296
- }
1297
- }
1298
- async function execute(file, args, options, baseEnv) {
1299
- const plan = parsePlan(file, args, options, baseEnv);
1300
- const result = await runPlan(plan);
1301
- if (result.status === "failed" && plan.reject) throw new HackshExecError(failureMessage(plan, result), result, result.reason === "spawn" ? { cause: result.error } : void 0);
1302
- return result;
1303
- }
1304
- /** Bind exec to an invocation environment; `undefined` lets children inherit `process.env`. */
1305
- function createExec(baseEnv) {
1306
- return ((file, args, options) => execute(file, args, options, baseEnv));
1307
- }
1308
- //#endregion
1309
1461
  //#region src/prompts.ts
1310
1462
  function details$1(kind, message, choices) {
1311
1463
  let normalizedMessage;
@@ -1335,11 +1487,12 @@ var PromptController = class {
1335
1487
  #inputValue;
1336
1488
  #printEvent;
1337
1489
  #bufferedKeys = [];
1338
- #active = false;
1490
+ #terminal;
1339
1491
  constructor(options) {
1340
1492
  this.#interactive = options.interactive;
1341
1493
  this.#inputValue = options.input;
1342
1494
  this.#printEvent = (event) => options.print(event);
1495
+ this.#terminal = options.terminal;
1343
1496
  }
1344
1497
  async #print(state, prompt) {
1345
1498
  try {
@@ -1353,9 +1506,8 @@ var PromptController = class {
1353
1506
  }
1354
1507
  async #run(prompt, initial, accept, update, submitted, closed) {
1355
1508
  if (!this.#interactive) throw promptError("non-interactive", prompt, `Cannot request ${prompt.kind} input in non-interactive mode: ${prompt.message}`);
1356
- if (this.#active) throw promptError("overlapping", prompt, "Another prompt is already active");
1357
1509
  const input = asInput(this.#inputValue, prompt);
1358
- this.#active = true;
1510
+ if (!this.#terminal.acquire("prompt")) throw promptError("overlapping", prompt, this.#terminal.owner === "exec" ? "A command is using the terminal" : "Another prompt is already active");
1359
1511
  let originallyFlowing = false;
1360
1512
  let rawChanged = false;
1361
1513
  let outcome;
@@ -1478,7 +1630,7 @@ var PromptController = class {
1478
1630
  try {
1479
1631
  await this.#print(closed(), prompt);
1480
1632
  } catch {} finally {
1481
- this.#active = false;
1633
+ this.#terminal.release();
1482
1634
  }
1483
1635
  throw failure;
1484
1636
  };
@@ -1493,7 +1645,7 @@ var PromptController = class {
1493
1645
  } catch {}
1494
1646
  throw error;
1495
1647
  } finally {
1496
- this.#active = false;
1648
+ this.#terminal.release();
1497
1649
  }
1498
1650
  }
1499
1651
  async select(options) {
@@ -1640,7 +1792,8 @@ function createLogger(printer, category, options) {
1640
1792
  const prompts = new PromptController({
1641
1793
  interactive: options.interactive,
1642
1794
  input: options.input,
1643
- print: (event) => options.printPrompt(event)
1795
+ print: (event) => options.printPrompt(event),
1796
+ terminal: options.terminal
1644
1797
  });
1645
1798
  return createBoundLogger(printer, [...category], {}, prompts);
1646
1799
  }
@@ -1865,8 +2018,8 @@ function resolveCommand(root, rawArgs) {
1865
2018
  remaining = remaining.slice(index + 1);
1866
2019
  }
1867
2020
  }
1868
- async function executeCommand(root, argv, displayUsage, printer, loggerOptions, invocationEnv) {
1869
- const env = invocationEnv ?? process.env;
2021
+ async function executeCommand(root, argv, displayUsage, printer, loggerOptions, execContext) {
2022
+ const env = execContext.env ?? process.env;
1870
2023
  const resolved = resolveCommand(root, argv);
1871
2024
  if (resolved.segments[0]?.tokenized.version) {
1872
2025
  await printer.print({
@@ -1888,7 +2041,7 @@ async function executeCommand(root, argv, displayUsage, printer, loggerOptions,
1888
2041
  cmd: resolved.command,
1889
2042
  rawArgs: resolved.rawArgs,
1890
2043
  logger: createLogger(printer, category, loggerOptions),
1891
- exec: createExec(invocationEnv)
2044
+ exec: createExec(execContext)
1892
2045
  });
1893
2046
  }
1894
2047
  function printUsage(displayUsage, command, printer, stream) {
@@ -1909,8 +2062,21 @@ function prepareOutput(options, env) {
1909
2062
  });
1910
2063
  return {
1911
2064
  ...createPrintQueue(selectOutputPrinter(options, options.printer, mode)),
2065
+ format: mode.format,
1912
2066
  interactive: mode.interactive,
1913
- input
2067
+ input,
2068
+ terminal: new TerminalLock()
2069
+ };
2070
+ }
2071
+ function invocationExec(output, env) {
2072
+ return {
2073
+ env,
2074
+ mode: {
2075
+ format: output.format,
2076
+ interactive: output.interactive
2077
+ },
2078
+ print: (event) => output.printer.print(event),
2079
+ terminal: output.terminal
1914
2080
  };
1915
2081
  }
1916
2082
  async function runCommand(root, argv, options = {}) {
@@ -1919,8 +2085,9 @@ async function runCommand(root, argv, options = {}) {
1919
2085
  return await executeCommand(root, argv, options.showUsage ?? showUsage, output.printer, {
1920
2086
  interactive: output.interactive,
1921
2087
  input: output.input,
1922
- printPrompt: (event) => output.printChecked(event)
1923
- }, options.env);
2088
+ printPrompt: (event) => output.printChecked(event),
2089
+ terminal: output.terminal
2090
+ }, invocationExec(output, options.env));
1924
2091
  } finally {
1925
2092
  await output.drain();
1926
2093
  }
@@ -1962,8 +2129,9 @@ async function runMain(command, options = {}) {
1962
2129
  await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage, output.printer, {
1963
2130
  interactive: output.interactive,
1964
2131
  input: output.input,
1965
- printPrompt: (event) => output.printChecked(event)
1966
- }, options.env);
2132
+ printPrompt: (event) => output.printChecked(event),
2133
+ terminal: output.terminal
2134
+ }, invocationExec(output, options.env));
1967
2135
  } catch (error) {
1968
2136
  if (options.onError) {
1969
2137
  await options.onError(error);