@naxodev/apnea 0.2.0 → 0.2.2
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 -1
- package/SECURITY.md +36 -0
- package/briefs/orchestrator.md +4 -3
- package/dist/cli.js +8375 -15093
- package/docs/protocol/artifacts.md +18 -2
- package/docs/protocol/config.md +15 -3
- package/docs/protocol/manual-gate.md +8 -8
- package/docs/protocol/overview.md +17 -4
- package/extension/adapters/commit.ts +5 -1
- package/extension/adapters/dispatch.ts +9 -1
- package/extension/adapters/setup.ts +15 -1
- package/extension/adapters/start.ts +5 -1
- package/extension/adapters/status.ts +17 -2
- package/extension/adapters/wait.ts +6 -1
- package/extension/api.ts +7 -1
- package/extension/cli/main.ts +67 -7
- package/extension/cli/parse.ts +172 -5
- package/extension/domain/paths.ts +2 -11
- package/extension/domain/timeouts.ts +4 -0
- package/extension/domain/types.ts +65 -3
- package/extension/errors.ts +51 -16
- package/extension/operation-hooks.ts +6 -0
- package/extension/registry.ts +29 -15
- package/extension/run-tool.ts +19 -2
- package/extension/schema/config.ts +58 -16
- package/extension/schema/frontmatter.ts +57 -0
- package/extension/schema/state.ts +210 -13
- package/extension/services/app-live.ts +2 -1
- package/extension/services/config.ts +6 -4
- package/extension/services/file-system.ts +346 -75
- package/extension/services/herdr.ts +466 -260
- package/extension/services/operation-lock.ts +452 -0
- package/extension/services/process.ts +477 -0
- package/extension/services/run-store.ts +38 -16
- package/extension/services/vcs.ts +1258 -328
- package/extension/workflows/commit.ts +214 -13
- package/extension/workflows/dispatch.ts +305 -67
- package/extension/workflows/setup.ts +59 -32
- package/extension/workflows/start.ts +6 -4
- package/extension/workflows/status.ts +2 -2
- package/extension/workflows/wait.ts +62 -77
- package/package.json +2 -2
- package/schemas/config.schema.json +5 -1
- package/schemas/state.schema.json +165 -11
|
@@ -1,4 +1,10 @@
|
|
|
1
1
|
import { Result, Schema } from "effect"
|
|
2
|
+
import { isCompleteArtifact } from "../domain/frontmatter.ts"
|
|
3
|
+
import {
|
|
4
|
+
stepAfterArtifact,
|
|
5
|
+
type DispatchKind,
|
|
6
|
+
} from "../domain/state-machine.ts"
|
|
7
|
+
import type { FrontMatter, ReworkTarget, Step } from "../domain/types.ts"
|
|
2
8
|
import { ArtifactInvalid } from "../errors.ts"
|
|
3
9
|
|
|
4
10
|
const VerdictSchema = Schema.Literals(["APPROVED", "CHANGES_REQUIRED"] as const)
|
|
@@ -17,6 +23,12 @@ export const FrontMatterResultSchema = Schema.Struct({
|
|
|
17
23
|
|
|
18
24
|
export type FrontMatterResult = typeof FrontMatterResultSchema.Type
|
|
19
25
|
|
|
26
|
+
export type AcceptedArtifactCompletion = {
|
|
27
|
+
frontmatter: FrontMatterResult
|
|
28
|
+
next: Step
|
|
29
|
+
rework: ReworkTarget | undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
20
32
|
export function decodeFrontMatterResult(
|
|
21
33
|
raw: unknown,
|
|
22
34
|
artifact = "artifact",
|
|
@@ -32,3 +44,48 @@ export function decodeFrontMatterResult(
|
|
|
32
44
|
}
|
|
33
45
|
return Result.succeed(decoded.success)
|
|
34
46
|
}
|
|
47
|
+
|
|
48
|
+
/** Shared acceptance boundary for wait advancement and redelivery refusal. */
|
|
49
|
+
export function validateArtifactCompletion(
|
|
50
|
+
kind: DispatchKind,
|
|
51
|
+
fm: FrontMatter | null,
|
|
52
|
+
artifact = "artifact",
|
|
53
|
+
): Result.Result<AcceptedArtifactCompletion | null, ArtifactInvalid> {
|
|
54
|
+
const requireVerdict = kind === "plan_review" || kind === "code_review"
|
|
55
|
+
if (!isCompleteArtifact(fm, { requireVerdict })) return Result.succeed(null)
|
|
56
|
+
|
|
57
|
+
if (
|
|
58
|
+
fm!.rework !== undefined &&
|
|
59
|
+
(kind !== "code_review" || fm!.verdict !== "CHANGES_REQUIRED")
|
|
60
|
+
) {
|
|
61
|
+
return Result.fail(
|
|
62
|
+
new ArtifactInvalid({
|
|
63
|
+
artifact,
|
|
64
|
+
message:
|
|
65
|
+
"rework is valid only on a code_review artifact with verdict CHANGES_REQUIRED",
|
|
66
|
+
}),
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const decoded = decodeFrontMatterResult(
|
|
71
|
+
{
|
|
72
|
+
status: fm!.status,
|
|
73
|
+
...(requireVerdict ? { verdict: fm!.verdict } : {}),
|
|
74
|
+
nits: fm!.nits,
|
|
75
|
+
...(kind === "code_review" && fm!.rework ? { rework: fm!.rework } : {}),
|
|
76
|
+
},
|
|
77
|
+
artifact,
|
|
78
|
+
)
|
|
79
|
+
if (Result.isFailure(decoded)) return Result.fail(decoded.failure)
|
|
80
|
+
|
|
81
|
+
const rework = kind === "code_review" ? decoded.success.rework : undefined
|
|
82
|
+
const next = stepAfterArtifact(kind, fm!.verdict, rework)
|
|
83
|
+
if (typeof next === "object") {
|
|
84
|
+
return Result.fail(new ArtifactInvalid({ artifact, message: next.error }))
|
|
85
|
+
}
|
|
86
|
+
return Result.succeed({
|
|
87
|
+
frontmatter: decoded.success,
|
|
88
|
+
next,
|
|
89
|
+
rework,
|
|
90
|
+
})
|
|
91
|
+
}
|
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { Result, Schema } from "effect"
|
|
2
2
|
import { StateCorrupt } from "../errors.ts"
|
|
3
|
-
import
|
|
3
|
+
import {
|
|
4
|
+
LEGACY_CODE_REWORK,
|
|
5
|
+
LEGACY_PLAN_REWORK,
|
|
6
|
+
type RequiredReworkTarget,
|
|
7
|
+
type RunState,
|
|
8
|
+
type Step,
|
|
9
|
+
} from "../domain/types.ts"
|
|
4
10
|
|
|
5
11
|
export const StepSchema = Schema.Literals([
|
|
6
12
|
"planning",
|
|
@@ -22,35 +28,94 @@ export const RoleSchema = Schema.Literals([
|
|
|
22
28
|
"coder",
|
|
23
29
|
] as const)
|
|
24
30
|
|
|
31
|
+
export const RequiredReworkSchema = Schema.NullOr(
|
|
32
|
+
Schema.Literals(["plan", "code", "phase_package"] as const),
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
export const PendingDeliverySchema = Schema.NullOr(
|
|
36
|
+
Schema.Literals(["manual", "interactive"] as const),
|
|
37
|
+
)
|
|
38
|
+
|
|
25
39
|
const PaneRefSchema = Schema.Struct({
|
|
26
40
|
pane_id: Schema.String,
|
|
27
41
|
label: Schema.String,
|
|
28
42
|
profile_fingerprint: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
29
43
|
})
|
|
30
44
|
|
|
45
|
+
const PositiveSafeInteger = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1))
|
|
46
|
+
const NonNegativeSafeInteger = Schema.Int.check(
|
|
47
|
+
Schema.isGreaterThanOrEqualTo(0),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
const PendingCommitFields = {
|
|
51
|
+
id: Schema.String.check(Schema.isMinLength(1)),
|
|
52
|
+
phase_index: PositiveSafeInteger,
|
|
53
|
+
message: Schema.String.check(Schema.isMinLength(1)),
|
|
54
|
+
no_remaining_phases: Schema.Boolean,
|
|
55
|
+
verify_log: Schema.String.check(Schema.isMinLength(1)),
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Anchor fields flow into git/jj argv, so they get format checks beyond
|
|
59
|
+
// minLength. Exploiting a loose anchor requires state.json write access
|
|
60
|
+
// (game-over elsewhere), but validating costs nothing and removes the class.
|
|
61
|
+
export const GitPendingCommitSchema = Schema.Struct({
|
|
62
|
+
backend: Schema.Literal("git"),
|
|
63
|
+
...PendingCommitFields,
|
|
64
|
+
branch: Schema.String.check(
|
|
65
|
+
Schema.isPattern(/^refs\/heads\/apnea\/[A-Za-z0-9._-]+$/),
|
|
66
|
+
),
|
|
67
|
+
parent_commit: Schema.String.check(Schema.isPattern(/^[0-9a-f]{40,64}$/)),
|
|
68
|
+
tree_id: Schema.String.check(Schema.isPattern(/^[0-9a-f]{40,64}$/)),
|
|
69
|
+
})
|
|
70
|
+
|
|
71
|
+
export const JjPendingCommitSchema = Schema.Struct({
|
|
72
|
+
backend: Schema.Literal("jj"),
|
|
73
|
+
...PendingCommitFields,
|
|
74
|
+
change_id: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9]{1,32}$/)),
|
|
75
|
+
content_fingerprint: Schema.String.check(
|
|
76
|
+
Schema.isPattern(/^[0-9a-f]{8,64}$/),
|
|
77
|
+
),
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
export const PendingCommitSchema = Schema.Union([
|
|
81
|
+
GitPendingCommitSchema,
|
|
82
|
+
JjPendingCommitSchema,
|
|
83
|
+
])
|
|
84
|
+
|
|
31
85
|
/**
|
|
32
|
-
* Runtime codec for `state.json
|
|
33
|
-
*
|
|
86
|
+
* Runtime codec for `state.json`.
|
|
87
|
+
*
|
|
88
|
+
* Input accepts version 1 (files written by 0.2.x) and version 2. Version-1
|
|
89
|
+
* files must not carry `pending_commit` — that is enforced in
|
|
90
|
+
* `decodeRunState`, which migrates every decoded state to version 2 with
|
|
91
|
+
* `pending_commit: null`. Version-2 files must record `pending_commit`
|
|
92
|
+
* explicitly, so a truncated or hand-edited v2 file fails closed instead of
|
|
93
|
+
* silently losing a durable commit transaction.
|
|
34
94
|
*/
|
|
35
95
|
export const RunStateSchema = Schema.Struct({
|
|
36
|
-
|
|
96
|
+
// v1 on Encoded so legacy files still decode; decodeRunState always
|
|
97
|
+
// outputs version 2.
|
|
98
|
+
version: Schema.Union([Schema.Literal(1), Schema.Literal(2)]),
|
|
37
99
|
slug: Schema.String.check(Schema.isMinLength(1)),
|
|
38
100
|
step: StepSchema,
|
|
39
|
-
phase_index:
|
|
40
|
-
phase_count_hint: Schema.NullOr(
|
|
41
|
-
rounds: Schema.Record(Schema.String,
|
|
101
|
+
phase_index: PositiveSafeInteger,
|
|
102
|
+
phase_count_hint: Schema.NullOr(NonNegativeSafeInteger),
|
|
103
|
+
rounds: Schema.Record(Schema.String, PositiveSafeInteger),
|
|
42
104
|
vcs: VcsBackendSchema,
|
|
43
105
|
allow_dirty: Schema.Boolean,
|
|
44
106
|
goal: Schema.String,
|
|
45
107
|
last_error: Schema.NullOr(Schema.String),
|
|
46
108
|
pending_artifact: Schema.NullOr(Schema.String),
|
|
47
109
|
pending_role: Schema.NullOr(RoleSchema),
|
|
110
|
+
pending_delivery: Schema.optionalKey(PendingDeliverySchema),
|
|
48
111
|
// optional on Encoded so legacy fixtures without pane fields still decode
|
|
49
112
|
pending_pane_id: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
50
113
|
pending_pane_label: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
51
|
-
pending_started_at: Schema.optionalKey(Schema.NullOr(
|
|
52
|
-
pending_deadline_ms: Schema.optionalKey(
|
|
53
|
-
|
|
114
|
+
pending_started_at: Schema.optionalKey(Schema.NullOr(NonNegativeSafeInteger)),
|
|
115
|
+
pending_deadline_ms: Schema.optionalKey(
|
|
116
|
+
Schema.NullOr(NonNegativeSafeInteger),
|
|
117
|
+
),
|
|
118
|
+
pending_nudged_at: Schema.optionalKey(Schema.NullOr(NonNegativeSafeInteger)),
|
|
54
119
|
pending_final_grace: Schema.optionalKey(Schema.Boolean),
|
|
55
120
|
pending_extended: Schema.optionalKey(Schema.Boolean),
|
|
56
121
|
role_panes: Schema.optionalKey(Schema.Record(Schema.String, PaneRefSchema)),
|
|
@@ -58,11 +123,38 @@ export const RunStateSchema = Schema.Struct({
|
|
|
58
123
|
reviewer_tree_fingerprint: Schema.NullOr(Schema.String),
|
|
59
124
|
current_phase_package: Schema.NullOr(Schema.String),
|
|
60
125
|
current_code_review: Schema.NullOr(Schema.String),
|
|
61
|
-
|
|
126
|
+
required_rework: Schema.optionalKey(RequiredReworkSchema),
|
|
127
|
+
// optional on Encoded so version-1 files without the key still decode;
|
|
128
|
+
// presence rules per version are enforced in `decodeRunState`.
|
|
129
|
+
pending_commit: Schema.optionalKey(Schema.NullOr(PendingCommitSchema)),
|
|
62
130
|
})
|
|
63
131
|
|
|
64
132
|
export type DecodedRunState = typeof RunStateSchema.Type
|
|
65
133
|
|
|
134
|
+
function isPersistedArtifactPath(value: string): boolean {
|
|
135
|
+
if (
|
|
136
|
+
!value.startsWith(".apnea/") ||
|
|
137
|
+
value.includes("\\") ||
|
|
138
|
+
value.includes("\0")
|
|
139
|
+
) {
|
|
140
|
+
return false
|
|
141
|
+
}
|
|
142
|
+
return value
|
|
143
|
+
.slice(".apnea/".length)
|
|
144
|
+
.split("/")
|
|
145
|
+
.every((part) => part !== "" && part !== "." && part !== "..")
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function hasMatchingPendingCoderDispatch(d: DecodedRunState): boolean {
|
|
149
|
+
const phase = String(d.phase_index).padStart(2, "0")
|
|
150
|
+
const round = d.rounds[`phase-${phase}/code_review`] ?? 1
|
|
151
|
+
return (
|
|
152
|
+
d.pending_role === "coder" &&
|
|
153
|
+
d.pending_artifact ===
|
|
154
|
+
`.apnea/artifacts/phase-${phase}/round-${round}/coder-result.md`
|
|
155
|
+
)
|
|
156
|
+
}
|
|
157
|
+
|
|
66
158
|
export function decodeRunState(
|
|
67
159
|
json: unknown,
|
|
68
160
|
path = "state.json",
|
|
@@ -83,6 +175,14 @@ export function decodeRunState(
|
|
|
83
175
|
)
|
|
84
176
|
}
|
|
85
177
|
|
|
178
|
+
const raw =
|
|
179
|
+
json !== null && typeof json === "object" && !Array.isArray(json)
|
|
180
|
+
? (json as Record<string, unknown>)
|
|
181
|
+
: {}
|
|
182
|
+
const hasRequiredRework = raw.required_rework !== undefined
|
|
183
|
+
const hasPendingDelivery = raw.pending_delivery !== undefined
|
|
184
|
+
const hasPendingCommit =
|
|
185
|
+
"pending_commit" in raw && raw.pending_commit !== undefined
|
|
86
186
|
const decoded = Schema.decodeUnknownResult(RunStateSchema)(json)
|
|
87
187
|
if (Result.isFailure(decoded)) {
|
|
88
188
|
return Result.fail(
|
|
@@ -93,9 +193,80 @@ export function decodeRunState(
|
|
|
93
193
|
)
|
|
94
194
|
}
|
|
95
195
|
const d = decoded.success
|
|
196
|
+
// A version-1 writer never emits `pending_commit`; its presence means the
|
|
197
|
+
// file was mixed across versions. Fail closed instead of guessing.
|
|
198
|
+
if (d.version === 1 && hasPendingCommit) {
|
|
199
|
+
return Result.fail(
|
|
200
|
+
new StateCorrupt({
|
|
201
|
+
path,
|
|
202
|
+
message:
|
|
203
|
+
"pending_commit requires state version 2; refusing version-1 file that carries it",
|
|
204
|
+
}),
|
|
205
|
+
)
|
|
206
|
+
}
|
|
207
|
+
if (d.version === 2 && !hasPendingCommit) {
|
|
208
|
+
return Result.fail(
|
|
209
|
+
new StateCorrupt({
|
|
210
|
+
path,
|
|
211
|
+
message:
|
|
212
|
+
"version-2 state must record pending_commit explicitly; refusing file that omits it",
|
|
213
|
+
}),
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
if (
|
|
217
|
+
d.pending_commit !== null &&
|
|
218
|
+
d.pending_commit !== undefined &&
|
|
219
|
+
d.step !== "committing"
|
|
220
|
+
) {
|
|
221
|
+
return Result.fail(
|
|
222
|
+
new StateCorrupt({
|
|
223
|
+
path,
|
|
224
|
+
message: `pending_commit requires step "committing", found "${d.step}"`,
|
|
225
|
+
}),
|
|
226
|
+
)
|
|
227
|
+
}
|
|
228
|
+
if (
|
|
229
|
+
d.pending_commit != null &&
|
|
230
|
+
d.pending_commit.phase_index !== d.phase_index
|
|
231
|
+
) {
|
|
232
|
+
return Result.fail(
|
|
233
|
+
new StateCorrupt({
|
|
234
|
+
path,
|
|
235
|
+
message: `pending_commit targets phase ${d.pending_commit.phase_index} but state is at phase ${d.phase_index}`,
|
|
236
|
+
}),
|
|
237
|
+
)
|
|
238
|
+
}
|
|
239
|
+
if (
|
|
240
|
+
d.pending_commit != null &&
|
|
241
|
+
!isPersistedArtifactPath(d.pending_commit.verify_log)
|
|
242
|
+
) {
|
|
243
|
+
return Result.fail(
|
|
244
|
+
new StateCorrupt({
|
|
245
|
+
path,
|
|
246
|
+
message:
|
|
247
|
+
"pending_commit.verify_log must be a repository-relative .apnea/ path",
|
|
248
|
+
}),
|
|
249
|
+
)
|
|
250
|
+
}
|
|
251
|
+
for (const [field, value] of [
|
|
252
|
+
["pending_artifact", d.pending_artifact],
|
|
253
|
+
["current_phase_package", d.current_phase_package],
|
|
254
|
+
["current_code_review", d.current_code_review],
|
|
255
|
+
] as const) {
|
|
256
|
+
if (value !== null && !isPersistedArtifactPath(value)) {
|
|
257
|
+
return Result.fail(
|
|
258
|
+
new StateCorrupt({
|
|
259
|
+
path,
|
|
260
|
+
message: `${field} must be a repository-relative .apnea/ path`,
|
|
261
|
+
}),
|
|
262
|
+
)
|
|
263
|
+
}
|
|
264
|
+
}
|
|
96
265
|
// Backward-compat defaults for state.json files predating pane tracking.
|
|
266
|
+
// Every decoded state migrates to version 2; the rewrite to disk happens
|
|
267
|
+
// at the next normal save.
|
|
97
268
|
const state: RunState = {
|
|
98
|
-
version:
|
|
269
|
+
version: 2,
|
|
99
270
|
slug: d.slug,
|
|
100
271
|
step: d.step as Step,
|
|
101
272
|
phase_index: d.phase_index,
|
|
@@ -107,6 +278,11 @@ export function decodeRunState(
|
|
|
107
278
|
last_error: d.last_error,
|
|
108
279
|
pending_artifact: d.pending_artifact,
|
|
109
280
|
pending_role: d.pending_role,
|
|
281
|
+
pending_delivery: hasPendingDelivery
|
|
282
|
+
? (d.pending_delivery ?? null)
|
|
283
|
+
: d.pending_artifact !== null && d.pending_pane_id != null
|
|
284
|
+
? "interactive"
|
|
285
|
+
: null,
|
|
110
286
|
pending_pane_id: d.pending_pane_id ?? null,
|
|
111
287
|
pending_pane_label: d.pending_pane_label ?? null,
|
|
112
288
|
pending_started_at: d.pending_started_at ?? null,
|
|
@@ -124,7 +300,28 @@ export function decodeRunState(
|
|
|
124
300
|
reviewer_tree_fingerprint: d.reviewer_tree_fingerprint,
|
|
125
301
|
current_phase_package: d.current_phase_package,
|
|
126
302
|
current_code_review: d.current_code_review,
|
|
127
|
-
|
|
303
|
+
required_rework: (hasRequiredRework
|
|
304
|
+
? d.required_rework
|
|
305
|
+
: raw.phase_package_rework === true
|
|
306
|
+
? "phase_package"
|
|
307
|
+
: null) as RequiredReworkTarget | null,
|
|
308
|
+
pending_commit: d.pending_commit ?? null,
|
|
309
|
+
}
|
|
310
|
+
if (
|
|
311
|
+
!hasRequiredRework &&
|
|
312
|
+
state.required_rework === null &&
|
|
313
|
+
d.step === "planning"
|
|
314
|
+
) {
|
|
315
|
+
Object.defineProperty(state, LEGACY_PLAN_REWORK, { value: true })
|
|
316
|
+
}
|
|
317
|
+
if (
|
|
318
|
+
!hasRequiredRework &&
|
|
319
|
+
state.required_rework === null &&
|
|
320
|
+
d.step === "coding" &&
|
|
321
|
+
d.current_code_review !== null &&
|
|
322
|
+
!hasMatchingPendingCoderDispatch(d)
|
|
323
|
+
) {
|
|
324
|
+
Object.defineProperty(state, LEGACY_CODE_REWORK, { value: true })
|
|
128
325
|
}
|
|
129
326
|
return Result.succeed(state)
|
|
130
327
|
}
|
|
@@ -5,6 +5,7 @@ import { FileSystemLive } from "./file-system.ts"
|
|
|
5
5
|
import { makeHerdrLive } from "./herdr.ts"
|
|
6
6
|
import { RunStoreLive } from "./run-store.ts"
|
|
7
7
|
import { VcsLive } from "./vcs.ts"
|
|
8
|
+
import { ProcessLive } from "./process.ts"
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Blueprint layer for tool calls. Built freshly on every `Effect.provide`
|
|
@@ -18,7 +19,7 @@ export const makeAppLive = (hostAdapter: ApneaHostAdapter) =>
|
|
|
18
19
|
VcsLive,
|
|
19
20
|
makeHerdrLive(hostAdapter),
|
|
20
21
|
),
|
|
21
|
-
FileSystemLive,
|
|
22
|
+
Layer.merge(FileSystemLive, ProcessLive),
|
|
22
23
|
)
|
|
23
24
|
|
|
24
25
|
export const AppLive = makeAppLive(neutralHostAdapter)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import * as os from "node:os"
|
|
1
2
|
import { Context, Effect, Layer, Result } from "effect"
|
|
2
3
|
import { globalConfigPath, projectConfigPath } from "../domain/paths.ts"
|
|
3
4
|
import { ConfigError } from "../errors.ts"
|
|
@@ -50,7 +51,8 @@ export const ConfigLive = Layer.effect(
|
|
|
50
51
|
|
|
51
52
|
const load = (root: string): Effect.Effect<ApneaConfig, ConfigError> =>
|
|
52
53
|
Effect.gen(function* () {
|
|
53
|
-
const
|
|
54
|
+
const trustedHome = os.homedir()
|
|
55
|
+
const gPath = globalConfigPath(trustedHome)
|
|
54
56
|
const gPresent = yield* fs.exists(gPath)
|
|
55
57
|
if (!gPresent) {
|
|
56
58
|
return yield* new ConfigError({
|
|
@@ -58,7 +60,7 @@ export const ConfigLive = Layer.effect(
|
|
|
58
60
|
path: gPath,
|
|
59
61
|
})
|
|
60
62
|
}
|
|
61
|
-
const gText = yield* fs.
|
|
63
|
+
const gText = yield* fs.readTrustedGlobalFile(trustedHome, gPath)
|
|
62
64
|
const gRaw = yield* parseJson(gText, gPath)
|
|
63
65
|
const gDecoded = decodeGlobalConfig(gRaw)
|
|
64
66
|
if (Result.isFailure(gDecoded)) {
|
|
@@ -67,9 +69,9 @@ export const ConfigLive = Layer.effect(
|
|
|
67
69
|
let cfg = gDecoded.success
|
|
68
70
|
|
|
69
71
|
const pPath = projectConfigPath(root)
|
|
70
|
-
const pPresent = yield* fs.
|
|
72
|
+
const pPresent = yield* fs.projectPathExists(root, pPath)
|
|
71
73
|
if (pPresent) {
|
|
72
|
-
const pText = yield* fs.
|
|
74
|
+
const pText = yield* fs.readProjectFile(root, pPath)
|
|
73
75
|
const pRaw = yield* parseJson(pText, pPath)
|
|
74
76
|
const pDecoded = decodeProjectConfig(pRaw)
|
|
75
77
|
if (Result.isFailure(pDecoded)) {
|