lalph 0.3.35 → 0.3.36
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.mjs +2264 -1202
- package/package.json +3 -3
- package/src/Github/TokenManager.ts +1 -1
- package/src/Github.ts +260 -16
- package/src/commands/root.ts +4 -2
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lalph",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.36",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
7
7
|
},
|
|
@@ -23,13 +23,13 @@
|
|
|
23
23
|
"@changesets/changelog-github": "^0.5.2",
|
|
24
24
|
"@changesets/cli": "^2.29.8",
|
|
25
25
|
"@effect/language-service": "^0.75.1",
|
|
26
|
-
"@effect/platform-node": "4.0.0-beta.
|
|
26
|
+
"@effect/platform-node": "4.0.0-beta.28",
|
|
27
27
|
"@linear/sdk": "^75.0.0",
|
|
28
28
|
"@octokit/plugin-rest-endpoint-methods": "^17.0.0",
|
|
29
29
|
"@octokit/types": "^16.0.0",
|
|
30
30
|
"@typescript/native-preview": "7.0.0-dev.20260219.1",
|
|
31
31
|
"concurrently": "^9.2.1",
|
|
32
|
-
"effect": "4.0.0-beta.
|
|
32
|
+
"effect": "4.0.0-beta.28",
|
|
33
33
|
"husky": "^9.1.7",
|
|
34
34
|
"lint-staged": "^16.2.7",
|
|
35
35
|
"octokit": "^5.0.5",
|
package/src/Github.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Api } from "@octokit/plugin-rest-endpoint-methods"
|
|
2
2
|
import type { OctokitResponse } from "@octokit/types"
|
|
3
3
|
import {
|
|
4
|
+
Array,
|
|
4
5
|
Cache,
|
|
5
6
|
Data,
|
|
6
7
|
DateTime,
|
|
@@ -14,6 +15,7 @@ import {
|
|
|
14
15
|
ServiceMap,
|
|
15
16
|
Stream,
|
|
16
17
|
String,
|
|
18
|
+
Unify,
|
|
17
19
|
} from "effect"
|
|
18
20
|
import { Octokit } from "octokit"
|
|
19
21
|
import { IssueSource, IssueSourceError } from "./IssueSource.ts"
|
|
@@ -39,6 +41,10 @@ export interface GithubService {
|
|
|
39
41
|
readonly request: <A>(
|
|
40
42
|
f: (_: GithubApi) => Promise<A>,
|
|
41
43
|
) => Effect.Effect<A, GithubError, never>
|
|
44
|
+
readonly graphql: <A>(
|
|
45
|
+
query: string,
|
|
46
|
+
variables?: Record<string, unknown>,
|
|
47
|
+
) => Effect.Effect<A, GithubError, never>
|
|
42
48
|
readonly wrap: <A, Args extends Array<unknown>>(
|
|
43
49
|
f: (_: GithubApi) => (...args: Args) => Promise<GithubResponse<A>>,
|
|
44
50
|
) => (...args: Args) => Effect.Effect<A, GithubError, never>
|
|
@@ -107,7 +113,7 @@ export class Github extends ServiceMap.Service<Github, GithubService>()(
|
|
|
107
113
|
}
|
|
108
114
|
})
|
|
109
115
|
|
|
110
|
-
return octokit
|
|
116
|
+
return octokit
|
|
111
117
|
}),
|
|
112
118
|
idleTimeToLive: "1 minute",
|
|
113
119
|
})
|
|
@@ -118,9 +124,9 @@ export class Github extends ServiceMap.Service<Github, GithubService>()(
|
|
|
118
124
|
|
|
119
125
|
const request = <A>(f: (_: GithubApi) => Promise<A>) =>
|
|
120
126
|
getClient.pipe(
|
|
121
|
-
Effect.flatMap((
|
|
127
|
+
Effect.flatMap((client) =>
|
|
122
128
|
Effect.tryPromise({
|
|
123
|
-
try: () => f(rest),
|
|
129
|
+
try: () => f(client.rest),
|
|
124
130
|
catch: (cause) => new GithubError({ cause }),
|
|
125
131
|
}),
|
|
126
132
|
),
|
|
@@ -128,15 +134,27 @@ export class Github extends ServiceMap.Service<Github, GithubService>()(
|
|
|
128
134
|
Effect.withSpan("Github.request"),
|
|
129
135
|
)
|
|
130
136
|
|
|
137
|
+
const graphql = <A>(query: string, variables?: Record<string, unknown>) =>
|
|
138
|
+
getClient.pipe(
|
|
139
|
+
Effect.flatMap((client) =>
|
|
140
|
+
Effect.tryPromise({
|
|
141
|
+
try: () => client.graphql<A>(query, variables),
|
|
142
|
+
catch: (cause) => new GithubError({ cause }),
|
|
143
|
+
}),
|
|
144
|
+
),
|
|
145
|
+
Effect.scoped,
|
|
146
|
+
Effect.withSpan("Github.graphql"),
|
|
147
|
+
)
|
|
148
|
+
|
|
131
149
|
const wrap =
|
|
132
150
|
<A, Args extends Array<unknown>>(
|
|
133
151
|
f: (_: GithubApi) => (...args: Args) => Promise<OctokitResponse<A>>,
|
|
134
152
|
) =>
|
|
135
153
|
(...args: Args) =>
|
|
136
154
|
getClient.pipe(
|
|
137
|
-
Effect.flatMap((
|
|
155
|
+
Effect.flatMap((client) =>
|
|
138
156
|
Effect.tryPromise({
|
|
139
|
-
try: () => f(rest)(...args),
|
|
157
|
+
try: () => f(client.rest)(...args),
|
|
140
158
|
catch: (cause) => new GithubError({ cause }),
|
|
141
159
|
}),
|
|
142
160
|
),
|
|
@@ -150,9 +168,9 @@ export class Github extends ServiceMap.Service<Github, GithubService>()(
|
|
|
150
168
|
) =>
|
|
151
169
|
Stream.paginate(0, (page) =>
|
|
152
170
|
getClient.pipe(
|
|
153
|
-
Effect.flatMap((
|
|
171
|
+
Effect.flatMap((client) =>
|
|
154
172
|
Effect.tryPromise({
|
|
155
|
-
try: () => f(rest, page),
|
|
173
|
+
try: () => f(client.rest, page),
|
|
156
174
|
catch: (cause) => new GithubError({ cause }),
|
|
157
175
|
}),
|
|
158
176
|
),
|
|
@@ -163,7 +181,7 @@ export class Github extends ServiceMap.Service<Github, GithubService>()(
|
|
|
163
181
|
),
|
|
164
182
|
)
|
|
165
183
|
|
|
166
|
-
return { request, wrap, stream } as const
|
|
184
|
+
return { request, graphql, wrap, stream } as const
|
|
167
185
|
}),
|
|
168
186
|
},
|
|
169
187
|
) {
|
|
@@ -181,8 +199,9 @@ export const GithubIssueSource = Layer.effect(
|
|
|
181
199
|
lookup: Effect.fnUntraced(
|
|
182
200
|
function* (_projectId: ProjectId) {
|
|
183
201
|
const labelFilter = yield* getOrSelectLabel
|
|
202
|
+
const projectFilter = yield* getOrSelectProjectFilter
|
|
184
203
|
const autoMergeLabelName = yield* getOrSelectAutoMergeLabel
|
|
185
|
-
return { labelFilter, autoMergeLabelName } as const
|
|
204
|
+
return { labelFilter, projectFilter, autoMergeLabelName } as const
|
|
186
205
|
},
|
|
187
206
|
Effect.orDie,
|
|
188
207
|
(effect, projectId) =>
|
|
@@ -236,9 +255,94 @@ export const GithubIssueSource = Layer.effect(
|
|
|
236
255
|
const presets = yield* getPresetsWithMetadata("github", PresetMetadata)
|
|
237
256
|
const issuePresetMap = new Map<string, CliAgentPreset>()
|
|
238
257
|
|
|
239
|
-
const
|
|
258
|
+
const projects = Stream.paginate(null, (cursor: string | null) =>
|
|
259
|
+
github
|
|
260
|
+
.graphql<GithubProjectsQuery>(githubProjectsQuery, {
|
|
261
|
+
after: cursor,
|
|
262
|
+
})
|
|
263
|
+
.pipe(
|
|
264
|
+
Effect.map((data) => [
|
|
265
|
+
data.viewer.projectsV2.nodes,
|
|
266
|
+
Option.fromNullOr(data.viewer.projectsV2.pageInfo.endCursor),
|
|
267
|
+
]),
|
|
268
|
+
),
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
const listProjects = Stream.runCollect(projects).pipe(
|
|
272
|
+
Effect.map((a) =>
|
|
273
|
+
a.sort((left, right) => left.title.localeCompare(right.title)),
|
|
274
|
+
),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
const projectFilterSelect = Effect.gen(function* () {
|
|
278
|
+
const projects = yield* listProjects
|
|
279
|
+
const selectedProject = yield* Prompt.autoComplete({
|
|
280
|
+
message: "Select a GitHub project to filter issues by",
|
|
281
|
+
choices: [
|
|
282
|
+
{
|
|
283
|
+
title: "No project",
|
|
284
|
+
value: Option.none<GithubProject>(),
|
|
285
|
+
},
|
|
286
|
+
].concat(
|
|
287
|
+
projects.map((project) => ({
|
|
288
|
+
title: `#${project.number} ${project.title}`,
|
|
289
|
+
description: project.url,
|
|
290
|
+
value: Option.some(project),
|
|
291
|
+
})),
|
|
292
|
+
),
|
|
293
|
+
})
|
|
294
|
+
yield* Settings.setProject(
|
|
295
|
+
selectedProjectFilter,
|
|
296
|
+
Option.some(selectedProject),
|
|
297
|
+
)
|
|
298
|
+
return selectedProject
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
const getOrSelectProjectFilter = Effect.gen(function* () {
|
|
302
|
+
const project = yield* Settings.getProject(selectedProjectFilter)
|
|
303
|
+
if (Option.isSome(project)) {
|
|
304
|
+
return project.value
|
|
305
|
+
}
|
|
306
|
+
return yield* projectFilterSelect
|
|
307
|
+
})
|
|
308
|
+
|
|
309
|
+
const repository = `${cli.owner}/${cli.repo}`.toLowerCase()
|
|
310
|
+
|
|
311
|
+
const projectIssues = (project: GithubProject) => {
|
|
312
|
+
const threeDaysAgo = DateTime.nowUnsafe().pipe(
|
|
313
|
+
DateTime.subtract({ days: 3 }),
|
|
314
|
+
)
|
|
315
|
+
return Stream.paginate(null, (cursor: string | null) =>
|
|
316
|
+
github
|
|
317
|
+
.graphql<GithubProjectItemsQuery>(githubProjectItemsQuery, {
|
|
318
|
+
projectId: project.id,
|
|
319
|
+
after: cursor,
|
|
320
|
+
})
|
|
321
|
+
.pipe(
|
|
322
|
+
Effect.map((data) => [
|
|
323
|
+
data.node.items.nodes.map((item) => item.content),
|
|
324
|
+
Option.fromNullOr(data.node.items.pageInfo.endCursor),
|
|
325
|
+
]),
|
|
326
|
+
),
|
|
327
|
+
).pipe(
|
|
328
|
+
Stream.filter(
|
|
329
|
+
(_) =>
|
|
330
|
+
_.repository.nameWithOwner.toLowerCase() === repository &&
|
|
331
|
+
(!_.closedAt ||
|
|
332
|
+
DateTime.makeUnsafe(_.closedAt).pipe(
|
|
333
|
+
DateTime.isGreaterThan(threeDaysAgo),
|
|
334
|
+
)),
|
|
335
|
+
),
|
|
336
|
+
Stream.map((issue) => ({
|
|
337
|
+
...issue,
|
|
338
|
+
state: issue.state.toLowerCase(),
|
|
339
|
+
labels: issue.labels.nodes.map((label) => label.name),
|
|
340
|
+
})),
|
|
341
|
+
)
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const repoIssues = (options: {
|
|
240
345
|
readonly labelFilter: Option.Option<string>
|
|
241
|
-
readonly autoMergeLabelName: Option.Option<string>
|
|
242
346
|
}) =>
|
|
243
347
|
pipe(
|
|
244
348
|
github.stream((rest, page) =>
|
|
@@ -253,6 +357,21 @@ export const GithubIssueSource = Layer.effect(
|
|
|
253
357
|
),
|
|
254
358
|
Stream.merge(recentlyClosed),
|
|
255
359
|
Stream.filter((issue) => issue.pull_request === undefined),
|
|
360
|
+
)
|
|
361
|
+
|
|
362
|
+
const issues = (options: {
|
|
363
|
+
readonly labelFilter: Option.Option<string>
|
|
364
|
+
readonly projectFilter: Option.Option<GithubProject>
|
|
365
|
+
readonly autoMergeLabelName: Option.Option<string>
|
|
366
|
+
}) => {
|
|
367
|
+
const source = Unify.unify(
|
|
368
|
+
Option.isSome(options.projectFilter)
|
|
369
|
+
? projectIssues(options.projectFilter.value)
|
|
370
|
+
: repoIssues(options),
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
return pipe(
|
|
374
|
+
source,
|
|
256
375
|
Stream.mapEffect(
|
|
257
376
|
Effect.fnUntraced(function* (issue) {
|
|
258
377
|
const id = `#${issue.number}`
|
|
@@ -294,6 +413,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
294
413
|
Stream.runCollect,
|
|
295
414
|
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
296
415
|
)
|
|
416
|
+
}
|
|
297
417
|
|
|
298
418
|
const createIssue = github.wrap((rest) => rest.issues.create)
|
|
299
419
|
const updateIssue = github.wrap((rest) => rest.issues.update)
|
|
@@ -362,7 +482,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
362
482
|
],
|
|
363
483
|
})
|
|
364
484
|
|
|
365
|
-
const blockedByNumbers = Array.
|
|
485
|
+
const blockedByNumbers = Array.fromIterable(
|
|
366
486
|
new Set(
|
|
367
487
|
issue.blockedBy
|
|
368
488
|
.map((id) => Number(id.slice(1)))
|
|
@@ -405,7 +525,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
405
525
|
issue_number: issueNumber,
|
|
406
526
|
}),
|
|
407
527
|
)
|
|
408
|
-
const labels = Array.
|
|
528
|
+
const labels = Array.fromIterable(
|
|
409
529
|
new Set([
|
|
410
530
|
...currentIssue.data.labels.flatMap((label) =>
|
|
411
531
|
typeof label === "string"
|
|
@@ -531,6 +651,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
531
651
|
),
|
|
532
652
|
reset: Effect.gen(function* () {
|
|
533
653
|
const projectId = yield* CurrentProjectId
|
|
654
|
+
yield* Settings.setProject(selectedProjectFilter, Option.none())
|
|
534
655
|
yield* Settings.setProject(labelFilter, Option.none())
|
|
535
656
|
yield* Settings.setProject(autoMergeLabel, Option.none())
|
|
536
657
|
yield* Cache.invalidate(projectSettings, projectId)
|
|
@@ -538,9 +659,13 @@ export const GithubIssueSource = Layer.effect(
|
|
|
538
659
|
settings: (projectId) =>
|
|
539
660
|
Effect.asVoid(Cache.get(projectSettings, projectId)),
|
|
540
661
|
info: Effect.fnUntraced(function* (projectId) {
|
|
541
|
-
const { labelFilter, autoMergeLabelName } =
|
|
542
|
-
projectSettings,
|
|
543
|
-
|
|
662
|
+
const { labelFilter, projectFilter, autoMergeLabelName } =
|
|
663
|
+
yield* Cache.get(projectSettings, projectId)
|
|
664
|
+
console.log(
|
|
665
|
+
` GitHub project: ${Option.match(projectFilter, {
|
|
666
|
+
onNone: () => "None",
|
|
667
|
+
onSome: (value) => `#${value.number} ${value.title}`,
|
|
668
|
+
})}`,
|
|
544
669
|
)
|
|
545
670
|
console.log(
|
|
546
671
|
` Label filter: ${Option.match(labelFilter, {
|
|
@@ -611,6 +736,21 @@ export class GithubRepoNotFound extends Data.TaggedError("GithubRepoNotFound") {
|
|
|
611
736
|
readonly message = "GitHub repository not found"
|
|
612
737
|
}
|
|
613
738
|
|
|
739
|
+
// == project filter
|
|
740
|
+
|
|
741
|
+
const GithubProject = Schema.Struct({
|
|
742
|
+
id: Schema.String,
|
|
743
|
+
number: Schema.Number,
|
|
744
|
+
title: Schema.String,
|
|
745
|
+
url: Schema.String,
|
|
746
|
+
})
|
|
747
|
+
type GithubProject = typeof GithubProject.Type
|
|
748
|
+
|
|
749
|
+
const selectedProjectFilter = new ProjectSetting(
|
|
750
|
+
"github.projectFilter",
|
|
751
|
+
Schema.Option(GithubProject),
|
|
752
|
+
)
|
|
753
|
+
|
|
614
754
|
// == label filter
|
|
615
755
|
|
|
616
756
|
const labelFilter = new ProjectSetting(
|
|
@@ -667,8 +807,112 @@ const PresetMetadata = Schema.Struct({
|
|
|
667
807
|
label: Schema.NonEmptyString,
|
|
668
808
|
})
|
|
669
809
|
|
|
810
|
+
// == project helpers
|
|
811
|
+
|
|
812
|
+
type GithubProjectsQuery = {
|
|
813
|
+
readonly viewer: {
|
|
814
|
+
readonly projectsV2: {
|
|
815
|
+
readonly nodes: ReadonlyArray<{
|
|
816
|
+
readonly id: string
|
|
817
|
+
readonly number: number
|
|
818
|
+
readonly title: string
|
|
819
|
+
readonly closed: boolean
|
|
820
|
+
readonly url: string
|
|
821
|
+
}>
|
|
822
|
+
readonly pageInfo: {
|
|
823
|
+
readonly endCursor: string | null
|
|
824
|
+
readonly hasNextPage: boolean
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
type GithubProjectItemsQuery = {
|
|
831
|
+
readonly node: {
|
|
832
|
+
readonly items: {
|
|
833
|
+
readonly nodes: ReadonlyArray<{
|
|
834
|
+
readonly content: {
|
|
835
|
+
readonly __typename: string
|
|
836
|
+
readonly number: number
|
|
837
|
+
readonly repository: {
|
|
838
|
+
readonly nameWithOwner: string
|
|
839
|
+
}
|
|
840
|
+
readonly title: string
|
|
841
|
+
readonly body: string
|
|
842
|
+
readonly state: string
|
|
843
|
+
readonly labels: {
|
|
844
|
+
readonly nodes: ReadonlyArray<{
|
|
845
|
+
readonly name: string
|
|
846
|
+
}>
|
|
847
|
+
}
|
|
848
|
+
readonly closedAt: string | null
|
|
849
|
+
}
|
|
850
|
+
}>
|
|
851
|
+
readonly pageInfo: {
|
|
852
|
+
readonly endCursor: string | null
|
|
853
|
+
readonly hasNextPage: boolean
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
|
|
670
859
|
// == helpers
|
|
671
860
|
|
|
861
|
+
const githubProjectsQuery = `
|
|
862
|
+
query GithubProjects($after: String) {
|
|
863
|
+
viewer {
|
|
864
|
+
projectsV2(first: 100, after: $after) {
|
|
865
|
+
nodes {
|
|
866
|
+
id
|
|
867
|
+
number
|
|
868
|
+
title
|
|
869
|
+
closed
|
|
870
|
+
url
|
|
871
|
+
}
|
|
872
|
+
pageInfo {
|
|
873
|
+
endCursor
|
|
874
|
+
hasNextPage
|
|
875
|
+
}
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
`
|
|
880
|
+
|
|
881
|
+
const githubProjectItemsQuery = `
|
|
882
|
+
query GithubProjectItems($projectId: ID!, $after: String) {
|
|
883
|
+
node(id: $projectId) {
|
|
884
|
+
... on ProjectV2 {
|
|
885
|
+
items(first: 100, after: $after) {
|
|
886
|
+
nodes {
|
|
887
|
+
content {
|
|
888
|
+
__typename
|
|
889
|
+
... on Issue {
|
|
890
|
+
number
|
|
891
|
+
repository {
|
|
892
|
+
nameWithOwner
|
|
893
|
+
}
|
|
894
|
+
labels(first: 20) {
|
|
895
|
+
nodes {
|
|
896
|
+
name
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
title
|
|
900
|
+
body
|
|
901
|
+
state
|
|
902
|
+
closedAt
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
pageInfo {
|
|
907
|
+
endCursor
|
|
908
|
+
hasNextPage
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
`
|
|
915
|
+
|
|
672
916
|
const maybeNextPage = (page: number, linkHeader?: string) =>
|
|
673
917
|
pipe(
|
|
674
918
|
Option.fromNullishOr(linkHeader),
|
package/src/commands/root.ts
CHANGED
|
@@ -381,9 +381,11 @@ export const commandRoot = Command.make("lalph", {
|
|
|
381
381
|
iterations,
|
|
382
382
|
maxIterationMinutes,
|
|
383
383
|
stallMinutes,
|
|
384
|
-
specsDirectory,
|
|
385
|
-
verbose,
|
|
386
384
|
}).pipe(
|
|
385
|
+
Command.withSharedFlags({
|
|
386
|
+
specsDirectory,
|
|
387
|
+
verbose,
|
|
388
|
+
}),
|
|
387
389
|
Command.withDescription(
|
|
388
390
|
"Run the task loop across all enabled projects in parallel: pull issues from the current issue source and execute them with your configured agent preset(s). Use --iterations for a bounded run, and configure per-project concurrency via lalph projects edit.",
|
|
389
391
|
),
|