hacksh 0.1.1 → 0.2.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
@@ -1,5 +1,7 @@
1
1
  import { globalRegistry, safeParseAsync } from "zod/v4/core";
2
- import { parseArgs as parseArgs$1 } from "node:util";
2
+ import { inspect, parseArgs as parseArgs$1, stripVTControlCharacters, styleText } from "node:util";
3
+ import { emitKeypressEvents } from "node:readline";
4
+ import { PassThrough } from "node:stream";
3
5
  //#region src/options.ts
4
6
  function kebabCase(value) {
5
7
  return value.replaceAll(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
@@ -144,6 +146,182 @@ var ZittyValidationError = class extends ZittyUsageError {
144
146
  this.issues = issues;
145
147
  }
146
148
  };
149
+ /** An input request that could not be completed safely. */
150
+ var HackshPromptError = class extends Error {
151
+ reason;
152
+ prompt;
153
+ constructor(reason, prompt, message, options) {
154
+ super(message, options);
155
+ this.name = "HackshPromptError";
156
+ this.reason = reason;
157
+ this.prompt = prompt;
158
+ }
159
+ };
160
+ function safeString$1(value, fallback) {
161
+ try {
162
+ return String(value);
163
+ } catch {
164
+ return fallback;
165
+ }
166
+ }
167
+ function readProperty(value, key) {
168
+ try {
169
+ return Reflect.get(value, key);
170
+ } catch {
171
+ return "[Unserializable]";
172
+ }
173
+ }
174
+ function enumerableKeys(value) {
175
+ try {
176
+ return Object.keys(value);
177
+ } catch {
178
+ return [];
179
+ }
180
+ }
181
+ function hasToJSON(value) {
182
+ try {
183
+ return typeof Reflect.get(value, "toJSON") === "function";
184
+ } catch {
185
+ return false;
186
+ }
187
+ }
188
+ function isPlainObject(value) {
189
+ try {
190
+ const prototype = Object.getPrototypeOf(value);
191
+ return prototype === Object.prototype || prototype === null;
192
+ } catch {
193
+ return false;
194
+ }
195
+ }
196
+ function summarizeIssue({ arg, message, issue }, seen) {
197
+ if (!issue) return {
198
+ arg,
199
+ message
200
+ };
201
+ const details = {};
202
+ for (const key of enumerableKeys(issue)) {
203
+ if ([
204
+ "code",
205
+ "path",
206
+ "message",
207
+ "inst"
208
+ ].includes(key)) continue;
209
+ details[key] = summarizeErrors(readProperty(issue, key), seen);
210
+ }
211
+ const code = readProperty(issue, "code");
212
+ const path = readProperty(issue, "path");
213
+ return {
214
+ arg,
215
+ message,
216
+ ...typeof code === "string" ? { code } : {},
217
+ ...Array.isArray(path) ? { path } : {},
218
+ ...Object.keys(details).length === 0 ? {} : { details }
219
+ };
220
+ }
221
+ function errorKind(error) {
222
+ if (error instanceof HackshPromptError) return "prompt";
223
+ if (error instanceof ZittyValidationError) return "validation";
224
+ if (error instanceof ZittyUsageError) return "usage";
225
+ return "error";
226
+ }
227
+ function summarizeErrorWithSeen(error, seen) {
228
+ const summary = {
229
+ kind: errorKind(error),
230
+ name: safeString$1(readProperty(error, "name"), "Error"),
231
+ message: safeString$1(readProperty(error, "message"), "[Unserializable error message]")
232
+ };
233
+ seen.set(error, summary);
234
+ const stack = readProperty(error, "stack");
235
+ const cause = readProperty(error, "cause");
236
+ if (typeof stack === "string") summary.stack = stack;
237
+ if (cause !== void 0) summary.cause = summarizeErrors(cause, seen);
238
+ if (error instanceof ZittyValidationError) summary.issues = error.issues.map((issue) => summarizeIssue(issue, seen));
239
+ const reserved = /* @__PURE__ */ new Set([
240
+ "name",
241
+ "message",
242
+ "stack",
243
+ "cause",
244
+ "command",
245
+ "issues"
246
+ ]);
247
+ const properties = {};
248
+ for (const key of enumerableKeys(error)) {
249
+ if (reserved.has(key)) continue;
250
+ properties[key] = summarizeErrors(readProperty(error, key), seen);
251
+ }
252
+ if (Object.keys(properties).length > 0) summary.properties = properties;
253
+ return summary;
254
+ }
255
+ /** Recursively replace Error values with command- and schema-free summaries. */
256
+ function summarizeErrors(value, seen = /* @__PURE__ */ new WeakMap()) {
257
+ if (typeof value !== "object" || value === null) return value;
258
+ const existing = seen.get(value);
259
+ if (existing !== void 0) return existing;
260
+ if (value instanceof Error) return summarizeErrorWithSeen(value, seen);
261
+ if (Array.isArray(value)) {
262
+ const result = [];
263
+ seen.set(value, result);
264
+ let changed = false;
265
+ for (const item of value) {
266
+ const summarized = summarizeErrors(item, seen);
267
+ result.push(summarized);
268
+ changed ||= summarized !== item;
269
+ }
270
+ if (changed) return result;
271
+ seen.set(value, value);
272
+ return value;
273
+ }
274
+ if (value instanceof Map) {
275
+ const result = /* @__PURE__ */ new Map();
276
+ seen.set(value, result);
277
+ let changed = false;
278
+ for (const [key, item] of value) {
279
+ const summarizedKey = summarizeErrors(key, seen);
280
+ const summarizedItem = summarizeErrors(item, seen);
281
+ result.set(summarizedKey, summarizedItem);
282
+ changed ||= summarizedKey !== key || summarizedItem !== item;
283
+ }
284
+ if (changed) return result;
285
+ seen.set(value, value);
286
+ return value;
287
+ }
288
+ if (value instanceof Set) {
289
+ const result = /* @__PURE__ */ new Set();
290
+ seen.set(value, result);
291
+ let changed = false;
292
+ for (const item of value) {
293
+ const summarized = summarizeErrors(item, seen);
294
+ result.add(summarized);
295
+ changed ||= summarized !== item;
296
+ }
297
+ if (changed) return result;
298
+ seen.set(value, value);
299
+ return value;
300
+ }
301
+ if (ArrayBuffer.isView(value) || hasToJSON(value) || !isPlainObject(value)) return value;
302
+ const result = {};
303
+ seen.set(value, result);
304
+ let changed = false;
305
+ for (const key of enumerableKeys(value)) {
306
+ const original = readProperty(value, key);
307
+ const summarized = summarizeErrors(original, seen);
308
+ result[key] = summarized;
309
+ changed ||= summarized !== original;
310
+ }
311
+ if (changed) return result;
312
+ seen.set(value, value);
313
+ return value;
314
+ }
315
+ /** Convert any thrown value into a canonical error description. */
316
+ function summarizeError(error) {
317
+ if (error instanceof Error) return summarizeErrorWithSeen(error, /* @__PURE__ */ new WeakMap());
318
+ return {
319
+ kind: "thrown",
320
+ name: "NonErrorThrown",
321
+ message: safeString$1(error, "[Unserializable thrown value]"),
322
+ value: summarizeErrors(error)
323
+ };
324
+ }
147
325
  //#endregion
148
326
  //#region src/parser.ts
149
327
  function optionModel(command) {
@@ -360,6 +538,801 @@ async function parseArgs(command, argv) {
360
538
  return (await parseCommandArgs(command, argv)).args;
361
539
  }
362
540
  //#endregion
541
+ //#region src/serialization.ts
542
+ function safeString(value) {
543
+ try {
544
+ return String(value);
545
+ } catch {
546
+ return "[Unserializable]";
547
+ }
548
+ }
549
+ function normalizeProperty(value, key, ancestors) {
550
+ try {
551
+ return normalizeJson(Reflect.get(value, key), ancestors);
552
+ } catch {
553
+ return "[Unserializable]";
554
+ }
555
+ }
556
+ function normalizedKeys(value) {
557
+ try {
558
+ return Object.keys(value);
559
+ } catch {
560
+ return;
561
+ }
562
+ }
563
+ const noToJson = Symbol("noToJson");
564
+ function normalizedToJson(value, ancestors) {
565
+ let toJSON;
566
+ try {
567
+ toJSON = Reflect.get(value, "toJSON");
568
+ } catch {
569
+ return noToJson;
570
+ }
571
+ if (typeof toJSON !== "function") return noToJson;
572
+ try {
573
+ const result = Reflect.apply(toJSON, value, []);
574
+ return result === value ? noToJson : normalizeJson(result, ancestors);
575
+ } catch {
576
+ return noToJson;
577
+ }
578
+ }
579
+ function isArrayBufferView(value) {
580
+ try {
581
+ return ArrayBuffer.isView(value);
582
+ } catch {
583
+ return false;
584
+ }
585
+ }
586
+ /** Normalize arbitrary event data into values accepted by JSON.stringify. */
587
+ function normalizeJson(value, ancestors = /* @__PURE__ */ new Set()) {
588
+ switch (typeof value) {
589
+ case "string": return stripVTControlCharacters(value);
590
+ case "boolean": return value;
591
+ case "number": return Number.isFinite(value) ? value : null;
592
+ case "bigint": return value.toString();
593
+ case "undefined": return "[undefined]";
594
+ case "symbol": return `[symbol:${stripVTControlCharacters(value.description ?? "")}]`;
595
+ case "function": return `[function:${stripVTControlCharacters(value.name || "anonymous")}]`;
596
+ }
597
+ if (value === null) return null;
598
+ if (ancestors.has(value)) return "[Circular]";
599
+ const nextAncestors = new Set(ancestors).add(value);
600
+ if (value instanceof Error) return normalizeJson(summarizeError(value), nextAncestors);
601
+ const jsonValue = normalizedToJson(value, nextAncestors);
602
+ if (jsonValue !== noToJson) return jsonValue;
603
+ if (isArrayBufferView(value)) {
604
+ if (value instanceof DataView) return Array.from(new Uint8Array(value.buffer, value.byteOffset, value.byteLength));
605
+ try {
606
+ return Array.from(value, (item) => normalizeJson(item, nextAncestors));
607
+ } catch {
608
+ return "[Unserializable]";
609
+ }
610
+ }
611
+ if (value instanceof ArrayBuffer) return Array.from(new Uint8Array(value));
612
+ if (Array.isArray(value)) return Array.from({ length: value.length }, (_, index) => normalizeProperty(value, index, nextAncestors));
613
+ if (value instanceof Map) try {
614
+ return [...value].map(([key, item]) => [normalizeJson(key, nextAncestors), normalizeJson(item, nextAncestors)]);
615
+ } catch {
616
+ return "[Unserializable]";
617
+ }
618
+ if (value instanceof Set) try {
619
+ return [...value].map((item) => normalizeJson(item, nextAncestors));
620
+ } catch {
621
+ return "[Unserializable]";
622
+ }
623
+ const keys = normalizedKeys(value);
624
+ if (!keys) return safeString(value);
625
+ const result = {};
626
+ for (const key of keys) result[stripVTControlCharacters(key)] = normalizeProperty(value, key, nextAncestors);
627
+ return result;
628
+ }
629
+ //#endregion
630
+ //#region src/printer.ts
631
+ function fallbackValue(value) {
632
+ try {
633
+ return String(value);
634
+ } catch {
635
+ return "[Unserializable]";
636
+ }
637
+ }
638
+ function validatePreference(value, source) {
639
+ if (value === void 0) return void 0;
640
+ if (value === "auto" || value === "pretty" || value === "json") return value;
641
+ throw new TypeError(`Invalid ${source} value ${fallbackValue(value)}; expected auto, pretty, or json`);
642
+ }
643
+ function readEnvironment(env, key) {
644
+ try {
645
+ return env[key];
646
+ } catch {
647
+ return;
648
+ }
649
+ }
650
+ /** Resolve output behavior once, before command routing or argument parsing. */
651
+ function resolveOutputMode(options = {}) {
652
+ const env = options.env ?? process.env;
653
+ const configured = validatePreference(options.output, "output");
654
+ const requested = validatePreference(readEnvironment(env, "HACKSH_OUTPUT"), "HACKSH_OUTPUT") ?? configured ?? "auto";
655
+ const ci = Boolean(readEnvironment(env, "CI"));
656
+ const format = requested === "auto" ? ci ? "json" : "pretty" : requested;
657
+ const inputIsTTY = options.inputIsTTY ?? process.stdin.isTTY === true;
658
+ const outputIsTTY = options.outputIsTTY ?? process.stdout.isTTY === true;
659
+ return {
660
+ format,
661
+ interactive: format === "pretty" && !ci && inputIsTTY && outputIsTTY,
662
+ color: format === "pretty" && outputIsTTY && !readEnvironment(env, "NO_COLOR")
663
+ };
664
+ }
665
+ function isErrorLevel(level) {
666
+ return level === "warning" || level === "error" || level === "fatal";
667
+ }
668
+ function eventStream(event) {
669
+ if (event.type === "error") return "stderr";
670
+ if (event.type === "usage") return event.stream;
671
+ if (event.type === "log" && isErrorLevel(event.record.level)) return "stderr";
672
+ return "stdout";
673
+ }
674
+ function formatTextEvent(event) {
675
+ switch (event.type) {
676
+ case "usage": return event.text;
677
+ case "version": return event.version;
678
+ case "error": return event.error.kind === "usage" || event.error.kind === "validation" ? event.error.message : `Error: ${event.error.message}`;
679
+ case "log": return event.record.message;
680
+ case "prompt": return event.prompt.message;
681
+ }
682
+ }
683
+ /** Undecorated console compatibility adapter; automatic output uses the selected format printer. */
684
+ const consolePrinter = { print(event) {
685
+ const text = formatTextEvent(event);
686
+ return eventStream(event) === "stdout" ? console.log(text) : console.error(text);
687
+ } };
688
+ /** Create an undecorated stream-backed compatibility adapter. */
689
+ function createStreamPrinter(options) {
690
+ return { print(event) {
691
+ (eventStream(event) === "stdout" ? options.stdout : options.stderr).write(`${formatTextEvent(event)}\n`);
692
+ } };
693
+ }
694
+ /** Create a one-semantic-event-per-line JSON printer. */
695
+ function createJsonPrinter(options = {}) {
696
+ const stdout = options.stdout ?? process.stdout;
697
+ const stderr = options.stderr ?? process.stderr;
698
+ return { print(event) {
699
+ let serialized;
700
+ try {
701
+ serialized = JSON.stringify(normalizeJson(event));
702
+ } catch {
703
+ serialized = JSON.stringify({
704
+ type: event.type,
705
+ serializationError: "Event data could not be serialized"
706
+ });
707
+ }
708
+ const line = `${serialized}\n`;
709
+ (eventStream(event) === "stdout" ? stdout : stderr).write(line);
710
+ } };
711
+ }
712
+ /** Serialize printer calls and retain failures until the invocation drains its output. */
713
+ function createPrintQueue(target) {
714
+ let pending;
715
+ let failure;
716
+ let failed = false;
717
+ function recordFailure(error) {
718
+ if (!failed) failure = error;
719
+ failed = true;
720
+ }
721
+ function track(execution, retainFailure) {
722
+ const barrier = execution.catch((error) => {
723
+ if (retainFailure) recordFailure(error);
724
+ });
725
+ pending = barrier;
726
+ barrier.then(() => {
727
+ if (pending === barrier) pending = void 0;
728
+ });
729
+ return retainFailure ? barrier : execution;
730
+ }
731
+ function enqueue(event, retainFailure) {
732
+ if (pending) return track(pending.then(() => target.print(event)), retainFailure);
733
+ try {
734
+ const result = target.print(event);
735
+ if (result) return track(Promise.resolve(result), retainFailure);
736
+ } catch (error) {
737
+ if (retainFailure) recordFailure(error);
738
+ else return Promise.reject(error);
739
+ }
740
+ }
741
+ return {
742
+ printer: { print(event) {
743
+ return enqueue(event, true);
744
+ } },
745
+ async printChecked(event) {
746
+ await enqueue(event, false);
747
+ },
748
+ async drain() {
749
+ await pending;
750
+ if (failed) throw failure;
751
+ }
752
+ };
753
+ }
754
+ //#endregion
755
+ //#region src/pretty.ts
756
+ const unicodeSymbols = {
757
+ bar: "│",
758
+ marks: {
759
+ trace: "·",
760
+ debug: "◆",
761
+ info: "●",
762
+ warning: "▲",
763
+ error: "■",
764
+ fatal: "✖",
765
+ step: "◇",
766
+ success: "✔",
767
+ usage: "◆",
768
+ version: "●",
769
+ errorEvent: "■",
770
+ prompt: "?"
771
+ }
772
+ };
773
+ const asciiSymbols = {
774
+ bar: "|",
775
+ marks: {
776
+ trace: ".",
777
+ debug: ":",
778
+ info: "i",
779
+ warning: "!",
780
+ error: "x",
781
+ fatal: "X",
782
+ step: ">",
783
+ success: "+",
784
+ usage: "?",
785
+ version: "i",
786
+ errorEvent: "x",
787
+ prompt: "?"
788
+ }
789
+ };
790
+ const markStyles = {
791
+ trace: "gray",
792
+ debug: "magenta",
793
+ info: "blue",
794
+ warning: "yellow",
795
+ error: "red",
796
+ fatal: [
797
+ "bgRed",
798
+ "white",
799
+ "bold"
800
+ ],
801
+ step: "cyan",
802
+ success: "green",
803
+ usage: "cyan",
804
+ version: "blue",
805
+ errorEvent: "red",
806
+ prompt: "cyan"
807
+ };
808
+ function safelyInspect(value) {
809
+ if (typeof value === "string") return value;
810
+ try {
811
+ return inspect(value, {
812
+ breakLength: 80,
813
+ colors: false,
814
+ compact: false,
815
+ depth: null,
816
+ sorted: true
817
+ });
818
+ } catch {
819
+ return "[Unserializable]";
820
+ }
821
+ }
822
+ function propertyLines(properties) {
823
+ const lines = [];
824
+ for (const key of Object.keys(properties).sort()) {
825
+ const rendered = ` ${key}: ${safelyInspect(properties[key])}`.split("\n");
826
+ lines.push(rendered[0] ?? "");
827
+ for (const continuation of rendered.slice(1)) lines.push(` ${continuation}`);
828
+ }
829
+ return lines;
830
+ }
831
+ function logLines(record) {
832
+ const category = record.category.length > 0 ? `[${record.category.join(":")}] ` : "";
833
+ const message = record.message.split("\n");
834
+ return [
835
+ `${category}${message[0] ?? ""}`,
836
+ ...message.slice(1),
837
+ ...propertyLines(record.properties)
838
+ ];
839
+ }
840
+ function errorLines(error) {
841
+ const lines = error.message.split("\n");
842
+ const details = {};
843
+ if (error.cause !== void 0) details.cause = error.cause;
844
+ if (error.properties !== void 0) Object.assign(details, error.properties);
845
+ if (error.value !== void 0) details.value = error.value;
846
+ return [...lines, ...propertyLines(details)];
847
+ }
848
+ function eventMark(event) {
849
+ if (event.type === "usage") return "usage";
850
+ if (event.type === "version") return "version";
851
+ if (event.type === "error") return "errorEvent";
852
+ if (event.type === "prompt") return event.prompt.status === "submitted" ? "success" : "prompt";
853
+ return event.record.kind === "message" ? event.record.level : event.record.kind;
854
+ }
855
+ function eventLines(event) {
856
+ switch (event.type) {
857
+ case "usage": return event.text.split("\n");
858
+ case "version": return event.version.split("\n");
859
+ case "error": return errorLines(event.error);
860
+ case "log": return logLines(event.record);
861
+ case "prompt": {
862
+ const { prompt } = event;
863
+ if (prompt.status === "submitted") {
864
+ const result = prompt.kind === "select" ? prompt.choices.find(({ value }) => value === prompt.value)?.label ?? prompt.value : prompt.kind === "confirm" ? prompt.value ? "Yes" : "No" : prompt.value;
865
+ return [`${prompt.message}: ${result}`];
866
+ }
867
+ const lines = [prompt.message];
868
+ if (prompt.kind === "select") for (const choice of prompt.choices) lines.push(`${choice.selected ? ">" : " "} ${choice.label}${choice.hint ? ` — ${choice.hint}` : ""}`);
869
+ else if (prompt.kind === "confirm") lines.push(`${prompt.value ? "Yes" : "No"}${prompt.defaultValue === void 0 ? "" : ` (default: ${prompt.defaultValue ? "yes" : "no"})`}`);
870
+ else lines.push(prompt.value || (prompt.placeholder ? `(${prompt.placeholder})` : ""));
871
+ if (prompt.error) lines.push(prompt.error);
872
+ return lines;
873
+ }
874
+ }
875
+ }
876
+ function colorize(enabled, style, text) {
877
+ return enabled ? styleText(style, text, { validateStream: false }) : text;
878
+ }
879
+ const graphemeSegmenter = new Intl.Segmenter(void 0, { granularity: "grapheme" });
880
+ const combiningMark = /\p{Mark}/u;
881
+ const pictograph = /\p{Extended_Pictographic}/u;
882
+ function isWideCodePoint(codePoint) {
883
+ return codePoint >= 4352 && (codePoint <= 4447 || codePoint === 9001 || codePoint === 9002 || codePoint >= 11904 && codePoint <= 42191 && codePoint !== 12351 || codePoint >= 44032 && codePoint <= 55203 || codePoint >= 63744 && codePoint <= 64255 || codePoint >= 65040 && codePoint <= 65049 || codePoint >= 65072 && codePoint <= 65135 || codePoint >= 65280 && codePoint <= 65376 || codePoint >= 65504 && codePoint <= 65510 || codePoint >= 131072 && codePoint <= 262141);
884
+ }
885
+ function displayWidth(text) {
886
+ let width = 0;
887
+ for (const { segment } of graphemeSegmenter.segment(stripVTControlCharacters(text))) {
888
+ if (segment === " ") {
889
+ width += 8 - width % 8;
890
+ continue;
891
+ }
892
+ if (pictograph.test(segment) || Array.from(segment).some((value) => isWideCodePoint(value.codePointAt(0)))) {
893
+ width += 2;
894
+ continue;
895
+ }
896
+ for (const value of segment) {
897
+ const codePoint = value.codePointAt(0);
898
+ if (codePoint < 32 || codePoint >= 127 && codePoint < 160) continue;
899
+ if (codePoint === 8205 || codePoint >= 65024 && codePoint <= 65039) continue;
900
+ if (!combiningMark.test(value)) width += 1;
901
+ }
902
+ }
903
+ return width;
904
+ }
905
+ function renderedRows(rendered, columns) {
906
+ const width = Number.isInteger(columns) && columns !== void 0 && columns > 0 ? columns : void 0;
907
+ return rendered.split("\n").reduce((rows, line) => {
908
+ if (!width) return rows + 1;
909
+ return rows + Math.max(1, Math.ceil(displayWidth(line) / width));
910
+ }, 0);
911
+ }
912
+ /** Create the fixed human-readable renderer used for pretty output. */
913
+ function createPrettyPrinter(options) {
914
+ const stdout = options.stdout ?? process.stdout;
915
+ const stderr = options.stderr ?? process.stderr;
916
+ const symbols = options.unicode ? unicodeSymbols : asciiSymbols;
917
+ let activePrompt;
918
+ function render(event) {
919
+ const mark = eventMark(event);
920
+ const lines = eventLines(event);
921
+ const firstPrefix = colorize(options.color, markStyles[mark], symbols.marks[mark]);
922
+ const barPrefix = colorize(options.color, "gray", symbols.bar);
923
+ return lines.map((line, index) => `${index === 0 ? firstPrefix : barPrefix}${line ? ` ${line}` : ""}`).join("\n");
924
+ }
925
+ function eraseActivePrompt() {
926
+ return activePrompt ? "\x1B[1A\x1B[2K".repeat(activePrompt.lines) : "";
927
+ }
928
+ return { print(event) {
929
+ const rendered = render(event);
930
+ if (event.type === "prompt") {
931
+ const erase = eraseActivePrompt();
932
+ if (event.prompt.status === "closed") {
933
+ activePrompt = void 0;
934
+ if (erase) stdout.write(erase);
935
+ return;
936
+ }
937
+ if (event.prompt.status === "submitted") activePrompt = void 0;
938
+ else activePrompt = {
939
+ rendered,
940
+ lines: renderedRows(rendered, stdout.columns)
941
+ };
942
+ stdout.write(`${erase}${rendered}\n`);
943
+ return;
944
+ }
945
+ const erase = eraseActivePrompt();
946
+ if (!activePrompt) {
947
+ (eventStream(event) === "stdout" ? stdout : stderr).write(`${rendered}\n`);
948
+ return;
949
+ }
950
+ stdout.write(erase);
951
+ if (event.type === "error") activePrompt = void 0;
952
+ (eventStream(event) === "stdout" ? stdout : stderr).write(`${rendered}\n`);
953
+ if (activePrompt) stdout.write(`${activePrompt.rendered}\n`);
954
+ } };
955
+ }
956
+ //#endregion
957
+ //#region src/prompts.ts
958
+ function details$1(kind, message, choices) {
959
+ let normalizedMessage;
960
+ try {
961
+ normalizedMessage = typeof message === "string" ? message : String(message);
962
+ } catch {
963
+ normalizedMessage = "[Unserializable prompt message]";
964
+ }
965
+ return {
966
+ kind,
967
+ message: normalizedMessage,
968
+ ...choices === void 0 ? {} : { choices }
969
+ };
970
+ }
971
+ function promptError(reason, prompt, message, cause) {
972
+ return new HackshPromptError(reason, prompt, message, cause === void 0 ? void 0 : { cause });
973
+ }
974
+ function validateMessage(kind, message, choices) {
975
+ if (typeof message !== "string" || message.length === 0) throw promptError("invalid-options", details$1(kind, message, choices), `Invalid ${kind} prompt: message must be a non-empty string`);
976
+ }
977
+ function asInput(value, prompt) {
978
+ if (typeof value !== "object" || value === null || typeof Reflect.get(value, "on") !== "function" || typeof Reflect.get(value, "removeListener") !== "function" || typeof Reflect.get(value, "resume") !== "function" || typeof Reflect.get(value, "pause") !== "function") throw promptError("input-error", prompt, "Interactive input is not a readable stream");
979
+ return value;
980
+ }
981
+ var PromptController = class {
982
+ #interactive;
983
+ #inputValue;
984
+ #printEvent;
985
+ #bufferedKeys = [];
986
+ #active = false;
987
+ constructor(options) {
988
+ this.#interactive = options.interactive;
989
+ this.#inputValue = options.input;
990
+ this.#printEvent = (event) => options.print(event);
991
+ }
992
+ async #print(state, prompt) {
993
+ try {
994
+ await this.#printEvent({
995
+ type: "prompt",
996
+ prompt: state
997
+ });
998
+ } catch (cause) {
999
+ throw promptError("printer-error", prompt, "The prompt could not be displayed", cause);
1000
+ }
1001
+ }
1002
+ async #run(prompt, initial, accept, update, submitted, closed) {
1003
+ if (!this.#interactive) throw promptError("non-interactive", prompt, `Cannot request ${prompt.kind} input in non-interactive mode: ${prompt.message}`);
1004
+ if (this.#active) throw promptError("overlapping", prompt, "Another prompt is already active");
1005
+ const input = asInput(this.#inputValue, prompt);
1006
+ this.#active = true;
1007
+ let originallyFlowing = false;
1008
+ let rawChanged = false;
1009
+ let outcome;
1010
+ try {
1011
+ originallyFlowing = input.readableFlowing === true;
1012
+ if (input.setRawMode && input.isRaw !== true) {
1013
+ input.setRawMode(true);
1014
+ rawChanged = true;
1015
+ }
1016
+ await this.#print(initial, prompt);
1017
+ outcome = {
1018
+ ok: true,
1019
+ value: await new Promise((resolve, reject) => {
1020
+ const keypressInput = new PassThrough();
1021
+ emitKeypressEvents(keypressInput);
1022
+ let settled = false;
1023
+ let draining = false;
1024
+ let finalized = false;
1025
+ let completion;
1026
+ const queuedKeys = this.#bufferedKeys.splice(0);
1027
+ const finalize = () => {
1028
+ if (finalized || !settled || draining) return;
1029
+ finalized = true;
1030
+ cleanup();
1031
+ completion?.();
1032
+ };
1033
+ const finish = (action) => {
1034
+ if (settled) return;
1035
+ settled = true;
1036
+ completion = action;
1037
+ finalize();
1038
+ };
1039
+ const fail = (reason, message, cause) => {
1040
+ finish(() => reject(promptError(reason, prompt, message, cause)));
1041
+ };
1042
+ const handleKeypress = async (character, key = {}) => {
1043
+ if (settled) return;
1044
+ if (key.ctrl && key.name === "c" || key.name === "escape") {
1045
+ fail("cancelled", "Prompt cancelled");
1046
+ return;
1047
+ }
1048
+ if (key.ctrl && key.name === "d") {
1049
+ fail("eof", "Prompt input ended before a value was submitted");
1050
+ return;
1051
+ }
1052
+ try {
1053
+ const value = accept(character ?? "", key);
1054
+ if (value !== void 0) {
1055
+ finish(() => resolve(value));
1056
+ return;
1057
+ }
1058
+ const state = update(character ?? "", key);
1059
+ if (state) await this.#print(state, prompt);
1060
+ } catch (cause) {
1061
+ if (cause instanceof HackshPromptError) finish(() => reject(cause));
1062
+ else fail("input-error", "Prompt input could not be processed", cause);
1063
+ }
1064
+ };
1065
+ const drainKeys = async () => {
1066
+ if (draining || finalized) return;
1067
+ draining = true;
1068
+ try {
1069
+ while (queuedKeys.length > 0) {
1070
+ if (settled) {
1071
+ this.#bufferedKeys.push(...queuedKeys.splice(0));
1072
+ break;
1073
+ }
1074
+ const next = queuedKeys.shift();
1075
+ await handleKeypress(next.character, next.key);
1076
+ }
1077
+ } finally {
1078
+ draining = false;
1079
+ finalize();
1080
+ }
1081
+ };
1082
+ const onKeypress = (character, key = {}) => {
1083
+ queuedKeys.push({
1084
+ character,
1085
+ key
1086
+ });
1087
+ drainKeys().catch((cause) => fail("input-error", "Prompt input could not be processed", cause));
1088
+ };
1089
+ const onEnd = () => fail("eof", "Prompt input ended before a value was submitted");
1090
+ const onError = (cause) => fail("input-error", "Prompt input failed", cause);
1091
+ const onData = (chunk) => {
1092
+ if (typeof chunk === "string" || chunk instanceof Uint8Array) keypressInput.write(chunk);
1093
+ else fail("input-error", "Prompt input produced a non-byte value");
1094
+ };
1095
+ const cleanup = () => {
1096
+ keypressInput.removeListener("keypress", onKeypress);
1097
+ keypressInput.destroy();
1098
+ input.removeListener("data", onData);
1099
+ input.removeListener("end", onEnd);
1100
+ input.removeListener("close", onEnd);
1101
+ input.removeListener("error", onError);
1102
+ };
1103
+ keypressInput.on("keypress", onKeypress);
1104
+ input.on("data", onData);
1105
+ input.on("end", onEnd);
1106
+ input.on("close", onEnd);
1107
+ input.on("error", onError);
1108
+ input.resume();
1109
+ drainKeys().catch((cause) => fail("input-error", "Prompt input could not be processed", cause));
1110
+ })
1111
+ };
1112
+ } catch (cause) {
1113
+ outcome = {
1114
+ ok: false,
1115
+ error: cause instanceof HackshPromptError ? cause : promptError("input-error", prompt, "Prompt input could not be initialized", cause)
1116
+ };
1117
+ }
1118
+ let restoreError;
1119
+ try {
1120
+ if (rawChanged) input.setRawMode?.(false);
1121
+ if (!originallyFlowing) input.pause();
1122
+ } catch (cause) {
1123
+ restoreError = promptError("input-error", prompt, "Prompt input could not be restored", cause);
1124
+ }
1125
+ const closeWithFailure = async (failure) => {
1126
+ try {
1127
+ await this.#print(closed(), prompt);
1128
+ } catch {} finally {
1129
+ this.#active = false;
1130
+ }
1131
+ throw failure;
1132
+ };
1133
+ if (restoreError) return closeWithFailure(restoreError);
1134
+ if (!outcome.ok) return closeWithFailure(outcome.error);
1135
+ try {
1136
+ await this.#print(submitted(outcome.value), prompt);
1137
+ return outcome.value;
1138
+ } catch (error) {
1139
+ try {
1140
+ await this.#print(closed(), prompt);
1141
+ } catch {}
1142
+ throw error;
1143
+ } finally {
1144
+ this.#active = false;
1145
+ }
1146
+ }
1147
+ async select(options) {
1148
+ const choices = Array.isArray(options?.options) ? options.options.map((option) => typeof option === "object" && option !== null && typeof option.value === "string" ? option.value : "[Invalid choice]") : [];
1149
+ const prompt = details$1("select", options?.message, choices);
1150
+ validateMessage("select", options?.message, choices);
1151
+ if (!Array.isArray(options.options) || options.options.length === 0) throw promptError("invalid-options", prompt, "A select prompt requires at least one choice");
1152
+ const seen = /* @__PURE__ */ new Set();
1153
+ for (const option of options.options) {
1154
+ if (typeof option !== "object" || option === null || typeof option.value !== "string" || option.value.length === 0 || option.label !== void 0 && typeof option.label !== "string" || option.hint !== void 0 && typeof option.hint !== "string") throw promptError("invalid-options", prompt, "Select choices must have valid string values");
1155
+ if (seen.has(option.value)) throw promptError("invalid-options", prompt, `Duplicate select choice: ${option.value}`);
1156
+ seen.add(option.value);
1157
+ }
1158
+ let selected = options.initialValue === void 0 ? 0 : choices.indexOf(options.initialValue);
1159
+ if (selected < 0) throw promptError("invalid-options", prompt, `Unknown initial select value: ${String(options.initialValue)}`);
1160
+ const state = (status = "active") => ({
1161
+ kind: "select",
1162
+ message: options.message,
1163
+ status,
1164
+ value: options.options[selected].value,
1165
+ choices: options.options.map(({ value, label, hint }, index) => ({
1166
+ value,
1167
+ label: label ?? value,
1168
+ ...hint === void 0 ? {} : { hint },
1169
+ selected: index === selected
1170
+ }))
1171
+ });
1172
+ return await this.#run(prompt, state(), (_character, key) => key.name === "return" || key.name === "enter" ? options.options[selected].value : void 0, (_character, key) => {
1173
+ if (key.name !== "up" && key.name !== "down") return void 0;
1174
+ selected = (selected + (key.name === "up" ? -1 : 1) + options.options.length) % options.options.length;
1175
+ return state();
1176
+ }, () => state("submitted"), () => state("closed"));
1177
+ }
1178
+ async confirm(options) {
1179
+ const prompt = details$1("confirm", options?.message);
1180
+ validateMessage("confirm", options?.message);
1181
+ if (options.initialValue !== void 0 && typeof options.initialValue !== "boolean") throw promptError("invalid-options", prompt, "Confirm initialValue must be a boolean");
1182
+ const value = options.initialValue ?? false;
1183
+ const state = (result, status = "active") => ({
1184
+ kind: "confirm",
1185
+ message: options.message,
1186
+ status,
1187
+ value: result,
1188
+ ...options.initialValue === void 0 ? {} : { defaultValue: options.initialValue }
1189
+ });
1190
+ return await this.#run(prompt, state(value), (character, key) => {
1191
+ if (key.name === "return" || key.name === "enter") return value;
1192
+ if (character.toLowerCase() === "y") return true;
1193
+ if (character.toLowerCase() === "n") return false;
1194
+ }, () => void 0, (submitted) => state(submitted, "submitted"), () => state(value, "closed"));
1195
+ }
1196
+ async text(options) {
1197
+ const prompt = details$1("text", options?.message);
1198
+ validateMessage("text", options?.message);
1199
+ if (options.placeholder !== void 0 && typeof options.placeholder !== "string") throw promptError("invalid-options", prompt, "Text placeholder must be a string");
1200
+ if (options.validate !== void 0 && typeof options.validate !== "function") throw promptError("invalid-options", prompt, "Text validate must be a function");
1201
+ let value = "";
1202
+ let validationError;
1203
+ const state = (status = validationError ? "error" : "active") => ({
1204
+ kind: "text",
1205
+ message: options.message,
1206
+ status,
1207
+ value,
1208
+ ...options.placeholder === void 0 ? {} : { placeholder: options.placeholder },
1209
+ ...validationError === void 0 ? {} : { error: validationError }
1210
+ });
1211
+ return await this.#run(prompt, state(), (_character, key) => {
1212
+ if (key.name !== "return" && key.name !== "enter") return void 0;
1213
+ let error;
1214
+ try {
1215
+ error = options.validate?.(value);
1216
+ } catch (cause) {
1217
+ throw promptError("input-error", prompt, "Text validation failed", cause);
1218
+ }
1219
+ if (error !== void 0 && typeof error !== "string") throw promptError("input-error", prompt, "Text validation must return a string or undefined");
1220
+ validationError = error || void 0;
1221
+ return validationError === void 0 ? value : void 0;
1222
+ }, (character, key) => {
1223
+ if (key.name === "return" || key.name === "enter") return state();
1224
+ validationError = void 0;
1225
+ if (key.name === "backspace") value = Array.from(value).slice(0, -1).join("");
1226
+ else if (!key.ctrl && !key.meta && character.length > 0) value += character;
1227
+ else return void 0;
1228
+ return state();
1229
+ }, () => state("submitted"), () => state("closed"));
1230
+ }
1231
+ };
1232
+ //#endregion
1233
+ //#region src/logger.ts
1234
+ function interpolate(message, properties) {
1235
+ return message.replace(/\{([^{}]+)\}/g, (placeholder, key) => {
1236
+ if (!Object.hasOwn(properties, key)) return placeholder;
1237
+ try {
1238
+ return String(properties[key]);
1239
+ } catch {
1240
+ return placeholder;
1241
+ }
1242
+ });
1243
+ }
1244
+ function createBoundLogger(printer, category, boundProperties, prompts) {
1245
+ function emit(level, kind, input, properties = {}) {
1246
+ const rawMessage = input instanceof Error ? input.message : input;
1247
+ const mergedProperties = {
1248
+ ...boundProperties,
1249
+ ...properties
1250
+ };
1251
+ if (input instanceof Error) mergedProperties.error = input;
1252
+ const summarizedProperties = summarizeErrors(mergedProperties);
1253
+ printer.print({
1254
+ type: "log",
1255
+ record: {
1256
+ level,
1257
+ kind,
1258
+ category,
1259
+ message: interpolate(rawMessage, mergedProperties),
1260
+ rawMessage,
1261
+ properties: summarizedProperties,
1262
+ timestamp: Date.now()
1263
+ }
1264
+ });
1265
+ }
1266
+ return {
1267
+ category,
1268
+ trace: (message, properties) => emit("trace", "message", message, properties),
1269
+ debug: (message, properties) => emit("debug", "message", message, properties),
1270
+ info: (message, properties) => emit("info", "message", message, properties),
1271
+ warn: (message, properties) => emit("warning", "message", message, properties),
1272
+ error: (message, properties) => emit("error", "message", message, properties),
1273
+ fatal: (message, properties) => emit("fatal", "message", message, properties),
1274
+ step: (message, properties) => emit("info", "step", message, properties),
1275
+ success: (message, properties) => emit("info", "success", message, properties),
1276
+ select: (options) => prompts.select(options),
1277
+ confirm: (options) => prompts.confirm(options),
1278
+ text: (options) => prompts.text(options),
1279
+ with: (properties) => createBoundLogger(printer, category, {
1280
+ ...boundProperties,
1281
+ ...properties
1282
+ }, prompts),
1283
+ getChild: (child) => createBoundLogger(printer, [...category, child], boundProperties, prompts)
1284
+ };
1285
+ }
1286
+ /** Create the logger for one command invocation. */
1287
+ function createLogger(printer, category, options) {
1288
+ const prompts = new PromptController({
1289
+ interactive: options.interactive,
1290
+ input: options.input,
1291
+ print: (event) => options.printPrompt(event)
1292
+ });
1293
+ return createBoundLogger(printer, [...category], {}, prompts);
1294
+ }
1295
+ //#endregion
1296
+ //#region src/output.ts
1297
+ function consoleWriter(method, stream) {
1298
+ return {
1299
+ get isTTY() {
1300
+ return stream.isTTY;
1301
+ },
1302
+ get columns() {
1303
+ return stream.columns;
1304
+ },
1305
+ write(text) {
1306
+ if (!text.endsWith("\n")) return stream.write(text);
1307
+ console[method](text.slice(0, -1));
1308
+ }
1309
+ };
1310
+ }
1311
+ /** Resolve the automatic output policy and select its printer for one operation. */
1312
+ function selectOutputPrinter(options, override, resolvedMode) {
1313
+ const stdin = options.stdin ?? process.stdin;
1314
+ const stdout = options.stdout ?? process.stdout;
1315
+ const stderr = options.stderr ?? process.stderr;
1316
+ const mode = resolvedMode ?? resolveOutputMode({
1317
+ output: options.output,
1318
+ env: options.env,
1319
+ inputIsTTY: stdin.isTTY,
1320
+ outputIsTTY: stdout.isTTY
1321
+ });
1322
+ if (override) return override;
1323
+ if (mode.format === "json") return createJsonPrinter({
1324
+ stdout,
1325
+ stderr
1326
+ });
1327
+ const useConsole = options.stdout === void 0 && options.stderr === void 0;
1328
+ return createPrettyPrinter({
1329
+ color: mode.color,
1330
+ unicode: options.unicode ?? true,
1331
+ stdout: options.stdout ?? (useConsole ? consoleWriter("log", stdout) : stdout),
1332
+ stderr: options.stderr ?? (useConsole ? consoleWriter("error", process.stderr) : stderr)
1333
+ });
1334
+ }
1335
+ //#endregion
363
1336
  //#region src/usage.ts
364
1337
  function details(description, required, defaultValue, choices) {
365
1338
  const parts = [description];
@@ -372,64 +1345,118 @@ function formatDefault(value) {
372
1345
  if (typeof value === "string") return value;
373
1346
  if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") return value.toString();
374
1347
  if (value === null) return "null";
375
- return JSON.stringify(value) ?? "undefined";
1348
+ try {
1349
+ return JSON.stringify(value) ?? "undefined";
1350
+ } catch {
1351
+ return "[Unserializable]";
1352
+ }
376
1353
  }
377
1354
  function heading(command) {
378
1355
  const identity = command.name ? `${command.name}${command.version ? ` v${command.version}` : ""}` : command.version ? `v${command.version}` : void 0;
379
1356
  if (command.description && identity) return `${command.description} (${identity})`;
380
1357
  return command.description ?? identity;
381
1358
  }
382
- function formatUsage(command) {
383
- const lines = [];
384
- const title = heading(command);
385
- if (title) lines.push(title, "");
1359
+ /** Build a serializable help summary without retaining command or schema objects. */
1360
+ function summarizeUsage(command) {
386
1361
  const shape = command.args ? schemaShape(command.args) : {};
387
1362
  const positionals = readPositionals(shape);
388
- const items = positionals?.items ?? [];
389
- const rest = positionals?.rest;
390
- const syntax = items.map(({ schema, label }) => readArgInfo(schema).required ? `<${label}>` : `[${label}]`);
391
- if (rest) syntax.push(`[${rest.label}...]`);
1363
+ const positionalItems = positionals?.items ?? [];
1364
+ const syntax = positionalItems.map(({ schema, label }) => readArgInfo(schema).required ? `<${label}>` : `[${label}]`);
1365
+ if (positionals?.rest) syntax.push(`[${positionals.rest.label}...]`);
392
1366
  if (command.subCommands && Object.keys(command.subCommands).length > 0) syntax.push("<COMMAND>");
393
- lines.push(`USAGE ${command.name ?? "command"} [OPTIONS]${syntax.length ? ` ${syntax.join(" ")}` : ""}`);
394
- if (items.length > 0 || rest) {
1367
+ const args = positionalItems.map(({ schema, label }) => {
1368
+ const info = readArgInfo(schema);
1369
+ return {
1370
+ name: label,
1371
+ required: info.required,
1372
+ variadic: false,
1373
+ ...info.description === void 0 ? {} : { description: info.description },
1374
+ ...info.choices === void 0 ? {} : { choices: info.choices },
1375
+ ...info.defaultValue === void 0 ? {} : { defaultValue: summarizeErrors(info.defaultValue) }
1376
+ };
1377
+ });
1378
+ if (positionals?.rest) {
1379
+ const info = readArgInfo(positionals.rest.schema);
1380
+ args.push({
1381
+ name: positionals.rest.label,
1382
+ required: false,
1383
+ variadic: true,
1384
+ ...info.description === void 0 ? {} : { description: info.description },
1385
+ ...info.choices === void 0 ? {} : { choices: info.choices }
1386
+ });
1387
+ }
1388
+ const options = Object.entries(shape).filter(([key]) => key !== "_").map(([key, schema]) => {
1389
+ const info = readArgInfo(schema);
1390
+ const argOptions = command.argOptions?.[key];
1391
+ const aliases = argOptions?.alias ? aliasList(argOptions.alias).map((item) => `-${item}`) : [];
1392
+ const name = kebabCase(key);
1393
+ const valueName = argOptions?.valueName ?? name;
1394
+ return {
1395
+ name,
1396
+ flags: [...aliases, `--${name}`],
1397
+ takesValue: info.takesValue,
1398
+ required: info.required,
1399
+ ...info.takesValue ? { valueName } : {},
1400
+ ...info.description === void 0 ? {} : { description: info.description },
1401
+ ...info.choices === void 0 ? {} : { choices: info.choices },
1402
+ ...info.defaultValue === void 0 ? {} : { defaultValue: summarizeErrors(info.defaultValue) },
1403
+ ...argOptions?.negatedDescription === void 0 ? {} : { negatedDescription: argOptions.negatedDescription }
1404
+ };
1405
+ });
1406
+ const commands = Object.entries(command.subCommands ?? {}).filter(([, child]) => !child.hidden).map(([name, child]) => ({
1407
+ name,
1408
+ ...child.description === void 0 ? {} : { description: child.description }
1409
+ }));
1410
+ const name = command.name ?? "command";
1411
+ const title = heading(command);
1412
+ return {
1413
+ name,
1414
+ ...title === void 0 ? {} : { heading: title },
1415
+ ...command.version === void 0 ? {} : { version: command.version },
1416
+ ...command.description === void 0 ? {} : { description: command.description },
1417
+ synopsis: `${name} [OPTIONS]${syntax.length ? ` ${syntax.join(" ")}` : ""}`,
1418
+ arguments: args,
1419
+ options,
1420
+ commands
1421
+ };
1422
+ }
1423
+ function formatUsage(summary) {
1424
+ const lines = [];
1425
+ if (summary.heading) lines.push(summary.heading, "");
1426
+ lines.push(`USAGE ${summary.synopsis}`);
1427
+ if (summary.arguments.length > 0) {
395
1428
  lines.push("", "ARGUMENTS", "");
396
- for (const { schema, label } of items) {
397
- const info = readArgInfo(schema);
398
- lines.push(` ${label} ${details(info.description, info.required, info.defaultValue, info.choices)}`.trimEnd());
399
- }
400
- if (rest) {
401
- const info = readArgInfo(rest.schema);
402
- lines.push(` ${rest.label}... ${details(info.description, false, void 0, info.choices)}`.trimEnd());
403
- }
1429
+ for (const argument of summary.arguments) lines.push(` ${argument.name}${argument.variadic ? "..." : ""} ${details(argument.description, argument.required, argument.defaultValue, argument.choices)}`.trimEnd());
404
1430
  }
405
- const options = Object.entries(shape).filter(([key]) => key !== "_");
406
- if (options.length > 0) {
1431
+ if (summary.options.length > 0) {
407
1432
  lines.push("", "OPTIONS", "");
408
- for (const [key, schema] of options) {
409
- const info = readArgInfo(schema);
410
- const argOptions = command.argOptions?.[key];
411
- const prefix = argOptions?.alias ? `${aliasList(argOptions.alias).map((item) => `-${item}`).join(", ")}, ` : "";
412
- const valueName = argOptions?.valueName ?? kebabCase(key);
413
- const long = `--${kebabCase(key)}${info.takesValue ? `=<${valueName}>` : ""}`;
414
- lines.push(` ${prefix}${long} ${details(info.description, info.required, info.defaultValue, info.choices)}`.trimEnd());
415
- if (!info.takesValue && argOptions?.negatedDescription) lines.push(` --no-${kebabCase(key)} ${argOptions.negatedDescription}`);
1433
+ for (const option of summary.options) {
1434
+ const flags = option.flags.map((flag) => flag.startsWith("--") && option.takesValue ? `${flag}=<${option.valueName}>` : flag);
1435
+ lines.push(` ${flags.join(", ")} ${details(option.description, option.required, option.defaultValue, option.choices)}`.trimEnd());
1436
+ if (!option.takesValue && option.negatedDescription) lines.push(` --no-${option.name} ${option.negatedDescription}`);
416
1437
  }
417
1438
  }
418
- const visibleSubCommands = Object.entries(command.subCommands ?? {}).filter(([, child]) => !child.hidden);
419
- if (visibleSubCommands.length > 0) {
1439
+ if (summary.commands.length > 0) {
420
1440
  lines.push("", "COMMANDS", "");
421
- for (const [name, child] of visibleSubCommands) lines.push(` ${name} ${child.description ?? ""}`.trimEnd());
1441
+ for (const command of summary.commands) lines.push(` ${command.name} ${command.description ?? ""}`.trimEnd());
422
1442
  }
423
1443
  return lines.join("\n");
424
1444
  }
425
1445
  /** Generate usage text without writing to stdout or stderr. */
426
1446
  async function renderUsage(command) {
427
- return formatUsage(command);
1447
+ return formatUsage(summarizeUsage(command));
428
1448
  }
429
1449
  /** Generate and write usage text, awaiting asynchronous writers. */
430
1450
  async function showUsage(command, options = {}) {
431
- const text = await renderUsage(command);
432
- return (options.write ?? console.log)(text);
1451
+ const usage = summarizeUsage(command);
1452
+ const text = formatUsage(usage);
1453
+ if (options.write) return options.write(text);
1454
+ return (options.printer ?? selectOutputPrinter(options)).print({
1455
+ type: "usage",
1456
+ usage,
1457
+ text,
1458
+ stream: options.stream ?? "stdout"
1459
+ });
433
1460
  }
434
1461
  //#endregion
435
1462
  //#region src/run.ts
@@ -475,46 +1502,124 @@ function resolveCommand(root, rawArgs) {
475
1502
  remaining = remaining.slice(index + 1);
476
1503
  }
477
1504
  }
478
- async function executeCommand(root, argv, displayUsage) {
1505
+ async function executeCommand(root, argv, displayUsage, printer, loggerOptions) {
479
1506
  const resolved = resolveCommand(root, argv);
480
1507
  if (resolved.segments[0]?.tokenized.version) {
481
- console.log(root.version ?? "");
1508
+ await printer.print({
1509
+ type: "version",
1510
+ version: root.version ?? ""
1511
+ });
482
1512
  return;
483
1513
  }
484
1514
  if (resolved.segments.some(({ tokenized }) => tokenized.help)) {
485
- await displayUsage(resolved.command, { write: (text) => console.log(text) });
1515
+ await printUsage(displayUsage, resolved.command, printer, "stdout");
486
1516
  return;
487
1517
  }
488
1518
  for (const parent of resolved.parents) await parseCommandArgs(parent.command, parent.rawArgs, parent.tokenized);
489
1519
  const parsed = await parseCommandArgs(resolved.command, resolved.rawArgs, resolved.tokenized);
490
1520
  if (!resolved.command.run) throw new ZittyUsageError("No command specified", resolved.command);
1521
+ const category = resolved.segments.flatMap(({ command }) => command.name === void 0 ? [] : [command.name]);
491
1522
  return resolved.command.run({
492
1523
  ...parsed,
493
1524
  cmd: resolved.command,
494
- rawArgs: resolved.rawArgs
1525
+ rawArgs: resolved.rawArgs,
1526
+ logger: createLogger(printer, category, loggerOptions)
1527
+ });
1528
+ }
1529
+ function printUsage(displayUsage, command, printer, stream) {
1530
+ return displayUsage(command, { write: (text) => printer.print({
1531
+ type: "usage",
1532
+ usage: summarizeUsage(command),
1533
+ text,
1534
+ stream
1535
+ }) });
1536
+ }
1537
+ function prepareOutput(options) {
1538
+ const input = options.stdin ?? process.stdin;
1539
+ const mode = resolveOutputMode({
1540
+ output: options.output,
1541
+ env: options.env,
1542
+ inputIsTTY: input.isTTY,
1543
+ outputIsTTY: (options.stdout ?? process.stdout).isTTY
495
1544
  });
1545
+ return {
1546
+ ...createPrintQueue(selectOutputPrinter(options, options.printer, mode)),
1547
+ interactive: mode.interactive,
1548
+ input
1549
+ };
1550
+ }
1551
+ async function runCommand(root, argv, options = {}) {
1552
+ const output = prepareOutput(options);
1553
+ try {
1554
+ return await executeCommand(root, argv, options.showUsage ?? showUsage, output.printer, {
1555
+ interactive: output.interactive,
1556
+ input: output.input,
1557
+ printPrompt: (event) => output.printChecked(event)
1558
+ });
1559
+ } finally {
1560
+ await output.drain();
1561
+ }
496
1562
  }
497
- async function runCommand(root, argv) {
498
- return executeCommand(root, argv, showUsage);
1563
+ async function drainAndExit(output, code = 1) {
1564
+ try {
1565
+ await output.drain();
1566
+ } finally {
1567
+ process.exit(code);
1568
+ }
1569
+ }
1570
+ function commandErrorExitCode(error) {
1571
+ return error instanceof HackshPromptError && error.reason === "cancelled" ? 130 : 1;
1572
+ }
1573
+ async function reportOutputSetupError(error, options) {
1574
+ if (options.onError) {
1575
+ await options.onError(error);
1576
+ return drainAndExit(createPrintQueue(options.printer ?? { print: () => void 0 }));
1577
+ }
1578
+ const output = createPrintQueue(options.printer ?? createJsonPrinter({
1579
+ stdout: options.stdout ?? process.stdout,
1580
+ stderr: options.stderr ?? process.stderr
1581
+ }));
1582
+ await output.printer.print({
1583
+ type: "error",
1584
+ error: summarizeError(error)
1585
+ });
1586
+ return drainAndExit(output);
499
1587
  }
500
1588
  async function runMain(command, options = {}) {
501
1589
  const displayUsage = options.showUsage ?? showUsage;
1590
+ let output;
502
1591
  try {
503
- await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage);
1592
+ output = prepareOutput(options);
1593
+ } catch (error) {
1594
+ return reportOutputSetupError(error, options);
1595
+ }
1596
+ try {
1597
+ await executeCommand(command, options.argv ?? process.argv.slice(2), displayUsage, output.printer, {
1598
+ interactive: output.interactive,
1599
+ input: output.input,
1600
+ printPrompt: (event) => output.printChecked(event)
1601
+ });
504
1602
  } catch (error) {
505
1603
  if (options.onError) {
506
1604
  await options.onError(error);
507
- process.exit(1);
1605
+ return drainAndExit(output, commandErrorExitCode(error));
508
1606
  }
509
1607
  try {
510
1608
  if (error instanceof ZittyUsageError) {
511
- console.error(error.message);
512
- await displayUsage(error.command, { write: (text) => console.error(text) });
513
- } else console.error(`Error: ${error instanceof Error ? error.message : String(error)}`);
1609
+ await output.printer.print({
1610
+ type: "error",
1611
+ error: summarizeError(error)
1612
+ });
1613
+ await printUsage(displayUsage, error.command, output.printer, "stderr");
1614
+ } else await output.printer.print({
1615
+ type: "error",
1616
+ error: summarizeError(error)
1617
+ });
514
1618
  } finally {
515
- process.exit(1);
1619
+ await drainAndExit(output, commandErrorExitCode(error));
516
1620
  }
517
1621
  }
1622
+ await output.drain();
518
1623
  }
519
1624
  function createMain(command, options = {}) {
520
1625
  return (argv) => runMain(command, argv === void 0 ? options : {
@@ -523,6 +1628,6 @@ function createMain(command, options = {}) {
523
1628
  });
524
1629
  }
525
1630
  //#endregion
526
- export { ZittyUsageError, ZittyValidationError, createMain, defineCommand, parseArgs, renderUsage, runCommand, runMain, showUsage };
1631
+ export { HackshPromptError, ZittyUsageError, ZittyValidationError, consolePrinter, createJsonPrinter, createMain, createPrettyPrinter, createStreamPrinter, defineCommand, parseArgs, renderUsage, resolveOutputMode, runCommand, runMain, showUsage, summarizeUsage };
527
1632
 
528
1633
  //# sourceMappingURL=index.js.map