@@ -37,6 +37,61 @@ import type {
3737 WizardOptions ,
3838} from "./types.js" ;
3939
40+ /** Whitespace characters used for JSON indentation. */
41+ const Indenter = {
42+ SPACE : " " ,
43+ TAB : "\t" ,
44+ } as const ;
45+
46+ /** Describes the indentation style of a JSON file. */
47+ type JsonIndent = {
48+ /** The whitespace character used for indentation. */
49+ replacer : ( typeof Indenter ) [ keyof typeof Indenter ] ;
50+ /** How many times the replacer is repeated per indent level. */
51+ length : number ;
52+ } ;
53+
54+ const DEFAULT_JSON_INDENT : JsonIndent = {
55+ replacer : Indenter . SPACE ,
56+ length : 2 ,
57+ } ;
58+
59+ /** Matches the first indented line in a string to detect whitespace style. */
60+ const INDENT_PATTERN = / ^ ( \s + ) / m;
61+
62+ /**
63+ * Detect the indentation style of a JSON string by inspecting the first
64+ * indented line. Returns a default of 2 spaces if no indentation is found.
65+ */
66+ function detectJsonIndent ( content : string ) : JsonIndent {
67+ const match = content . match ( INDENT_PATTERN ) ;
68+ if ( ! match ?. [ 1 ] ) {
69+ return DEFAULT_JSON_INDENT ;
70+ }
71+ const indent = match [ 1 ] ;
72+ if ( indent . includes ( "\t" ) ) {
73+ return { replacer : Indenter . TAB , length : indent . length } ;
74+ }
75+ return { replacer : Indenter . SPACE , length : indent . length } ;
76+ }
77+
78+ /** Build the third argument for `JSON.stringify` from a `JsonIndent`. */
79+ function jsonIndentArg ( indent : JsonIndent ) : string {
80+ return indent . replacer . repeat ( indent . length ) ;
81+ }
82+
83+ /**
84+ * Pretty-print a JSON string using the given indentation style.
85+ * Returns the original string if it cannot be parsed as valid JSON.
86+ */
87+ function prettyPrintJson ( content : string , indent : JsonIndent ) : string {
88+ try {
89+ return `${ JSON . stringify ( JSON . parse ( content ) , null , jsonIndentArg ( indent ) ) } \n` ;
90+ } catch {
91+ return content ;
92+ }
93+ }
94+
4095/**
4196 * Shell metacharacters that enable chaining, piping, substitution, or redirection.
4297 * All legitimate install commands are simple single commands that don't need these.
@@ -511,6 +566,28 @@ function applyPatchsetDryRun(payload: ApplyPatchsetPayload): LocalOpResult {
511566 return { ok : true , data : { applied } } ;
512567}
513568
569+ /**
570+ * Resolve the final file content for a patch, pretty-printing JSON files
571+ * to preserve readable formatting. For `modify` actions, the existing file's
572+ * indentation style is detected and preserved. For `create` actions, a default
573+ * of 2-space indentation is used.
574+ */
575+ function resolvePatchContent (
576+ absPath : string ,
577+ filePath : string ,
578+ rawContent : string ,
579+ action : string
580+ ) : string {
581+ if ( ! filePath . endsWith ( ".json" ) ) {
582+ return rawContent ;
583+ }
584+ if ( action === "modify" ) {
585+ const existing = fs . readFileSync ( absPath , "utf-8" ) ;
586+ return prettyPrintJson ( rawContent , detectJsonIndent ( existing ) ) ;
587+ }
588+ return prettyPrintJson ( rawContent , DEFAULT_JSON_INDENT ) ;
589+ }
590+
514591function applyPatchset (
515592 payload : ApplyPatchsetPayload ,
516593 dryRun ?: boolean
@@ -542,7 +619,13 @@ function applyPatchset(
542619 case "create" : {
543620 const dir = path . dirname ( absPath ) ;
544621 fs . mkdirSync ( dir , { recursive : true } ) ;
545- fs . writeFileSync ( absPath , patch . patch , "utf-8" ) ;
622+ const content = resolvePatchContent (
623+ absPath ,
624+ patch . path ,
625+ patch . patch ,
626+ patch . action
627+ ) ;
628+ fs . writeFileSync ( absPath , content , "utf-8" ) ;
546629 applied . push ( { path : patch . path , action : "create" } ) ;
547630 break ;
548631 }
@@ -554,7 +637,13 @@ function applyPatchset(
554637 data : { applied } ,
555638 } ;
556639 }
557- fs . writeFileSync ( absPath , patch . patch , "utf-8" ) ;
640+ const content = resolvePatchContent (
641+ absPath ,
642+ patch . path ,
643+ patch . patch ,
644+ patch . action
645+ ) ;
646+ fs . writeFileSync ( absPath , content , "utf-8" ) ;
558647 applied . push ( { path : patch . path , action : "modify" } ) ;
559648 break ;
560649 }
0 commit comments