@moritzwilksch/diffle 0.1.0 → 0.1.1

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.
Files changed (83) hide show
  1. package/README.md +18 -2
  2. package/dist/client/assets/{angular-html-FsiSMH3Y.js → angular-html-CkdWwNDI.js} +1 -1
  3. package/dist/client/assets/{angular-ts-BSikFT16.js → angular-ts-CuvP0IGu.js} +1 -1
  4. package/dist/client/assets/{apl-1IDS0jHU.js → apl-pdW-dc02.js} +1 -1
  5. package/dist/client/assets/{astro-CvjIcDdF.js → astro-zkH-hPtA.js} +1 -1
  6. package/dist/client/assets/{blade-iab1_tYE.js → blade-ddlHE2HC.js} +1 -1
  7. package/dist/client/assets/{c-CvC-n5Dt.js → c-Ctj6WiaA.js} +1 -1
  8. package/dist/client/assets/{chapel-DWMV2k4B.js → chapel-W7vmpmYc.js} +1 -1
  9. package/dist/client/assets/{cobol-Dr8jUXVY.js → cobol-DhORiGLX.js} +1 -1
  10. package/dist/client/assets/{coffee-DogfyzLt.js → coffee-CmpKakZz.js} +1 -1
  11. package/dist/client/assets/{cpp-DY0XLqOn.js → cpp-aSK51Qyo.js} +1 -1
  12. package/dist/client/assets/{crystal-DTt4VJ7I.js → crystal-CGGaMVqP.js} +1 -1
  13. package/dist/client/assets/{css-CWsyBCI2.js → css-CbDWH8CU.js} +1 -1
  14. package/dist/client/assets/{edge-Cbd0Hldf.js → edge-Bb_E62jX.js} +1 -1
  15. package/dist/client/assets/{elixir-Dlgm2eX2.js → elixir-DjPlIKeD.js} +1 -1
  16. package/dist/client/assets/{elm-tQ1jykNc.js → elm-B55oXPOF.js} +1 -1
  17. package/dist/client/assets/{erb-lRQQr1Sc.js → erb-CXXpq1zV.js} +1 -1
  18. package/dist/client/assets/{git-rebase-mPGpy9fK.js → git-rebase-7PMXuU9B.js} +1 -1
  19. package/dist/client/assets/{glimmer-js-Cnwlg_IR.js → glimmer-js-BBBBNj8H.js} +1 -1
  20. package/dist/client/assets/{glimmer-ts-lS7iLUfN.js → glimmer-ts-BJzb0Uby.js} +1 -1
  21. package/dist/client/assets/{glsl-Ddwau8g3.js → glsl-CBzawuyB.js} +1 -1
  22. package/dist/client/assets/{graphql-hYYZYECc.js → graphql-DRKcwjOE.js} +1 -1
  23. package/dist/client/assets/{hack-tVOl2PyM.js → hack-CsFE62pw.js} +1 -1
  24. package/dist/client/assets/{haml-Cwtz-Dq4.js → haml--KtktDGY.js} +1 -1
  25. package/dist/client/assets/{handlebars-CyvaDab9.js → handlebars-D56IR41J.js} +1 -1
  26. package/dist/client/assets/{html-BUo5i1NC.js → html-Baytl-FU.js} +1 -1
  27. package/dist/client/assets/{html-derivative-DtP04Ng0.js → html-derivative-Cu0xLfuG.js} +1 -1
  28. package/dist/client/assets/{http-D1AHtlGd.js → http-DBz-9Rm1.js} +1 -1
  29. package/dist/client/assets/{hurl-C15vSi1V.js → hurl-CTnFLYIC.js} +1 -1
  30. package/dist/client/assets/index-BcpNE2iR.js +2924 -0
  31. package/dist/client/assets/index-CBL0EMlj.css +2 -0
  32. package/dist/client/assets/{java-DPFrtqcs.js → java-Dno3m1M7.js} +1 -1
  33. package/dist/client/assets/{javascript-D33iozV3.js → javascript-_XI5Q4LF.js} +1 -1
  34. package/dist/client/assets/{jinja-DRYgl0dQ.js → jinja-CjeRdbbL.js} +1 -1
  35. package/dist/client/assets/{jison-DXyfqsqv.js → jison-C3ET6HHP.js} +1 -1
  36. package/dist/client/assets/{json-BH2tJSV_.js → json-BEsHf2su.js} +1 -1
  37. package/dist/client/assets/{jsx-BNYiHgWd.js → jsx-C3QpkgV8.js} +1 -1
  38. package/dist/client/assets/{julia-DWndCVqe.js → julia-BtHfH46Z.js} +1 -1
  39. package/dist/client/assets/{just-CaFvzxl4.js → just-DRu-Pg-e.js} +1 -1
  40. package/dist/client/assets/{latex-DqDoao-r.js → latex-1BoN898Q.js} +1 -1
  41. package/dist/client/assets/{liquid-DYBj14Ih.js → liquid-CL6XoFA-.js} +1 -1
  42. package/dist/client/assets/{lua-DISCLF_p.js → lua-ClGqXLNh.js} +1 -1
  43. package/dist/client/assets/{marko-BaO-VaHf.js → marko-BQZ0rvkW.js} +1 -1
  44. package/dist/client/assets/{mdc-5Q3_gdW7.js → mdc-CqTpgJx6.js} +1 -1
  45. package/dist/client/assets/{nginx-CQ4BjNxJ.js → nginx-B3fPtzbP.js} +1 -1
  46. package/dist/client/assets/{nim-OJ1IfTX5.js → nim-CcgLIp3n.js} +1 -1
  47. package/dist/client/assets/{org-DE2pvWZc.js → org-5bpEDggi.js} +1 -1
  48. package/dist/client/assets/{perl-8QKFSfpf.js → perl-B_kewDRb.js} +1 -1
  49. package/dist/client/assets/{php-DHN01N8j.js → php-DCeoSILL.js} +1 -1
  50. package/dist/client/assets/{pug-DCS1tN7i.js → pug-Dojg3vEi.js} +1 -1
  51. package/dist/client/assets/{qml-6exH9-g5.js → qml-s5uziDLI.js} +1 -1
  52. package/dist/client/assets/{r-DcfcrTMj.js → r-C9fV-mBh.js} +1 -1
  53. package/dist/client/assets/{razor-CIrVisVV.js → razor-BTysEkha.js} +1 -1
  54. package/dist/client/assets/{regexp-OAMUjW-k.js → regexp-CsIP6diF.js} +1 -1
  55. package/dist/client/assets/{rst-Dt5AjMM2.js → rst-8a-JD2vB.js} +1 -1
  56. package/dist/client/assets/{ruby-E8arA1q2.js → ruby-BP-9Eb2k.js} +1 -1
  57. package/dist/client/assets/{sas-_hYONkAv.js → sas-zIaAmRUD.js} +1 -1
  58. package/dist/client/assets/{scss-CuI5mog1.js → scss-D_zCS4rR.js} +1 -1
  59. package/dist/client/assets/{shellscript-brVl5slU.js → shellscript-DRVCBED5.js} +1 -1
  60. package/dist/client/assets/{shellsession-kD5Km7Fl.js → shellsession-B6vXqKcQ.js} +1 -1
  61. package/dist/client/assets/{soy-AZoL22qE.js → soy-CnSSPgTt.js} +1 -1
  62. package/dist/client/assets/{sql-P6NiMgNx.js → sql-DNUVNZl5.js} +1 -1
  63. package/dist/client/assets/{stata-CaLkIj3d.js → stata-BTM4COU8.js} +1 -1
  64. package/dist/client/assets/{surrealql-DVyAIU7z.js → surrealql-D0RpP-tU.js} +1 -1
  65. package/dist/client/assets/{svelte-CSnNgDkA.js → svelte-Bgjr5_uo.js} +1 -1
  66. package/dist/client/assets/{templ-Dqr_Un-e.js → templ-j4HlTKjQ.js} +1 -1
  67. package/dist/client/assets/{tex-CvSrixZF.js → tex-Da2hGdgf.js} +1 -1
  68. package/dist/client/assets/{ts-tags-DT7kX2Qk.js → ts-tags-DHDgIe3m.js} +1 -1
  69. package/dist/client/assets/{tsx-DtULR4Hc.js → tsx-BiQG1bF2.js} +1 -1
  70. package/dist/client/assets/{twig-D_yBfqrR.js → twig-nmyN70HH.js} +1 -1
  71. package/dist/client/assets/{typescript-D8R1VR1Y.js → typescript-B2CoeRXG.js} +1 -1
  72. package/dist/client/assets/{typst-C9bHxBJa.js → typst-DYnFmsYK.js} +1 -1
  73. package/dist/client/assets/{vue-Bv6cNqLz.js → vue-D4W-_FLh.js} +1 -1
  74. package/dist/client/assets/{vue-html-CJupgPdM.js → vue-html-JguYDiPp.js} +1 -1
  75. package/dist/client/assets/{vue-vine-6vI54xHX.js → vue-vine-EdQ0itvY.js} +1 -1
  76. package/dist/client/assets/{xml-CYK4-lRM.js → xml-C6OfKyfM.js} +1 -1
  77. package/dist/client/assets/{xsl-BOaafYfD.js → xsl-C_NW51H4.js} +1 -1
  78. package/dist/client/assets/{yaml-CvfdNt6k.js → yaml-DExcc3cX.js} +1 -1
  79. package/dist/client/index.html +2 -2
  80. package/dist/server/main.js +1505 -248
  81. package/package.json +6 -1
  82. package/dist/client/assets/index-9z4xRD3M.css +0 -1
  83. package/dist/client/assets/index-Dtx7INNk.js +0 -2925
@@ -2997,8 +2997,961 @@ function useColor$1() {
2997
2997
  }
2998
2998
  new Command();
2999
2999
  //#endregion
3000
+ //#region node_modules/commander-static-completion/dist/model.js
3001
+ const definitions = /* @__PURE__ */ new WeakMap();
3002
+ function validateDefinition(target, definition) {
3003
+ if (!target._executableHandler) throw new TypeError(`Completion definition target ${target.name()} must be an executable subcommand.`);
3004
+ if (definition === target || definition.parent || definition._executableHandler) throw new TypeError(`Completion definition for ${target.name()} must be an independent, in-process root Command.`);
3005
+ if (target.options.length || target.commands.length) throw new TypeError(`Executable subcommand ${target.name()} has conflicting parser definitions; put options and children on its completion definition.`);
3006
+ }
3007
+ function checkCompatibility(command) {
3008
+ const internal = command;
3009
+ if (command.commands.some((child) => child.name() === "*" || child.aliases().includes("*"))) throw new Error("Legacy wildcard subcommands are unsupported; use { isDefault: true } with an ordinary command name.");
3010
+ if (internal._executableHandler && !definitions.has(command)) throw new Error(`Supply a definition with completionDefinition() for executable subcommand ${command.name()}.`);
3011
+ }
3012
+ function description(value) {
3013
+ return stripVTControlCharacters(value).replace(/[\s\x00-\x1f\x7f-\x9f]+/gu, " ").trim();
3014
+ }
3015
+ const hints = /* @__PURE__ */ new WeakMap();
3016
+ function valueSpec(target) {
3017
+ const hint = hints.get(target);
3018
+ if (hint) return hint;
3019
+ return target.argChoices ? {
3020
+ kind: "choices",
3021
+ values: [...target.argChoices]
3022
+ } : { kind: "none" };
3023
+ }
3024
+ function extract(program) {
3025
+ const nodes = [];
3026
+ const active = /* @__PURE__ */ new Set();
3027
+ function visit(command, inherited = [], ancestorDigit = false) {
3028
+ checkCompatibility(command);
3029
+ const definition = definitions.get(command);
3030
+ if (definition) {
3031
+ validateDefinition(command, definition);
3032
+ if (inherited.some((option) => option.flags.some((flag) => /^-\d$/u.test(flag)))) throw new Error(`Executable subcommand ${command.name()} has active ancestor digit flags; use enablePositionalOptions() on their declaring commands to separate the parsers.`);
3033
+ ancestorDigit = false;
3034
+ command = definition;
3035
+ checkCompatibility(command);
3036
+ }
3037
+ if (active.has(command)) throw new TypeError(`Cyclic completion definition at ${command.name()}.`);
3038
+ active.add(command);
3039
+ const internal = command;
3040
+ const hasDigit = ancestorDigit || command.options.some((option) => /^-\d$/u.test(option.short ?? ""));
3041
+ const help = command.createHelp();
3042
+ const helpCommand = internal._getHelpCommand();
3043
+ const helpOption = internal._getHelpOption();
3044
+ const visibleOptions = help.visibleOptions(command);
3045
+ const optionFlags = (option) => [option.short, option.long].filter((flag) => flag !== void 0);
3046
+ const registeredFlags = new Set(command.options.flatMap(optionFlags));
3047
+ for (const option of visibleOptions) {
3048
+ if (command.options.includes(option) || option === helpOption) continue;
3049
+ const flags = optionFlags(option);
3050
+ if (!helpOption || !flags.length || !flags.every((flag) => optionFlags(helpOption).includes(flag) && !registeredFlags.has(flag))) throw new Error(`configureHelp().visibleOptions() returned an unregistered option ${option.flags} for ${command.name()}. Supply parser definitions with addOption().`);
3051
+ }
3052
+ const localOptions = [.../* @__PURE__ */ new Set([...command.options, ...visibleOptions])].map((option) => ({
3053
+ flags: [option.short, option.long].filter((flag) => flag !== void 0),
3054
+ visible: visibleOptions.includes(option),
3055
+ description: description(option.description),
3056
+ inherited: command.options.includes(option),
3057
+ combineOptional: internal._combineFlagAndOptionalValue !== false,
3058
+ mode: command.options.includes(option) ? option.required ? "required" : option.optional ? "optional" : "boolean" : "boolean",
3059
+ variadic: command.options.includes(option) && option.variadic,
3060
+ value: command.options.includes(option) ? valueSpec(option) : { kind: "none" }
3061
+ }));
3062
+ const seen = /* @__PURE__ */ new Set();
3063
+ const options = [...inherited, ...localOptions].map((option) => ({
3064
+ ...option,
3065
+ flags: option.flags.filter((flag) => {
3066
+ if (seen.has(flag)) return false;
3067
+ seen.add(flag);
3068
+ return true;
3069
+ })
3070
+ }));
3071
+ const node = {
3072
+ id: nodes.length,
3073
+ defaultCommand: -1,
3074
+ options,
3075
+ localOptions,
3076
+ positional: internal._enablePositionalOptions ?? false,
3077
+ passThrough: internal._passThroughOptions ?? false,
3078
+ negativeNumbers: !hasDigit,
3079
+ arguments: command.registeredArguments.map((arg) => ({
3080
+ variadic: arg.variadic,
3081
+ value: valueSpec(arg),
3082
+ description: description(arg.description)
3083
+ })),
3084
+ children: []
3085
+ };
3086
+ nodes.push(node);
3087
+ const visibleCommands = help.visibleCommands(command);
3088
+ for (const child of visibleCommands) if (!command.commands.includes(child) && child !== helpCommand) throw new Error(`configureHelp().visibleCommands() returned an unregistered command ${child.name()} for ${command.name()}. Supply parser definitions with addCommand().`);
3089
+ const implicitHelp = helpCommand && !command.commands.some((child) => [child.name(), ...child.aliases()].includes(helpCommand.name())) ? helpCommand : null;
3090
+ for (const child of /* @__PURE__ */ new Set([...command.commands, ...implicitHelp ? [implicitHelp] : []])) {
3091
+ const synthetic = child === implicitHelp;
3092
+ let childNode;
3093
+ if (synthetic) {
3094
+ childNode = {
3095
+ id: nodes.length,
3096
+ defaultCommand: -1,
3097
+ options: [],
3098
+ localOptions: [],
3099
+ passThrough: false,
3100
+ positional: false,
3101
+ negativeNumbers: !hasDigit,
3102
+ arguments: [{
3103
+ variadic: false,
3104
+ description: "",
3105
+ value: {
3106
+ kind: "choices",
3107
+ values: visibleCommands.filter((c) => c !== child).flatMap((c) => [c.name(), ...c.aliases()])
3108
+ }
3109
+ }],
3110
+ children: []
3111
+ };
3112
+ nodes.push(childNode);
3113
+ } else childNode = visit(child, internal._enablePositionalOptions || internal._passThroughOptions ? inherited : [...inherited, ...localOptions.filter((option) => option.inherited)], hasDigit);
3114
+ if (!synthetic && child.name() === internal._defaultCommandName) node.defaultCommand = childNode.id;
3115
+ node.children.push({
3116
+ id: childNode.id,
3117
+ names: synthetic ? [child.name()] : [child.name(), ...child.aliases()],
3118
+ description: description(child.description()),
3119
+ visible: visibleCommands.includes(child)
3120
+ });
3121
+ }
3122
+ if (internal._defaultCommandName && node.defaultCommand < 0) throw new Error(`Default subcommand ${internal._defaultCommandName} is missing from ${command.name()}.`);
3123
+ active.delete(command);
3124
+ return node;
3125
+ }
3126
+ visit(program);
3127
+ return nodes;
3128
+ }
3129
+ //#endregion
3130
+ //#region node_modules/commander-static-completion/dist/bourne-runtime.js
3131
+ function bourneRuntime(shell) {
3132
+ return `${filterRuntime}\n${shell === "bash" ? bashDecode + bashWrapper : ""}__PREFIX__${shell === "bash" ? "_complete" : ""}() {
3133
+ ${shell === "zsh" ? zshInput : bashInput}
3134
+ local state=0 position=0 operands=0 end=0 pending= pending_value=-1 pending_variadic=0
3135
+ local passthrough positional negative default_command consume join_next=0
3136
+ # Isolate regex checks below: Bash/Zsh otherwise overwrite caller match variables.
3137
+ local number_pattern='^-([0-9]+|[0-9]*[.][0-9]+)(e[+-]?[0-9]+)?$'
3138
+ local combine mode value=-1 variadic=0 next kind word current flag rest attached
3139
+ local i j count=0 candidate lead= cluster_prefix= raw_current="\${COMP_WORDS[COMP_CWORD]}"
3140
+ local -a tokens candidates flags alternatives
3141
+ COMPREPLY=()
3142
+ __PREFIX___settings
3143
+
3144
+ # Direct scanner callers may supply split assignments without a source line.
3145
+ for ((i=1; i<=COMP_CWORD; i++)); do
3146
+ word=\${COMP_WORDS[i]}
3147
+ if [[ -z $COMP_LINE ]] && ((count > 0)) && [[ $word == = && \${tokens[count-1]} == --* ]]; then
3148
+ tokens[count-1]+='='
3149
+ join_next=1
3150
+ elif ((join_next)) && [[ $word != = ]]; then
3151
+ tokens[count-1]+=$word
3152
+ join_next=0
3153
+ else
3154
+ tokens[count]=$word
3155
+ ((count+=1))
3156
+ fi
3157
+ done
3158
+ current=\${tokens[count-1]}
3159
+
3160
+ __PREFIX___filter
3161
+ for ((i=0; i<count-1; i++)); do
3162
+ word=\${tokens[i]}
3163
+ if (( ! end )) && [[ $word == -- ]]; then end=1; continue; fi
3164
+ if ((operands == 0)); then
3165
+ __PREFIX___child "$word"
3166
+ consume=1
3167
+ if [[ -z $next ]] && ((default_command >= 0)); then next=$default_command; consume=0; fi
3168
+ if [[ -n $next ]]; then
3169
+ state=$next; position=0
3170
+ tokens=("\${tokens[@]:$((i+consume))}")
3171
+ count=\${#tokens[@]}; i=-1
3172
+ __PREFIX___settings
3173
+ __PREFIX___filter
3174
+ continue
3175
+ fi
3176
+ fi
3177
+ ((operands+=1))
3178
+ if ((passthrough)); then end=1; fi
3179
+ __PREFIX___argument
3180
+ if (( ! variadic )); then ((position+=1)); fi
3181
+ done
3182
+
3183
+ # Until an operand commits a route, offer explicit commands and the default
3184
+ # command's initial completions. A known parent option still owns its value.
3185
+ while ((operands == 0 && default_command >= 0)) && [[ -z $pending ]]; do
3186
+ if (( ! end )) && [[ $current == -?* ]]; then
3187
+ if [[ $current == --* ]]; then
3188
+ __PREFIX___option "\${current%%=*}"
3189
+ else
3190
+ rest=\${current:1}; attached=
3191
+ while [[ -n $rest ]]; do
3192
+ __PREFIX___option "-\${rest:0:1}"
3193
+ if [[ -z $mode ]]; then
3194
+ cluster_prefix+=$attached
3195
+ current=-$rest
3196
+ break
3197
+ fi
3198
+ [[ $mode == required || ( $mode == optional && ( $combine == 1 || \${#rest} == 1 ) ) ]] && break
3199
+ attached+=\${rest:0:1}; rest=\${rest:1}
3200
+ done
3201
+ fi
3202
+ [[ -n $mode ]] && break
3203
+ fi
3204
+ __PREFIX___suggestions
3205
+ alternatives+=("\${candidates[@]}")
3206
+ if (( ! end )); then alternatives+=("\${flags[@]}"); fi
3207
+ state=$default_command
3208
+ __PREFIX___settings
3209
+ done
3210
+
3211
+ value=-1
3212
+ if [[ -n $pending ]] && { [[ $pending == required || $current != -* || $current == - ]] || { ((negative)) && ( [[ $current =~ $number_pattern ]] ); }; }; then
3213
+ value=$pending_value
3214
+ elif (( ! end )) && [[ $current == --*=* ]]; then
3215
+ __PREFIX___option "\${current%%=*}"
3216
+ if [[ -z $mode || $mode == boolean ]]; then return 0; fi
3217
+ lead=\${current%%=*}=
3218
+ current=\${current#*=}
3219
+ # Native Bash trims the actual replacement prefix after generating candidates.
3220
+ if [[ -z $COMP_LINE && $COMP_WORDBREAKS == *=* && $raw_current != --*=* ]]; then lead=; fi
3221
+ elif (( ! end )) && [[ $current == -?* && $current != --* ]]; then
3222
+ rest=\${current:1}; attached=-
3223
+ while [[ -n $rest ]]; do
3224
+ flag=-\${rest:0:1}; attached+=\${rest:0:1}; rest=\${rest:1}
3225
+ __PREFIX___option "$flag"
3226
+ if [[ -z $mode ]]; then value=-1; break; fi
3227
+ if [[ ( $mode == required || ( $mode == optional && $combine == 1 ) ) && -n $rest ]]; then lead=$attached; current=$rest; break; fi
3228
+ value=-1
3229
+ done
3230
+ fi
3231
+
3232
+ ${shell === "bash" ? bashUnquote : ""}
3233
+ if ((value >= 0)); then
3234
+ __PREFIX___values
3235
+ else
3236
+ __PREFIX___suggestions
3237
+ if ((operands > 0)); then candidates=(); fi
3238
+ if (( ! end )); then candidates+=("\${flags[@]}"); fi
3239
+ local -a base=("\${alternatives[@]}" "\${candidates[@]}")
3240
+ __PREFIX___argument
3241
+ __PREFIX___values
3242
+ candidates=("\${base[@]}" "\${candidates[@]}")
3243
+ fi
3244
+ if [[ -n $cluster_prefix && -n $lead ]]; then lead=-$cluster_prefix\${lead#-}; fi
3245
+ ${shell === "zsh" ? " local description has_descriptions=0\n local -a display\n" : ""} for candidate in "\${candidates[@]}"; do
3246
+ ${shell === "zsh" ? ` __PREFIX___description "\${candidate%%:*}"
3247
+ candidate=\${candidate#*:}\n` : ""} [[ $candidate == "$current"* ]] || continue
3248
+ if [[ -n $cluster_prefix && -z $lead && $candidate == -?* && $candidate != --* ]]; then candidate=-$cluster_prefix\${candidate#-}; fi
3249
+ local duplicate=0 existing
3250
+ for existing in "\${COMPREPLY[@]}"; do [[ $existing == "$lead$candidate" ]] && duplicate=1; done
3251
+ ((duplicate)) || COMPREPLY+=("$lead$candidate")
3252
+ ${shell === "zsh" ? ` if (( ! duplicate )); then
3253
+ if [[ -n $description ]]; then
3254
+ display+=("$lead$candidate -- $description"); has_descriptions=1
3255
+ else
3256
+ display+=("$lead$candidate")
3257
+ fi
3258
+ fi\n` : ""} done
3259
+ ${shell === "bash" ? bashFiles : zshOutput}
3260
+ return 0
3261
+ }
3262
+ `;
3263
+ }
3264
+ const bashWrapper = `__PREFIX__() {
3265
+ local _csc_nounset=0 _csc_nocasematch=0 _csc_status=0
3266
+ case $- in *u*) _csc_nounset=1 ;; esac
3267
+ if shopt -q nocasematch; then _csc_nocasematch=1; fi
3268
+ set +u
3269
+ shopt -u nocasematch
3270
+ if __PREFIX___complete "$@"; then :; else _csc_status=$?; fi
3271
+ if ((_csc_nocasematch)); then shopt -s nocasematch; fi
3272
+ if ((_csc_nounset)); then set -u; fi
3273
+ return "$_csc_status"
3274
+ }
3275
+ `;
3276
+ const bashFiles = String.raw` if [[ $kind == file || $kind == directory ]]; then
3277
+ local action=file search=$current
3278
+ # compgen expands a leading tilde even when the user quoted or escaped it.
3279
+ # Keep './' in the result: Readline can otherwise expand the literal tilde
3280
+ # during insertion, even when completing inside quotes on newer Bash.
3281
+ if ((literal_tilde)); then search=./$current; fi
3282
+ [[ $kind == directory ]] && action=directory
3283
+ while IFS= read -r candidate; do
3284
+ # After trimming a word-break prefix, Readline cannot stat the original
3285
+ # directory. Preserve its slash before returning the replacement suffix.
3286
+ if [[ -n $COMP_LINE && -d $candidate && $candidate != */ ]]; then candidate+=/; fi
3287
+ COMPREPLY+=("$lead$candidate")
3288
+ done < <(compgen -A "$action" -- "$search")
3289
+ # compopt is a Bash 4+ builtin. Never resolve an external command on 3.2.
3290
+ if ((BASH_VERSINFO[0] >= 4)); then compopt -o filenames 2>/dev/null || :; fi
3291
+ if ((BASH_VERSINFO[0] >= 4 && __DOLLAR__{#COMPREPLY[@]} == 1)) && [[ __DOLLAR__{COMPREPLY[0]} == */ ]]; then
3292
+ compopt -o nospace 2>/dev/null || :
3293
+ fi
3294
+ fi
3295
+ # Readline can replace only the suffix after a configured word break. Remove
3296
+ # the already-present prefix from both static candidates and filesystem paths.
3297
+ if [[ -n $readline_prefix ]]; then
3298
+ for ((j=0; j<__DOLLAR__{#COMPREPLY[@]}; j++)); do
3299
+ COMPREPLY[j]=__DOLLAR__{COMPREPLY[j]#"$readline_prefix"}
3300
+ done
3301
+ fi
3302
+ if ((BASH_VERSINFO[0] >= 4)) && [[ $kind != file && $kind != directory && -n $COMP_LINE ]] && compopt -o noquote +o filenames 2>/dev/null; then
3303
+ # Quote static words ourselves: newer Readline can preserve expansion syntax
3304
+ # even with filename quoting enabled. Bash 3.2 uses registration-time quoting.
3305
+ for ((j=0; j<__DOLLAR__{#COMPREPLY[@]}; j++)); do
3306
+ candidate=__DOLLAR__{COMPREPLY[j]}
3307
+ if [[ $quote_char == "'" ]]; then
3308
+ candidate=__DOLLAR__{candidate//\'/\'\\\'\'}
3309
+ COMPREPLY[j]="$candidate'"
3310
+ elif [[ $quote_char == '"' ]]; then
3311
+ local k quoted=
3312
+ for ((k=0; k<__DOLLAR__{#candidate}; k++)); do
3313
+ char=__DOLLAR__{candidate:k:1}
3314
+ if [[ $char == '$' || $char == $'\x60' || $char == '"' || $char == \\ ]]; then quoted+='\'; fi
3315
+ quoted+=$char
3316
+ done
3317
+ COMPREPLY[j]=$quoted'"'
3318
+ else
3319
+ printf -v 'COMPREPLY[j]' '%q' "$candidate"
3320
+ fi
3321
+ done
3322
+ fi
3323
+ `.replaceAll("__DOLLAR__", "$");
3324
+ const bashDecode = String.raw`__PREFIX___decode() {
3325
+ local part=$1 char escaped=0 offset
3326
+ decoded=; quote_char=
3327
+ for ((offset=0; offset<__DOLLAR__{#part}; offset++)); do
3328
+ char=__DOLLAR__{part:offset:1}
3329
+ if ((escaped)); then
3330
+ if [[ $quote_char == '"' && $char != '$' && $char != $'\x60' && $char != '"' && $char != \\ && $char != $'\n' ]]; then decoded+='\'; fi
3331
+ decoded+=$char; escaped=0
3332
+ elif [[ $char == \\ && $quote_char != "'" ]]; then escaped=1
3333
+ elif [[ -z $quote_char && ( $char == "'" || $char == '"' ) ]]; then quote_char=$char
3334
+ elif [[ -n $quote_char && $char == "$quote_char" ]]; then quote_char=
3335
+ else decoded+=$char
3336
+ fi
3337
+ done
3338
+ ((escaped)) && decoded+='\'
3339
+ return 0
3340
+ }
3341
+ `.replaceAll("__DOLLAR__", "$");
3342
+ const bashUnquote = String.raw` if [[ -n $COMP_LINE ]]; then
3343
+ local decoded char quote_char path_raw=$current
3344
+ __PREFIX___decode "$readline_prefix"
3345
+ readline_prefix=$decoded
3346
+ __PREFIX___decode "$current"
3347
+ current=$decoded
3348
+ if [[ $current == '~'* && $path_raw != '~'* ]]; then literal_tilde=1; fi
3349
+ fi
3350
+ `.replaceAll("__DOLLAR__", "$");
3351
+ const zshInput = ` emulate -L ksh
3352
+ local -a COMP_WORDS=("\${(@Q)words[@]}") COMPREPLY
3353
+ local COMP_CWORD=$((CURRENT - 1)) COMP_WORDBREAKS=
3354
+ # Ignore text after the cursor in the current token.
3355
+ COMP_WORDS[COMP_CWORD]=\${(Q)PREFIX}`;
3356
+ const bashInput = String.raw` local -a COMP_WORDS=("__DOLLAR__{COMP_WORDS[@]}")
3357
+ local COMP_CWORD=$COMP_CWORD readline_prefix= literal_tilde=0
3358
+ if [[ -n $COMP_LINE && -n $COMP_POINT ]]; then
3359
+ local before after
3360
+ # Bash 4.3+ counts characters; older versions report a byte offset.
3361
+ if ((BASH_VERSINFO[0] > 4 || (BASH_VERSINFO[0] == 4 && BASH_VERSINFO[1] >= 3))); then
3362
+ before=__DOLLAR__{COMP_LINE:0:COMP_POINT}
3363
+ else
3364
+ printf -v before '%.*s' "$COMP_POINT" "$COMP_LINE"
3365
+ fi
3366
+ after=__DOLLAR__{COMP_LINE#"$before"}
3367
+ local token=__DOLLAR__{COMP_WORDS[COMP_CWORD]} prefix suffix cut
3368
+ for ((cut=__DOLLAR__{#token}; cut>=0; cut--)); do
3369
+ prefix=__DOLLAR__{token:0:cut}; suffix=__DOLLAR__{token:cut}
3370
+ if [[ $before == *"$prefix" && $after == "$suffix"* ]]; then
3371
+ COMP_WORDS[COMP_CWORD]=$prefix
3372
+ break
3373
+ fi
3374
+ done
3375
+ # Bash versions disagree about splitting punctuation in COMP_WORDS. Join
3376
+ # only physically adjacent fragments; whitespace around '=' stays meaningful.
3377
+ local remaining=$before fragment gap index joined_count=0
3378
+ local -a joined
3379
+ for ((index=0; index<=COMP_CWORD; index++)); do
3380
+ fragment=__DOLLAR__{COMP_WORDS[index]}
3381
+ if [[ -n $fragment ]]; then
3382
+ [[ $remaining == *"$fragment"* ]] || { COMPREPLY=(); return 0; }
3383
+ gap=__DOLLAR__{remaining%%"$fragment"*}
3384
+ remaining=__DOLLAR__{remaining#*"$fragment"}
3385
+ else
3386
+ gap=$remaining; remaining=
3387
+ fi
3388
+ if ((joined_count > 0)) && [[ -z $gap ]]; then
3389
+ joined[joined_count-1]+=$fragment
3390
+ else
3391
+ joined[joined_count]=$fragment
3392
+ ((joined_count+=1))
3393
+ fi
3394
+ done
3395
+ COMP_WORDS=("__DOLLAR__{joined[@]}")
3396
+ COMP_CWORD=$((joined_count-1))
3397
+ # Decode committed tokens too, so inserted quoted names route correctly.
3398
+ local decoded quote_char
3399
+ for ((index=1; index<COMP_CWORD; index++)); do
3400
+ __PREFIX___decode "__DOLLAR__{COMP_WORDS[index]}"
3401
+ COMP_WORDS[index]=$decoded
3402
+ done
3403
+ # $2 is Readline's replacement text. It can be shorter than the logical word
3404
+ # even on Bash 3.2, where COMP_WORDS retains punctuation within each word.
3405
+ token=__DOLLAR__{COMP_WORDS[COMP_CWORD]}
3406
+ if (($# >= 2)) && [[ $token == *"$2" ]]; then
3407
+ readline_prefix=__DOLLAR__{token%"$2"}
3408
+ fi
3409
+ fi`.replaceAll("__DOLLAR__", "$");
3410
+ const zshOutput = ` emulate -L zsh
3411
+ # Restore compinit options before calling native completion helpers.
3412
+ if ((\${#_comp_options[@]})); then setopt "\${_comp_options[@]}"; fi
3413
+ if ((\${#COMPREPLY[@]})); then
3414
+ if ((has_descriptions)); then compadd -d display -- "\${COMPREPLY[@]}";
3415
+ else compadd -- "\${COMPREPLY[@]}"; fi
3416
+ fi
3417
+ if [[ $kind == file || $kind == directory ]]; then
3418
+ # Native file completion handles quoting and directory suffixes.
3419
+ if [[ -n $lead ]]; then compset -P "\${(b)lead}"; fi
3420
+ # _files can fall back to all files when no directory matches. Use the
3421
+ # lower-level helper to keep directory hints strict, including symlinks.
3422
+ if [[ $kind == directory ]]; then _path_files -/; else _files; fi
3423
+ fi`;
3424
+ const filterRuntime = `__PREFIX___filter() {
3425
+ local p seen=0 token rest attached=0
3426
+ local -a filtered
3427
+ pending=; pending_value=-1; pending_variadic=0
3428
+ if ((end)); then return; fi
3429
+ for ((p=0; p<count-1; p++)); do
3430
+ token=\${tokens[p]}
3431
+ if [[ -n $pending ]]; then
3432
+ if [[ $pending == required || $token != -* || $token == - ]] || { ((negative)) && ( [[ $token =~ $number_pattern ]] ); }; then
3433
+ if ((pending_variadic)); then pending=optional; else pending=; fi
3434
+ continue
3435
+ fi
3436
+ pending=
3437
+ fi
3438
+ if [[ $token == -- ]]; then
3439
+ filtered+=("\${tokens[@]:$p:$((count-1-p))}")
3440
+ break
3441
+ fi
3442
+ __PREFIX___local_option "$token"
3443
+ attached=0
3444
+ if [[ -z $mode && $token == --*=* ]]; then
3445
+ __PREFIX___local_option "\${token%%=*}"
3446
+ if [[ $mode == boolean ]]; then mode=; fi
3447
+ attached=1
3448
+ elif [[ -z $mode && $token == -?* && $token != --* ]]; then
3449
+ rest=\${token:1}
3450
+ while [[ -n $rest ]]; do
3451
+ __PREFIX___local_option "-\${rest:0:1}"
3452
+ if [[ -z $mode ]]; then token=-$rest; break; fi
3453
+ rest=\${rest:1}
3454
+ if [[ $mode == required || ( $mode == optional && ( $combine == 1 || -z $rest ) ) ]]; then
3455
+ [[ -n $rest ]] && attached=1
3456
+ break
3457
+ fi
3458
+ done
3459
+ fi
3460
+ if [[ -n $mode ]]; then
3461
+ if [[ $mode != boolean ]] && (( ! attached )); then
3462
+ pending=$mode; pending_value=$value; pending_variadic=$variadic
3463
+ fi
3464
+ continue
3465
+ fi
3466
+ if (((positional || passthrough) && seen == 0)); then
3467
+ __PREFIX___child "$token"
3468
+ if [[ -n $next ]] || ((default_command >= 0)); then
3469
+ filtered+=("$token" "\${tokens[@]:$((p+1)):$((count-2-p))}")
3470
+ break
3471
+ fi
3472
+ fi
3473
+ filtered+=("$token")
3474
+ ((seen+=1))
3475
+ if ((passthrough)); then
3476
+ filtered+=("\${tokens[@]:$((p+1)):$((count-2-p))}")
3477
+ break
3478
+ fi
3479
+ done
3480
+ if [[ -n $pending ]] && { [[ $pending == required || $current != -* || $current == - ]] || { ((negative)) && ( [[ $current =~ $number_pattern ]] ); }; }; then
3481
+ # Ancestor options consume values before a child ever sees the remaining words.
3482
+ tokens=("$current"); count=1
3483
+ else
3484
+ pending=
3485
+ tokens=("\${filtered[@]}" "$current"); count=\${#tokens[@]}
3486
+ fi
3487
+ }
3488
+ `;
3489
+ //#endregion
3490
+ //#region node_modules/commander-static-completion/dist/bourne.js
3491
+ function quote$1(value) {
3492
+ if (value.includes("\0")) throw new TypeError("Shell completion strings cannot contain NUL.");
3493
+ return "'" + value.replaceAll("'", "'\\''") + "'";
3494
+ }
3495
+ const array = (values) => `(${values.map(quote$1).join(" ")})`;
3496
+ function renderBourne(nodes, executable, prefix, shell) {
3497
+ const descriptions = [];
3498
+ const described = (words, description) => {
3499
+ if (shell === "bash") return [...words];
3500
+ let id = descriptions.indexOf(description);
3501
+ if (id < 0) {
3502
+ id = descriptions.length;
3503
+ descriptions.push(description);
3504
+ }
3505
+ return words.map((word) => `${id}:${word}`);
3506
+ };
3507
+ const specs = [];
3508
+ const spec = (hint, description) => {
3509
+ specs.push({
3510
+ hint,
3511
+ description
3512
+ });
3513
+ return specs.length - 1;
3514
+ };
3515
+ const localCases = [];
3516
+ const optionCases = [], argumentCases = [], childCases = [], suggestionCases = [];
3517
+ for (const node of nodes) {
3518
+ for (const option of node.options) {
3519
+ const id = spec(option.value, option.description);
3520
+ for (const flag of option.flags) optionCases.push(`${quote$1(`${node.id}:${flag}`)}) mode=${option.mode}; combine=${option.combineOptional ? 1 : 0}; value=${id}; variadic=${option.variadic ? 1 : 0} ;;`);
3521
+ }
3522
+ for (const option of node.localOptions) {
3523
+ const id = spec(option.value, option.description);
3524
+ for (const flag of option.flags) localCases.push(`${quote$1(`${node.id}:${flag}`)}) mode=${option.mode}; combine=${option.combineOptional ? 1 : 0}; value=${id}; variadic=${option.variadic ? 1 : 0} ;;`);
3525
+ }
3526
+ node.arguments.forEach((arg, index) => {
3527
+ argumentCases.push(`${quote$1(`${node.id}:${index}`)}) value=${spec(arg.value, arg.description)}; variadic=${arg.variadic ? 1 : 0} ;;`);
3528
+ });
3529
+ for (const child of node.children) for (const name of child.names) childCases.push(`${quote$1(`${node.id}:${name}`)}) next=${child.id} ;;`);
3530
+ suggestionCases.push(`${node.id}) candidates=${array(node.children.filter((c) => c.visible).flatMap((c) => described(c.names, c.description)))}; flags=${array([...new Set(node.options.filter((o) => o.visible).flatMap((o) => described(o.flags, o.description)))])} ;;`);
3531
+ }
3532
+ const functions = [
3533
+ `${prefix}_settings() {\n case "$state" in\n${nodes.map((node) => `${node.id}) passthrough=${node.passThrough ? 1 : 0}; positional=${node.positional ? 1 : 0}; negative=${node.negativeNumbers ? 1 : 0}; default_command=${node.defaultCommand} ;;`).join("\n")}\n esac\n}`,
3534
+ `${prefix}_local_option() {\n mode=; value=-1; variadic=0\n case "$state:$1" in\n${localCases.join("\n")}\n esac\n}`,
3535
+ `${prefix}_option() {\n mode=; value=-1; variadic=0\n case "$state:$1" in\n${optionCases.join("\n")}\n esac\n}`,
3536
+ `${prefix}_argument() {\n value=-1; variadic=0\n case "$state:$position" in\n${argumentCases.join("\n")}\n esac\n}`,
3537
+ `${prefix}_child() {\n next=\n case "$state:$1" in\n${childCases.join("\n")}\n esac\n}`,
3538
+ `${prefix}_suggestions() {\n candidates=(); flags=()\n case "$state" in\n${suggestionCases.join("\n")}\n esac\n}`,
3539
+ `${prefix}_values() {\n candidates=(); kind=none\n case "$value" in\n${specs.map((s, i) => `${i}) kind=${s.hint.kind}; candidates=${array(described(s.hint.kind === "choices" ? s.hint.values : [], s.description))} ;;`).join("\n")}\n esac\n}`
3540
+ ];
3541
+ if (shell === "zsh") functions.push(`${prefix}_description() {\n description=\n case "$1" in\n${descriptions.map((text, id) => `${id}) description=${quote$1(text)} ;;`).join("\n")}\n esac\n}`);
3542
+ const runtime = bourneRuntime(shell).replaceAll("__PREFIX__", prefix);
3543
+ const registration = shell === "bash" ? `complete -o filenames -F ${prefix} -- ${quote$1(executable)}` : `# Autoload executes this file as a function; sourcing only registers it.
3544
+ if [[ $ZSH_EVAL_CONTEXT == *:shfunc || $ZSH_EVAL_CONTEXT == *:loadautofunc ]]; then
3545
+ ${prefix} "$@"
3546
+ else
3547
+ compdef ${prefix} ${quote$1(executable)}
3548
+ fi`;
3549
+ return `${shell === "zsh" ? `#compdef ${executable}\n` : ""}# Generated by commander-static-completion. Requires ${shell === "bash" ? "Bash 3.2+" : "Zsh with compinit loaded"}.\n${functions.join("\n\n")}\n\n${runtime}\n${registration}\n`;
3550
+ }
3551
+ //#endregion
3552
+ //#region node_modules/commander-static-completion/dist/fish-runtime.js
3553
+ const fishRuntime = String.raw`
3554
+ function __PREFIX__
3555
+ set -l tokens (commandline -xpc)
3556
+ set -l current (commandline -ct)
3557
+ set -l raw_current "$current"
3558
+ # Unescape a complete token; leave incomplete quoting to Fish's matcher.
3559
+ set -l unescaped (string unescape -- "$current")
3560
+ if test (count $unescaped) -eq 1
3561
+ set current "$unescaped"
3562
+ end
3563
+ __PREFIX___scan "$current" "$raw_current" $tokens[2..-1]
3564
+ end
3565
+
3566
+ function __PREFIX___scan
3567
+ set -l current "$argv[1]"
3568
+ set -l raw_current "$argv[2]"
3569
+ set -l state 0
3570
+ set -l position 0
3571
+ set -l operands 0
3572
+ set -l settings (__PREFIX___settings $state)
3573
+ set -l ended 0
3574
+ set -l pending ''
3575
+ set -l pending_value -1
3576
+ set -l pending_variadic 0
3577
+ set -l info
3578
+ set -l lead ''
3579
+ set -l cluster_prefix ''
3580
+ set -l alternatives
3581
+
3582
+ set -l tokens $argv[3..-1]
3583
+ while true
3584
+ set settings (__PREFIX___settings $state)
3585
+ set pending ''
3586
+ if test $ended -eq 0
3587
+ set -l filtered (__PREFIX___filter $state "$current" $tokens | string split0)
3588
+ set pending "$filtered[1]"
3589
+ set pending_value $filtered[2]
3590
+ set pending_variadic $filtered[3]
3591
+ set tokens $filtered[4..-1]
3592
+ end
3593
+ if test -n "$pending"; and begin; test "$pending" = required; or not string match -q -- '-*' "$current"; or test "$current" = -; or begin; test "$settings[2]" = 1; and string match -qr -- '^-([0-9]+|[0-9]*[.][0-9]+)(e[+-]?[0-9]+)?$' "$current"; end; end
3594
+ break
3595
+ end
3596
+ set pending ''
3597
+ set -l dispatched 0
3598
+ set -l index 1
3599
+ while test $index -le (count $tokens)
3600
+ set -l word "$tokens[$index]"
3601
+ set index (math $index + 1)
3602
+ if test $ended -eq 0; and test "$word" = --
3603
+ set ended 1
3604
+ continue
3605
+ end
3606
+ if test $operands -eq 0
3607
+ set -l next (__PREFIX___child $state "$word")
3608
+ set -l consume 1
3609
+ if test "$next" = -1; and test "$settings[4]" != -1
3610
+ set next $settings[4]
3611
+ set consume 0
3612
+ end
3613
+ if test "$next" != -1
3614
+ set state $next
3615
+ set position 0
3616
+ if test $consume -eq 0
3617
+ set index (math $index - 1)
3618
+ end
3619
+ set tokens $tokens[$index..-1]
3620
+ set dispatched 1
3621
+ break
3622
+ end
3623
+ end
3624
+ set operands (math $operands + 1)
3625
+ if test "$settings[1]" = 1
3626
+ set ended 1
3627
+ end
3628
+ set info (__PREFIX___argument $state $position)
3629
+ if test "$info[2]" = 0
3630
+ set position (math $position + 1)
3631
+ end
3632
+ end
3633
+ if test $dispatched -eq 0
3634
+ if test $operands -ne 0; or test "$settings[4]" = -1
3635
+ break
3636
+ end
3637
+ if test $ended -eq 0; and string match -qr -- '^-.+' "$current"
3638
+ if string match -q -- '--*' "$current"
3639
+ set -l flag (string split -m 1 = -- "$current")[1]
3640
+ set info (__PREFIX___option $state "$flag")
3641
+ else
3642
+ set -l rest (string sub -s 2 -- "$current")
3643
+ set -l consumed ''
3644
+ while test -n "$rest"
3645
+ set -l letter (string sub -l 1 -- "$rest")
3646
+ set info (__PREFIX___option $state "-$letter")
3647
+ if test "$info[1]" = unknown
3648
+ set cluster_prefix "$cluster_prefix$consumed"
3649
+ set current "-$rest"
3650
+ break
3651
+ end
3652
+ if test "$info[1]" = required; or begin; test "$info[1]" = optional; and begin; test "$info[4]" = 1; or test (string length -- "$rest") -eq 1; end; end
3653
+ break
3654
+ end
3655
+ set consumed "$consumed$letter"
3656
+ set rest (string sub -s 2 -- "$rest")
3657
+ end
3658
+ end
3659
+ if test "$info[1]" != unknown
3660
+ break
3661
+ end
3662
+ end
3663
+ set -a alternatives (__PREFIX___commands $state)
3664
+ if test $ended -eq 0
3665
+ set -a alternatives (__PREFIX___flags $state)
3666
+ end
3667
+ set state $settings[4]
3668
+ set tokens
3669
+ end
3670
+ end
3671
+
3672
+ set -l value -1
3673
+ if test -n "$pending"; and begin; test "$pending" = required; or not string match -q -- '-*' "$current"; or test "$current" = -; or begin; test "$settings[2]" = 1; and string match -qr -- '^-([0-9]+|[0-9]*[.][0-9]+)(e[+-]?[0-9]+)?$' "$current"; end; end
3674
+ set value $pending_value
3675
+ else if test $ended -eq 0; and string match -q -- '--*=*' "$current"
3676
+ set -l parts (string split -m 1 = -- "$current")
3677
+ set info (__PREFIX___option $state "$parts[1]")
3678
+ if not contains -- "$info[1]" required optional
3679
+ return
3680
+ end
3681
+ set value $info[2]
3682
+ set lead "$parts[1]="
3683
+ set current "$parts[2]"
3684
+ else if test $ended -eq 0; and string match -qr -- '^-.+' "$current"; and not string match -q -- '--*' "$current"
3685
+ set -l rest (string sub -s 2 -- "$current")
3686
+ set -l attached -
3687
+ while test -n "$rest"
3688
+ set -l letter (string sub -l 1 -- "$rest")
3689
+ set attached "$attached$letter"
3690
+ set rest (string sub -s 2 -- "$rest")
3691
+ set info (__PREFIX___option $state "-$letter")
3692
+ if test "$info[1]" = unknown
3693
+ break
3694
+ end
3695
+ if begin; test "$info[1]" = required; or begin; test "$info[1]" = optional; and test "$info[4]" = 1; end; end; and test -n "$rest"
3696
+ set value $info[2]
3697
+ set lead "$attached"
3698
+ set current "$rest"
3699
+ break
3700
+ end
3701
+ end
3702
+ end
3703
+
3704
+ if test $value -lt 0
3705
+ set -l suggestions $alternatives
3706
+ if test $operands -eq 0
3707
+ set -a suggestions (__PREFIX___commands $state)
3708
+ end
3709
+ if test $ended -eq 0
3710
+ set -a suggestions (__PREFIX___flags $state)
3711
+ end
3712
+ for candidate in $suggestions
3713
+ if test -n "$cluster_prefix"; and string match -qr -- '^-[^-]' "$candidate"
3714
+ set candidate "-$cluster_prefix"(string sub -s 2 -- "$candidate")
3715
+ end
3716
+ printf '%s\n' "$candidate"
3717
+ end
3718
+ set info (__PREFIX___argument $state $position)
3719
+ set value $info[1]
3720
+ end
3721
+ if test -n "$cluster_prefix"; and test -n "$lead"
3722
+ set lead "-$cluster_prefix"(string sub -s 2 -- "$lead")
3723
+ end
3724
+ for candidate in (__PREFIX___values $value)
3725
+ printf '%s\n' "$lead$candidate"
3726
+ end
3727
+ set -l kind (__PREFIX___kind $value)
3728
+ if test "$kind" = file; or test "$kind" = directory
3729
+ set -l escaped (string escape -- "$current")
3730
+ # Preserve an unquoted home prefix, while escaping the rest of the path
3731
+ # before handing it to native completion. Quoted/escaped '~' stays literal.
3732
+ set -l raw_value (string sub -s (math (string length -- "$lead") + 1) -- "$raw_current")
3733
+ if string match -q -- '~/*' "$raw_value"
3734
+ set escaped '~/'(string escape -- (string sub -s 3 -- "$current"))
3735
+ end
3736
+ set -l paths
3737
+ if test "$kind" = directory
3738
+ set paths (__fish_complete_directories "$escaped")
3739
+ else
3740
+ set paths (__fish_complete_path "$escaped")
3741
+ end
3742
+ for candidate in $paths
3743
+ printf '%s\n' "$lead$candidate"
3744
+ end
3745
+ end
3746
+ end
3747
+ function __PREFIX___filter
3748
+ set -l state $argv[1]
3749
+ set -l tokens $argv[3..-1]
3750
+ set -l settings (__PREFIX___settings $state)
3751
+ set -l pending ''
3752
+ set -l pending_value -1
3753
+ set -l pending_variadic 0
3754
+ set -l filtered
3755
+ set -l index 1
3756
+ set -l seen 0
3757
+ while test $index -le (count $tokens)
3758
+ set -l token "$tokens[$index]"
3759
+ set -l original_index $index
3760
+ set index (math $index + 1)
3761
+ if test -n "$pending"
3762
+ if test "$pending" = required; or not string match -q -- '-*' "$token"; or test "$token" = -; or begin; test "$settings[2]" = 1; and string match -qr -- '^-([0-9]+|[0-9]*[.][0-9]+)(e[+-]?[0-9]+)?$' "$token"; end
3763
+ if test $pending_variadic -eq 1
3764
+ set pending optional
3765
+ else
3766
+ set pending ''
3767
+ end
3768
+ continue
3769
+ end
3770
+ set pending ''
3771
+ end
3772
+ if test "$token" = --
3773
+ set -a filtered $tokens[$original_index..-1]
3774
+ break
3775
+ end
3776
+ set -l info (__PREFIX___local_option $state "$token")
3777
+ set -l attached 0
3778
+ if test "$info[1]" = unknown; and string match -q -- '--*=*' "$token"
3779
+ set -l parts (string split -m 1 = -- "$token")
3780
+ set info (__PREFIX___local_option $state "$parts[1]")
3781
+ if test "$info[1]" = boolean
3782
+ set info unknown -1 0
3783
+ end
3784
+ set attached 1
3785
+ else if test "$info[1]" = unknown; and string match -qr -- '^-.+' "$token"; and not string match -q -- '--*' "$token"
3786
+ set -l rest (string sub -s 2 -- "$token")
3787
+ while test -n "$rest"
3788
+ set -l flag -(string sub -l 1 -- "$rest")
3789
+ set info (__PREFIX___local_option $state "$flag")
3790
+ if test "$info[1]" = unknown
3791
+ set token "-$rest"
3792
+ break
3793
+ end
3794
+ set rest (string sub -s 2 -- "$rest")
3795
+ if test "$info[1]" = required; or begin; test "$info[1]" = optional; and begin; test "$info[4]" = 1; or test -z "$rest"; end; end
3796
+ if test -n "$rest"
3797
+ set attached 1
3798
+ end
3799
+ break
3800
+ end
3801
+ end
3802
+ end
3803
+ if test "$info[1]" != unknown
3804
+ if test "$info[1]" != boolean; and test $attached -eq 0
3805
+ set pending $info[1]
3806
+ set pending_value $info[2]
3807
+ set pending_variadic $info[3]
3808
+ end
3809
+ continue
3810
+ end
3811
+ if begin; test "$settings[3]" = 1; or test "$settings[1]" = 1; end; and test $seen -eq 0
3812
+ set -l next (__PREFIX___child $state "$token")
3813
+ if test "$next" != -1; or test "$settings[4]" != -1
3814
+ set -a filtered "$token" $tokens[$index..-1]
3815
+ break
3816
+ end
3817
+ end
3818
+ set -a filtered "$token"
3819
+ set seen (math $seen + 1)
3820
+ if test "$settings[1]" = 1
3821
+ set -a filtered $tokens[$index..-1]
3822
+ break
3823
+ end
3824
+ end
3825
+ printf '%s\0' "$pending" "$pending_value" "$pending_variadic" $filtered
3826
+ end
3827
+ `;
3828
+ //#endregion
3829
+ //#region node_modules/commander-static-completion/dist/fish.js
3830
+ function quote(value) {
3831
+ if (/[\0\r\n\t]/u.test(value)) throw new TypeError("Fish completion strings cannot contain NUL, tabs, or newlines.");
3832
+ return "'" + value.replaceAll("\\", "\\\\").replaceAll("'", "\\'") + "'";
3833
+ }
3834
+ const print = (values) => values.length ? `printf '%s\\n' ${values.map(quote).join(" ")}` : "return 0";
3835
+ const literalCase = (key, values) => `if test "$argv[1]:$argv[2]" = ${quote(key)}\n ${print(values)}\n return 0\n end`;
3836
+ function renderFish(nodes, executable, prefix) {
3837
+ const specs = [];
3838
+ const spec = (hint, description) => {
3839
+ specs.push({
3840
+ hint,
3841
+ description
3842
+ });
3843
+ return specs.length - 1;
3844
+ };
3845
+ const described = (words, description) => words.map((word) => description ? `printf '%s\\t%s\\n' ${quote(word)} ${quote(description)}` : print([word])).join("\n ") || "return 0";
3846
+ const localCases = [];
3847
+ const optionCases = [], argumentCases = [], childCases = [];
3848
+ const commandCases = [], flagCases = [];
3849
+ for (const node of nodes) {
3850
+ for (const option of node.options) {
3851
+ const value = spec(option.value, option.description);
3852
+ for (const flag of option.flags) optionCases.push(literalCase(`${node.id}:${flag}`, [
3853
+ option.mode,
3854
+ String(value),
3855
+ option.variadic ? "1" : "0",
3856
+ option.combineOptional ? "1" : "0"
3857
+ ]));
3858
+ }
3859
+ for (const option of node.localOptions) {
3860
+ const value = spec(option.value, option.description);
3861
+ for (const flag of option.flags) localCases.push(literalCase(`${node.id}:${flag}`, [
3862
+ option.mode,
3863
+ String(value),
3864
+ option.variadic ? "1" : "0",
3865
+ option.combineOptional ? "1" : "0"
3866
+ ]));
3867
+ }
3868
+ node.arguments.forEach((arg, index) => {
3869
+ argumentCases.push(`case ${quote(`${node.id}:${index}`)}\n ${print([String(spec(arg.value, arg.description)), arg.variadic ? "1" : "0"])}`);
3870
+ });
3871
+ for (const child of node.children) for (const name of child.names) childCases.push(literalCase(`${node.id}:${name}`, [String(child.id)]));
3872
+ commandCases.push(`case ${node.id}\n ${node.children.filter((c) => c.visible).map((c) => described(c.names, c.description)).join("\n ") || "return 0"}`);
3873
+ flagCases.push(`case ${node.id}\n ${node.options.filter((o) => o.visible && o.flags.length > 0).map((o) => described(o.flags, o.description)).join("\n ") || "return 0"}`);
3874
+ }
3875
+ const helper = (name, key, cases, fallback = []) => `function ${prefix}_${name}\n switch "${key}"\n ${cases.join("\n ")}\n case '*'\n ${print(fallback)}\n end\nend`;
3876
+ const literalHelper = (name, cases, fallback) => `function ${prefix}_${name}\n ${cases.join("\n ")}\n ${print(fallback)}\nend`;
3877
+ return `# Generated by commander-static-completion. Requires Fish 4+.\n${[
3878
+ helper("settings", "$argv[1]", nodes.map((node) => `case ${node.id}\n ${print([
3879
+ node.passThrough ? "1" : "0",
3880
+ node.negativeNumbers ? "1" : "0",
3881
+ node.positional ? "1" : "0",
3882
+ String(node.defaultCommand)
3883
+ ])}`)),
3884
+ literalHelper("local_option", localCases, [
3885
+ "unknown",
3886
+ "-1",
3887
+ "0"
3888
+ ]),
3889
+ literalHelper("option", optionCases, [
3890
+ "unknown",
3891
+ "-1",
3892
+ "0"
3893
+ ]),
3894
+ helper("argument", "$argv[1]:$argv[2]", argumentCases, ["-1", "0"]),
3895
+ literalHelper("child", childCases, ["-1"]),
3896
+ helper("commands", "$argv[1]", commandCases),
3897
+ helper("flags", "$argv[1]", flagCases),
3898
+ helper("values", "$argv[1]", specs.map((s, i) => `case ${i}\n ${described(s.hint.kind === "choices" ? s.hint.values : [], s.description)}`)),
3899
+ helper("kind", "$argv[1]", specs.map((s, i) => `case ${i}\n ${print([s.hint.kind])}`), ["none"])
3900
+ ].join("\n\n")}\n${fishRuntime.replaceAll("__PREFIX__", prefix)}\n# Replace this command's registration; leave other commands untouched.\ncomplete -e -- ${quote(executable)}\ncomplete -f -k -a '(${prefix})' -- ${quote(executable)}\n`;
3901
+ }
3902
+ //#endregion
3903
+ //#region node_modules/commander-static-completion/dist/validation.js
3904
+ /** Insertable text must survive shell strings and completion record formats. */
3905
+ function validateText(value, label, shell) {
3906
+ if (typeof value !== "string" || value.length === 0) throw new TypeError(`${label} must be a nonempty string.`);
3907
+ if (value.includes("\0")) throw new TypeError(`${label} cannot contain NUL.`);
3908
+ if (shell === "fish" && /[\t\r\n]/u.test(value)) throw new TypeError(`${label} cannot contain tabs or newlines: Fish uses them as completion record delimiters.`);
3909
+ if (/[\x01-\x1f\x7f-\x9f]/u.test(value)) throw new TypeError(`${label} cannot contain control characters.`);
3910
+ }
3911
+ function validateModel(nodes, shell) {
3912
+ for (const node of nodes) {
3913
+ for (const child of node.children) for (const name of child.names) validateText(name, `Command name or alias ${JSON.stringify(name)}`, shell);
3914
+ for (const option of node.localOptions) {
3915
+ for (const flag of option.flags) validateText(flag, `Option flag ${JSON.stringify(flag)}`, shell);
3916
+ if (option.value.kind === "choices") for (const [index, value] of option.value.values.entries()) validateText(value, `Choice ${index + 1} for option ${JSON.stringify(option.flags.join(", "))}`, shell);
3917
+ }
3918
+ for (const [index, argument] of node.arguments.entries()) if (argument.value.kind === "choices") for (const [choice, value] of argument.value.values.entries()) validateText(value, `Choice ${choice + 1} for argument ${index + 1} in command ${node.id}`, shell);
3919
+ }
3920
+ }
3921
+ //#endregion
3922
+ //#region node_modules/commander-static-completion/dist/index.js
3923
+ /** Attach static completion metadata without changing Commander parsing. */
3924
+ function completionHint(target, hint) {
3925
+ if (!hint || ![
3926
+ "file",
3927
+ "directory",
3928
+ "none",
3929
+ "choices"
3930
+ ].includes(hint.kind)) throw new TypeError("Expected a file, directory, none, or choices completion hint.");
3931
+ if (hint.kind === "choices" && (!Array.isArray(hint.values) || [...hint.values].some((value) => typeof value !== "string"))) throw new TypeError("Completion choices must be an array of strings.");
3932
+ hints.set(target, hint.kind === "choices" ? {
3933
+ kind: hint.kind,
3934
+ values: [...hint.values]
3935
+ } : { kind: hint.kind });
3936
+ return target;
3937
+ }
3938
+ /** Generate a standalone shell script from a fully configured command tree. */
3939
+ function generateCompletion(program, { shell, executable = program.name() }) {
3940
+ if (![
3941
+ "bash",
3942
+ "zsh",
3943
+ "fish"
3944
+ ].includes(shell)) throw new Error(`Unsupported shell: ${shell}. Expected bash, zsh, or fish.`);
3945
+ validateText(executable, "executable name", shell);
3946
+ if (/\s/u.test(executable)) throw new TypeError("Provide an executable name without whitespace.");
3947
+ const name = "_csc_" + createHash("sha256").update(`${shell}:${executable}`).digest("hex").slice(0, 16);
3948
+ const nodes = extract(program);
3949
+ validateModel(nodes, shell);
3950
+ return shell === "fish" ? renderFish(nodes, executable, name) : renderBourne(nodes, executable, name, shell);
3951
+ }
3952
+ //#endregion
3000
3953
  //#region package.json
3001
- var version = "0.1.0";
3954
+ var version = "0.1.1";
3002
3955
  //#endregion
3003
3956
  //#region src/server/comments/anchor.ts
3004
3957
  function splitLines(contents) {
@@ -3143,11 +4096,9 @@ const CONFIG_ARGS = [
3143
4096
  "core.quotePath=false"
3144
4097
  ];
3145
4098
  /** Fixed header prefixes and no `diff.external`, so a patch is always a unified diff. */
3146
- const PATCH_ARGS = [
3147
- "--no-ext-diff",
3148
- "--src-prefix=a/",
3149
- "--dst-prefix=b/"
3150
- ];
4099
+ function patchArgs(oldRev) {
4100
+ return ["--no-ext-diff", ...oldRev === "worktree" ? ["--src-prefix=b/", "--dst-prefix=a/"] : ["--src-prefix=a/", "--dst-prefix=b/"]];
4101
+ }
3151
4102
  /** An idle `cat-file --batch` is reaped after this long; the next request respawns one. */
3152
4103
  const CAT_FILE_IDLE_MS = 2e3;
3153
4104
  var GitError = class extends Error {
@@ -3208,7 +4159,7 @@ var GitRepo = class GitRepo {
3208
4159
  ])).toString("utf8").trim().split("\n");
3209
4160
  if (!root || !gitDir) throw new GitError(`not a git repository: ${dir}`, [], 128, "");
3210
4161
  const commonDir = common ? resolve(root, common) : gitDir;
3211
- return new GitRepo(root, gitDir, commonDir);
4162
+ return new GitRepo(resolve(root), resolve(gitDir), commonDir);
3212
4163
  }
3213
4164
  exec(args, opts = {}) {
3214
4165
  return execGit(this.root, args, opts);
@@ -3302,6 +4253,53 @@ var GitRepo = class GitRepo {
3302
4253
  } catch {}
3303
4254
  throw new GitError("cannot determine default branch; pass a base explicitly", [], null, "");
3304
4255
  }
4256
+ /** Full ref for an exact branch; expressions, worktree, and detached HEAD have none. */
4257
+ async branchRef(rev) {
4258
+ if (rev === "worktree") return null;
4259
+ let ref;
4260
+ try {
4261
+ ref = (await this.text(rev === "HEAD" ? [
4262
+ "symbolic-ref",
4263
+ "--quiet",
4264
+ "HEAD"
4265
+ ] : [
4266
+ "rev-parse",
4267
+ "--symbolic-full-name",
4268
+ "--verify",
4269
+ "--end-of-options",
4270
+ rev
4271
+ ])).trim();
4272
+ } catch (e) {
4273
+ if (e instanceof GitError) return null;
4274
+ throw e;
4275
+ }
4276
+ return ref.startsWith("refs/heads/") || ref.startsWith("refs/remotes/") ? ref : null;
4277
+ }
4278
+ /** Remote branch identity for an exact local/remote branch; expressions and detached HEAD have none. */
4279
+ async upstreamBranch(rev) {
4280
+ const ref = await this.branchRef(rev);
4281
+ if (!ref) return null;
4282
+ if (ref.startsWith("refs/heads/")) {
4283
+ const [remote, upstream] = (await this.text([
4284
+ "for-each-ref",
4285
+ "--format=%(upstream:remotename)%00%(upstream:remoteref)%00",
4286
+ ref
4287
+ ])).split("\0");
4288
+ if (!remote || remote === "." || !upstream?.startsWith("refs/heads/")) return null;
4289
+ return {
4290
+ remote,
4291
+ branch: upstream.slice(11)
4292
+ };
4293
+ }
4294
+ if (ref.startsWith("refs/remotes/")) {
4295
+ const remote = (await this.remotes()).sort((a, b) => b.name.length - a.name.length).find((r) => ref.startsWith(`refs/remotes/${r.name}/`));
4296
+ if (remote) return {
4297
+ remote: remote.name,
4298
+ branch: ref.slice(`refs/remotes/${remote.name}/`.length)
4299
+ };
4300
+ }
4301
+ return null;
4302
+ }
3305
4303
  /** Configured remotes, in git's order, with their fetch URLs. */
3306
4304
  async remotes() {
3307
4305
  const out = await this.text([
@@ -3368,7 +4366,9 @@ var GitRepo = class GitRepo {
3368
4366
  * blob; the worktree's blobs are hashed from disk.
3369
4367
  */
3370
4368
  async numstat(oldRev, newRev) {
3371
- const range = newRev === "worktree" ? [oldRev] : [oldRev, newRev];
4369
+ if (oldRev === "worktree" && newRev === "worktree") return [];
4370
+ const reverse = oldRev === "worktree";
4371
+ const range = diffRange(oldRev, newRev);
3372
4372
  const [diff, untracked] = await Promise.all([this.exec([
3373
4373
  "diff",
3374
4374
  "-z",
@@ -3377,13 +4377,13 @@ var GitRepo = class GitRepo {
3377
4377
  "--no-abbrev",
3378
4378
  "--numstat",
3379
4379
  ...range
3380
- ]), newRev === "worktree" ? this.untracked() : Promise.resolve([])]);
4380
+ ]), [oldRev, newRev].includes("worktree") ? this.untracked() : Promise.resolve([])]);
3381
4381
  const files = parseRawNumstat(diff);
3382
- const known = new Set(files.map((f) => f.path));
4382
+ const known = new Set(files.flatMap((f) => [f.path, ...f.oldPath ? [f.oldPath] : []]));
3383
4383
  const extra = await mapLimit(untracked.filter((p) => !known.has(p) && !p.endsWith("/")), READ_CONCURRENCY$1, async (path) => {
3384
4384
  const file = {
3385
4385
  path,
3386
- status: "A",
4386
+ status: reverse ? "D" : "A",
3387
4387
  additions: 0,
3388
4388
  deletions: 0,
3389
4389
  binary: false,
@@ -3397,7 +4397,10 @@ var GitRepo = class GitRepo {
3397
4397
  };
3398
4398
  const buf = await this.readWorktree(path).catch(() => null) ?? Buffer.alloc(0);
3399
4399
  file.binary = isBinary(buf);
3400
- if (!file.binary) file.additions = countLines(buf);
4400
+ if (!file.binary) {
4401
+ if (reverse) file.deletions = countLines(buf);
4402
+ else file.additions = countLines(buf);
4403
+ }
3401
4404
  return file;
3402
4405
  });
3403
4406
  const all = [...files, ...extra].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
@@ -3566,21 +4569,28 @@ var GitRepo = class GitRepo {
3566
4569
  * `untracked` when the caller already knows, else an added file is probed.
3567
4570
  */
3568
4571
  async patch(oldRev, newRev, file, context = 3, untracked) {
3569
- if (newRev === "worktree" && file.status === "A" && (untracked ?? !await this.isTracked(file.path))) return (await this.text([
3570
- "diff",
3571
- "--no-index",
3572
- ...PATCH_ARGS,
3573
- `-U${context}`,
3574
- "--",
3575
- "/dev/null",
3576
- file.path
3577
- ], { okCodes: [0, 1] })).replace(/^diff --git "?a\/dev\/null"? ("?)b\/(.*)$/m, (_m, q, rest) => `diff --git ${q}a/${rest} ${q}b/${rest}`);
3578
- const range = newRev === "worktree" ? [oldRev] : [oldRev, newRev];
4572
+ if (oldRev === "worktree" && newRev === "worktree") return "";
4573
+ const reverse = oldRev === "worktree";
4574
+ if ((reverse ? file.status === "D" : newRev === "worktree" && file.status === "A") && (untracked ?? !await this.isTracked(file.path))) {
4575
+ const out = await this.text([
4576
+ "diff",
4577
+ "--no-index",
4578
+ ...reverse ? ["-R"] : [],
4579
+ ...patchArgs(oldRev),
4580
+ `-U${context}`,
4581
+ "--",
4582
+ "/dev/null",
4583
+ file.path
4584
+ ], { okCodes: [0, 1] });
4585
+ if (reverse) return out.replace(/^diff --git ("?)a\/(.*) "?b\/dev\/null"?$/m, (_m, q, rest) => `diff --git ${q}a/${rest} ${q}b/${rest}`);
4586
+ return out.replace(/^diff --git "?a\/dev\/null"? ("?)b\/(.*)$/m, (_m, q, rest) => `diff --git ${q}a/${rest} ${q}b/${rest}`);
4587
+ }
4588
+ const range = diffRange(oldRev, newRev);
3579
4589
  const paths = file.oldPath ? [file.oldPath, file.path] : [file.path];
3580
4590
  return this.text([
3581
4591
  "diff",
3582
4592
  "-M",
3583
- ...PATCH_ARGS,
4593
+ ...patchArgs(oldRev),
3584
4594
  `-U${context}`,
3585
4595
  ...range,
3586
4596
  "--",
@@ -3589,17 +4599,18 @@ var GitRepo = class GitRepo {
3589
4599
  }
3590
4600
  /** Patches for all changed files. Tracked files in one call; untracked appended. */
3591
4601
  async patchAll(oldRev, newRev, files, context = 3) {
3592
- const range = newRev === "worktree" ? [oldRev] : [oldRev, newRev];
4602
+ if (oldRev === "worktree" && newRev === "worktree") return "";
4603
+ const range = diffRange(oldRev, newRev);
3593
4604
  const tracked = this.text([
3594
4605
  "diff",
3595
4606
  "-M",
3596
- ...PATCH_ARGS,
4607
+ ...patchArgs(oldRev),
3597
4608
  `-U${context}`,
3598
4609
  ...range
3599
4610
  ]);
3600
4611
  let untracked = [];
3601
- if (newRev === "worktree") {
3602
- const added = files.filter((f) => f.status === "A" && !f.binary);
4612
+ if ([oldRev, newRev].includes("worktree")) {
4613
+ const added = files.filter((f) => f.status === (oldRev === "worktree" ? "D" : "A") && !f.binary);
3603
4614
  if (added.length) {
3604
4615
  const known = new Set(await this.lsFiles());
3605
4616
  untracked = added.filter((f) => !known.has(f.path));
@@ -3613,14 +4624,15 @@ var GitRepo = class GitRepo {
3613
4624
  * (`patch` probes whether they are untracked). The caller bounds the batch, so the paths ride argv.
3614
4625
  */
3615
4626
  async patchMany(oldRev, newRev, files, context = 3) {
3616
- const single = newRev === "worktree" ? files.filter((f) => f.status === "A") : [];
4627
+ if (oldRev === "worktree" && newRev === "worktree") return "";
4628
+ const single = [oldRev, newRev].includes("worktree") ? files.filter((f) => f.status === (oldRev === "worktree" ? "D" : "A")) : [];
3617
4629
  const batched = files.filter((f) => !single.includes(f));
3618
- const range = newRev === "worktree" ? [oldRev] : [oldRev, newRev];
4630
+ const range = diffRange(oldRev, newRev);
3619
4631
  const paths = batched.flatMap((f) => f.oldPath ? [f.oldPath, f.path] : [f.path]);
3620
4632
  const tracked = batched.length ? this.text([
3621
4633
  "diff",
3622
4634
  "-M",
3623
- ...PATCH_ARGS,
4635
+ ...patchArgs(oldRev),
3624
4636
  `-U${context}`,
3625
4637
  ...range,
3626
4638
  "--",
@@ -4050,6 +5062,10 @@ function countLines(buf) {
4050
5062
  if (buf[buf.length - 1] !== 10) n++;
4051
5063
  return n;
4052
5064
  }
5065
+ /** Git reverses the commit-to-worktree diff when the worktree is the old endpoint. */
5066
+ function diffRange(oldRev, newRev) {
5067
+ return oldRev === "worktree" ? ["-R", newRev] : newRev === "worktree" ? [oldRev] : [oldRev, newRev];
5068
+ }
4053
5069
  //#endregion
4054
5070
  //#region src/server/github.ts
4055
5071
  /** A precondition the user can fix (no gh, no PR, wrong mode, unpushed head): the route answers 4xx. */
@@ -4134,42 +5150,33 @@ function repoOfPrUrl(url) {
4134
5150
  repo: m[2]
4135
5151
  };
4136
5152
  }
4137
- /** Owns one server's export queue so overlapping requests cannot add the same thread twice. */
5153
+ /** Serializes validation and writes so overlapping exports cannot add the same thread twice. */
4138
5154
  var GithubExporter = class {
4139
5155
  queue = Promise.resolve();
4140
- export(input) {
4141
- const result = this.queue.then(() => exportToGithub(input));
5156
+ export(prepare) {
5157
+ const result = this.queue.then(async () => exportToGithub(await prepare()));
4142
5158
  this.queue = result.catch(() => {});
4143
5159
  return result;
4144
5160
  }
4145
5161
  };
4146
5162
  /**
4147
- * Adds the threads to a *pending* review on the PR of the checked-out branch, creating
4148
- * the pending review when there is none. The review is never submitted: the human opens
4149
- * the PR and submits it themselves. Refuses when the new side is not the checked-out
4150
- * commit: GitHub anchors comments to a commit, and only HEAD is what the PR shows.
5163
+ * Adds the threads to a *pending* review on the active PR, creating the pending review
5164
+ * when there is none. The review is never submitted: the human opens the PR and submits
5165
+ * it themselves. The caller validates the comparison inside the export queue before posting.
4151
5166
  *
4152
5167
  * Re-exporting matches a hidden thread ID and its anchor in the pending review.
4153
5168
  * A matching comment is left alone when its body matches and rewritten when it does not, so
4154
5169
  * editing a thread and exporting again does not stack a second comment on the line. Comments
4155
5170
  * of an already-submitted review are out of reach — those would have to be replied to.
4156
5171
  */
4157
- async function exportToGithub({ snap, threads, threadIds, run = runGh }) {
4158
- if (snap.newSha === "worktree") throw new GithubError("GitHub cannot anchor comments to uncommitted lines; commit and review the commit (pr or a revspec ending at HEAD)");
4159
- if (snap.headSha === "" || snap.newSha !== snap.headSha) throw new GithubError("the new side must be the checked-out commit (HEAD) to post to its pull request");
5172
+ async function exportToGithub({ snap, pullRequest: pr, threads, threadIds, run = runGh }) {
5173
+ if (snap.newSha === "worktree") throw new GithubError("GitHub cannot anchor comments to the worktree");
4160
5174
  const { review, ids, skipped } = buildReview(threads, snap.newSha, threadIds);
4161
5175
  if (review.comments.length === 0) throw new GithubError(skipped.length ? `nothing to post: ${describe(skipped)}` : "nothing to post", 400);
4162
- const pr = parsePr(await run([
4163
- "pr",
4164
- "view",
4165
- "--json",
4166
- "number,url,headRefOid"
4167
- ], { cwd: snap.root }));
4168
- if (pr.headRefOid !== snap.newSha) throw new GithubError(`the pull request head is ${pr.headRefOid.slice(0, 7)} but HEAD is ${snap.newSha.slice(0, 7)}; push first`);
4169
5176
  for (const [i, comment] of review.comments.entries()) comment.body += `\n\n${threadMarker(ids[i])}`;
4170
5177
  const pending = await findPendingReview(run, snap.root, pr);
4171
5178
  if (!pending) {
4172
- await createPendingReview(run, snap.root, pr.number, review);
5179
+ await createPendingReview(run, snap.root, pr.repository, pr.number, review);
4173
5180
  return {
4174
5181
  url: pr.url,
4175
5182
  posted: review.comments.length,
@@ -4308,12 +5315,12 @@ async function pendingComments(run, cwd, pr, reviewId) {
4308
5315
  }
4309
5316
  const UPDATE_COMMENT = `mutation($input:UpdatePullRequestReviewCommentInput!){updatePullRequestReviewComment(input:$input){pullRequestReviewComment{id}}}`;
4310
5317
  /** No `event` in the body, so GitHub keeps the new review pending with all of its comments. */
4311
- async function createPendingReview(run, cwd, number, review) {
5318
+ async function createPendingReview(run, cwd, repository, number, review) {
4312
5319
  await run([
4313
5320
  "api",
4314
5321
  "--method",
4315
5322
  "POST",
4316
- `repos/{owner}/{repo}/pulls/${number}/reviews`,
5323
+ `repos/${repository}/pulls/${number}/reviews`,
4317
5324
  "--input",
4318
5325
  "-"
4319
5326
  ], {
@@ -4365,21 +5372,12 @@ function describe(skipped) {
4365
5372
  for (const s of skipped) counts.set(s.reason, (counts.get(s.reason) ?? 0) + 1);
4366
5373
  return [...counts].map(([reason, n]) => `${n} ${reason}`).join(", ");
4367
5374
  }
4368
- function parsePr(out) {
4369
- let pr;
4370
- try {
4371
- pr = JSON.parse(out);
4372
- } catch {
4373
- throw new GithubError(`unexpected output from gh pr view: ${out.slice(0, 200)}`, 502);
4374
- }
4375
- if (typeof pr.number !== "number" || typeof pr.url !== "string" || typeof pr.headRefOid !== "string") throw new GithubError(`unexpected output from gh pr view: ${out.slice(0, 200)}`, 502);
4376
- return pr;
4377
- }
4378
5375
  /** Spawns the local `gh`. A missing binary or a failing command becomes a GithubError carrying gh's own message. */
4379
- const runGh = (args, { cwd, input }) => new Promise((resolve, reject) => {
5376
+ const runGh = (args, { cwd, input, timeoutMs }) => new Promise((resolve, reject) => {
4380
5377
  const child = execFile("gh", args, {
4381
5378
  cwd,
4382
- maxBuffer: 16777216
5379
+ maxBuffer: 16777216,
5380
+ timeout: timeoutMs
4383
5381
  }, (err, stdout, stderr) => {
4384
5382
  if (!err) return resolve(stdout);
4385
5383
  if (err.code === "ENOENT") return reject(new GithubError("gh is not installed; see https://cli.github.com"));
@@ -4389,20 +5387,55 @@ const runGh = (args, { cwd, input }) => new Promise((resolve, reject) => {
4389
5387
  if (input != null) child.stdin?.end(input);
4390
5388
  else child.stdin?.end();
4391
5389
  });
4392
- const PR_FIELDS = "number,url,baseRefName,headRefName,headRefOid";
5390
+ const PR_FIELDS = "number,url,title,state,isDraft,baseRefOid,baseRefName,headRefName,headRefOid,headRepository,headRepositoryOwner";
4393
5391
  /**
4394
5392
  * `gh pr view` for a PR named by number, `#number`, URL or branch. Without a
4395
5393
  * selector, the PR of the checked-out branch. Option-shaped selectors are
4396
5394
  * refused so a stray flag never reaches gh.
4397
5395
  */
4398
- async function viewPr(selector, cwd, run = runGh) {
4399
- return parsePrView(await run([
5396
+ async function viewPr(selector, cwd, run = runGh, timeoutMs) {
5397
+ return parsePullRequest(parseJson(await run([
4400
5398
  "pr",
4401
5399
  "view",
4402
5400
  ...selector == null ? [] : [normalizeSelector(selector)],
4403
5401
  "--json",
4404
5402
  PR_FIELDS
4405
- ], { cwd }));
5403
+ ], {
5404
+ cwd,
5405
+ timeoutMs
5406
+ }), "view"));
5407
+ }
5408
+ /** Open pull requests whose head is the named branch in the named remote repository. */
5409
+ async function listPrsForHead(baseRepo, base, head, headRepo, cwd, run = runGh, timeoutMs) {
5410
+ const out = await run([
5411
+ "pr",
5412
+ "list",
5413
+ "--repo",
5414
+ baseRepo,
5415
+ "--base",
5416
+ base,
5417
+ "--head",
5418
+ head,
5419
+ "--state",
5420
+ "open",
5421
+ "--limit",
5422
+ "100",
5423
+ "--json",
5424
+ PR_FIELDS
5425
+ ], {
5426
+ cwd,
5427
+ timeoutMs
5428
+ });
5429
+ const values = parseJson(out, "list");
5430
+ if (!Array.isArray(values)) throw new GithubError(`unexpected output from gh pr list: ${out.slice(0, 200)}`, 502);
5431
+ return values.flatMap((value) => {
5432
+ const candidate = value;
5433
+ const owner = candidate?.headRepositoryOwner?.login;
5434
+ const name = candidate?.headRepository?.name;
5435
+ if (typeof owner !== "string" || typeof name !== "string") throw new GithubError(`unexpected output from gh pr list: ${out.slice(0, 200)}`, 502);
5436
+ if (`${owner}/${name}`.toLowerCase() !== headRepo.toLowerCase()) return [];
5437
+ return [parsePullRequest(value)];
5438
+ });
4406
5439
  }
4407
5440
  function normalizeSelector(selector) {
4408
5441
  const s = selector.trim();
@@ -4410,23 +5443,46 @@ function normalizeSelector(selector) {
4410
5443
  const num = /^#?(\d+)$/.exec(s);
4411
5444
  return num ? num[1] : s;
4412
5445
  }
4413
- function parsePrView(out) {
4414
- let pr;
5446
+ function parseJson(out, command) {
4415
5447
  try {
4416
- pr = JSON.parse(out);
5448
+ return JSON.parse(out);
4417
5449
  } catch {
4418
- throw new GithubError(`unexpected output from gh pr view: ${out.slice(0, 200)}`, 502);
5450
+ throw new GithubError(`unexpected output from gh pr ${command}: ${out.slice(0, 200)}`, 502);
4419
5451
  }
4420
- if (!(typeof pr.number === "number" && typeof pr.url === "string" && typeof pr.baseRefName === "string" && typeof pr.headRefName === "string" && typeof pr.headRefOid === "string")) throw new GithubError(`unexpected output from gh pr view: ${out.slice(0, 200)}`, 502);
5452
+ }
5453
+ function parsePullRequest(value) {
5454
+ const pr = value;
5455
+ if (!(pr != null && typeof pr.title === "string" && [
5456
+ "OPEN",
5457
+ "CLOSED",
5458
+ "MERGED"
5459
+ ].includes(pr.state ?? "") && typeof pr.isDraft === "boolean" && typeof pr.baseRefOid === "string" && typeof pr.number === "number" && typeof pr.url === "string" && typeof pr.baseRefName === "string" && typeof pr.headRefName === "string" && typeof pr.headRefOid === "string")) throw new GithubError("unexpected pull request data from gh", 502);
4421
5460
  const slug = /^https?:\/\/[^/]+\/([^/]+\/[^/]+)\/pull\/\d+/.exec(pr.url);
4422
5461
  if (!slug) throw new GithubError(`cannot read the repository from the pull request url: ${pr.url}`, 502);
5462
+ const source = value;
5463
+ const owner = source.headRepositoryOwner?.login;
5464
+ const name = source.headRepository?.name;
4423
5465
  return {
4424
- ...pr,
4425
- baseRepo: slug[1]
5466
+ headRepository: typeof owner === "string" && typeof name === "string" ? `${owner}/${name}` : null,
5467
+ number: pr.number,
5468
+ url: pr.url,
5469
+ title: pr.title,
5470
+ state: pr.state,
5471
+ isDraft: pr.isDraft,
5472
+ repository: slug[1],
5473
+ baseRefOid: pr.baseRefOid,
5474
+ baseRefName: pr.baseRefName,
5475
+ headRefName: pr.headRefName,
5476
+ headRefOid: pr.headRefOid
4426
5477
  };
4427
5478
  }
4428
5479
  //#endregion
4429
5480
  //#region src/shared/protocol.ts
5481
+ /** Display comparison endpoints with branch names instead of internal ref namespaces. */
5482
+ function comparisonLabel(mode) {
5483
+ const name = (rev) => rev.replace(/^refs\/diffle\/[^/]+\/\d+\/(?:base|head)\//, "").replace(/^refs\/(?:heads|remotes)\//, "");
5484
+ return `${name(mode.old)}${mode.mergeBase ? "..." : ".."}${name(mode.new)}`;
5485
+ }
4430
5486
  const DEFAULT_USER_CONFIG = {
4431
5487
  autoViewed: [],
4432
5488
  contextLines: 5,
@@ -4756,13 +5812,6 @@ const DEFAULT_TOKEN_TYPES = [
4756
5812
  "operator",
4757
5813
  "decorator"
4758
5814
  ];
4759
- /**
4760
- * pyrefly reports nothing about its workspace index over the protocol (no
4761
- * `$/progress`, even when asked) but logs it on stderr at the default level.
4762
- * Other servers never match and simply never report indexing.
4763
- */
4764
- const INDEX_START = /Populating up to \d+ files in the workspace/;
4765
- const INDEX_DONE = /Populated all files in the workspace/;
4766
5815
  const INIT_TIMEOUT_MS = 6e4;
4767
5816
  /**
4768
5817
  * Navigation opens whatever file a query lands in. Without a cap every path
@@ -4794,8 +5843,7 @@ var LspBridge = class LspBridge {
4794
5843
  current;
4795
5844
  ready;
4796
5845
  stderr = "";
4797
- /** Partial last stderr line, so a log phrase split across chunks still matches. */
4798
- stderrTail = "";
5846
+ progress = /* @__PURE__ */ new Map();
4799
5847
  /** Open documents, least recently synced first. */
4800
5848
  open = /* @__PURE__ */ new Map();
4801
5849
  /** Syncs in flight by path, so a track pass and a query racing on one file send a single didOpen. */
@@ -5088,7 +6136,10 @@ var LspBridge = class LspBridge {
5088
6136
  child.stderr?.on("data", (d) => {
5089
6137
  const chunk = d.toString("utf8");
5090
6138
  this.stderr = (this.stderr + chunk).slice(-2e3);
5091
- this.noteIndexing(chunk);
6139
+ if (!this.closing && this.current.state !== "unavailable") this.set({
6140
+ ...this.current,
6141
+ stderr: this.stderr.trim()
6142
+ });
5092
6143
  });
5093
6144
  child.on("error", (e) => this.fail(`cannot start "${this.opts.command}": ${e.message}`));
5094
6145
  child.on("exit", (code, signal) => {
@@ -5101,6 +6152,7 @@ var LspBridge = class LspBridge {
5101
6152
  }
5102
6153
  const rpc = new JsonRpcConnection(child.stdout, child.stdin);
5103
6154
  this.rpc = rpc;
6155
+ rpc.onNotification((method, params) => this.noteWorkspace(method, params));
5104
6156
  const rootUri = pathToFileURL(this.opts.root).href;
5105
6157
  try {
5106
6158
  const legend = (await rpc.request("initialize", {
@@ -5113,6 +6165,7 @@ var LspBridge = class LspBridge {
5113
6165
  name: "workspace"
5114
6166
  }],
5115
6167
  capabilities: {
6168
+ window: { workDoneProgress: true },
5116
6169
  textDocument: {
5117
6170
  synchronization: { didSave: false },
5118
6171
  definition: { linkSupport: true },
@@ -5144,15 +6197,39 @@ var LspBridge = class LspBridge {
5144
6197
  state: "ready"
5145
6198
  });
5146
6199
  }
5147
- noteIndexing(chunk) {
5148
- const text = this.stderrTail + chunk;
5149
- const nl = text.lastIndexOf("\n");
5150
- this.stderrTail = nl === -1 ? text.slice(-200) : text.slice(nl + 1);
5151
- const indexing = INDEX_DONE.test(text) ? false : INDEX_START.test(text) ? true : null;
5152
- if (indexing == null || this.current.state !== "ready" || this.current.indexing === indexing) return;
6200
+ noteWorkspace(method, params) {
6201
+ if (!params || typeof params !== "object" || this.closing || this.current.state === "unavailable") return;
6202
+ const p = params;
6203
+ if (method === "window/showMessage" || method === "window/logMessage" || method === "window/showMessageRequest") {
6204
+ if (p.type !== 1 && p.type !== 2 || typeof p.message !== "string" || !p.message.trim()) return;
6205
+ const notice = {
6206
+ severity: p.type === 1 ? "error" : "warning",
6207
+ message: p.message
6208
+ };
6209
+ if (notice.severity !== this.current.notice?.severity || notice.message !== this.current.notice?.message) this.set({
6210
+ ...this.current,
6211
+ notice
6212
+ });
6213
+ return;
6214
+ }
6215
+ if (method !== "$/progress" || typeof p.token !== "string" && typeof p.token !== "number") return;
6216
+ if (!p.value || typeof p.value !== "object") return;
6217
+ const { kind, title, message, percentage } = p.value;
6218
+ const percent = typeof percentage === "number" && percentage >= 0 && percentage <= 100 ? percentage : void 0;
6219
+ if (kind === "begin" && typeof title === "string") this.progress.set(p.token, {
6220
+ title,
6221
+ message: typeof message === "string" ? message : void 0,
6222
+ percentage: percent
6223
+ });
6224
+ else if (kind === "report" && this.progress.has(p.token)) {
6225
+ if (typeof message === "string") this.progress.get(p.token).message = message;
6226
+ if (percent !== void 0) this.progress.get(p.token).percentage = percent;
6227
+ } else if (kind === "end") {
6228
+ if (!this.progress.delete(p.token)) return;
6229
+ } else return;
5153
6230
  this.set({
5154
6231
  ...this.current,
5155
- indexing
6232
+ activity: [...this.progress.values()].map((p) => `${p.title}${p.percentage == null ? "" : ` (${p.percentage}%)`}${p.message ? `: ${p.message}` : ""}`)
5156
6233
  });
5157
6234
  }
5158
6235
  fail(message) {
@@ -5166,6 +6243,7 @@ var LspBridge = class LspBridge {
5166
6243
  this.rpc?.dispose(new LspUnavailableError(message));
5167
6244
  }
5168
6245
  set(status) {
6246
+ if (JSON.stringify(status) === JSON.stringify(this.current)) return;
5169
6247
  this.current = status;
5170
6248
  this.opts.onStatus(status);
5171
6249
  }
@@ -5190,7 +6268,17 @@ var LspBridge = class LspBridge {
5190
6268
  for (let attempt = 0;; attempt++) {
5191
6269
  const rpc = path == null ? await this.ensureReady() : await this.sync(path);
5192
6270
  try {
5193
- return await rpc.request(method, params);
6271
+ const pending = busyReason(this.current);
6272
+ const result = await rpc.request(method, params);
6273
+ if (result == null || Array.isArray(result) && !result.length) {
6274
+ const busy = busyReason(this.current) ?? pending;
6275
+ if (busy) throw new LspUnavailableError(busy);
6276
+ if (this.current.notice) {
6277
+ const { severity, message } = this.current.notice;
6278
+ throw new LspUnavailableError(`${this.current.name} returned no result; last server ${severity}: ${message}`);
6279
+ }
6280
+ }
6281
+ return result;
5194
6282
  } catch (e) {
5195
6283
  if (!(e instanceof JsonRpcError) || e.code !== CONTENT_MODIFIED) throw e;
5196
6284
  const backoff = CONTENT_MODIFIED_BACKOFF_MS[attempt];
@@ -5374,9 +6462,15 @@ function programName(command) {
5374
6462
  const prog = argv0(command);
5375
6463
  return prog.split(/[\\/]/).pop() || prog;
5376
6464
  }
6465
+ /**
6466
+ * Canonical `p`, or `p` itself when it cannot be resolved. Uses libuv's
6467
+ * realpath, the same one the async `realpath` in `pathOf` uses: the JS
6468
+ * `realpathSync` keeps a Windows 8.3 short name (`RUNNER~1`) that libuv
6469
+ * expands, and the two answers would never compare equal.
6470
+ */
5377
6471
  function safeRealpathSync(p) {
5378
6472
  try {
5379
- return realpathSync(p);
6473
+ return realpathSync.native(p);
5380
6474
  } catch {
5381
6475
  return p;
5382
6476
  }
@@ -5417,6 +6511,11 @@ function lastLine(s) {
5417
6511
  const lines = s.trim().split("\n");
5418
6512
  return lines[lines.length - 1] ?? "";
5419
6513
  }
6514
+ /** Why an empty result may be incomplete while the server reports background work. */
6515
+ function busyReason(s) {
6516
+ const work = s.activity?.length ? s.activity.join("; ") : null;
6517
+ return work ? `${s.name} is busy: ${work}; try again when it finishes` : null;
6518
+ }
5420
6519
  //#endregion
5421
6520
  //#region src/server/lsp/registry.ts
5422
6521
  /**
@@ -5722,7 +6821,7 @@ var LspPool = class {
5722
6821
  };
5723
6822
  //#endregion
5724
6823
  //#region src/server/revspec.ts
5725
- /** The uncommitted new side, spelled as a revision so every form can name it. */
6824
+ /** The uncommitted tree, accepted on either side of a comparison. */
5726
6825
  const WORKTREE = "worktree";
5727
6826
  /**
5728
6827
  * git-diff style revision arguments:
@@ -5739,58 +6838,33 @@ function parseRevspec(args) {
5739
6838
  const [a, b] = args;
5740
6839
  if (a.includes("..") || b.includes("..")) throw new RevspecError("cannot combine \"..\" with two revisions");
5741
6840
  return {
5742
- old: {
5743
- kind: "rev",
5744
- rev: a
5745
- },
5746
- newRev: b,
5747
- label: `${a}..${b}`
6841
+ old: a,
6842
+ new: b,
6843
+ mergeBase: false
5748
6844
  };
5749
6845
  }
5750
6846
  const arg = args[0];
5751
6847
  const three = arg.indexOf("...");
5752
- if (three !== -1) {
5753
- const a = arg.slice(0, three) || "HEAD";
5754
- const b = arg.slice(three + 3) || "HEAD";
5755
- return {
5756
- old: {
5757
- kind: "merge-base",
5758
- a,
5759
- b: b === WORKTREE ? "HEAD" : b
5760
- },
5761
- newRev: b,
5762
- label: `${a}...${b}`
5763
- };
5764
- }
6848
+ if (three !== -1) return {
6849
+ old: arg.slice(0, three) || "HEAD",
6850
+ new: arg.slice(three + 3) || "HEAD",
6851
+ mergeBase: true
6852
+ };
5765
6853
  const two = arg.indexOf("..");
5766
- if (two !== -1) {
5767
- const a = arg.slice(0, two) || "HEAD";
5768
- const b = arg.slice(two + 2) || "HEAD";
5769
- return {
5770
- old: {
5771
- kind: "rev",
5772
- rev: a
5773
- },
5774
- newRev: b,
5775
- label: `${a}..${b}`
5776
- };
5777
- }
6854
+ if (two !== -1) return {
6855
+ old: arg.slice(0, two) || "HEAD",
6856
+ new: arg.slice(two + 2) || "HEAD",
6857
+ mergeBase: false
6858
+ };
5778
6859
  if (arg === WORKTREE) return {
5779
- old: {
5780
- kind: "rev",
5781
- rev: "HEAD"
5782
- },
5783
- newRev: WORKTREE,
5784
- label: `HEAD..${WORKTREE}`
6860
+ old: "HEAD",
6861
+ new: WORKTREE,
6862
+ mergeBase: false
5785
6863
  };
5786
6864
  return {
5787
- old: {
5788
- kind: "merge-base",
5789
- a: arg,
5790
- b: "HEAD"
5791
- },
5792
- newRev: "HEAD",
5793
- label: `${arg}...HEAD`
6865
+ old: arg,
6866
+ new: "HEAD",
6867
+ mergeBase: true
5794
6868
  };
5795
6869
  }
5796
6870
  var RevspecError = class extends Error {};
@@ -9086,7 +10160,6 @@ const MAX_PATCH_PATHS = 200;
9086
10160
  function createApi(deps) {
9087
10161
  const { session, hub } = deps;
9088
10162
  const app = new Hono();
9089
- const github = new GithubExporter();
9090
10163
  app.onError((err, c) => {
9091
10164
  if (err instanceof NotFoundError) return c.json({ error: "not found" }, 404);
9092
10165
  if (err instanceof RevspecError || err instanceof GitError) return c.json({ error: err.message }, 400);
@@ -9106,6 +10179,10 @@ function createApi(deps) {
9106
10179
  if (!isModeRequest(req)) return c.json({ error: "invalid mode request" }, 400);
9107
10180
  return c.json(await session.switchMode(req));
9108
10181
  });
10182
+ app.get("/api/github", async (c) => {
10183
+ const snap = await session.snapshotter.current();
10184
+ return c.json(await session.github(snap));
10185
+ });
9109
10186
  app.get("/api/refs", async (c) => c.json(await session.repo.refs()));
9110
10187
  app.get("/api/last-commits-preview", async (c) => {
9111
10188
  const values = [c.req.query("oldOffset") ?? "", c.req.query("newOffset") ?? ""];
@@ -9235,12 +10312,7 @@ function createApi(deps) {
9235
10312
  app.post("/api/github/export", async (c) => {
9236
10313
  const ids = (await c.req.json().catch(() => ({}))).threadIds;
9237
10314
  if (ids != null && !(Array.isArray(ids) && ids.every((id) => typeof id === "string"))) return c.json({ error: "threadIds must be a string list" }, 400);
9238
- const snap = await session.snapshotter.current();
9239
- return c.json(await github.export({
9240
- snap,
9241
- threads: session.comments.threads({ state: "all" }),
9242
- threadIds: ids
9243
- }));
10315
+ return c.json(await session.exportGithub({ threadIds: ids }));
9244
10316
  });
9245
10317
  app.get("/api/viewed", (c) => c.json(session.comments.viewed()));
9246
10318
  app.put("/api/viewed/bulk", async (c) => {
@@ -9508,94 +10580,212 @@ function hasClientBuild() {
9508
10580
  return existsSync(join(resolveClientDir(), "index.html"));
9509
10581
  }
9510
10582
  //#endregion
9511
- //#region src/server/mode.ts
9512
- async function resolveOrExplain(repo, rev) {
10583
+ //#region src/server/GithubMetadata.ts
10584
+ const LOOKUP_TIMEOUT_MS = 5e3;
10585
+ /** GitHub.com identity from a configured HTTPS, SSH, or scp-style remote URL. */
10586
+ function githubRepository(url) {
10587
+ const match = /^(?:https?:\/\/github\.com\/|ssh:\/\/(?:[^/@]+@)?github\.com(?::\d+)?\/|(?:[^/@]+@)?github\.com:)([^/]+)\/([^/]+?)\/?$/i.exec(url);
10588
+ return match ? `${match[1]}/${match[2].replace(/\.git$/i, "")}` : null;
10589
+ }
10590
+ /** Local-only origin identity; works without gh, authentication, or a network connection. */
10591
+ async function originRepository(repo) {
10592
+ const origin = (await repo.remotes()).find((r) => r.name === "origin");
10593
+ return origin ? githubRepository(origin.url) : null;
10594
+ }
10595
+ /** Enriches a comparison without fetching commits, changing its endpoints, or changing comment storage. */
10596
+ async function discoverGithub(repo, snap, { run = runGh, prUrl } = {}) {
10597
+ const repository = await originRepository(repo);
10598
+ const absent = (reason) => ({
10599
+ version: snap.version,
10600
+ repository,
10601
+ pullRequest: null,
10602
+ reason
10603
+ });
9513
10604
  try {
9514
- return await repo.resolve(rev);
9515
- } catch (e) {
9516
- if (e instanceof GitError) throw new RevspecError(`unknown revision: ${rev}`);
9517
- throw e;
10605
+ let pr;
10606
+ if (prUrl) pr = await viewPr(prUrl, repo.root, run, LOOKUP_TIMEOUT_MS);
10607
+ else {
10608
+ const [old, next, remotes] = await Promise.all([
10609
+ repo.upstreamBranch(snap.mode.old),
10610
+ repo.upstreamBranch(snap.mode.new),
10611
+ repo.remotes()
10612
+ ]);
10613
+ if (!old || !next) return absent("No branch with an upstream available for both endpoints");
10614
+ const repository = (remote) => {
10615
+ const url = remotes.find((r) => r.name === remote)?.url;
10616
+ return url ? githubRepository(url) : null;
10617
+ };
10618
+ const baseRepo = repository(old.remote);
10619
+ const headRepo = repository(next.remote);
10620
+ if (!baseRepo || !headRepo) return absent("Both branches must have GitHub remotes");
10621
+ const prs = await listPrsForHead(baseRepo, old.branch, next.branch, headRepo, repo.root, run, LOOKUP_TIMEOUT_MS);
10622
+ if (!prs.length) return absent("No matching pull request");
10623
+ if (prs.length !== 1) return absent("Multiple matching pull requests; open one explicitly");
10624
+ pr = prs[0];
10625
+ }
10626
+ let reason = null;
10627
+ if (pr.state !== "OPEN") reason = "The pull request is not open";
10628
+ else if (snap.oldSha === "worktree" || snap.newSha === "worktree") reason = "GitHub cannot anchor comments to the worktree";
10629
+ else if (snap.newSha !== pr.headRefOid) reason = "The comparison does not end at the pull request head";
10630
+ else try {
10631
+ if (snap.oldSha !== await repo.mergeBase(pr.baseRefOid, pr.headRefOid)) reason = "The comparison does not match the pull request diff";
10632
+ } catch (e) {
10633
+ if (!(e instanceof GitError)) throw e;
10634
+ reason = "Fetch the pull request base to verify its diff";
10635
+ }
10636
+ return {
10637
+ version: snap.version,
10638
+ repository,
10639
+ pullRequest: {
10640
+ repository: pr.repository,
10641
+ number: pr.number,
10642
+ url: pr.url,
10643
+ title: pr.title,
10644
+ state: pr.state,
10645
+ isDraft: pr.isDraft
10646
+ },
10647
+ reason
10648
+ };
10649
+ } catch (error) {
10650
+ if (!(error instanceof GithubError)) throw error;
10651
+ return absent(`Lookup failed: ${error.message}`);
9518
10652
  }
9519
10653
  }
9520
- async function mergeBaseOrExplain(repo, a, b) {
9521
- await Promise.all([resolveOrExplain(repo, a), resolveOrExplain(repo, b)]);
9522
- try {
9523
- return await repo.mergeBase(a, b);
9524
- } catch (e) {
9525
- if (e instanceof GitError) throw new RevspecError(`no merge base between ${a} and ${b}`);
9526
- throw e;
10654
+ //#endregion
10655
+ //#region src/server/mode.ts
10656
+ /**
10657
+ * Resolves a comparison to Git endpoints for initial mode setup, PR setup after
10658
+ * fetching, and snapshot refreshes. Revisions become full hashes; "worktree"
10659
+ * is accepted on either side and stays literal unless replaced by a merge base.
10660
+ *
10661
+ * With main at C and HEAD at E, sharing ancestor B:
10662
+ * A--B--C main
10663
+ * \--D--E HEAD
10664
+ *
10665
+ * old new mergeBase oldSha newSha live
10666
+ * main HEAD false C E refs
10667
+ * main HEAD true B E refs
10668
+ * HEAD worktree false E worktree worktree
10669
+ * worktree HEAD false worktree E worktree
10670
+ * main worktree true B worktree worktree
10671
+ * <C hash> <E hash> false C E none
10672
+ *
10673
+ * mergeBase replaces only the old endpoint with the common ancestor, using
10674
+ * HEAD for any worktree input. live is "worktree" if either input is worktree,
10675
+ * "none" if both inputs are pinned hashes, and "refs" otherwise.
10676
+ *
10677
+ * In an unborn repository, HEAD compared directly with worktree resolves to
10678
+ * the empty tree. Invalid revisions or missing merge bases throw RevspecError.
10679
+ */
10680
+ async function resolveComparison(repo, comparison) {
10681
+ const endpoints = [comparison.old, comparison.new];
10682
+ const resolve = async (rev) => {
10683
+ if (rev === "worktree") return rev;
10684
+ try {
10685
+ return await repo.resolve(rev);
10686
+ } catch (e) {
10687
+ if (!comparison.mergeBase && rev === "HEAD" && endpoints.includes("worktree") && e instanceof GitError && e.code === 1) return repo.emptyTree();
10688
+ if (e instanceof GitError) throw new RevspecError(`unknown revision: ${rev}`);
10689
+ throw e;
10690
+ }
10691
+ };
10692
+ const [old, next] = await Promise.all([resolve(comparison.old), resolve(comparison.new)]);
10693
+ let oldSha = old;
10694
+ if (comparison.mergeBase) {
10695
+ const [a, b] = await Promise.all([old === "worktree" ? resolve("HEAD") : old, next === "worktree" ? resolve("HEAD") : next]);
10696
+ try {
10697
+ oldSha = await repo.mergeBase(a, b);
10698
+ } catch (e) {
10699
+ if (e instanceof GitError) throw new RevspecError(`no merge base between ${comparison.old} and ${comparison.new}`);
10700
+ throw e;
10701
+ }
9527
10702
  }
10703
+ const pinned = (rev, sha) => rev.length >= 7 && sha.startsWith(rev.toLowerCase());
10704
+ return {
10705
+ oldSha,
10706
+ newSha: next,
10707
+ live: endpoints.includes("worktree") ? "worktree" : pinned(comparison.old, old) && pinned(comparison.new, next) ? "none" : "refs"
10708
+ };
9528
10709
  }
9529
- function isShaPrefix(rev, sha) {
9530
- return rev.length >= 7 && sha.startsWith(rev.toLowerCase());
10710
+ /** Resolves an input command into a comparison and optional explicit PR identity. */
10711
+ async function resolveReview(req, repo, gh = runGh) {
10712
+ if (req.kind === "pr") return resolvePr(req, repo, gh);
10713
+ const parsed = req.kind === "working" ? {
10714
+ old: "HEAD",
10715
+ new: "worktree",
10716
+ mergeBase: false
10717
+ } : parseRevspec(req.args);
10718
+ const { oldSha, newSha, live } = await resolveComparison(repo, parsed);
10719
+ return { mode: {
10720
+ ...parsed,
10721
+ live,
10722
+ commentKey: req.kind === "working" ? "working" : await commentKey(repo, parsed, oldSha, newSha)
10723
+ } };
9531
10724
  }
9532
- /** Resolves a ModeRequest into a ModeSpec, including the comment key. */
9533
- async function resolveMode(req, repo, gh = runGh) {
9534
- switch (req.kind) {
9535
- case "working": return {
9536
- kind: "working",
9537
- request: req,
9538
- old: {
9539
- kind: "rev",
9540
- rev: "HEAD"
9541
- },
9542
- newRev: "worktree",
9543
- label: "HEAD → worktree",
9544
- live: "worktree",
9545
- commentKey: "working"
9546
- };
9547
- case "pr": return resolvePr(req, repo, gh);
9548
- case "revspec": {
9549
- const parsed = parseRevspec(req.args);
9550
- const oldSha = parsed.old.kind === "rev" ? await resolveOrExplain(repo, parsed.old.rev) : await mergeBaseOrExplain(repo, parsed.old.a, parsed.old.b);
9551
- let live = "none";
9552
- let newKey = parsed.newRev;
9553
- if (parsed.newRev === "worktree") live = "worktree";
9554
- else {
9555
- const sha = await resolveOrExplain(repo, parsed.newRev);
9556
- if (!isShaPrefix(parsed.newRev, sha)) live = "refs";
9557
- newKey = sha;
9558
- }
9559
- return {
9560
- kind: "revspec",
9561
- request: req,
9562
- old: parsed.old,
9563
- newRev: parsed.newRev,
9564
- label: parsed.label,
9565
- live,
9566
- commentKey: `revspec:${oldSha}..${newKey}`
9567
- };
9568
- }
9569
- }
10725
+ /**
10726
+ * Branch identities keep review state stable across pushes and local branch aliases.
10727
+ * For origin = github.com/o/r and HEAD on feat tracking origin/feat:
10728
+ * origin/main...HEAD → branches:["o/r:main","o/r:feat",true]
10729
+ * origin/main..HEAD → branches:["o/r:main","o/r:feat",false]
10730
+ * The matching explicit PR uses the first key too. If feat tracks a fork,
10731
+ * its identity becomes e.g. "contributor/r:feat", keeping forks distinct.
10732
+ *
10733
+ * Without a GitHub upstream, branches use full refs:
10734
+ * main...feat → branches:["refs/heads/main","refs/heads/feat",true]
10735
+ * Non-branch endpoints use their resolved hash or literal "worktree":
10736
+ * main..worktree → branches:["refs/heads/main","worktree",false]
10737
+ * If neither endpoint is a branch, the key is revspec:<oldSha>..<newSha>.
10738
+ * Hash placeholders denote full resolved hashes, with oldSha already adjusted
10739
+ * for mergeBase. The working command bypasses this helper and uses "working".
10740
+ */
10741
+ async function commentKey(repo, comparison, oldSha, newSha) {
10742
+ const remotes = await repo.remotes();
10743
+ const identity = async (rev) => {
10744
+ const upstream = await repo.upstreamBranch(rev);
10745
+ if (upstream) {
10746
+ const url = remotes.find((remote) => remote.name === upstream.remote)?.url;
10747
+ const repository = url && githubRepository(url);
10748
+ if (repository) return `${repository.toLowerCase()}:${upstream.branch}`;
10749
+ }
10750
+ return repo.branchRef(rev);
10751
+ };
10752
+ const [old, next] = await Promise.all([identity(comparison.old), identity(comparison.new)]);
10753
+ return old || next ? branchKey(old ?? oldSha, next ?? newSha, comparison.mergeBase) : `revspec:${oldSha}..${newSha}`;
10754
+ }
10755
+ function branchKey(old, next, mergeBase) {
10756
+ return `branches:${JSON.stringify([
10757
+ old,
10758
+ next,
10759
+ mergeBase
10760
+ ])}`;
9570
10761
  }
9571
10762
  /**
9572
10763
  * A GitHub pull request, as GitHub shows it: merge-base(base tip, head) vs head.
9573
10764
  * `gh` names the PR, then one fetch brings the base tip and the PR head into
9574
- * session-owned refs, so a PR nobody has checked out is reviewable. Both
9575
- * sides are pinned to commits, so the mode is static and the comment key follows
9576
- * the PR number: comments survive a force-push.
10765
+ * session-owned refs named after the branches, so a PR nobody has checked out
10766
+ * is reviewable. These refs stay fixed until another fetch; review state uses
10767
+ * the same branch identities as ordinary comparisons, so comments survive
10768
+ * pushes and reopening by branch.
9577
10769
  */
9578
10770
  async function resolvePr(req, repo, gh) {
9579
10771
  const pr = await viewPr(req.pr, repo.root, gh);
9580
- const head = `${repo.reviewRefs}/${pr.number}/head`;
9581
- const base = `${repo.reviewRefs}/${pr.number}/base`;
10772
+ const head = `${repo.reviewRefs}/${pr.number}/head/${pr.headRefName}`;
10773
+ const base = `${repo.reviewRefs}/${pr.number}/base/${pr.baseRefName}`;
9582
10774
  await repo.fetch(await fetchSource(repo, pr), [`+refs/pull/${pr.number}/head:${head}`, `+refs/heads/${pr.baseRefName}:${base}`]);
9583
- const [headSha, mb] = await Promise.all([resolveOrExplain(repo, head), mergeBaseOrExplain(repo, base, head)]);
10775
+ await resolveComparison(repo, {
10776
+ old: base,
10777
+ new: head,
10778
+ mergeBase: true
10779
+ });
9584
10780
  return {
9585
- kind: "pr",
9586
- request: req,
9587
- old: {
9588
- kind: "rev",
9589
- rev: mb
9590
- },
9591
- newRev: headSha,
9592
- label: `#${pr.number} ${pr.baseRefName}...${pr.headRefName}`,
9593
- pullRequest: {
9594
- repository: pr.baseRepo,
9595
- number: pr.number
9596
- },
9597
- live: "none",
9598
- commentKey: `pr:#${pr.number}`
10781
+ prUrl: pr.url,
10782
+ mode: {
10783
+ old: base,
10784
+ new: head,
10785
+ mergeBase: true,
10786
+ live: "none",
10787
+ commentKey: branchKey(`${pr.repository.toLowerCase()}:${pr.baseRefName}`, `${pr.headRepository?.toLowerCase() ?? `deleted:${pr.url}`}:${pr.headRefName}`, true)
10788
+ }
9599
10789
  };
9600
10790
  }
9601
10791
  /**
@@ -9603,7 +10793,7 @@ async function resolvePr(req, repo, gh) {
9603
10793
  * that points at it. Foreign repositories must be opened separately by the CLI.
9604
10794
  */
9605
10795
  async function fetchSource(repo, pr) {
9606
- const slug = pr.baseRepo.toLowerCase();
10796
+ const slug = pr.repository.toLowerCase();
9607
10797
  const match = (await repo.remotes()).find((r) => remoteSlug(r.url) === slug);
9608
10798
  if (!match) throw new RevspecError(`foreign repository; open it with diffle pr ${pr.url}`);
9609
10799
  return match.name;
@@ -9740,12 +10930,8 @@ var Snapshotter = class {
9740
10930
  return p;
9741
10931
  }
9742
10932
  async compute(version) {
9743
- const [oldSha, newSha, headSha] = await Promise.all([
9744
- this.resolveOld(),
9745
- this.resolveNew(),
9746
- this.repo.resolve("HEAD").catch(() => "")
9747
- ]);
9748
- const [tracked, changed] = await Promise.all([newSha === "worktree" ? this.repo.lsFiles() : this.repo.lsTree(newSha), this.repo.numstat(oldSha, newSha)]);
10933
+ const [{ oldSha, newSha }, headSha] = await Promise.all([resolveComparison(this.repo, this.mode), this.repo.resolve("HEAD").catch(() => "")]);
10934
+ const [tracked, changed] = await Promise.all([newSha === "worktree" ? Promise.all([this.repo.lsFiles(), this.repo.untracked()]).then((lists) => lists.flat().filter((p) => !p.endsWith("/"))) : this.repo.lsTree(newSha), this.repo.numstat(oldSha, newSha)]);
9749
10935
  await this.fillGenerated(changed, newSha);
9750
10936
  const tree = new Set(tracked);
9751
10937
  for (const f of changed) {
@@ -9795,19 +10981,6 @@ var Snapshotter = class {
9795
10981
  if (this.sniffed.size >= SNIFF_CACHE_MAX) this.sniffed.clear();
9796
10982
  this.sniffed.set(f.blob, f.generated);
9797
10983
  }
9798
- async resolveOld() {
9799
- const o = this.mode.old;
9800
- if (o.kind !== "rev") return this.repo.mergeBase(o.a, o.b);
9801
- try {
9802
- return await this.repo.resolve(o.rev);
9803
- } catch (e) {
9804
- if (o.rev === "HEAD" && e instanceof GitError && e.code === 1) return this.repo.emptyTree();
9805
- throw e;
9806
- }
9807
- }
9808
- resolveNew() {
9809
- return this.mode.newRev === "worktree" ? Promise.resolve("worktree") : this.repo.resolve(this.mode.newRev);
9810
- }
9811
10984
  };
9812
10985
  //#endregion
9813
10986
  //#region src/server/Session.ts
@@ -9825,6 +10998,7 @@ var Session = class {
9825
10998
  hub;
9826
10999
  opts;
9827
11000
  active = null;
11001
+ exporter = new GithubExporter();
9828
11002
  readyPromise;
9829
11003
  resolveReady;
9830
11004
  rejectReady;
@@ -9853,6 +11027,48 @@ var Session = class {
9853
11027
  get snapshotter() {
9854
11028
  return this.require().snapshotter;
9855
11029
  }
11030
+ /** Cached independently of snapshot construction; callers never hold the transition queue. */
11031
+ github(snap) {
11032
+ const active = this.require();
11033
+ if (snap.mode !== active.mode) return Promise.reject(new GithubError("Comparison changed; try again"));
11034
+ const cached = active.github;
11035
+ if (cached?.snapshot === snap && cached.expires > Date.now()) return cached.promise;
11036
+ const promise = discoverGithub(this.repo, snap, {
11037
+ run: this.opts.gh,
11038
+ prUrl: active.prUrl
11039
+ });
11040
+ const entry = {
11041
+ snapshot: snap,
11042
+ expires: Date.now() + 3e4,
11043
+ promise
11044
+ };
11045
+ active.github = entry;
11046
+ promise.catch(() => {
11047
+ if (active.github === entry) active.github = void 0;
11048
+ });
11049
+ return promise;
11050
+ }
11051
+ /** Validate fresh PR data inside the export queue against the current comparison. */
11052
+ exportGithub({ threadIds }) {
11053
+ return this.exporter.export(async () => {
11054
+ const active = this.require();
11055
+ const snap = await active.snapshotter.current();
11056
+ if (active !== this.active) throw new GithubError("Comparison changed; try again");
11057
+ const metadata = await discoverGithub(this.repo, snap, {
11058
+ run: this.opts.gh,
11059
+ prUrl: active.prUrl
11060
+ });
11061
+ if (!metadata.pullRequest || metadata.reason !== null) throw new GithubError(metadata.reason ?? "No matching pull request");
11062
+ if (active !== this.active || snap !== await active.snapshotter.current()) throw new GithubError("Comparison changed; try again");
11063
+ return {
11064
+ snap,
11065
+ pullRequest: metadata.pullRequest,
11066
+ threads: active.comments.threads({ state: "all" }),
11067
+ threadIds,
11068
+ run: this.opts.gh
11069
+ };
11070
+ });
11071
+ }
9856
11072
  get comments() {
9857
11073
  return this.require().comments;
9858
11074
  }
@@ -9909,11 +11125,12 @@ var Session = class {
9909
11125
  return this.run(() => this.transition(req));
9910
11126
  }
9911
11127
  async transition(req) {
9912
- const mode = await resolveMode(req, this.repo);
11128
+ const { mode, prUrl } = await resolveReview(req, this.repo, this.opts.gh);
9913
11129
  const snapshotter = new Snapshotter(this.repo, mode, ++this.version, this.opts.context);
9914
11130
  const [comments, snap] = await Promise.all([CommentStore.open(this.repo.gitDir, mode.commentKey), snapshotter.current()]);
9915
11131
  const next = {
9916
11132
  mode,
11133
+ prUrl,
9917
11134
  snapshotter,
9918
11135
  comments,
9919
11136
  watcher: null
@@ -9996,7 +11213,7 @@ var Session = class {
9996
11213
  const target = sidePath(this.readablePaths(snap), path, side);
9997
11214
  if (target == null) return Promise.resolve(null);
9998
11215
  if (side === "new") return snap.newSha === "worktree" ? this.repo.readWorktree(target) : this.repo.show(snap.newSha, target);
9999
- return this.repo.show(snap.oldSha, target);
11216
+ return snap.oldSha === "worktree" ? this.repo.readWorktree(target) : this.repo.show(snap.oldSha, target);
10000
11217
  }
10001
11218
  /** Whether `readSide` would find `path` on `side`: the allowlist alone, no read. */
10002
11219
  hasSide(snap, path, side) {
@@ -13855,6 +15072,30 @@ function watchBrowserLifetime(clients, enabled, onClose, graceMs = 1e3) {
13855
15072
  };
13856
15073
  }
13857
15074
  //#endregion
15075
+ //#region src/cli/completion.ts
15076
+ const SHELLS = [
15077
+ "bash",
15078
+ "zsh",
15079
+ "fish"
15080
+ ];
15081
+ /**
15082
+ * Adds `diffle completion --shell <shell>`, which prints a standalone script derived from
15083
+ * this command tree. The script completes in the shell alone: tab completion never starts
15084
+ * node, so it stays fast, and it goes stale until the command is run again after an upgrade.
15085
+ */
15086
+ function addCompletionCommand(program) {
15087
+ return program.command("completion").description("print a shell completion script for bash, zsh, or fish").addOption(new Option("-s, --shell <shell>", "shell to generate the script for").choices(SHELLS).makeOptionMandatory()).addHelpText("after", `
15088
+ Install it for the current user, then start a new shell:
15089
+ bash diffle completion --shell bash > ~/.local/share/bash-completion/completions/diffle
15090
+ zsh diffle completion --shell zsh > ~/.local/share/zsh/site-functions/_diffle
15091
+ (any directory that .zshrc puts on fpath before compinit)
15092
+ fish diffle completion --shell fish > ~/.config/fish/completions/diffle.fish
15093
+
15094
+ Run it again after upgrading diffle: the script is static, so new options do not appear on their own.`).action((opts) => {
15095
+ process.stdout.write(generateCompletion(program, { shell: opts.shell }));
15096
+ });
15097
+ }
15098
+ //#endregion
13858
15099
  //#region src/cli/open.ts
13859
15100
  /** Opens a URL in the default browser. Detached; never awaited. */
13860
15101
  function openBrowser(url) {
@@ -13890,7 +15131,7 @@ async function openReviewRepository(req, cwd, gh = runGh) {
13890
15131
  close: () => local.cleanReviewRefs()
13891
15132
  };
13892
15133
  const pr = await viewPr(req.pr, cwd, gh);
13893
- if (local && (await local.remotes()).some((r) => remoteSlug(r.url) === pr.baseRepo.toLowerCase())) return {
15134
+ if (local && (await local.remotes()).some((r) => remoteSlug(r.url) === pr.repository.toLowerCase())) return {
13894
15135
  repo: local,
13895
15136
  close: () => local.cleanReviewRefs()
13896
15137
  };
@@ -13947,7 +15188,10 @@ const c = {
13947
15188
  bold: paint("1"),
13948
15189
  dim: paint("2")
13949
15190
  };
13950
- const program = new Command().name("diffle").description("Review a git diff in the browser and export line comments as an agent prompt.").version(version, "-v, --version", "print the version and exit").option("-C <path>", "run as if started in <path> (any directory inside a git worktree)").option("-p, --port <port>", `port to listen on (default: ${DEFAULT_PORT}, or the next free one; 0 = random)`, parsePort).option("-H, --host <host>", "address to bind; use 0.0.0.0 to expose on the network", "127.0.0.1").option("--no-open", "do not open a browser").option("--keep-alive", "keep the server running after all browser tabs close").option("--no-watch", "do not watch for changes").addOption(new Option("--auto-viewed <glob>", "mark matching files viewed for this session (repeatable)").argParser(collect).default([], "none")).option("-U, --context <n>", "context lines around changes for this session (default: config, 5)", parseContext).addOption(new Option("--lsp <language=command>", "language server command for one language, started for this run (repeatable); by default the diff's languages are served by whatever is on PATH").argParser(collectLspOverride).default(true, "the diff's languages, resolved on PATH")).option("--no-lsp", "do not start any language server").option("--timing", "print startup phase timings to stderr").option("--dev", "serve the client through Vite (development)", process.env.DIFFLE_DEV === "1" ? true : void 0).argument("[revs...]", "git-diff style revisions: <rev> | <a>..<b> | <a>...<b> | <a> <b>").addHelpText("after", `
15191
+ const program = new Command().name("diffle").description("Review a git diff in the browser and export line comments as an agent prompt.").version(version, "-v, --version", "print the version and exit").addOption(completionHint(new Option("-C <path>", "run as if started in <path> (any directory inside a git worktree)"), { kind: "directory" })).option("-p, --port <port>", `port to listen on (default: ${DEFAULT_PORT}, or the next free one; 0 = random)`, parsePort).option("-H, --host <host>", "address to bind; use 0.0.0.0 to expose on the network", "127.0.0.1").option("--no-open", "do not open a browser").option("--keep-alive", "keep the server running after all browser tabs close").option("--no-watch", "do not watch for changes").addOption(new Option("--auto-viewed <glob>", "mark matching files viewed for this session (repeatable)").argParser(collect).default([], "none")).option("-U, --context <n>", "context lines around changes for this session (default: config, 5)", parseContext).addOption(new Option("--lsp <language=command>", "language server command for one language, started for this run (repeatable); by default the diff's languages are served by whatever is on PATH").argParser(collectLspOverride).default(true, "the diff's languages, resolved on PATH")).option("--no-lsp", "do not start any language server").option("--timing", "print startup phase timings to stderr").option("--dev", "serve the client through Vite (development)", process.env.DIFFLE_DEV === "1" ? true : void 0).addArgument(completionHint(new Argument("[revs...]", "git-diff style revisions: <rev> | <a>..<b> | <a>...<b> | <a> <b>"), {
15192
+ kind: "choices",
15193
+ values: ["working", "pr"]
15194
+ })).addHelpText("after", `
13951
15195
  Shorthands (in place of <revs>):
13952
15196
  working same as HEAD..worktree: uncommitted changes, staged and untracked included
13953
15197
  pr [number|url] a GitHub pull request: its base...head, fetched if needed
@@ -13990,7 +15234,7 @@ config.command("set-context").description("set context lines around changes").ar
13990
15234
  await store.set({ contextLines: n });
13991
15235
  console.log(store.get().contextLines);
13992
15236
  });
13993
- config.command("set-lsp").description("set the language-server command for one language").argument("<language>", `one of: ${LANGUAGE_IDS.join(", ")}`, parseLanguage).argument("<command>", "shell command that starts a stdio language server, e.g. \"pyrefly lsp\"; \"\" to turn it off").action(async (language, command) => {
15237
+ config.command("set-lsp").description("set the language-server command for one language").addArgument(languageArgument("<language>", `one of: ${LANGUAGE_IDS.join(", ")}`).argParser(parseLanguage)).argument("<command>", "shell command that starts a stdio language server, e.g. \"pyrefly lsp\"; \"\" to turn it off").action(async (language, command) => {
13994
15238
  const store = await UserConfigStore.open();
13995
15239
  await store.set({ lspCommands: {
13996
15240
  ...store.get().lspCommands,
@@ -13998,7 +15242,7 @@ config.command("set-lsp").description("set the language-server command for one l
13998
15242
  } });
13999
15243
  printLspCommands(store.get());
14000
15244
  });
14001
- config.command("unset-lsp").description("forget a language-server override, back to PATH").argument("<language...>", `one or more of: ${LANGUAGE_IDS.join(", ")}`, collectLanguage).action(async (languages) => {
15245
+ config.command("unset-lsp").description("forget a language-server override, back to PATH").addArgument(languageArgument("<language...>", `one or more of: ${LANGUAGE_IDS.join(", ")}`).argParser(collectLanguage)).action(async (languages) => {
14002
15246
  const store = await UserConfigStore.open();
14003
15247
  const drop = new Set(languages);
14004
15248
  await store.set({ lspCommands: Object.fromEntries(Object.entries(store.get().lspCommands).filter(([l]) => !drop.has(l))) });
@@ -14018,9 +15262,17 @@ program.command("lsp").description("show the language server each language would
14018
15262
  const width = Math.max(...LANGUAGE_IDS.map((l) => l.length));
14019
15263
  for (const language of LANGUAGE_IDS) console.log(`${language.padEnd(width)} ${answer.get(language) ?? ""}`);
14020
15264
  });
15265
+ addCompletionCommand(program);
14021
15266
  function collect(value, prev) {
14022
15267
  return [...prev, value];
14023
15268
  }
15269
+ /** A `<language>` argument, completing to the languages diffle knows. */
15270
+ function languageArgument(name, description) {
15271
+ return completionHint(new Argument(name, description), {
15272
+ kind: "choices",
15273
+ values: LANGUAGE_IDS
15274
+ });
15275
+ }
14024
15276
  function printLspCommands(config) {
14025
15277
  for (const [language, command] of Object.entries(config.lspCommands)) console.log(`${language}=${command}`);
14026
15278
  }
@@ -14103,7 +15355,7 @@ async function serve(req, opts, repo, closeRepo, config, timing) {
14103
15355
  const n = snap.changed.length;
14104
15356
  const adds = snap.changed.reduce((a, f) => a + f.additions, 0);
14105
15357
  const dels = snap.changed.reduce((a, f) => a + f.deletions, 0);
14106
- console.error(`🔍 ${c.dim("comparing")} ${c.bold(snap.mode.label)}`);
15358
+ console.error(`🔍 ${c.dim("comparing")} ${c.bold(comparisonLabel(snap.mode))}`);
14107
15359
  if (n === 0) console.error(`${c.yellow("!")} No differences. Open any file from the tree to comment on it.`);
14108
15360
  else console.error(`📝 ${n} changed file${n === 1 ? "" : "s"} ${c.green(`+${adds}`)} ${c.red(`−${dels}`)}`);
14109
15361
  if (snap.mode.live !== "none") console.error(`👀 ${c.dim(snap.mode.live === "worktree" ? "watching the worktree" : "watching refs")}${opts.watch ? "" : c.dim(" (disabled with --no-watch)")}`);
@@ -14137,7 +15389,7 @@ function startLsp(repo, session, hub, overrides, preload) {
14137
15389
  const paths = followsCheckout(snap) ? snap.changed.filter((f) => f.status !== "D" && !f.binary).map((f) => f.path) : [];
14138
15390
  lsp.track(paths);
14139
15391
  });
14140
- const reported = /* @__PURE__ */ new Set();
15392
+ const reported = /* @__PURE__ */ new Map();
14141
15393
  const lsp = new LspPool({
14142
15394
  root: repo.root,
14143
15395
  overrides,
@@ -14148,9 +15400,14 @@ function startLsp(repo, session, hub, overrides, preload) {
14148
15400
  has: async (path) => session.hasSide(await session.snapshotter.current(), path, "new"),
14149
15401
  onStatus: (status) => {
14150
15402
  for (const s of status.servers) {
14151
- if (s.state !== "unavailable" || reported.has(s.command)) continue;
14152
- reported.add(s.command);
14153
- console.error(`${c.red("✖")} lsp ${s.name} unavailable: ${s.message}`);
15403
+ const message = s.state === "unavailable" ? s.message ?? "unavailable" : s.notice?.message;
15404
+ if (!message) {
15405
+ reported.delete(s.command);
15406
+ continue;
15407
+ }
15408
+ if (reported.get(s.command) === message) continue;
15409
+ reported.set(s.command, message);
15410
+ console.error(`${c.red("✖")} lsp ${s.name}: ${message}`);
14154
15411
  }
14155
15412
  hub.broadcast({
14156
15413
  type: "lsp",