@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.
- package/README.md +18 -2
- package/dist/client/assets/{angular-html-FsiSMH3Y.js → angular-html-CkdWwNDI.js} +1 -1
- package/dist/client/assets/{angular-ts-BSikFT16.js → angular-ts-CuvP0IGu.js} +1 -1
- package/dist/client/assets/{apl-1IDS0jHU.js → apl-pdW-dc02.js} +1 -1
- package/dist/client/assets/{astro-CvjIcDdF.js → astro-zkH-hPtA.js} +1 -1
- package/dist/client/assets/{blade-iab1_tYE.js → blade-ddlHE2HC.js} +1 -1
- package/dist/client/assets/{c-CvC-n5Dt.js → c-Ctj6WiaA.js} +1 -1
- package/dist/client/assets/{chapel-DWMV2k4B.js → chapel-W7vmpmYc.js} +1 -1
- package/dist/client/assets/{cobol-Dr8jUXVY.js → cobol-DhORiGLX.js} +1 -1
- package/dist/client/assets/{coffee-DogfyzLt.js → coffee-CmpKakZz.js} +1 -1
- package/dist/client/assets/{cpp-DY0XLqOn.js → cpp-aSK51Qyo.js} +1 -1
- package/dist/client/assets/{crystal-DTt4VJ7I.js → crystal-CGGaMVqP.js} +1 -1
- package/dist/client/assets/{css-CWsyBCI2.js → css-CbDWH8CU.js} +1 -1
- package/dist/client/assets/{edge-Cbd0Hldf.js → edge-Bb_E62jX.js} +1 -1
- package/dist/client/assets/{elixir-Dlgm2eX2.js → elixir-DjPlIKeD.js} +1 -1
- package/dist/client/assets/{elm-tQ1jykNc.js → elm-B55oXPOF.js} +1 -1
- package/dist/client/assets/{erb-lRQQr1Sc.js → erb-CXXpq1zV.js} +1 -1
- package/dist/client/assets/{git-rebase-mPGpy9fK.js → git-rebase-7PMXuU9B.js} +1 -1
- package/dist/client/assets/{glimmer-js-Cnwlg_IR.js → glimmer-js-BBBBNj8H.js} +1 -1
- package/dist/client/assets/{glimmer-ts-lS7iLUfN.js → glimmer-ts-BJzb0Uby.js} +1 -1
- package/dist/client/assets/{glsl-Ddwau8g3.js → glsl-CBzawuyB.js} +1 -1
- package/dist/client/assets/{graphql-hYYZYECc.js → graphql-DRKcwjOE.js} +1 -1
- package/dist/client/assets/{hack-tVOl2PyM.js → hack-CsFE62pw.js} +1 -1
- package/dist/client/assets/{haml-Cwtz-Dq4.js → haml--KtktDGY.js} +1 -1
- package/dist/client/assets/{handlebars-CyvaDab9.js → handlebars-D56IR41J.js} +1 -1
- package/dist/client/assets/{html-BUo5i1NC.js → html-Baytl-FU.js} +1 -1
- package/dist/client/assets/{html-derivative-DtP04Ng0.js → html-derivative-Cu0xLfuG.js} +1 -1
- package/dist/client/assets/{http-D1AHtlGd.js → http-DBz-9Rm1.js} +1 -1
- package/dist/client/assets/{hurl-C15vSi1V.js → hurl-CTnFLYIC.js} +1 -1
- package/dist/client/assets/index-BcpNE2iR.js +2924 -0
- package/dist/client/assets/index-CBL0EMlj.css +2 -0
- package/dist/client/assets/{java-DPFrtqcs.js → java-Dno3m1M7.js} +1 -1
- package/dist/client/assets/{javascript-D33iozV3.js → javascript-_XI5Q4LF.js} +1 -1
- package/dist/client/assets/{jinja-DRYgl0dQ.js → jinja-CjeRdbbL.js} +1 -1
- package/dist/client/assets/{jison-DXyfqsqv.js → jison-C3ET6HHP.js} +1 -1
- package/dist/client/assets/{json-BH2tJSV_.js → json-BEsHf2su.js} +1 -1
- package/dist/client/assets/{jsx-BNYiHgWd.js → jsx-C3QpkgV8.js} +1 -1
- package/dist/client/assets/{julia-DWndCVqe.js → julia-BtHfH46Z.js} +1 -1
- package/dist/client/assets/{just-CaFvzxl4.js → just-DRu-Pg-e.js} +1 -1
- package/dist/client/assets/{latex-DqDoao-r.js → latex-1BoN898Q.js} +1 -1
- package/dist/client/assets/{liquid-DYBj14Ih.js → liquid-CL6XoFA-.js} +1 -1
- package/dist/client/assets/{lua-DISCLF_p.js → lua-ClGqXLNh.js} +1 -1
- package/dist/client/assets/{marko-BaO-VaHf.js → marko-BQZ0rvkW.js} +1 -1
- package/dist/client/assets/{mdc-5Q3_gdW7.js → mdc-CqTpgJx6.js} +1 -1
- package/dist/client/assets/{nginx-CQ4BjNxJ.js → nginx-B3fPtzbP.js} +1 -1
- package/dist/client/assets/{nim-OJ1IfTX5.js → nim-CcgLIp3n.js} +1 -1
- package/dist/client/assets/{org-DE2pvWZc.js → org-5bpEDggi.js} +1 -1
- package/dist/client/assets/{perl-8QKFSfpf.js → perl-B_kewDRb.js} +1 -1
- package/dist/client/assets/{php-DHN01N8j.js → php-DCeoSILL.js} +1 -1
- package/dist/client/assets/{pug-DCS1tN7i.js → pug-Dojg3vEi.js} +1 -1
- package/dist/client/assets/{qml-6exH9-g5.js → qml-s5uziDLI.js} +1 -1
- package/dist/client/assets/{r-DcfcrTMj.js → r-C9fV-mBh.js} +1 -1
- package/dist/client/assets/{razor-CIrVisVV.js → razor-BTysEkha.js} +1 -1
- package/dist/client/assets/{regexp-OAMUjW-k.js → regexp-CsIP6diF.js} +1 -1
- package/dist/client/assets/{rst-Dt5AjMM2.js → rst-8a-JD2vB.js} +1 -1
- package/dist/client/assets/{ruby-E8arA1q2.js → ruby-BP-9Eb2k.js} +1 -1
- package/dist/client/assets/{sas-_hYONkAv.js → sas-zIaAmRUD.js} +1 -1
- package/dist/client/assets/{scss-CuI5mog1.js → scss-D_zCS4rR.js} +1 -1
- package/dist/client/assets/{shellscript-brVl5slU.js → shellscript-DRVCBED5.js} +1 -1
- package/dist/client/assets/{shellsession-kD5Km7Fl.js → shellsession-B6vXqKcQ.js} +1 -1
- package/dist/client/assets/{soy-AZoL22qE.js → soy-CnSSPgTt.js} +1 -1
- package/dist/client/assets/{sql-P6NiMgNx.js → sql-DNUVNZl5.js} +1 -1
- package/dist/client/assets/{stata-CaLkIj3d.js → stata-BTM4COU8.js} +1 -1
- package/dist/client/assets/{surrealql-DVyAIU7z.js → surrealql-D0RpP-tU.js} +1 -1
- package/dist/client/assets/{svelte-CSnNgDkA.js → svelte-Bgjr5_uo.js} +1 -1
- package/dist/client/assets/{templ-Dqr_Un-e.js → templ-j4HlTKjQ.js} +1 -1
- package/dist/client/assets/{tex-CvSrixZF.js → tex-Da2hGdgf.js} +1 -1
- package/dist/client/assets/{ts-tags-DT7kX2Qk.js → ts-tags-DHDgIe3m.js} +1 -1
- package/dist/client/assets/{tsx-DtULR4Hc.js → tsx-BiQG1bF2.js} +1 -1
- package/dist/client/assets/{twig-D_yBfqrR.js → twig-nmyN70HH.js} +1 -1
- package/dist/client/assets/{typescript-D8R1VR1Y.js → typescript-B2CoeRXG.js} +1 -1
- package/dist/client/assets/{typst-C9bHxBJa.js → typst-DYnFmsYK.js} +1 -1
- package/dist/client/assets/{vue-Bv6cNqLz.js → vue-D4W-_FLh.js} +1 -1
- package/dist/client/assets/{vue-html-CJupgPdM.js → vue-html-JguYDiPp.js} +1 -1
- package/dist/client/assets/{vue-vine-6vI54xHX.js → vue-vine-EdQ0itvY.js} +1 -1
- package/dist/client/assets/{xml-CYK4-lRM.js → xml-C6OfKyfM.js} +1 -1
- package/dist/client/assets/{xsl-BOaafYfD.js → xsl-C_NW51H4.js} +1 -1
- package/dist/client/assets/{yaml-CvfdNt6k.js → yaml-DExcc3cX.js} +1 -1
- package/dist/client/index.html +2 -2
- package/dist/server/main.js +1505 -248
- package/package.json +6 -1
- package/dist/client/assets/index-9z4xRD3M.css +0 -1
- package/dist/client/assets/index-Dtx7INNk.js +0 -2925
package/dist/server/main.js
CHANGED
|
@@ -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.
|
|
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
|
-
|
|
3147
|
-
"--no-ext-diff",
|
|
3148
|
-
|
|
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
|
-
|
|
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
|
|
4380
|
+
]), [oldRev, newRev].includes("worktree") ? this.untracked() : Promise.resolve([])]);
|
|
3381
4381
|
const files = parseRawNumstat(diff);
|
|
3382
|
-
const known = new Set(files.
|
|
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)
|
|
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 (
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3573
|
-
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
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
|
-
...
|
|
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
|
-
|
|
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
|
-
...
|
|
4607
|
+
...patchArgs(oldRev),
|
|
3597
4608
|
`-U${context}`,
|
|
3598
4609
|
...range
|
|
3599
4610
|
]);
|
|
3600
4611
|
let untracked = [];
|
|
3601
|
-
if (newRev
|
|
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
|
-
|
|
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 =
|
|
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
|
-
...
|
|
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
|
-
/**
|
|
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(
|
|
4141
|
-
const result = this.queue.then(() => exportToGithub(
|
|
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
|
|
4148
|
-
*
|
|
4149
|
-
*
|
|
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
|
|
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
|
|
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
|
|
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
|
-
], {
|
|
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
|
|
4414
|
-
let pr;
|
|
5446
|
+
function parseJson(out, command) {
|
|
4415
5447
|
try {
|
|
4416
|
-
|
|
5448
|
+
return JSON.parse(out);
|
|
4417
5449
|
} catch {
|
|
4418
|
-
throw new GithubError(`unexpected output from gh pr
|
|
5450
|
+
throw new GithubError(`unexpected output from gh pr ${command}: ${out.slice(0, 200)}`, 502);
|
|
4419
5451
|
}
|
|
4420
|
-
|
|
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
|
-
|
|
4425
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
5148
|
-
|
|
5149
|
-
const
|
|
5150
|
-
|
|
5151
|
-
|
|
5152
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
5744
|
-
|
|
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
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
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
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
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
|
-
|
|
5781
|
-
|
|
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
|
-
|
|
5789
|
-
|
|
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
|
-
|
|
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/
|
|
9512
|
-
|
|
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
|
-
|
|
9515
|
-
|
|
9516
|
-
|
|
9517
|
-
|
|
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
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9523
|
-
|
|
9524
|
-
|
|
9525
|
-
|
|
9526
|
-
|
|
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
|
-
|
|
9530
|
-
|
|
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
|
-
/**
|
|
9533
|
-
|
|
9534
|
-
|
|
9535
|
-
|
|
9536
|
-
|
|
9537
|
-
|
|
9538
|
-
|
|
9539
|
-
|
|
9540
|
-
|
|
9541
|
-
|
|
9542
|
-
|
|
9543
|
-
|
|
9544
|
-
|
|
9545
|
-
|
|
9546
|
-
|
|
9547
|
-
|
|
9548
|
-
|
|
9549
|
-
|
|
9550
|
-
|
|
9551
|
-
|
|
9552
|
-
|
|
9553
|
-
|
|
9554
|
-
|
|
9555
|
-
|
|
9556
|
-
|
|
9557
|
-
|
|
9558
|
-
|
|
9559
|
-
|
|
9560
|
-
|
|
9561
|
-
|
|
9562
|
-
|
|
9563
|
-
|
|
9564
|
-
|
|
9565
|
-
|
|
9566
|
-
|
|
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
|
|
9575
|
-
*
|
|
9576
|
-
* the
|
|
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
|
-
|
|
10775
|
+
await resolveComparison(repo, {
|
|
10776
|
+
old: base,
|
|
10777
|
+
new: head,
|
|
10778
|
+
mergeBase: true
|
|
10779
|
+
});
|
|
9584
10780
|
return {
|
|
9585
|
-
|
|
9586
|
-
|
|
9587
|
-
|
|
9588
|
-
|
|
9589
|
-
|
|
9590
|
-
|
|
9591
|
-
|
|
9592
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
|
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.
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
|
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
|
|
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
|
-
|
|
14152
|
-
|
|
14153
|
-
|
|
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",
|