hacksh 0.6.0 → 0.8.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
@@ -3,6 +3,8 @@ import { inspect, parseArgs as parseArgs$1, stripVTControlCharacters, styleText
3
3
  import { spawn } from "node:child_process";
4
4
  import { emitKeypressEvents } from "node:readline";
5
5
  import { PassThrough } from "node:stream";
6
+ import { readFile, realpath } from "node:fs/promises";
7
+ import { dirname, join } from "node:path";
6
8
  //#region src/options.ts
7
9
  function kebabCase(value) {
8
10
  return value.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
@@ -643,6 +645,395 @@ async function parseArgs(command, argv, options) {
643
645
  return (await parseCommandArgs(command, argv, tokenizeCommandArgs(command, argv), options?.env, envPath)).args;
644
646
  }
645
647
  //#endregion
648
+ //#region src/terminal.ts
649
+ var TerminalLock = class {
650
+ #owner;
651
+ get owner() {
652
+ return this.#owner;
653
+ }
654
+ acquire(owner) {
655
+ if (this.#owner !== void 0) return false;
656
+ this.#owner = owner;
657
+ return true;
658
+ }
659
+ release() {
660
+ this.#owner = void 0;
661
+ }
662
+ };
663
+ //#endregion
664
+ //#region src/exec.ts
665
+ const OPTION_KEYS = /* @__PURE__ */ new Set([
666
+ "cwd",
667
+ "env",
668
+ "input",
669
+ "reject",
670
+ "timeout",
671
+ "signal",
672
+ "maxBuffer",
673
+ "forceKillAfterDelay",
674
+ "stdin",
675
+ "stdout",
676
+ "stderr",
677
+ "interactive",
678
+ "display",
679
+ "redact"
680
+ ]);
681
+ const MAX_DELAY = 2147483647;
682
+ const REFUSAL_MESSAGE = {
683
+ "json-output": "Command cannot use the terminal in JSON output mode",
684
+ "non-interactive": "Command requires an interactive terminal",
685
+ overlapping: "Command cannot use the terminal while it is in use"
686
+ };
687
+ function isRecord(value) {
688
+ return typeof value === "object" && value !== null && !Array.isArray(value);
689
+ }
690
+ function assertNoNul(value, label) {
691
+ if (value.includes("\0")) throw new TypeError(`${label} must not contain NUL characters`);
692
+ }
693
+ function readInteger(value, label, minimum, maximum) {
694
+ if (typeof value !== "number" || !Number.isInteger(value) || value < minimum || value > maximum) throw new TypeError(`${label} must be an integer from ${minimum} to ${maximum}`);
695
+ return value;
696
+ }
697
+ function readEnum(value, label, allowed) {
698
+ if (typeof value === "string" && allowed.includes(value)) return value;
699
+ throw new TypeError(`Exec option \`${label}\` must be ${allowed.map((item) => JSON.stringify(item)).join(" or ")}`);
700
+ }
701
+ function readBoolean(value, label) {
702
+ if (typeof value !== "boolean") throw new TypeError(`Exec option \`${label}\` must be a boolean`);
703
+ return value;
704
+ }
705
+ function readRedact(value) {
706
+ if (!Array.isArray(value)) throw new TypeError("Exec option `redact` must be an array");
707
+ for (const item of value) {
708
+ if (typeof item !== "string" || item === "") throw new TypeError("Exec option `redact` must contain only non-empty strings");
709
+ assertNoNul(item, "Exec option `redact`");
710
+ }
711
+ return value;
712
+ }
713
+ function mergeEnvironment(base, overrides) {
714
+ if (overrides === void 0 && base === void 0) return void 0;
715
+ if (overrides !== void 0 && !isRecord(overrides)) throw new TypeError("Exec option `env` must be an object");
716
+ const merged = {};
717
+ for (const [key, value] of Object.entries(base ?? process.env)) if (value !== void 0) merged[key] = value;
718
+ for (const [key, value] of Object.entries(overrides ?? {})) {
719
+ if (key === "" || key.includes("=") || key.includes("\0")) throw new TypeError(`Invalid environment variable name ${JSON.stringify(key)}`);
720
+ if (value === void 0) {
721
+ delete merged[key];
722
+ continue;
723
+ }
724
+ if (typeof value !== "string") throw new TypeError(`Environment variable ${key} must be a string or undefined`);
725
+ assertNoNul(value, `Environment variable ${key}`);
726
+ merged[key] = value;
727
+ }
728
+ return merged;
729
+ }
730
+ function redactValue(value, secrets) {
731
+ let redacted = value;
732
+ for (const secret of [...secrets].sort((left, right) => right.length - left.length)) redacted = redacted.split(secret).join("***");
733
+ return redacted;
734
+ }
735
+ function redactCommand(command, secrets) {
736
+ if (secrets.length === 0) return command;
737
+ return {
738
+ file: redactValue(command.file, secrets),
739
+ args: command.args.map((arg) => redactValue(arg, secrets)),
740
+ cwd: command.cwd === void 0 ? void 0 : redactValue(command.cwd, secrets)
741
+ };
742
+ }
743
+ function redactError(error, secrets) {
744
+ const source = error;
745
+ const redacted = new Error(redactValue(error.message, secrets));
746
+ if (typeof error.stack === "string") redacted.stack = redactValue(error.stack, secrets);
747
+ if (source.code !== void 0) redacted.code = source.code;
748
+ if (source.errno !== void 0) redacted.errno = source.errno;
749
+ if (typeof source.syscall === "string") redacted.syscall = redactValue(source.syscall, secrets);
750
+ if (typeof source.path === "string") redacted.path = redactValue(source.path, secrets);
751
+ if (Array.isArray(source.spawnargs)) redacted.spawnargs = source.spawnargs.map((arg) => typeof arg === "string" ? redactValue(arg, secrets) : arg);
752
+ return redacted;
753
+ }
754
+ function observedError(plan, error) {
755
+ return plan.redact.length === 0 ? error : redactError(error, plan.redact);
756
+ }
757
+ function parsePlan(file, args, options, baseEnv) {
758
+ if (typeof file !== "string" || file === "") throw new TypeError("Exec file must be a non-empty string");
759
+ assertNoNul(file, "Exec file");
760
+ const argv = args === void 0 ? [] : args;
761
+ if (!Array.isArray(argv) || !argv.every((arg) => typeof arg === "string")) throw new TypeError("Exec args must be an array of strings");
762
+ for (const arg of argv) assertNoNul(arg, "Exec args");
763
+ const raw = options === void 0 ? {} : options;
764
+ if (!isRecord(raw)) throw new TypeError("Exec options must be an object");
765
+ for (const key of Object.keys(raw)) if (!OPTION_KEYS.has(key)) throw new TypeError(`Unknown exec option \`${key}\``);
766
+ const { cwd, input, reject, timeout, signal, maxBuffer, forceKillAfterDelay } = raw;
767
+ if (cwd !== void 0) {
768
+ if (typeof cwd !== "string" || cwd === "") throw new TypeError("Exec option `cwd` must be a non-empty string");
769
+ assertNoNul(cwd, "Exec option `cwd`");
770
+ }
771
+ if (input !== void 0 && typeof input !== "string") throw new TypeError("Exec option `input` must be a string");
772
+ if (reject !== void 0 && typeof reject !== "boolean") throw new TypeError("Exec option `reject` must be a boolean");
773
+ if (signal !== void 0 && !(signal instanceof AbortSignal)) throw new TypeError("Exec option `signal` must be an AbortSignal");
774
+ const stdin = raw.stdin === void 0 ? "pipe" : readEnum(raw.stdin, "stdin", ["pipe", "inherit"]);
775
+ const stdout = raw.stdout === void 0 ? "pipe" : readEnum(raw.stdout, "stdout", ["pipe", "inherit"]);
776
+ const stderr = raw.stderr === void 0 ? "pipe" : readEnum(raw.stderr, "stderr", ["pipe", "inherit"]);
777
+ if (input !== void 0 && stdin === "inherit") throw new TypeError("Exec option `input` cannot be combined with stdin inherit");
778
+ const command = {
779
+ file,
780
+ args: [...argv],
781
+ cwd
782
+ };
783
+ const redact = raw.redact === void 0 ? [] : readRedact(raw.redact);
784
+ return {
785
+ command,
786
+ reported: redactCommand(command, redact),
787
+ env: mergeEnvironment(baseEnv, raw.env),
788
+ input,
789
+ reject: reject ?? true,
790
+ timeout: timeout === void 0 ? void 0 : readInteger(timeout, "Exec option `timeout`", 1, MAX_DELAY),
791
+ signal,
792
+ maxBuffer: maxBuffer === void 0 ? 1e8 : readInteger(maxBuffer, "Exec option `maxBuffer`", 1, Number.MAX_SAFE_INTEGER),
793
+ forceKillAfterDelay: forceKillAfterDelay === void 0 ? 5e3 : readInteger(forceKillAfterDelay, "Exec option `forceKillAfterDelay`", 0, MAX_DELAY),
794
+ stdin,
795
+ stdout,
796
+ stderr,
797
+ interactive: raw.interactive === void 0 ? false : readBoolean(raw.interactive, "interactive"),
798
+ display: raw.display === void 0 ? true : readBoolean(raw.display, "display"),
799
+ redact
800
+ };
801
+ }
802
+ function stripFinalNewline(text) {
803
+ if (text.endsWith("\r\n")) return text.slice(0, -2);
804
+ if (text.endsWith("\n")) return text.slice(0, -1);
805
+ return text;
806
+ }
807
+ function collect(stream, limit, onLimit) {
808
+ const chunks = [];
809
+ let length = 0;
810
+ let exceeded = false;
811
+ stream.on("data", (chunk) => {
812
+ if (exceeded) return;
813
+ const remaining = limit - length;
814
+ if (chunk.length <= remaining) {
815
+ chunks.push(chunk);
816
+ length += chunk.length;
817
+ return;
818
+ }
819
+ chunks.push(chunk.subarray(0, remaining));
820
+ length = limit;
821
+ exceeded = true;
822
+ onLimit();
823
+ });
824
+ return () => stripFinalNewline(Buffer.concat(chunks).toString("utf8"));
825
+ }
826
+ function toResult(command, outcome, stdout, stderr) {
827
+ const failed = {
828
+ status: "failed",
829
+ command,
830
+ stdout,
831
+ stderr
832
+ };
833
+ if (outcome.cause) return {
834
+ ...failed,
835
+ reason: outcome.cause
836
+ };
837
+ if (outcome.spawnError) return {
838
+ ...failed,
839
+ reason: "spawn",
840
+ error: outcome.spawnError
841
+ };
842
+ const { code, signal } = outcome.exit ?? {
843
+ code: null,
844
+ signal: null
845
+ };
846
+ if (signal !== null) return {
847
+ ...failed,
848
+ reason: "signal",
849
+ signal
850
+ };
851
+ if (code !== 0) return {
852
+ ...failed,
853
+ reason: "exit-code",
854
+ exitCode: code ?? 1
855
+ };
856
+ return {
857
+ status: "succeeded",
858
+ command,
859
+ stdout,
860
+ stderr,
861
+ exitCode: 0
862
+ };
863
+ }
864
+ function isSpawnErrno(error) {
865
+ return error instanceof Error && "syscall" in error;
866
+ }
867
+ function commandStdio(plan) {
868
+ return {
869
+ stdin: plan.stdin === "inherit" ? "inherit" : plan.input === void 0 ? "ignore" : "input",
870
+ stdout: plan.stdout,
871
+ stderr: plan.stderr
872
+ };
873
+ }
874
+ function usesTerminal(plan, stdio) {
875
+ return plan.interactive || stdio.stdin === "inherit" || stdio.stdout === "inherit" || stdio.stderr === "inherit";
876
+ }
877
+ function startChild(plan) {
878
+ const stdio = commandStdio(plan);
879
+ try {
880
+ return spawn(plan.command.file, plan.command.args, {
881
+ cwd: plan.command.cwd,
882
+ env: plan.env,
883
+ shell: false,
884
+ windowsHide: true,
885
+ stdio: [
886
+ stdio.stdin === "inherit" ? "inherit" : stdio.stdin === "input" ? "pipe" : "ignore",
887
+ stdio.stdout,
888
+ stdio.stderr
889
+ ]
890
+ });
891
+ } catch (error) {
892
+ if (isSpawnErrno(error)) return error;
893
+ throw error;
894
+ }
895
+ }
896
+ function runPlan(plan) {
897
+ const outcome = {
898
+ cause: void 0,
899
+ spawnError: void 0,
900
+ exit: void 0
901
+ };
902
+ if (plan.signal?.aborted) {
903
+ outcome.cause = "aborted";
904
+ return Promise.resolve(toResult(plan.reported, outcome, "", ""));
905
+ }
906
+ const child = startChild(plan);
907
+ if (child instanceof Error) {
908
+ outcome.spawnError = observedError(plan, child);
909
+ return Promise.resolve(toResult(plan.reported, outcome, "", ""));
910
+ }
911
+ return new Promise((resolve) => {
912
+ const stdout = child.stdout;
913
+ const stderr = child.stderr;
914
+ let settled = false;
915
+ let killTimer;
916
+ const settle = () => {
917
+ if (settled) return;
918
+ settled = true;
919
+ clearTimeout(timeoutTimer);
920
+ clearTimeout(killTimer);
921
+ plan.signal?.removeEventListener("abort", onAbort);
922
+ resolve(toResult(plan.reported, outcome, readStdout(), readStderr()));
923
+ };
924
+ const release = () => {
925
+ stdout?.destroy();
926
+ stderr?.destroy();
927
+ settle();
928
+ };
929
+ const terminate = (cause) => {
930
+ if (settled || outcome.cause) return;
931
+ outcome.cause = cause;
932
+ if (outcome.exit) {
933
+ release();
934
+ return;
935
+ }
936
+ child.kill("SIGTERM");
937
+ killTimer = setTimeout(() => child.kill("SIGKILL"), plan.forceKillAfterDelay);
938
+ };
939
+ const onAbort = () => terminate("aborted");
940
+ const readStdout = stdout ? collect(stdout, plan.maxBuffer, () => terminate("output-limit")) : () => "";
941
+ const readStderr = stderr ? collect(stderr, plan.maxBuffer, () => terminate("output-limit")) : () => "";
942
+ const timeoutTimer = plan.timeout === void 0 ? void 0 : setTimeout(() => terminate("timeout"), plan.timeout);
943
+ plan.signal?.addEventListener("abort", onAbort, { once: true });
944
+ child.on("error", (error) => {
945
+ if (child.pid !== void 0) return;
946
+ outcome.spawnError = observedError(plan, error);
947
+ release();
948
+ });
949
+ child.on("exit", (code, signal) => {
950
+ outcome.exit = {
951
+ code,
952
+ signal
953
+ };
954
+ clearTimeout(killTimer);
955
+ if (outcome.cause) release();
956
+ });
957
+ child.on("close", settle);
958
+ if (child.stdin) {
959
+ child.stdin.on("error", () => void 0);
960
+ child.stdin.end(plan.input);
961
+ }
962
+ });
963
+ }
964
+ function formatArgument(value) {
965
+ return value === "" || /[\s"'\\$`;|&<>*?()[\]{}~#!]/.test(value) ? JSON.stringify(value) : value;
966
+ }
967
+ function formatExecCommand(command) {
968
+ return [command.file, ...command.args].map(formatArgument).join(" ");
969
+ }
970
+ function failureMessage(plan, result) {
971
+ const display = formatExecCommand(plan.reported);
972
+ switch (result.reason) {
973
+ case "exit-code": return `Command failed with exit code ${result.exitCode}: ${display}`;
974
+ case "signal": return `Command was terminated by ${result.signal}: ${display}`;
975
+ case "spawn": return `Command could not be started (${result.error.code ?? result.error.message}): ${display}`;
976
+ case "timeout": return `Command timed out after ${plan.timeout} ms: ${display}`;
977
+ case "aborted": return `Command was aborted: ${display}`;
978
+ case "output-limit": return `Command output exceeded ${plan.maxBuffer} bytes: ${display}`;
979
+ case "json-output":
980
+ case "non-interactive":
981
+ case "overlapping": return `${REFUSAL_MESSAGE[result.reason]}: ${display}`;
982
+ }
983
+ }
984
+ function emptyFailure(plan, reason) {
985
+ return {
986
+ status: "failed",
987
+ command: plan.reported,
988
+ stdout: "",
989
+ stderr: "",
990
+ reason
991
+ };
992
+ }
993
+ function complete(plan, result) {
994
+ if (result.status === "failed" && plan.reject) throw new HackshExecError(failureMessage(plan, result), result, result.reason === "spawn" ? { cause: result.error } : void 0);
995
+ return result;
996
+ }
997
+ function blockedReason(plan, context, stdio) {
998
+ const inherited = stdio.stdin === "inherit" || stdio.stdout === "inherit" || stdio.stderr === "inherit";
999
+ if (context.mode.format === "json" && inherited) return "json-output";
1000
+ if (plan.interactive && !context.mode.interactive) return "non-interactive";
1001
+ }
1002
+ async function execute(file, args, options, context) {
1003
+ const plan = parsePlan(file, args, options, context.env);
1004
+ if (plan.signal?.aborted) return complete(plan, emptyFailure(plan, "aborted"));
1005
+ const stdio = commandStdio(plan);
1006
+ const blocked = blockedReason(plan, context, stdio);
1007
+ if (blocked) return complete(plan, emptyFailure(plan, blocked));
1008
+ const holdTerminal = usesTerminal(plan, stdio);
1009
+ if (holdTerminal && !context.terminal.acquire("exec")) return complete(plan, emptyFailure(plan, "overlapping"));
1010
+ try {
1011
+ if (plan.display) {
1012
+ const { file, args, cwd } = plan.reported;
1013
+ await context.print({
1014
+ type: "exec",
1015
+ phase: "start",
1016
+ command: cwd === void 0 ? {
1017
+ file,
1018
+ args
1019
+ } : {
1020
+ file,
1021
+ args,
1022
+ cwd
1023
+ },
1024
+ stdio,
1025
+ interactive: plan.interactive
1026
+ });
1027
+ }
1028
+ return complete(plan, await runPlan(plan));
1029
+ } finally {
1030
+ if (holdTerminal) context.terminal.release();
1031
+ }
1032
+ }
1033
+ function createExec(context) {
1034
+ return ((file, args, options) => execute(file, args, options, context));
1035
+ }
1036
+ //#endregion
646
1037
  //#region src/serialization.ts
647
1038
  function safeString(value) {
648
1039
  try {
@@ -771,7 +1162,7 @@ function isErrorLevel(level) {
771
1162
  return level === "warning" || level === "error" || level === "fatal";
772
1163
  }
773
1164
  function eventStream(event) {
774
- if (event.type === "error") return "stderr";
1165
+ if (event.type === "error" || event.type === "exec") return "stderr";
775
1166
  if (event.type === "usage") return event.stream;
776
1167
  if (event.type === "log" && isErrorLevel(event.record.level)) return "stderr";
777
1168
  return "stdout";
@@ -783,6 +1174,7 @@ function formatTextEvent(event) {
783
1174
  case "error": return event.error.kind === "usage" || event.error.kind === "validation" ? event.error.message : `Error: ${event.error.message}`;
784
1175
  case "log": return event.record.message;
785
1176
  case "prompt": return event.prompt.message;
1177
+ case "exec": return `$ ${formatExecCommand(event.command)}`;
786
1178
  }
787
1179
  }
788
1180
  /** Undecorated console compatibility adapter; automatic output uses the selected format printer. */
@@ -872,7 +1264,8 @@ const unicodeSymbols = {
872
1264
  usage: "◆",
873
1265
  version: "●",
874
1266
  errorEvent: "■",
875
- prompt: "?"
1267
+ prompt: "?",
1268
+ exec: "$"
876
1269
  }
877
1270
  };
878
1271
  const asciiSymbols = {
@@ -889,7 +1282,8 @@ const asciiSymbols = {
889
1282
  usage: "?",
890
1283
  version: "i",
891
1284
  errorEvent: "x",
892
- prompt: "?"
1285
+ prompt: "?",
1286
+ exec: "$"
893
1287
  }
894
1288
  };
895
1289
  const markStyles = {
@@ -908,7 +1302,8 @@ const markStyles = {
908
1302
  usage: "cyan",
909
1303
  version: "blue",
910
1304
  errorEvent: "red",
911
- prompt: "cyan"
1305
+ prompt: "cyan",
1306
+ exec: "gray"
912
1307
  };
913
1308
  function safelyInspect(value) {
914
1309
  if (typeof value === "string") return value;
@@ -955,6 +1350,7 @@ function eventMark(event) {
955
1350
  if (event.type === "version") return "version";
956
1351
  if (event.type === "error") return "errorEvent";
957
1352
  if (event.type === "prompt") return event.prompt.status === "submitted" ? "success" : "prompt";
1353
+ if (event.type === "exec") return "exec";
958
1354
  return event.record.kind === "message" ? event.record.level : event.record.kind;
959
1355
  }
960
1356
  function eventLines(event) {
@@ -976,6 +1372,11 @@ function eventLines(event) {
976
1372
  if (prompt.error) lines.push(prompt.error);
977
1373
  return lines;
978
1374
  }
1375
+ case "exec": {
1376
+ const lines = [formatExecCommand(event.command)];
1377
+ if (event.command.cwd !== void 0) lines.push(...propertyLines({ cwd: event.command.cwd }));
1378
+ return lines;
1379
+ }
979
1380
  }
980
1381
  }
981
1382
  function colorize(enabled, style, text) {
@@ -1059,253 +1460,6 @@ function createPrettyPrinter(options) {
1059
1460
  } };
1060
1461
  }
1061
1462
  //#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
1463
  //#region src/prompts.ts
1310
1464
  function details$1(kind, message, choices) {
1311
1465
  let normalizedMessage;
@@ -1335,11 +1489,12 @@ var PromptController = class {
1335
1489
  #inputValue;
1336
1490
  #printEvent;
1337
1491
  #bufferedKeys = [];
1338
- #active = false;
1492
+ #terminal;
1339
1493
  constructor(options) {
1340
1494
  this.#interactive = options.interactive;
1341
1495
  this.#inputValue = options.input;
1342
1496
  this.#printEvent = (event) => options.print(event);
1497
+ this.#terminal = options.terminal;
1343
1498
  }
1344
1499
  async #print(state, prompt) {
1345
1500
  try {
@@ -1353,9 +1508,8 @@ var PromptController = class {
1353
1508
  }
1354
1509
  async #run(prompt, initial, accept, update, submitted, closed) {
1355
1510
  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
1511
  const input = asInput(this.#inputValue, prompt);
1358
- this.#active = true;
1512
+ 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
1513
  let originallyFlowing = false;
1360
1514
  let rawChanged = false;
1361
1515
  let outcome;
@@ -1478,7 +1632,7 @@ var PromptController = class {
1478
1632
  try {
1479
1633
  await this.#print(closed(), prompt);
1480
1634
  } catch {} finally {
1481
- this.#active = false;
1635
+ this.#terminal.release();
1482
1636
  }
1483
1637
  throw failure;
1484
1638
  };
@@ -1493,7 +1647,7 @@ var PromptController = class {
1493
1647
  } catch {}
1494
1648
  throw error;
1495
1649
  } finally {
1496
- this.#active = false;
1650
+ this.#terminal.release();
1497
1651
  }
1498
1652
  }
1499
1653
  async select(options) {
@@ -1640,11 +1794,44 @@ function createLogger(printer, category, options) {
1640
1794
  const prompts = new PromptController({
1641
1795
  interactive: options.interactive,
1642
1796
  input: options.input,
1643
- print: (event) => options.printPrompt(event)
1797
+ print: (event) => options.printPrompt(event),
1798
+ terminal: options.terminal
1644
1799
  });
1645
1800
  return createBoundLogger(printer, [...category], {}, prompts);
1646
1801
  }
1647
1802
  //#endregion
1803
+ //#region src/package-version.ts
1804
+ async function findPackageVersion(entry) {
1805
+ if (!entry) return void 0;
1806
+ let directory;
1807
+ try {
1808
+ directory = dirname(await realpath(entry));
1809
+ } catch {
1810
+ return;
1811
+ }
1812
+ for (;;) {
1813
+ const version = await readPackageVersion(join(directory, "package.json"));
1814
+ if (version !== void 0) return version;
1815
+ const parent = dirname(directory);
1816
+ if (parent === directory) return void 0;
1817
+ directory = parent;
1818
+ }
1819
+ }
1820
+ async function readPackageVersion(path) {
1821
+ let text;
1822
+ try {
1823
+ text = await readFile(path, "utf8");
1824
+ } catch {
1825
+ return;
1826
+ }
1827
+ try {
1828
+ const { version } = JSON.parse(text);
1829
+ return typeof version === "string" ? version : void 0;
1830
+ } catch {
1831
+ return;
1832
+ }
1833
+ }
1834
+ //#endregion
1648
1835
  //#region src/output.ts
1649
1836
  function consoleWriter(method, stream) {
1650
1837
  return {
@@ -1865,13 +2052,14 @@ function resolveCommand(root, rawArgs) {
1865
2052
  remaining = remaining.slice(index + 1);
1866
2053
  }
1867
2054
  }
1868
- async function executeCommand(root, argv, displayUsage, printer, loggerOptions, invocationEnv) {
1869
- const env = invocationEnv ?? process.env;
2055
+ async function executeCommand(root, argv, displayUsage, printer, loggerOptions, execContext, fallbackVersion = async () => void 0) {
2056
+ const env = execContext.env ?? process.env;
1870
2057
  const resolved = resolveCommand(root, argv);
1871
2058
  if (resolved.segments[0]?.tokenized.version) {
2059
+ const version = root.version ?? await fallbackVersion() ?? "";
1872
2060
  await printer.print({
1873
2061
  type: "version",
1874
- version: root.version ?? ""
2062
+ version
1875
2063
  });
1876
2064
  return;
1877
2065
  }
@@ -1888,7 +2076,7 @@ async function executeCommand(root, argv, displayUsage, printer, loggerOptions,
1888
2076
  cmd: resolved.command,
1889
2077
  rawArgs: resolved.rawArgs,
1890
2078
  logger: createLogger(printer, category, loggerOptions),
1891
- exec: createExec(invocationEnv)
2079
+ exec: createExec(execContext)
1892
2080
  });
1893
2081
  }
1894
2082
  function printUsage(displayUsage, command, printer, stream) {
@@ -1909,8 +2097,21 @@ function prepareOutput(options, env) {
1909
2097
  });
1910
2098
  return {
1911
2099
  ...createPrintQueue(selectOutputPrinter(options, options.printer, mode)),
2100
+ format: mode.format,
1912
2101
  interactive: mode.interactive,
1913
- input
2102
+ input,
2103
+ terminal: new TerminalLock()
2104
+ };
2105
+ }
2106
+ function invocationExec(output, env) {
2107
+ return {
2108
+ env,
2109
+ mode: {
2110
+ format: output.format,
2111
+ interactive: output.interactive
2112
+ },
2113
+ print: (event) => output.printer.print(event),
2114
+ terminal: output.terminal
1914
2115
  };
1915
2116
  }
1916
2117
  async function runCommand(root, argv, options = {}) {
@@ -1919,8 +2120,9 @@ async function runCommand(root, argv, options = {}) {
1919
2120
  return await executeCommand(root, argv, options.showUsage ?? showUsage, output.printer, {
1920
2121
  interactive: output.interactive,
1921
2122
  input: output.input,
1922
- printPrompt: (event) => output.printChecked(event)
1923
- }, options.env);
2123
+ printPrompt: (event) => output.printChecked(event),
2124
+ terminal: output.terminal
2125
+ }, invocationExec(output, options.env));
1924
2126
  } finally {
1925
2127
  await output.drain();
1926
2128
  }
@@ -1962,8 +2164,9 @@ async function runMain(command, options = {}) {
1962
2164
  await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage, output.printer, {
1963
2165
  interactive: output.interactive,
1964
2166
  input: output.input,
1965
- printPrompt: (event) => output.printChecked(event)
1966
- }, options.env);
2167
+ printPrompt: (event) => output.printChecked(event),
2168
+ terminal: output.terminal
2169
+ }, invocationExec(output, options.env), () => findPackageVersion(process.argv[1]));
1967
2170
  } catch (error) {
1968
2171
  if (options.onError) {
1969
2172
  await options.onError(error);