lalph 0.1.113 → 0.2.0
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 +4975 -3512
- package/package.json +1 -1
- package/src/GitFlow.ts +6 -3
- package/src/Github.ts +97 -40
- package/src/IssueSource.ts +41 -6
- package/src/IssueSources.ts +32 -34
- package/src/Kvs.ts +13 -1
- package/src/Linear.ts +138 -63
- package/src/Prd.ts +14 -6
- package/src/Projects.ts +134 -0
- package/src/Settings.ts +202 -14
- package/src/Workers.ts +24 -16
- package/src/cli.ts +9 -13
- package/src/commands/agent.ts +4 -4
- package/src/commands/edit.ts +27 -17
- package/src/commands/issue.ts +19 -4
- package/src/commands/plan.ts +6 -4
- package/src/commands/projects/add.ts +64 -0
- package/src/commands/projects/edit.ts +62 -0
- package/src/commands/projects/ls.ts +38 -0
- package/src/commands/projects/rm.ts +22 -0
- package/src/commands/projects/toggle.ts +37 -0
- package/src/commands/projects.ts +24 -0
- package/src/commands/root.ts +124 -152
- package/src/commands/{shell.ts → sh.ts} +10 -3
- package/src/domain/Project.ts +22 -0
- package/src/domain/WorkerState.ts +9 -4
package/package.json
CHANGED
package/src/GitFlow.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { CurrentWorkerState } from "./Workers.ts"
|
|
|
7
7
|
import { Atom } from "effect/unstable/reactivity"
|
|
8
8
|
import { parseBranch } from "./shared/git.ts"
|
|
9
9
|
import { AtomRegistry } from "effect/unstable/reactivity"
|
|
10
|
+
import { CurrentProjectId } from "./Settings.ts"
|
|
10
11
|
|
|
11
12
|
// @effect-diagnostics-next-line leakingRequirements:off
|
|
12
13
|
export class GitFlow extends ServiceMap.Service<
|
|
@@ -30,7 +31,7 @@ export class GitFlow extends ServiceMap.Service<
|
|
|
30
31
|
}) => Effect.Effect<
|
|
31
32
|
void,
|
|
32
33
|
IssueSourceError | PlatformError | GitFlowError,
|
|
33
|
-
Prd | IssueSource
|
|
34
|
+
Prd | IssueSource | CurrentProjectId
|
|
34
35
|
>
|
|
35
36
|
readonly autoMerge: (options: {
|
|
36
37
|
readonly targetBranch: string | undefined
|
|
@@ -39,7 +40,7 @@ export class GitFlow extends ServiceMap.Service<
|
|
|
39
40
|
}) => Effect.Effect<
|
|
40
41
|
void,
|
|
41
42
|
IssueSourceError | PlatformError | GitFlowError,
|
|
42
|
-
Prd | IssueSource
|
|
43
|
+
Prd | IssueSource | CurrentProjectId
|
|
43
44
|
>
|
|
44
45
|
}
|
|
45
46
|
>()("lalph/GitFlow") {}
|
|
@@ -115,7 +116,7 @@ export const GitFlowCommit = Layer.effect(
|
|
|
115
116
|
|
|
116
117
|
return GitFlow.of({
|
|
117
118
|
requiresGithubPr: false,
|
|
118
|
-
branch: `lalph/worker-${workerState.
|
|
119
|
+
branch: `lalph/worker-${workerState.id}`,
|
|
119
120
|
|
|
120
121
|
setupInstructions: () =>
|
|
121
122
|
`You are already on a new branch for this task. You do not need to checkout any other branches.`,
|
|
@@ -161,12 +162,14 @@ After making any changes, commit them to the same branch. Do not git push your c
|
|
|
161
162
|
}),
|
|
162
163
|
autoMerge: Effect.fnUntraced(function* (options) {
|
|
163
164
|
const prd = yield* Prd
|
|
165
|
+
const projectId = yield* CurrentProjectId
|
|
164
166
|
const issue = yield* prd.findById(options.issueId)
|
|
165
167
|
if (!issue || issue.state !== "in-review") {
|
|
166
168
|
return
|
|
167
169
|
}
|
|
168
170
|
const source = yield* IssueSource
|
|
169
171
|
yield* source.updateIssue({
|
|
172
|
+
projectId,
|
|
170
173
|
issueId: options.issueId,
|
|
171
174
|
state: "done",
|
|
172
175
|
})
|
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
|
+
Cache,
|
|
4
5
|
Data,
|
|
5
6
|
DateTime,
|
|
6
7
|
Effect,
|
|
@@ -17,11 +18,12 @@ import {
|
|
|
17
18
|
import { Octokit } from "octokit"
|
|
18
19
|
import { IssueSource, IssueSourceError } from "./IssueSource.ts"
|
|
19
20
|
import { PrdIssue } from "./domain/PrdIssue.ts"
|
|
20
|
-
import {
|
|
21
|
+
import { CurrentProjectId, ProjectSetting, Settings } from "./Settings.ts"
|
|
21
22
|
import { Prompt } from "effect/unstable/cli"
|
|
22
23
|
import { TokenManager } from "./Github/TokenManager.ts"
|
|
23
24
|
import { GithubCli } from "./Github/Cli.ts"
|
|
24
25
|
import { Reactivity } from "effect/unstable/reactivity"
|
|
26
|
+
import type { ProjectId } from "./domain/Project.ts"
|
|
25
27
|
|
|
26
28
|
export class GithubError extends Data.TaggedError("GithubError")<{
|
|
27
29
|
readonly cause: unknown
|
|
@@ -100,8 +102,19 @@ export const GithubIssueSource = Layer.effect(
|
|
|
100
102
|
Effect.gen(function* () {
|
|
101
103
|
const github = yield* Github
|
|
102
104
|
const cli = yield* GithubCli
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
+
const projectSettings = yield* Cache.make({
|
|
106
|
+
lookup: Effect.fnUntraced(
|
|
107
|
+
function* (_projectId: ProjectId) {
|
|
108
|
+
const labelFilter = yield* getOrSelectLabel
|
|
109
|
+
const autoMergeLabelName = yield* getOrSelectAutoMergeLabel
|
|
110
|
+
return { labelFilter, autoMergeLabelName } as const
|
|
111
|
+
},
|
|
112
|
+
Effect.orDie,
|
|
113
|
+
(effect, projectId) =>
|
|
114
|
+
Effect.provideService(effect, CurrentProjectId, projectId),
|
|
115
|
+
),
|
|
116
|
+
capacity: Number.POSITIVE_INFINITY,
|
|
117
|
+
})
|
|
105
118
|
|
|
106
119
|
const hasLabel = (
|
|
107
120
|
label: ReadonlyArray<
|
|
@@ -115,8 +128,8 @@ export const GithubIssueSource = Layer.effect(
|
|
|
115
128
|
label.some((l) => (typeof l === "string" ? l === name : l.name === name))
|
|
116
129
|
|
|
117
130
|
const listOpenBlockedBy = (issueId: number) =>
|
|
118
|
-
|
|
119
|
-
.stream((rest, page) =>
|
|
131
|
+
pipe(
|
|
132
|
+
github.stream((rest, page) =>
|
|
120
133
|
rest.issues.listDependenciesBlockedBy({
|
|
121
134
|
owner: cli.owner,
|
|
122
135
|
repo: cli.repo,
|
|
@@ -124,38 +137,42 @@ export const GithubIssueSource = Layer.effect(
|
|
|
124
137
|
per_page: 100,
|
|
125
138
|
page,
|
|
126
139
|
}),
|
|
127
|
-
)
|
|
128
|
-
|
|
140
|
+
),
|
|
141
|
+
Stream.filter((issue) => issue.state === "open"),
|
|
142
|
+
)
|
|
129
143
|
|
|
130
|
-
const recentlyClosed =
|
|
131
|
-
.stream((rest, page) =>
|
|
144
|
+
const recentlyClosed = pipe(
|
|
145
|
+
github.stream((rest, page) =>
|
|
132
146
|
rest.issues.listForRepo({
|
|
133
147
|
owner: cli.owner,
|
|
134
148
|
repo: cli.repo,
|
|
135
149
|
state: "closed",
|
|
136
150
|
per_page: 100,
|
|
137
151
|
page,
|
|
138
|
-
labels: Option.getOrUndefined(labelFilter),
|
|
139
152
|
since: DateTime.nowUnsafe().pipe(
|
|
140
153
|
DateTime.subtract({ days: 3 }),
|
|
141
154
|
DateTime.formatIso,
|
|
142
155
|
),
|
|
143
156
|
}),
|
|
144
|
-
)
|
|
145
|
-
|
|
157
|
+
),
|
|
158
|
+
Stream.filter((issue) => issue.state_reason !== "not_planned"),
|
|
159
|
+
)
|
|
146
160
|
|
|
147
|
-
const issues =
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
161
|
+
const issues = (options: {
|
|
162
|
+
readonly labelFilter: Option.Option<string>
|
|
163
|
+
readonly autoMergeLabelName: Option.Option<string>
|
|
164
|
+
}) =>
|
|
165
|
+
pipe(
|
|
166
|
+
github.stream((rest, page) =>
|
|
167
|
+
rest.issues.listForRepo({
|
|
168
|
+
owner: cli.owner,
|
|
169
|
+
repo: cli.repo,
|
|
170
|
+
state: "open",
|
|
171
|
+
per_page: 100,
|
|
172
|
+
page,
|
|
173
|
+
labels: Option.getOrUndefined(options.labelFilter),
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
159
176
|
Stream.merge(recentlyClosed),
|
|
160
177
|
Stream.filter((issue) => issue.pull_request === undefined),
|
|
161
178
|
Stream.mapEffect(
|
|
@@ -179,7 +196,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
179
196
|
estimate: null,
|
|
180
197
|
state,
|
|
181
198
|
blockedBy: dependencies.map((dep) => `#${dep.number}`),
|
|
182
|
-
autoMerge: autoMergeLabelName.pipe(
|
|
199
|
+
autoMerge: options.autoMergeLabelName.pipe(
|
|
183
200
|
Option.map((labelName) => hasLabel(issue.labels, labelName)),
|
|
184
201
|
Option.getOrElse(() => false),
|
|
185
202
|
),
|
|
@@ -237,9 +254,16 @@ export const GithubIssueSource = Layer.effect(
|
|
|
237
254
|
})
|
|
238
255
|
|
|
239
256
|
return yield* IssueSource.make({
|
|
240
|
-
issues
|
|
257
|
+
issues: Effect.fnUntraced(function* (projectId) {
|
|
258
|
+
const settings = yield* Cache.get(projectSettings, projectId)
|
|
259
|
+
return yield* issues(settings)
|
|
260
|
+
}),
|
|
241
261
|
createIssue: Effect.fnUntraced(
|
|
242
|
-
function* (issue
|
|
262
|
+
function* (projectId, issue) {
|
|
263
|
+
const { labelFilter, autoMergeLabelName } = yield* Cache.get(
|
|
264
|
+
projectSettings,
|
|
265
|
+
projectId,
|
|
266
|
+
)
|
|
243
267
|
const created = yield* createIssue({
|
|
244
268
|
owner: cli.owner,
|
|
245
269
|
repo: cli.repo,
|
|
@@ -282,6 +306,10 @@ export const GithubIssueSource = Layer.effect(
|
|
|
282
306
|
),
|
|
283
307
|
updateIssue: Effect.fnUntraced(
|
|
284
308
|
function* (options) {
|
|
309
|
+
const { labelFilter, autoMergeLabelName } = yield* Cache.get(
|
|
310
|
+
projectSettings,
|
|
311
|
+
options.projectId,
|
|
312
|
+
)
|
|
285
313
|
const issueNumber = Number(options.issueId.slice(1))
|
|
286
314
|
const update: {
|
|
287
315
|
owner: string
|
|
@@ -372,7 +400,7 @@ export const GithubIssueSource = Layer.effect(
|
|
|
372
400
|
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
373
401
|
),
|
|
374
402
|
cancelIssue: Effect.fnUntraced(
|
|
375
|
-
function* (issueId
|
|
403
|
+
function* (_project, issueId) {
|
|
376
404
|
yield* updateIssue({
|
|
377
405
|
owner: cli.owner,
|
|
378
406
|
repo: cli.repo,
|
|
@@ -382,8 +410,34 @@ export const GithubIssueSource = Layer.effect(
|
|
|
382
410
|
},
|
|
383
411
|
Effect.mapError((cause) => new IssueSourceError({ cause })),
|
|
384
412
|
),
|
|
413
|
+
reset: Effect.gen(function* () {
|
|
414
|
+
const projectId = yield* CurrentProjectId
|
|
415
|
+
yield* Settings.setProject(labelFilter, Option.none())
|
|
416
|
+
yield* Settings.setProject(autoMergeLabel, Option.none())
|
|
417
|
+
yield* Cache.invalidate(projectSettings, projectId)
|
|
418
|
+
}),
|
|
419
|
+
settings: (projectId) =>
|
|
420
|
+
Effect.asVoid(Cache.get(projectSettings, projectId)),
|
|
421
|
+
info: Effect.fnUntraced(function* (projectId) {
|
|
422
|
+
const { labelFilter, autoMergeLabelName } = yield* Cache.get(
|
|
423
|
+
projectSettings,
|
|
424
|
+
projectId,
|
|
425
|
+
)
|
|
426
|
+
console.log(
|
|
427
|
+
` Label filter: ${Option.match(labelFilter, {
|
|
428
|
+
onNone: () => "None",
|
|
429
|
+
onSome: (value) => value,
|
|
430
|
+
})}`,
|
|
431
|
+
)
|
|
432
|
+
console.log(
|
|
433
|
+
` Auto-merge label: ${Option.match(autoMergeLabelName, {
|
|
434
|
+
onNone: () => "Disabled",
|
|
435
|
+
onSome: (value) => value,
|
|
436
|
+
})}`,
|
|
437
|
+
)
|
|
438
|
+
}),
|
|
385
439
|
ensureInProgress: Effect.fnUntraced(
|
|
386
|
-
function* (issueId
|
|
440
|
+
function* (_project, issueId) {
|
|
387
441
|
const issueNumber = Number(issueId.slice(1))
|
|
388
442
|
yield* pipe(
|
|
389
443
|
github.request((rest) =>
|
|
@@ -403,7 +457,14 @@ export const GithubIssueSource = Layer.effect(
|
|
|
403
457
|
),
|
|
404
458
|
})
|
|
405
459
|
}),
|
|
406
|
-
).pipe(
|
|
460
|
+
).pipe(
|
|
461
|
+
Layer.provide([
|
|
462
|
+
Github.layer,
|
|
463
|
+
GithubCli.layer,
|
|
464
|
+
Reactivity.layer,
|
|
465
|
+
Settings.layer,
|
|
466
|
+
]),
|
|
467
|
+
)
|
|
407
468
|
|
|
408
469
|
export class GithubRepoNotFound extends Data.TaggedError("GithubRepoNotFound") {
|
|
409
470
|
readonly message = "GitHub repository not found"
|
|
@@ -411,7 +472,7 @@ export class GithubRepoNotFound extends Data.TaggedError("GithubRepoNotFound") {
|
|
|
411
472
|
|
|
412
473
|
// == label filter
|
|
413
474
|
|
|
414
|
-
const labelFilter = new
|
|
475
|
+
const labelFilter = new ProjectSetting(
|
|
415
476
|
"github.labelFilter",
|
|
416
477
|
Schema.Option(Schema.String),
|
|
417
478
|
)
|
|
@@ -423,11 +484,11 @@ const labelSelect = Effect.gen(function* () {
|
|
|
423
484
|
const labelOption = Option.some(label.trim()).pipe(
|
|
424
485
|
Option.filter(String.isNonEmpty),
|
|
425
486
|
)
|
|
426
|
-
yield*
|
|
487
|
+
yield* Settings.setProject(labelFilter, Option.some(labelOption))
|
|
427
488
|
return labelOption
|
|
428
489
|
})
|
|
429
490
|
const getOrSelectLabel = Effect.gen(function* () {
|
|
430
|
-
const label = yield* labelFilter
|
|
491
|
+
const label = yield* Settings.getProject(labelFilter)
|
|
431
492
|
if (Option.isSome(label)) {
|
|
432
493
|
return label.value
|
|
433
494
|
}
|
|
@@ -436,7 +497,7 @@ const getOrSelectLabel = Effect.gen(function* () {
|
|
|
436
497
|
|
|
437
498
|
// == auto merge label
|
|
438
499
|
|
|
439
|
-
const autoMergeLabel = new
|
|
500
|
+
const autoMergeLabel = new ProjectSetting(
|
|
440
501
|
"github.autoMergeLabel",
|
|
441
502
|
Schema.Option(Schema.String),
|
|
442
503
|
)
|
|
@@ -448,21 +509,17 @@ const autoMergeLabelSelect = Effect.gen(function* () {
|
|
|
448
509
|
const labelOption = Option.some(label.trim()).pipe(
|
|
449
510
|
Option.filter(String.isNonEmpty),
|
|
450
511
|
)
|
|
451
|
-
yield*
|
|
512
|
+
yield* Settings.setProject(autoMergeLabel, Option.some(labelOption))
|
|
452
513
|
return labelOption
|
|
453
514
|
})
|
|
454
515
|
const getOrSelectAutoMergeLabel = Effect.gen(function* () {
|
|
455
|
-
const label = yield* autoMergeLabel
|
|
516
|
+
const label = yield* Settings.getProject(autoMergeLabel)
|
|
456
517
|
if (Option.isSome(label)) {
|
|
457
518
|
return label.value
|
|
458
519
|
}
|
|
459
520
|
return yield* autoMergeLabelSelect
|
|
460
521
|
})
|
|
461
522
|
|
|
462
|
-
export const resetGithub = labelFilter
|
|
463
|
-
.set(Option.none())
|
|
464
|
-
.pipe(Effect.andThen(autoMergeLabel.set(Option.none())))
|
|
465
|
-
|
|
466
523
|
// == helpers
|
|
467
524
|
|
|
468
525
|
const maybeNextPage = (page: number, linkHeader?: string) =>
|
package/src/IssueSource.ts
CHANGED
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import { Effect, Schema, ServiceMap } from "effect"
|
|
2
2
|
import type { PrdIssue } from "./domain/PrdIssue.ts"
|
|
3
3
|
import { Reactivity } from "effect/unstable/reactivity"
|
|
4
|
+
import type { ProjectId } from "./domain/Project.ts"
|
|
5
|
+
import type { CurrentProjectId, Settings } from "./Settings.ts"
|
|
4
6
|
|
|
5
7
|
export class IssueSource extends ServiceMap.Service<
|
|
6
8
|
IssueSource,
|
|
7
9
|
{
|
|
8
|
-
readonly issues:
|
|
10
|
+
readonly issues: (
|
|
11
|
+
projectId: ProjectId,
|
|
12
|
+
) => Effect.Effect<ReadonlyArray<PrdIssue>, IssueSourceError>
|
|
9
13
|
|
|
10
14
|
readonly createIssue: (
|
|
15
|
+
projectId: ProjectId,
|
|
11
16
|
issue: PrdIssue,
|
|
12
17
|
) => Effect.Effect<{ id: string; url: string }, IssueSourceError>
|
|
13
18
|
|
|
14
19
|
readonly updateIssue: (options: {
|
|
20
|
+
readonly projectId: ProjectId
|
|
15
21
|
readonly issueId: string
|
|
16
22
|
readonly title?: string
|
|
17
23
|
readonly description?: string
|
|
@@ -21,10 +27,24 @@ export class IssueSource extends ServiceMap.Service<
|
|
|
21
27
|
}) => Effect.Effect<void, IssueSourceError>
|
|
22
28
|
|
|
23
29
|
readonly cancelIssue: (
|
|
30
|
+
projectId: ProjectId,
|
|
24
31
|
issueId: string,
|
|
25
32
|
) => Effect.Effect<void, IssueSourceError>
|
|
26
33
|
|
|
34
|
+
readonly reset: Effect.Effect<
|
|
35
|
+
void,
|
|
36
|
+
IssueSourceError,
|
|
37
|
+
CurrentProjectId | Settings
|
|
38
|
+
>
|
|
39
|
+
readonly settings: (
|
|
40
|
+
projectId: ProjectId,
|
|
41
|
+
) => Effect.Effect<void, IssueSourceError>
|
|
42
|
+
readonly info: (
|
|
43
|
+
projectId: ProjectId,
|
|
44
|
+
) => Effect.Effect<void, IssueSourceError>
|
|
45
|
+
|
|
27
46
|
readonly ensureInProgress: (
|
|
47
|
+
projectId: ProjectId,
|
|
28
48
|
issueId: string,
|
|
29
49
|
) => Effect.Effect<void, IssueSourceError>
|
|
30
50
|
}
|
|
@@ -34,12 +54,27 @@ export class IssueSource extends ServiceMap.Service<
|
|
|
34
54
|
const reactivity = yield* Reactivity.Reactivity
|
|
35
55
|
return IssueSource.of({
|
|
36
56
|
...impl,
|
|
37
|
-
createIssue: (issue) =>
|
|
38
|
-
reactivity.mutation(
|
|
57
|
+
createIssue: (projectId, issue) =>
|
|
58
|
+
reactivity.mutation(
|
|
59
|
+
{
|
|
60
|
+
issues: [projectId],
|
|
61
|
+
},
|
|
62
|
+
impl.createIssue(projectId, issue),
|
|
63
|
+
),
|
|
39
64
|
updateIssue: (options) =>
|
|
40
|
-
reactivity.mutation(
|
|
41
|
-
|
|
42
|
-
|
|
65
|
+
reactivity.mutation(
|
|
66
|
+
{
|
|
67
|
+
issues: [options.projectId],
|
|
68
|
+
},
|
|
69
|
+
impl.updateIssue(options),
|
|
70
|
+
),
|
|
71
|
+
cancelIssue: (projectId, issueId) =>
|
|
72
|
+
reactivity.mutation(
|
|
73
|
+
{
|
|
74
|
+
issues: [projectId],
|
|
75
|
+
},
|
|
76
|
+
impl.cancelIssue(projectId, issueId),
|
|
77
|
+
),
|
|
43
78
|
})
|
|
44
79
|
})
|
|
45
80
|
}
|
package/src/IssueSources.ts
CHANGED
|
@@ -1,27 +1,26 @@
|
|
|
1
1
|
import { Effect, Layer, Option, pipe, Schema, ServiceMap } from "effect"
|
|
2
|
-
import { Setting, Settings } from "./Settings.ts"
|
|
3
|
-
import { LinearIssueSource
|
|
2
|
+
import { CurrentProjectId, Setting, Settings } from "./Settings.ts"
|
|
3
|
+
import { LinearIssueSource } from "./Linear.ts"
|
|
4
4
|
import { Prompt } from "effect/unstable/cli"
|
|
5
|
-
import { GithubIssueSource
|
|
5
|
+
import { GithubIssueSource } from "./Github.ts"
|
|
6
6
|
import { IssueSource } from "./IssueSource.ts"
|
|
7
7
|
import { PlatformServices } from "./shared/platform.ts"
|
|
8
8
|
import { atomRuntime } from "./shared/runtime.ts"
|
|
9
9
|
import { Atom, Reactivity } from "effect/unstable/reactivity"
|
|
10
10
|
import type { PrdIssue } from "./domain/PrdIssue.ts"
|
|
11
|
+
import type { ProjectId } from "./domain/Project.ts"
|
|
11
12
|
|
|
12
13
|
const issueSources: ReadonlyArray<typeof CurrentIssueSource.Service> = [
|
|
13
14
|
{
|
|
14
15
|
id: "linear",
|
|
15
16
|
name: "Linear",
|
|
16
17
|
layer: LinearIssueSource,
|
|
17
|
-
reset: resetLinear,
|
|
18
18
|
githubPrInstructions: `The title of the PR should include the task id.`,
|
|
19
19
|
},
|
|
20
20
|
{
|
|
21
21
|
id: "github",
|
|
22
22
|
name: "GitHub Issues",
|
|
23
23
|
layer: GithubIssueSource,
|
|
24
|
-
reset: resetGithub,
|
|
25
24
|
githubPrInstructions: `At the start of your PR description, include a line that closes the issue: Closes {task id}`,
|
|
26
25
|
},
|
|
27
26
|
]
|
|
@@ -39,24 +38,18 @@ export const selectIssueSource = Effect.gen(function* () {
|
|
|
39
38
|
value: s,
|
|
40
39
|
})),
|
|
41
40
|
})
|
|
42
|
-
yield*
|
|
43
|
-
yield* source.reset
|
|
41
|
+
yield* Settings.set(selectedIssueSource, Option.some(source.id))
|
|
44
42
|
return source
|
|
45
43
|
})
|
|
46
44
|
|
|
47
45
|
const getOrSelectIssueSource = Effect.gen(function* () {
|
|
48
|
-
const issueSource = yield*
|
|
46
|
+
const issueSource = yield* Settings.get(selectedIssueSource)
|
|
49
47
|
if (Option.isSome(issueSource)) {
|
|
50
48
|
return issueSources.find((s) => s.id === issueSource.value)!
|
|
51
49
|
}
|
|
52
50
|
return yield* selectIssueSource
|
|
53
51
|
})
|
|
54
52
|
|
|
55
|
-
export const resetCurrentIssueSource = Effect.gen(function* () {
|
|
56
|
-
const source = yield* getOrSelectIssueSource
|
|
57
|
-
yield* source.reset
|
|
58
|
-
})
|
|
59
|
-
|
|
60
53
|
export class CurrentIssueSource extends ServiceMap.Service<
|
|
61
54
|
CurrentIssueSource,
|
|
62
55
|
{
|
|
@@ -67,7 +60,6 @@ export class CurrentIssueSource extends ServiceMap.Service<
|
|
|
67
60
|
Layer.Error<typeof LinearIssueSource | typeof GithubIssueSource>,
|
|
68
61
|
Layer.Services<typeof LinearIssueSource | typeof GithubIssueSource>
|
|
69
62
|
>
|
|
70
|
-
readonly reset: Effect.Effect<void, never, Settings>
|
|
71
63
|
readonly githubPrInstructions: string
|
|
72
64
|
}
|
|
73
65
|
>()("lalph/CurrentIssueSource") {
|
|
@@ -88,32 +80,36 @@ export const issueSourceRuntime = atomRuntime(
|
|
|
88
80
|
CurrentIssueSource.layer.pipe(Layer.orDie),
|
|
89
81
|
)
|
|
90
82
|
|
|
91
|
-
export const currentIssuesAtom =
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
83
|
+
export const currentIssuesAtom = Atom.family((projectId: ProjectId) =>
|
|
84
|
+
pipe(
|
|
85
|
+
issueSourceRuntime.atom(
|
|
86
|
+
Effect.fnUntraced(function* (get) {
|
|
87
|
+
const source = yield* IssueSource
|
|
88
|
+
const issues = yield* source
|
|
89
|
+
.issues(projectId)
|
|
90
|
+
.pipe(Effect.withSpan("currentIssuesAtom.refresh"))
|
|
91
|
+
const handle = setTimeout(() => {
|
|
92
|
+
get.refreshSelf()
|
|
93
|
+
}, 30_000)
|
|
94
|
+
get.addFinalizer(() => clearTimeout(handle))
|
|
95
|
+
return issues
|
|
96
|
+
}),
|
|
97
|
+
),
|
|
98
|
+
atomRuntime.withReactivity([`issues:${projectId}`]),
|
|
99
|
+
Atom.keepAlive,
|
|
104
100
|
),
|
|
105
|
-
atomRuntime.withReactivity(["issues"]),
|
|
106
|
-
Atom.keepAlive,
|
|
107
101
|
)
|
|
108
102
|
|
|
109
103
|
// Helpers
|
|
110
104
|
|
|
111
|
-
const getCurrentIssues =
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
const getCurrentIssues = (projectId: ProjectId) =>
|
|
106
|
+
Atom.getResult(currentIssuesAtom(projectId), {
|
|
107
|
+
suspendOnWaiting: true,
|
|
108
|
+
})
|
|
114
109
|
|
|
115
110
|
export const checkForWork = Effect.gen(function* () {
|
|
116
|
-
const
|
|
111
|
+
const projectId = yield* CurrentProjectId
|
|
112
|
+
const issues = yield* getCurrentIssues(projectId)
|
|
117
113
|
const hasIncomplete = issues.some(
|
|
118
114
|
(issue) => issue.state === "todo" && issue.blockedBy.length === 0,
|
|
119
115
|
)
|
|
@@ -125,7 +121,8 @@ export const checkForWork = Effect.gen(function* () {
|
|
|
125
121
|
export const resetInProgress = Effect.gen(function* () {
|
|
126
122
|
const source = yield* IssueSource
|
|
127
123
|
const reactivity = yield* Reactivity.Reactivity
|
|
128
|
-
const
|
|
124
|
+
const projectId = yield* CurrentProjectId
|
|
125
|
+
const issues = yield* getCurrentIssues(projectId)
|
|
129
126
|
const inProgress = issues.filter(
|
|
130
127
|
(issue): issue is PrdIssue & { id: string } =>
|
|
131
128
|
issue.state === "in-progress" && issue.id !== null,
|
|
@@ -135,6 +132,7 @@ export const resetInProgress = Effect.gen(function* () {
|
|
|
135
132
|
inProgress,
|
|
136
133
|
(issue) =>
|
|
137
134
|
source.updateIssue({
|
|
135
|
+
projectId,
|
|
138
136
|
issueId: issue.id,
|
|
139
137
|
state: "todo",
|
|
140
138
|
}),
|
package/src/Kvs.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
|
-
import { Layer } from "effect"
|
|
1
|
+
import { Layer, LayerMap } from "effect"
|
|
2
2
|
import { KeyValueStore } from "effect/unstable/persistence"
|
|
3
3
|
import { PlatformServices } from "./shared/platform.ts"
|
|
4
|
+
import { ProjectId } from "./domain/Project.ts"
|
|
4
5
|
|
|
5
6
|
export const layerKvs = KeyValueStore.layerFileSystem(".lalph/config").pipe(
|
|
6
7
|
Layer.provide(PlatformServices),
|
|
7
8
|
)
|
|
9
|
+
|
|
10
|
+
export class ProjectsKvs extends LayerMap.Service<ProjectsKvs>()(
|
|
11
|
+
"lalph/ProjectsKvs",
|
|
12
|
+
{
|
|
13
|
+
lookup: (projectId: ProjectId) =>
|
|
14
|
+
KeyValueStore.layerFileSystem(
|
|
15
|
+
`.lalph/projects/${encodeURIComponent(projectId)}`,
|
|
16
|
+
).pipe(Layer.orDie),
|
|
17
|
+
dependencies: [PlatformServices],
|
|
18
|
+
},
|
|
19
|
+
) {}
|