| 3914 |
3914
|
|
}),
|
| 3915 |
3915
|
|
).pipe(Command.withDescription("Read, add, or remove the prerequisites of an issue"));
|
| 3916 |
3916
|
|
|
|
3917
|
+ |
/**
|
|
3918
|
+ |
* The milestone surface, on both CLIs, because the API always had it and
|
|
3919
|
+ |
* neither client reached it.
|
|
3920
|
+ |
*
|
|
3921
|
+ |
* `issue create --milestone` was the only write: an issue filed without one
|
|
3922
|
+ |
* could never be given one, and a milestone could only be opened or removed in
|
|
3923
|
+ |
* a browser, which made milestones useless to agents -- and agents file most of
|
|
3924
|
+ |
* the issues here.
|
|
3925
|
+ |
*/
|
|
3926
|
+ |
const milestoneNumberArgument = Argument.string("milestone").pipe(
|
|
3927
|
+ |
Argument.withDescription("Milestone number"),
|
|
3928
|
+ |
);
|
|
3929
|
+ |
|
|
3930
|
+ |
const milestoneRow = (value: Record<string, unknown>): string =>
|
|
3931
|
+ |
`${`#${String(value["number"] ?? "?")}`.padEnd(7)}${String(value["state"] ?? "").padEnd(8)}${String(
|
|
3932
|
+ |
value["title"] ?? "",
|
|
3933
|
+ |
)}`;
|
|
3934
|
+ |
|
|
3935
|
+ |
/** One renderer, so `milestone list` and any other listing cannot disagree. */
|
|
3936
|
+ |
const milestoneListHuman = (value: unknown): ReadonlyArray<string> => {
|
|
3937
|
+ |
const listed = rows(value, "milestones");
|
|
3938
|
+ |
return listed.length === 0 ? ["No milestones found."] : listed.map(milestoneRow);
|
|
3939
|
+ |
};
|
|
3940
|
+ |
|
|
3941
|
+ |
/**
|
|
3942
|
+ |
* The listing, under two names.
|
|
3943
|
+ |
*
|
|
3944
|
+ |
* `oa issue milestones` is the name the Rust CLI already published, and
|
|
3945
|
+ |
* `openagents milestone list` is where the write half lives. Both are the same
|
|
3946
|
+ |
* handler, so the two cannot describe the same repository differently.
|
|
3947
|
+ |
*/
|
|
3948
|
+ |
const makeMilestoneListCommand = (name: "list" | "milestones") =>
|
|
3949
|
+ |
Command.make(name, { repo: repositoryOverrideFlag }, ({ repo }) =>
|
|
3950
|
+ |
Effect.gen(function* () {
|
|
3951
|
+ |
const flags = yield* rootCommand;
|
|
3952
|
+ |
const session = yield* resolveApiSession(endpointOverrides(flags));
|
|
3953
|
+ |
const target = yield* resolveTrackerTarget(repo, session.endpoint.origin);
|
|
3954
|
+ |
const issues = yield* IssueClient;
|
|
3955
|
+ |
const output = yield* Output;
|
|
3956
|
+ |
const value = yield* issues.milestones({
|
|
3957
|
+ |
origin: session.endpoint.origin,
|
|
3958
|
+ |
token: session.token,
|
|
3959
|
+ |
...target,
|
|
3960
|
+ |
});
|
|
3961
|
+ |
yield* output.write({ value, human: milestoneListHuman(value) }, outputMode(flags.json));
|
|
3962
|
+ |
}),
|
|
3963
|
+ |
).pipe(Command.withDescription("List the milestones of a repository"));
|
|
3964
|
+ |
|
|
3965
|
+ |
const milestoneListCommand = makeMilestoneListCommand("list");
|
|
3966
|
+ |
const issueMilestonesCommand = makeMilestoneListCommand("milestones");
|
|
3967
|
+ |
|
|
3968
|
+ |
const milestoneTitleArgument = Argument.string("title").pipe(
|
|
3969
|
+ |
Argument.withDescription("Milestone title"),
|
|
3970
|
+ |
);
|
|
3971
|
+ |
const milestoneDescriptionFlag = Flag.string("description").pipe(
|
|
3972
|
+ |
Flag.optional,
|
|
3973
|
+ |
Flag.withDescription("What the milestone is for"),
|
|
3974
|
+ |
);
|
|
3975
|
+ |
const milestoneDueOnFlag = Flag.string("due-on").pipe(
|
|
3976
|
+ |
Flag.optional,
|
|
3977
|
+ |
Flag.withDescription("Due date, as the server stores it"),
|
|
3978
|
+ |
);
|
|
3979
|
+ |
|
|
3980
|
+ |
const milestoneCreateCommand = Command.make(
|
|
3981
|
+ |
"create",
|
|
3982
|
+ |
{
|
|
3983
|
+ |
title: milestoneTitleArgument,
|
|
3984
|
+ |
repo: repositoryOverrideFlag,
|
|
3985
|
+ |
description: milestoneDescriptionFlag,
|
|
3986
|
+ |
dueOn: milestoneDueOnFlag,
|
|
3987
|
+ |
},
|
|
3988
|
+ |
({ description, dueOn, repo, title }) =>
|
|
3989
|
+ |
Effect.gen(function* () {
|
|
3990
|
+ |
if (title.trim() === "") {
|
|
3991
|
+ |
return yield* new InputError({ message: "Pass the milestone title." });
|
|
3992
|
+ |
}
|
|
3993
|
+ |
const flags = yield* rootCommand;
|
|
3994
|
+ |
const session = yield* resolveApiSession(endpointOverrides(flags));
|
|
3995
|
+ |
const target = yield* resolveTrackerTarget(repo, session.endpoint.origin);
|
|
3996
|
+ |
const issues = yield* IssueClient;
|
|
3997
|
+ |
const output = yield* Output;
|
|
3998
|
+ |
const value = yield* issues.createMilestone({
|
|
3999
|
+ |
origin: session.endpoint.origin,
|
|
4000
|
+ |
token: session.token,
|
|
4001
|
+ |
...target,
|
|
4002
|
+ |
title,
|
|
4003
|
+ |
...(Option.isNone(description) ? {} : { description: description.value }),
|
|
4004
|
+ |
...(Option.isNone(dueOn) ? {} : { dueOn: dueOn.value }),
|
|
4005
|
+ |
});
|
|
4006
|
+ |
// The server assigns the number. Printing the one it returned is how the
|
|
4007
|
+ |
// caller learns what to pass to `issue milestone --set`.
|
|
4008
|
+ |
const created = record(value);
|
|
4009
|
+ |
yield* output.write(
|
|
4010
|
+ |
{
|
|
4011
|
+ |
value,
|
|
4012
|
+ |
human: [
|
|
4013
|
+ |
`Opened milestone #${String(created["number"] ?? "?")} ${String(created["title"] ?? "")}`,
|
|
4014
|
+ |
],
|
|
4015
|
+ |
},
|
|
4016
|
+ |
outputMode(flags.json),
|
|
4017
|
+ |
);
|
|
4018
|
+ |
}),
|
|
4019
|
+ |
).pipe(Command.withDescription("Open a new milestone"));
|
|
4020
|
+ |
|
|
4021
|
+ |
const milestoneDeleteCommand = Command.make(
|
|
4022
|
+ |
"delete",
|
|
4023
|
+ |
{ milestone: milestoneNumberArgument, repo: repositoryOverrideFlag },
|
|
4024
|
+ |
({ milestone, repo }) =>
|
|
4025
|
+ |
Effect.gen(function* () {
|
|
4026
|
+ |
const number = yield* parseTrackerNumber("A milestone number", milestone);
|
|
4027
|
+ |
const flags = yield* rootCommand;
|
|
4028
|
+ |
const session = yield* resolveApiSession(endpointOverrides(flags));
|
|
4029
|
+ |
const target = yield* resolveTrackerTarget(repo, session.endpoint.origin);
|
|
4030
|
+ |
const issues = yield* IssueClient;
|
|
4031
|
+ |
const output = yield* Output;
|
|
4032
|
+ |
// A 204 carries no body, so there is nothing to report but the number
|
|
4033
|
+ |
// that was asked for and the fact that the server accepted it.
|
|
4034
|
+ |
const value = yield* issues.deleteMilestone({
|
|
4035
|
+ |
origin: session.endpoint.origin,
|
|
4036
|
+ |
token: session.token,
|
|
4037
|
+ |
...target,
|
|
4038
|
+ |
milestone: number,
|
|
4039
|
+ |
});
|
|
4040
|
+ |
yield* output.write(
|
|
4041
|
+ |
{ value, human: [`Deleted milestone #${String(number)}.`] },
|
|
4042
|
+ |
outputMode(flags.json),
|
|
4043
|
+ |
);
|
|
4044
|
+ |
}),
|
|
4045
|
+ |
).pipe(Command.withDescription("Delete a milestone"));
|
|
4046
|
+ |
|
|
4047
|
+ |
const milestoneCommand = Command.make("milestone").pipe(
|
|
4048
|
+ |
Command.withDescription("Repository milestones"),
|
|
4049
|
+ |
Command.withSubcommands([milestoneListCommand, milestoneCreateCommand, milestoneDeleteCommand]),
|
|
4050
|
+ |
);
|
|
4051
|
+ |
|
|
4052
|
+ |
const issueMilestoneSetFlag = Flag.string("set").pipe(
|
|
4053
|
+ |
Flag.optional,
|
|
4054
|
+ |
Flag.withDescription("Milestone number to put the issue on"),
|
|
4055
|
+ |
);
|
|
4056
|
+ |
const issueMilestoneClearFlag = Flag.boolean("clear").pipe(
|
|
4057
|
+ |
Flag.withDescription("Take the issue off whatever milestone it is on"),
|
|
4058
|
+ |
);
|
|
4059
|
+ |
|
|
4060
|
+ |
const issueMilestoneCommand = Command.make(
|
|
4061
|
+ |
"milestone",
|
|
4062
|
+ |
{
|
|
4063
|
+ |
number: issueNumberArgument,
|
|
4064
|
+ |
repo: repositoryOverrideFlag,
|
|
4065
|
+ |
set: issueMilestoneSetFlag,
|
|
4066
|
+ |
clear: issueMilestoneClearFlag,
|
|
4067
|
+ |
},
|
|
4068
|
+ |
({ clear, number, repo, set }) =>
|
|
4069
|
+ |
Effect.gen(function* () {
|
|
4070
|
+ |
// Two ways to say what the milestone should become, and they disagree.
|
|
4071
|
+ |
// Guessing which was meant is how an issue lands on a milestone the
|
|
4072
|
+ |
// caller was trying to take it off.
|
|
4073
|
+ |
if (Option.isSome(set) && clear) {
|
|
4074
|
+ |
return yield* new InputError({ message: "Use either --set or --clear, not both." });
|
|
4075
|
+ |
}
|
|
4076
|
+ |
if (Option.isNone(set) && !clear) {
|
|
4077
|
+ |
return yield* new InputError({
|
|
4078
|
+ |
message:
|
|
4079
|
+ |
"Say what the milestone should become: --set <number> to put the issue on one, or --clear to take it off.",
|
|
4080
|
+ |
});
|
|
4081
|
+ |
}
|
|
4082
|
+ |
const issueNumber = yield* parseTrackerNumber("An issue number", number);
|
|
4083
|
+ |
const milestone = Option.isNone(set)
|
|
4084
|
+ |
? null
|
|
4085
|
+ |
: yield* parseTrackerNumber("A milestone number", set.value);
|
|
4086
|
+ |
const flags = yield* rootCommand;
|
|
4087
|
+ |
const session = yield* resolveApiSession(endpointOverrides(flags));
|
|
4088
|
+ |
const target = yield* resolveTrackerTarget(repo, session.endpoint.origin);
|
|
4089
|
+ |
const issues = yield* IssueClient;
|
|
4090
|
+ |
const output = yield* Output;
|
|
4091
|
+ |
const value = yield* issues.setMilestone({
|
|
4092
|
+ |
origin: session.endpoint.origin,
|
|
4093
|
+ |
token: session.token,
|
|
4094
|
+ |
...target,
|
|
4095
|
+ |
number: issueNumber,
|
|
4096
|
+ |
milestone,
|
|
4097
|
+ |
});
|
|
4098
|
+ |
// Report what came BACK, not what was asked for. A server that accepted
|
|
4099
|
+ |
// the request and stored something else is exactly what an echo hides.
|
|
4100
|
+ |
const stored = record(value)["milestone"];
|
|
4101
|
+ |
const human =
|
|
4102
|
+ |
stored === null || stored === undefined
|
|
4103
|
+ |
? `Issue #${String(issueNumber)} is on no milestone.`
|
|
4104
|
+ |
: `Issue #${String(issueNumber)} is on milestone #${String(
|
|
4105
|
+ |
record(stored)["number"] ?? "?",
|
|
4106
|
+ |
)} ${String(record(stored)["title"] ?? "")}`;
|
|
4107
|
+ |
yield* output.write({ value, human: [human] }, outputMode(flags.json));
|
|
4108
|
+ |
}),
|
|
4109
|
+ |
).pipe(Command.withDescription("Put an existing issue on a milestone, or take it off one"));
|
|
4110
|
+ |
|
| 3917 |
4111
|
|
const issueCommand = Command.make("issue").pipe(
|
| 3918 |
4112
|
|
Command.withDescription("Read and write issues"),
|
| 3919 |
4113
|
|
Command.withSubcommands([
|