@markjaquith/agency 2.17.0 → 2.19.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/README.md +29 -8
- package/cli.ts +94 -8
- package/package.json +1 -1
- package/skills/agency/SKILL.md +13 -6
- package/src/cli-parser.test.ts +111 -2
- package/src/cli-parser.ts +273 -36
- package/src/cli.test.ts +105 -5
- package/src/commands/context.ts +3 -0
- package/src/commands/epic.test.ts +22 -0
- package/src/commands/epic.ts +38 -2
- package/src/commands/init.ts +3 -1
- package/src/commands/phase.ts +52 -2
- package/src/commands/status.test.ts +44 -0
- package/src/commands/status.ts +51 -2
- package/src/commands/task-phase.test.ts +20 -0
- package/src/commands/task.ts +49 -2
- package/src/commands/validate.test.ts +2 -0
- package/src/commands/work.test.ts +1 -0
- package/src/commands/work.ts +15 -7
- package/src/commands/workbase.test.ts +63 -2
- package/src/commands/workbase.ts +77 -10
- package/src/services/WorkbaseService.test.ts +147 -4
- package/src/services/WorkbaseService.ts +214 -12
- package/src/utils/table.ts +19 -0
- package/src/work-view.test.ts +151 -0
- package/src/work-view.ts +253 -0
- package/src/workbase/schemas.test.ts +17 -6
- package/src/workbase/schemas.ts +16 -1
- package/src/workbase/workbase-choice.ts +2 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
|
2
|
+
import { Effect } from "effect"
|
|
3
|
+
import { mkdir } from "node:fs/promises"
|
|
4
|
+
import { dirname, join } from "node:path"
|
|
5
|
+
import { cleanupTempDir, createTempDir, runTestEffect } from "./test-utils"
|
|
6
|
+
import { getWorkViews } from "./work-view"
|
|
7
|
+
|
|
8
|
+
const write = async (root: string, path: string, content: string) => {
|
|
9
|
+
const fullPath = join(root, path)
|
|
10
|
+
await mkdir(dirname(fullPath), { recursive: true })
|
|
11
|
+
await Bun.write(fullPath, content)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe("work views", () => {
|
|
15
|
+
let root: string
|
|
16
|
+
|
|
17
|
+
beforeEach(async () => {
|
|
18
|
+
root = await createTempDir()
|
|
19
|
+
await write(root, "agency.json", '{"version":2}\n')
|
|
20
|
+
await mkdir(join(root, "repos/agency"), { recursive: true })
|
|
21
|
+
await write(
|
|
22
|
+
root,
|
|
23
|
+
"epics/delivery/EPIC.md",
|
|
24
|
+
`---
|
|
25
|
+
ticketUrl: https://example.com/delivery
|
|
26
|
+
repos:
|
|
27
|
+
- repo: agency
|
|
28
|
+
ref: main
|
|
29
|
+
tasks:
|
|
30
|
+
- id: zeta
|
|
31
|
+
- id: alpha
|
|
32
|
+
---
|
|
33
|
+
`,
|
|
34
|
+
)
|
|
35
|
+
await write(
|
|
36
|
+
root,
|
|
37
|
+
"tasks/zeta/TASK.md",
|
|
38
|
+
`---
|
|
39
|
+
ticketUrl: null
|
|
40
|
+
epic: delivery
|
|
41
|
+
phases:
|
|
42
|
+
- id: verify
|
|
43
|
+
dependsOn: [implement]
|
|
44
|
+
- id: implement
|
|
45
|
+
---
|
|
46
|
+
`,
|
|
47
|
+
)
|
|
48
|
+
await write(
|
|
49
|
+
root,
|
|
50
|
+
"tasks/zeta/phases/verify/PHASE.md",
|
|
51
|
+
`---
|
|
52
|
+
repo: agency
|
|
53
|
+
branch: feat/verify
|
|
54
|
+
base: main
|
|
55
|
+
pr: null
|
|
56
|
+
status: open
|
|
57
|
+
---
|
|
58
|
+
`,
|
|
59
|
+
)
|
|
60
|
+
await write(
|
|
61
|
+
root,
|
|
62
|
+
"tasks/zeta/phases/implement/PHASE.md",
|
|
63
|
+
`---
|
|
64
|
+
repo: agency
|
|
65
|
+
branch: feat/implement
|
|
66
|
+
base: main
|
|
67
|
+
pr: https://github.com/example/agency/pull/1
|
|
68
|
+
status: dropped
|
|
69
|
+
---
|
|
70
|
+
`,
|
|
71
|
+
)
|
|
72
|
+
await mkdir(join(root, "tasks/zeta/phases/implement/code/agency"), {
|
|
73
|
+
recursive: true,
|
|
74
|
+
})
|
|
75
|
+
await write(
|
|
76
|
+
root,
|
|
77
|
+
"tasks/alpha/TASK.md",
|
|
78
|
+
`---
|
|
79
|
+
ticketUrl: null
|
|
80
|
+
epic: delivery
|
|
81
|
+
repo: agency
|
|
82
|
+
branch: feat/alpha
|
|
83
|
+
base: main
|
|
84
|
+
pr: null
|
|
85
|
+
status: open
|
|
86
|
+
---
|
|
87
|
+
`,
|
|
88
|
+
)
|
|
89
|
+
})
|
|
90
|
+
|
|
91
|
+
afterEach(async () => cleanupTempDir(root))
|
|
92
|
+
|
|
93
|
+
const views = (options: Record<string, unknown> = {}) =>
|
|
94
|
+
runTestEffect(Effect.suspend(() => getWorkViews({ cwd: root, ...options })))
|
|
95
|
+
|
|
96
|
+
test("uses declared graph order and exposes operational state", async () => {
|
|
97
|
+
const result = await views()
|
|
98
|
+
|
|
99
|
+
expect(result.taskRows.map((row) => row.id)).toEqual(["zeta", "alpha"])
|
|
100
|
+
expect(result.phaseRows.map((row) => row.id)).toEqual([
|
|
101
|
+
"verify",
|
|
102
|
+
"implement",
|
|
103
|
+
])
|
|
104
|
+
expect(result.executionRows.map((row) => row.key)).toEqual([
|
|
105
|
+
"zeta/verify",
|
|
106
|
+
"zeta/implement",
|
|
107
|
+
"alpha",
|
|
108
|
+
])
|
|
109
|
+
expect(result.phaseRows[0]).toMatchObject({
|
|
110
|
+
parent: "zeta",
|
|
111
|
+
status: "open",
|
|
112
|
+
readiness: "blocked",
|
|
113
|
+
repositories: "agency",
|
|
114
|
+
branch: "feat/verify",
|
|
115
|
+
pr: "absent",
|
|
116
|
+
worktree: "absent",
|
|
117
|
+
})
|
|
118
|
+
expect(result.phaseRows[1]).toMatchObject({
|
|
119
|
+
readiness: "terminal",
|
|
120
|
+
pr: "present",
|
|
121
|
+
worktree: "materialized",
|
|
122
|
+
})
|
|
123
|
+
expect(result.taskRows[0]).toMatchObject({
|
|
124
|
+
parent: "delivery",
|
|
125
|
+
branch: "multiple",
|
|
126
|
+
pr: "1/2 present",
|
|
127
|
+
worktree: "1/2 materialized",
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
|
|
131
|
+
test("composes lifecycle, repository, readiness, and PR filters", async () => {
|
|
132
|
+
expect(
|
|
133
|
+
(await views({ statuses: ["dropped"] })).executionRows.map(
|
|
134
|
+
(row) => row.key,
|
|
135
|
+
),
|
|
136
|
+
).toEqual(["zeta/implement"])
|
|
137
|
+
expect(
|
|
138
|
+
(await views({ repositories: ["agency"], blocked: true })).phaseRows.map(
|
|
139
|
+
(row) => row.id,
|
|
140
|
+
),
|
|
141
|
+
).toEqual(["verify"])
|
|
142
|
+
expect(
|
|
143
|
+
(await views({ ready: true, pr: false })).executionRows.map(
|
|
144
|
+
(row) => row.key,
|
|
145
|
+
),
|
|
146
|
+
).toEqual(["alpha"])
|
|
147
|
+
expect((await views({ pr: true })).phaseRows.map((row) => row.id)).toEqual([
|
|
148
|
+
"implement",
|
|
149
|
+
])
|
|
150
|
+
})
|
|
151
|
+
})
|
package/src/work-view.ts
ADDED
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
import { Effect } from "effect"
|
|
2
|
+
import type { GraphNode } from "./graph-schema"
|
|
3
|
+
import { GraphService } from "./services/GraphService"
|
|
4
|
+
import type { WorkStatus } from "./workbase/schemas"
|
|
5
|
+
|
|
6
|
+
type WorkNode = Extract<
|
|
7
|
+
GraphNode,
|
|
8
|
+
{ readonly kind: "epic" | "task" | "phase" | "execution-unit" }
|
|
9
|
+
>
|
|
10
|
+
type EntityNode = Exclude<WorkNode, { readonly kind: "execution-unit" }>
|
|
11
|
+
type ExecutionNode = Extract<WorkNode, { readonly kind: "execution-unit" }>
|
|
12
|
+
|
|
13
|
+
export interface WorkViewOptions {
|
|
14
|
+
readonly cwd?: string
|
|
15
|
+
readonly statuses?: readonly string[]
|
|
16
|
+
readonly repositories?: readonly string[]
|
|
17
|
+
readonly ready?: boolean
|
|
18
|
+
readonly blocked?: boolean
|
|
19
|
+
readonly pr?: boolean
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WorkViewRow {
|
|
23
|
+
readonly kind: "epic" | "task" | "phase"
|
|
24
|
+
readonly id: string
|
|
25
|
+
readonly key: string
|
|
26
|
+
readonly parent: string
|
|
27
|
+
readonly status: WorkStatus
|
|
28
|
+
readonly readiness: "ready" | "blocked" | "waiting" | "terminal"
|
|
29
|
+
readonly repositories: string
|
|
30
|
+
readonly branch: string
|
|
31
|
+
readonly pr: string
|
|
32
|
+
readonly worktree: string
|
|
33
|
+
readonly hasPr: boolean
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const allowedStatuses = new Set<WorkStatus>([
|
|
37
|
+
"open",
|
|
38
|
+
"working",
|
|
39
|
+
"delegated",
|
|
40
|
+
"done",
|
|
41
|
+
"dropped",
|
|
42
|
+
])
|
|
43
|
+
|
|
44
|
+
const validatedStatuses = (values: readonly string[] | undefined) =>
|
|
45
|
+
(values ?? []).map((value) => {
|
|
46
|
+
if (!allowedStatuses.has(value as WorkStatus)) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
`Invalid --status value '${value}'. Expected one of: ${[...allowedStatuses].join(", ")}`,
|
|
49
|
+
)
|
|
50
|
+
}
|
|
51
|
+
return value as WorkStatus
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
const readinessLabel = (node: EntityNode): WorkViewRow["readiness"] =>
|
|
55
|
+
node.readiness.ready
|
|
56
|
+
? "ready"
|
|
57
|
+
: node.readiness.terminal
|
|
58
|
+
? "terminal"
|
|
59
|
+
: node.readiness.blocked
|
|
60
|
+
? "blocked"
|
|
61
|
+
: "waiting"
|
|
62
|
+
|
|
63
|
+
const aggregateLabel = (
|
|
64
|
+
executions: readonly ExecutionNode[],
|
|
65
|
+
predicate: (node: ExecutionNode) => boolean,
|
|
66
|
+
singular: readonly [present: string, absent: string],
|
|
67
|
+
) => {
|
|
68
|
+
if (executions.length === 0) return "-"
|
|
69
|
+
const count = executions.filter(predicate).length
|
|
70
|
+
if (executions.length === 1) return count === 1 ? singular[0] : singular[1]
|
|
71
|
+
return `${count}/${executions.length} ${singular[0]}`
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const rowFor = (
|
|
75
|
+
node: EntityNode,
|
|
76
|
+
executions: readonly ExecutionNode[],
|
|
77
|
+
): WorkViewRow => {
|
|
78
|
+
const branches = [
|
|
79
|
+
...new Set(executions.map((execution) => execution.data.branch)),
|
|
80
|
+
]
|
|
81
|
+
const parent =
|
|
82
|
+
node.kind === "task"
|
|
83
|
+
? (node.data.epic ?? "-")
|
|
84
|
+
: node.kind === "phase"
|
|
85
|
+
? node.key.slice(0, node.key.indexOf("/"))
|
|
86
|
+
: "-"
|
|
87
|
+
const id =
|
|
88
|
+
node.kind === "phase" ? node.key.slice(node.key.indexOf("/") + 1) : node.key
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
kind: node.kind,
|
|
92
|
+
id,
|
|
93
|
+
key: node.key,
|
|
94
|
+
parent,
|
|
95
|
+
status: node.status,
|
|
96
|
+
readiness: readinessLabel(node),
|
|
97
|
+
repositories: node.repositories.join(",") || "-",
|
|
98
|
+
branch:
|
|
99
|
+
branches.length === 0
|
|
100
|
+
? "-"
|
|
101
|
+
: branches.length === 1
|
|
102
|
+
? branches[0]!
|
|
103
|
+
: "multiple",
|
|
104
|
+
pr: aggregateLabel(executions, (execution) => Boolean(execution.data.pr), [
|
|
105
|
+
"present",
|
|
106
|
+
"absent",
|
|
107
|
+
]),
|
|
108
|
+
worktree: aggregateLabel(
|
|
109
|
+
executions,
|
|
110
|
+
(execution) => execution.workspace?.materialized === true,
|
|
111
|
+
["materialized", "absent"],
|
|
112
|
+
),
|
|
113
|
+
hasPr: executions.some((execution) => Boolean(execution.data.pr)),
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const orderedTasks = (
|
|
118
|
+
epics: readonly Extract<EntityNode, { readonly kind: "epic" }>[],
|
|
119
|
+
tasks: ReadonlyMap<string, Extract<EntityNode, { readonly kind: "task" }>>,
|
|
120
|
+
) => {
|
|
121
|
+
const ordered: Extract<EntityNode, { readonly kind: "task" }>[] = []
|
|
122
|
+
const seen = new Set<string>()
|
|
123
|
+
for (const epic of epics) {
|
|
124
|
+
for (const item of epic.data.tasks) {
|
|
125
|
+
const task = tasks.get(item.id)
|
|
126
|
+
if (task && !seen.has(task.key)) {
|
|
127
|
+
ordered.push(task)
|
|
128
|
+
seen.add(task.key)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
for (const task of [...tasks.values()].sort((a, b) =>
|
|
133
|
+
a.key.localeCompare(b.key),
|
|
134
|
+
)) {
|
|
135
|
+
if (!seen.has(task.key)) ordered.push(task)
|
|
136
|
+
}
|
|
137
|
+
return ordered
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const orderedPhases = (
|
|
141
|
+
task: Extract<EntityNode, { readonly kind: "task" }>,
|
|
142
|
+
phases: ReadonlyMap<string, Extract<EntityNode, { readonly kind: "phase" }>>,
|
|
143
|
+
) => {
|
|
144
|
+
const ordered: Extract<EntityNode, { readonly kind: "phase" }>[] = []
|
|
145
|
+
const seen = new Set<string>()
|
|
146
|
+
if ("phases" in task.data) {
|
|
147
|
+
for (const item of task.data.phases) {
|
|
148
|
+
const phase = phases.get(`${task.key}/${item.id}`)
|
|
149
|
+
if (phase) {
|
|
150
|
+
ordered.push(phase)
|
|
151
|
+
seen.add(phase.key)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
for (const phase of [...phases.values()]
|
|
156
|
+
.filter((value) => value.key.startsWith(`${task.key}/`))
|
|
157
|
+
.sort((a, b) => a.key.localeCompare(b.key))) {
|
|
158
|
+
if (!seen.has(phase.key)) ordered.push(phase)
|
|
159
|
+
}
|
|
160
|
+
return ordered
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export const getWorkViews = (options: WorkViewOptions = {}) =>
|
|
164
|
+
Effect.gen(function* () {
|
|
165
|
+
const graphService = yield* GraphService
|
|
166
|
+
const statuses = yield* Effect.sync(() =>
|
|
167
|
+
validatedStatuses(options.statuses),
|
|
168
|
+
)
|
|
169
|
+
const graph = yield* graphService.get({
|
|
170
|
+
cwd: options.cwd,
|
|
171
|
+
include: ["workspace"],
|
|
172
|
+
})
|
|
173
|
+
const workNodes = graph.nodes.filter(
|
|
174
|
+
(node): node is WorkNode => node.kind !== "repository",
|
|
175
|
+
)
|
|
176
|
+
const epics = workNodes
|
|
177
|
+
.filter(
|
|
178
|
+
(node): node is Extract<EntityNode, { readonly kind: "epic" }> =>
|
|
179
|
+
node.kind === "epic",
|
|
180
|
+
)
|
|
181
|
+
.sort((a, b) => a.key.localeCompare(b.key))
|
|
182
|
+
const tasks = new Map(
|
|
183
|
+
workNodes
|
|
184
|
+
.filter(
|
|
185
|
+
(node): node is Extract<EntityNode, { readonly kind: "task" }> =>
|
|
186
|
+
node.kind === "task",
|
|
187
|
+
)
|
|
188
|
+
.map((node) => [node.key, node]),
|
|
189
|
+
)
|
|
190
|
+
const phases = new Map(
|
|
191
|
+
workNodes
|
|
192
|
+
.filter(
|
|
193
|
+
(node): node is Extract<EntityNode, { readonly kind: "phase" }> =>
|
|
194
|
+
node.kind === "phase",
|
|
195
|
+
)
|
|
196
|
+
.map((node) => [node.key, node]),
|
|
197
|
+
)
|
|
198
|
+
const executions = workNodes.filter(
|
|
199
|
+
(node): node is ExecutionNode => node.kind === "execution-unit",
|
|
200
|
+
)
|
|
201
|
+
const taskOrder = orderedTasks(epics, tasks)
|
|
202
|
+
const phaseOrder = taskOrder.flatMap((task) => orderedPhases(task, phases))
|
|
203
|
+
const executionsFor = (node: EntityNode) => {
|
|
204
|
+
if (node.kind === "phase") {
|
|
205
|
+
const separator = node.key.indexOf("/")
|
|
206
|
+
const taskId = node.key.slice(0, separator)
|
|
207
|
+
const phaseId = node.key.slice(separator + 1)
|
|
208
|
+
return executions.filter(
|
|
209
|
+
(execution) =>
|
|
210
|
+
execution.data.taskId === taskId &&
|
|
211
|
+
execution.data.phaseId === phaseId,
|
|
212
|
+
)
|
|
213
|
+
}
|
|
214
|
+
if (node.kind === "task") {
|
|
215
|
+
return executions.filter(
|
|
216
|
+
(execution) => execution.data.taskId === node.key,
|
|
217
|
+
)
|
|
218
|
+
}
|
|
219
|
+
const taskIds = new Set(node.data.tasks.map((item) => item.id))
|
|
220
|
+
return executions.filter((execution) =>
|
|
221
|
+
taskIds.has(execution.data.taskId),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
const makeRows = (nodes: readonly EntityNode[]) =>
|
|
225
|
+
nodes.map((node) => rowFor(node, executionsFor(node)))
|
|
226
|
+
const filterRows = (rows: readonly WorkViewRow[]) =>
|
|
227
|
+
rows
|
|
228
|
+
.filter((row) => statuses.length === 0 || statuses.includes(row.status))
|
|
229
|
+
.filter(
|
|
230
|
+
(row) =>
|
|
231
|
+
!options.repositories?.length ||
|
|
232
|
+
row.repositories
|
|
233
|
+
.split(",")
|
|
234
|
+
.some((repo) => options.repositories!.includes(repo)),
|
|
235
|
+
)
|
|
236
|
+
.filter((row) => options.ready !== true || row.readiness === "ready")
|
|
237
|
+
.filter(
|
|
238
|
+
(row) => options.blocked !== true || row.readiness === "blocked",
|
|
239
|
+
)
|
|
240
|
+
.filter((row) => options.pr === undefined || row.hasPr === options.pr)
|
|
241
|
+
|
|
242
|
+
const epicRows = filterRows(makeRows(epics))
|
|
243
|
+
const taskRows = filterRows(makeRows(taskOrder))
|
|
244
|
+
const phaseRows = filterRows(makeRows(phaseOrder))
|
|
245
|
+
const executionEntities: EntityNode[] = []
|
|
246
|
+
for (const task of taskOrder) {
|
|
247
|
+
const taskPhases = orderedPhases(task, phases)
|
|
248
|
+
executionEntities.push(...(taskPhases.length > 0 ? taskPhases : [task]))
|
|
249
|
+
}
|
|
250
|
+
const executionRows = filterRows(makeRows(executionEntities))
|
|
251
|
+
|
|
252
|
+
return { epicRows, taskRows, phaseRows, executionRows }
|
|
253
|
+
})
|
|
@@ -185,23 +185,34 @@ describe("workbase registry", () => {
|
|
|
185
185
|
test("accepts registered paths", () => {
|
|
186
186
|
expect(
|
|
187
187
|
Schema.decodeUnknownSync(WorkbaseRegistry)({
|
|
188
|
-
version:
|
|
189
|
-
workbases: [
|
|
188
|
+
version: 2,
|
|
189
|
+
workbases: [
|
|
190
|
+
{ id: "wb-one", name: "one", path: "/work/one" },
|
|
191
|
+
{ id: "wb-two", path: "/work/two" },
|
|
192
|
+
],
|
|
193
|
+
defaultId: "wb-one",
|
|
190
194
|
}),
|
|
191
|
-
).toEqual({
|
|
195
|
+
).toEqual({
|
|
196
|
+
version: 2,
|
|
197
|
+
workbases: [
|
|
198
|
+
{ id: "wb-one", name: "one", path: "/work/one" },
|
|
199
|
+
{ id: "wb-two", path: "/work/two" },
|
|
200
|
+
],
|
|
201
|
+
defaultId: "wb-one",
|
|
202
|
+
})
|
|
192
203
|
})
|
|
193
204
|
|
|
194
205
|
test("rejects invalid versions and empty paths", () => {
|
|
195
206
|
expect(() =>
|
|
196
207
|
Schema.decodeUnknownSync(WorkbaseRegistry)({
|
|
197
|
-
version:
|
|
208
|
+
version: 3,
|
|
198
209
|
workbases: [],
|
|
199
210
|
}),
|
|
200
211
|
).toThrow()
|
|
201
212
|
expect(() =>
|
|
202
213
|
Schema.decodeUnknownSync(WorkbaseRegistry)({
|
|
203
|
-
version:
|
|
204
|
-
workbases: [""],
|
|
214
|
+
version: 2,
|
|
215
|
+
workbases: [{ id: "wb-one", path: "" }],
|
|
205
216
|
}),
|
|
206
217
|
).toThrow()
|
|
207
218
|
})
|
package/src/workbase/schemas.ts
CHANGED
|
@@ -54,11 +54,23 @@ export const WorkbaseConfig = Schema.Struct({
|
|
|
54
54
|
worktreeCreateCommand: Schema.optional(Schema.NonEmptyArray(NonEmptyString)),
|
|
55
55
|
})
|
|
56
56
|
|
|
57
|
-
export const
|
|
57
|
+
export const LegacyWorkbaseRegistry = Schema.Struct({
|
|
58
58
|
version: Schema.Literal(1),
|
|
59
59
|
workbases: Schema.Array(NonEmptyString),
|
|
60
60
|
})
|
|
61
61
|
|
|
62
|
+
export const WorkbaseRegistration = Schema.Struct({
|
|
63
|
+
id: EntityId,
|
|
64
|
+
name: Schema.optional(EntityId),
|
|
65
|
+
path: NonEmptyString,
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
export const WorkbaseRegistry = Schema.Struct({
|
|
69
|
+
version: Schema.Literal(2),
|
|
70
|
+
workbases: Schema.Array(WorkbaseRegistration),
|
|
71
|
+
defaultId: Schema.optional(EntityId),
|
|
72
|
+
})
|
|
73
|
+
|
|
62
74
|
export const Dependency = Schema.Struct({
|
|
63
75
|
id: EntityId,
|
|
64
76
|
dependsOn: Schema.optional(Schema.Array(EntityId)),
|
|
@@ -107,6 +119,9 @@ export const PhaseFrontmatter = Schema.Struct({
|
|
|
107
119
|
|
|
108
120
|
export type WorkbaseConfig = Schema.Schema.Type<typeof WorkbaseConfig>
|
|
109
121
|
export type WorkbaseRegistry = Schema.Schema.Type<typeof WorkbaseRegistry>
|
|
122
|
+
export type WorkbaseRegistration = Schema.Schema.Type<
|
|
123
|
+
typeof WorkbaseRegistration
|
|
124
|
+
>
|
|
110
125
|
export type Dependency = Schema.Schema.Type<typeof Dependency>
|
|
111
126
|
export type RepositoryReference = Schema.Schema.Type<typeof RepositoryReference>
|
|
112
127
|
export type WorkStatus = Schema.Schema.Type<typeof WorkStatus>
|
|
@@ -30,6 +30,8 @@ export const resolveWorkbase = (
|
|
|
30
30
|
return yield* workbase.discover(startPath).pipe(
|
|
31
31
|
Effect.catchTag("WorkbaseNotFoundError", () =>
|
|
32
32
|
Effect.gen(function* () {
|
|
33
|
+
const defaultWorkbase = yield* workbase.getDefault()
|
|
34
|
+
if (defaultWorkbase) return defaultWorkbase.path
|
|
33
35
|
const registered = yield* workbase.listRegistered()
|
|
34
36
|
if (registered.length === 0) {
|
|
35
37
|
return yield* Effect.fail(
|