@ref0/cli 0.11.0 → 0.12.1
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 +144 -2
- package/dist/main.mjs +791 -555
- package/package.json +2 -2
package/dist/main.mjs
CHANGED
|
@@ -798,7 +798,7 @@ const ScopeKind = Schema.Literals([
|
|
|
798
798
|
"session",
|
|
799
799
|
"agent"
|
|
800
800
|
]);
|
|
801
|
-
const Scope$
|
|
801
|
+
const Scope$2 = Schema.Struct({
|
|
802
802
|
kind: ScopeKind,
|
|
803
803
|
id: Schema.String.check(Schema.isMinLength(1)),
|
|
804
804
|
action: ScopeAction
|
|
@@ -971,7 +971,7 @@ const SessionDepth = Schema.Int.check(Schema.isBetween({
|
|
|
971
971
|
* attachment's authorization there and the agent's target there.
|
|
972
972
|
*/
|
|
973
973
|
const AskingHeld = Schema.Struct({
|
|
974
|
-
scopes: Schema.Array(Scope$
|
|
974
|
+
scopes: Schema.Array(Scope$2),
|
|
975
975
|
pins: Schema.Struct({
|
|
976
976
|
agent: Generation,
|
|
977
977
|
attachment: Generation,
|
|
@@ -1010,7 +1010,7 @@ const CredentialFacts = Schema.Struct({
|
|
|
1010
1010
|
}),
|
|
1011
1011
|
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis),
|
|
1012
1012
|
/** What it may do, and where. Rows a reader can't decode were left out: they grant nothing. */
|
|
1013
|
-
scopes: Schema.Array(Scope$
|
|
1013
|
+
scopes: Schema.Array(Scope$2),
|
|
1014
1014
|
/**
|
|
1015
1015
|
* For a session's token: its project's owner, which answers what its agent holds there; the
|
|
1016
1016
|
* session, whose lease its project's Project DO holds; and how deep it is in its chain, 1 for
|
|
@@ -5186,7 +5186,7 @@ const commandFields = {
|
|
|
5186
5186
|
* What the credential may do, and where, as its person's Owner DO said: the writer checks
|
|
5187
5187
|
* the command's action against it on what it holds, such as which thread a number is.
|
|
5188
5188
|
*/
|
|
5189
|
-
scopes: Schema.Array(Scope$
|
|
5189
|
+
scopes: Schema.Array(Scope$2),
|
|
5190
5190
|
/** For a command a person approved: their `approval.answered`, which its events name as their cause. */
|
|
5191
5191
|
cause: Schema.optionalKey(EventRef),
|
|
5192
5192
|
/**
|
|
@@ -5705,7 +5705,7 @@ const Me = Schema.Struct({
|
|
|
5705
5705
|
session: SessionId,
|
|
5706
5706
|
depth: SessionDepth
|
|
5707
5707
|
})),
|
|
5708
|
-
scopes: Schema.Array(Scope$
|
|
5708
|
+
scopes: Schema.Array(Scope$2),
|
|
5709
5709
|
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
|
|
5710
5710
|
});
|
|
5711
5711
|
/**
|
|
@@ -6075,7 +6075,7 @@ const Reader = Schema.Struct({
|
|
|
6075
6075
|
agent: Schema.optionalKey(AgentRef),
|
|
6076
6076
|
/** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
|
|
6077
6077
|
attachment: Schema.NullOr(CredentialRef),
|
|
6078
|
-
scopes: Schema.Array(Scope$
|
|
6078
|
+
scopes: Schema.Array(Scope$2)
|
|
6079
6079
|
});
|
|
6080
6080
|
Schema.Struct({
|
|
6081
6081
|
reader: Reader,
|
|
@@ -6589,14 +6589,13 @@ const ConnectOrganization = Schema.Struct({
|
|
|
6589
6589
|
organization: OrgId,
|
|
6590
6590
|
name: OwnerName
|
|
6591
6591
|
});
|
|
6592
|
-
/**
|
|
6592
|
+
/**
|
|
6593
|
+
* A folder's host grant from before: on and the person's, so the new grant's confirm turns it off,
|
|
6594
|
+
* or skipped, off already or not theirs, which a yes leaves as it is.
|
|
6595
|
+
*/
|
|
6593
6596
|
const GrantReplacement = Schema.Struct({
|
|
6594
6597
|
grant: TokenId,
|
|
6595
|
-
state: Schema.Literals([
|
|
6596
|
-
"replaces",
|
|
6597
|
-
"revoked",
|
|
6598
|
-
"skipped"
|
|
6599
|
-
]),
|
|
6598
|
+
state: Schema.Literals(["replaces", "skipped"]),
|
|
6600
6599
|
attachedAt: Schema.optionalKey(Timestamp)
|
|
6601
6600
|
});
|
|
6602
6601
|
/**
|
|
@@ -6661,12 +6660,15 @@ const RequestOrganization = Schema.Struct({
|
|
|
6661
6660
|
/**
|
|
6662
6661
|
* The result collected with the machine's two proofs: each included folder's identity and grant,
|
|
6663
6662
|
* its agent's owner name and organizations, and excluded paths. Confirmation checks each saved
|
|
6664
|
-
* grant's live deadline in the Owner DO.
|
|
6663
|
+
* grant's live deadline in the Owner DO. Once a yes connected and the machine hasn't sent its
|
|
6664
|
+
* answer code, waiting says until when: `confirmBy`, when ref0 revokes each grant the machine
|
|
6665
|
+
* hasn't confirmed.
|
|
6665
6666
|
*/
|
|
6666
6667
|
const RequestState = Schema.Union([
|
|
6667
6668
|
Schema.Struct({
|
|
6668
6669
|
state: Schema.Literal("waiting"),
|
|
6669
|
-
expiresAt: Timestamp
|
|
6670
|
+
expiresAt: Timestamp,
|
|
6671
|
+
confirmBy: Schema.optionalKey(Timestamp)
|
|
6670
6672
|
}),
|
|
6671
6673
|
Schema.Struct({
|
|
6672
6674
|
state: Schema.Literal("connected"),
|
|
@@ -13242,7 +13244,7 @@ Schema.Struct({
|
|
|
13242
13244
|
actor: ActorRef,
|
|
13243
13245
|
/** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
|
|
13244
13246
|
attachment: Schema.NullOr(CredentialRef),
|
|
13245
|
-
scopes: Schema.Array(Scope$
|
|
13247
|
+
scopes: Schema.Array(Scope$2),
|
|
13246
13248
|
ownerName: OwnerName,
|
|
13247
13249
|
number: ThreadNumber
|
|
13248
13250
|
});
|
|
@@ -19109,271 +19111,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
|
|
|
19109
19111
|
/** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
|
|
19110
19112
|
const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
|
|
19111
19113
|
//#endregion
|
|
19112
|
-
//#region src/session.ts
|
|
19113
|
-
/**
|
|
19114
|
-
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
19115
|
-
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
19116
|
-
* person instead.
|
|
19117
|
-
*/
|
|
19118
|
-
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
19119
|
-
/**
|
|
19120
|
-
* The token of the session this command runs in, if it runs in one: an agent's, which its
|
|
19121
|
-
* harness bound when the session started. Where it's found is the harness's to say. Outside a
|
|
19122
|
-
* session, there's none, and every command is the person's, asked by approval.
|
|
19123
|
-
*/
|
|
19124
|
-
var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
|
|
19125
|
-
/** No session: until a harness binds one, the person asks, by approval. */
|
|
19126
|
-
static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
|
|
19127
|
-
};
|
|
19128
|
-
//#endregion
|
|
19129
|
-
//#region src/call.ts
|
|
19130
|
-
/**
|
|
19131
|
-
* Every command's one way to ref0: an endpoint's request, sent with the session's token when
|
|
19132
|
-
* the command runs in a session, or else asked of the person, by approval, from this terminal.
|
|
19133
|
-
* Either way the answer is the endpoint's own, as JSON.
|
|
19134
|
-
*/
|
|
19135
|
-
/** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
|
|
19136
|
-
var CallFailed = class extends Schema.TaggedError()("CallFailed", {
|
|
19137
|
-
status: Schema.Int,
|
|
19138
|
-
kind: Schema.String,
|
|
19139
|
-
nextStep: Schema.String
|
|
19140
|
-
}) {};
|
|
19141
|
-
const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
19142
|
-
const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
|
|
19143
|
-
const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
|
|
19144
|
-
const decodeRefusal = Schema.decodeUnknownOption(AnyError);
|
|
19145
|
-
/** How often a terminal asks whether its person has answered. */
|
|
19146
|
-
const pollMillis$1 = 2e3;
|
|
19147
|
-
/** How long one request may take before it's asked again, or the call gives up. */
|
|
19148
|
-
const requestTimeout$1 = Duration.seconds(15);
|
|
19149
|
-
/**
|
|
19150
|
-
* How long a session's request may take for the commands that read and compile before they
|
|
19151
|
-
* answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
|
|
19152
|
-
* lacks the check, one paced read at a time, up to 256 files each.
|
|
19153
|
-
*/
|
|
19154
|
-
const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
|
|
19155
|
-
/** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
|
|
19156
|
-
const giveUpAfterMillis$1 = 3e5;
|
|
19157
|
-
/** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
|
|
19158
|
-
const outageMillis = 3e5;
|
|
19159
|
-
/** What a change that failed partway says: ref0 may have made it all the same. */
|
|
19160
|
-
const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
|
|
19161
|
-
/**
|
|
19162
|
-
* An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
|
|
19163
|
-
* 5xx, may have been made before it failed, so it isn't said to try again.
|
|
19164
|
-
*/
|
|
19165
|
-
const failedWith$15 = (status, body, change) => Option.match(decodeRefusal(body), {
|
|
19166
|
-
onNone: () => new CallFailed({
|
|
19167
|
-
status,
|
|
19168
|
-
kind: "Unexpected",
|
|
19169
|
-
nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
|
|
19170
|
-
}),
|
|
19171
|
-
onSome: (refusal) => new CallFailed({
|
|
19172
|
-
status,
|
|
19173
|
-
kind: refusal._tag,
|
|
19174
|
-
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19175
|
-
})
|
|
19176
|
-
});
|
|
19177
|
-
/**
|
|
19178
|
-
* A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
|
|
19179
|
-
* command's failure: a change that failed at a 5xx may have been made all the same.
|
|
19180
|
-
*/
|
|
19181
|
-
const refusedBy = (status, refusal, change) => new CallFailed({
|
|
19182
|
-
status,
|
|
19183
|
-
kind: refusal._tag,
|
|
19184
|
-
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19185
|
-
});
|
|
19186
|
-
/**
|
|
19187
|
-
* An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
|
|
19188
|
-
* the CLI made. A change it answered may have run.
|
|
19189
|
-
*/
|
|
19190
|
-
const unreadable = (endpoint, change) => new CallFailed({
|
|
19191
|
-
status: 0,
|
|
19192
|
-
kind: "Unexpected",
|
|
19193
|
-
nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
|
|
19194
|
-
});
|
|
19195
|
-
const unreachable = new CallFailed({
|
|
19196
|
-
status: 0,
|
|
19197
|
-
kind: "Unreachable",
|
|
19198
|
-
nextStep: "Couldn’t reach ref0. Try again."
|
|
19199
|
-
});
|
|
19200
|
-
/**
|
|
19201
|
-
* An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
|
|
19202
|
-
* change may have run, and asking again could make it twice.
|
|
19203
|
-
*/
|
|
19204
|
-
const gone = (change, nextStep) => new CallFailed({
|
|
19205
|
-
status: 404,
|
|
19206
|
-
kind: "ApprovalNotFound",
|
|
19207
|
-
nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
|
|
19208
|
-
});
|
|
19209
|
-
/** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
|
|
19210
|
-
const unanswered$1 = new CallFailed({
|
|
19211
|
-
status: 0,
|
|
19212
|
-
kind: "Unreachable",
|
|
19213
|
-
nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
|
|
19214
|
-
});
|
|
19215
|
-
/**
|
|
19216
|
-
* Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
|
|
19217
|
-
* stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
|
|
19218
|
-
* What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
|
|
19219
|
-
*/
|
|
19220
|
-
const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
|
|
19221
|
-
const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
|
|
19222
|
-
let approved = false;
|
|
19223
|
-
let heardAt = yield* Clock.currentTimeMillis;
|
|
19224
|
-
while (true) {
|
|
19225
|
-
const answered = yield* collect;
|
|
19226
|
-
const now = yield* Clock.currentTimeMillis;
|
|
19227
|
-
if (Option.isNone(answered)) {
|
|
19228
|
-
if (now - heardAt > outageMillis) return yield* new CallFailed({
|
|
19229
|
-
status: 0,
|
|
19230
|
-
kind: "Unreachable",
|
|
19231
|
-
nextStep: approved ? "It was approved, and ref0 stopped answering before it said it ran. Check whether it did before you run it again." : "ref0 stopped answering before it said how it ended. Check whether it ran before you run it again."
|
|
19232
|
-
});
|
|
19233
|
-
yield* Effect.sleep(pollMillis$1);
|
|
19234
|
-
continue;
|
|
19235
|
-
}
|
|
19236
|
-
const state = answered.value;
|
|
19237
|
-
heardAt = now;
|
|
19238
|
-
if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
|
|
19239
|
-
onNone: () => Effect.fail(unreadable(endpoint, change)),
|
|
19240
|
-
onSome: Result.match({
|
|
19241
|
-
onSuccess: () => Effect.succeed(state.result),
|
|
19242
|
-
onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
|
|
19243
|
-
})
|
|
19244
|
-
});
|
|
19245
|
-
if (state.state === "refused") return yield* new CallFailed({
|
|
19246
|
-
status: 403,
|
|
19247
|
-
kind: "Refused",
|
|
19248
|
-
nextStep: "It was refused."
|
|
19249
|
-
});
|
|
19250
|
-
if (state.state === "approved") {
|
|
19251
|
-
if (!approved) yield* Console.error("Approved. Waiting for it to run.");
|
|
19252
|
-
approved = true;
|
|
19253
|
-
yield* Effect.sleep(pollMillis$1);
|
|
19254
|
-
continue;
|
|
19255
|
-
}
|
|
19256
|
-
if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
|
|
19257
|
-
status: 408,
|
|
19258
|
-
kind: "Expired",
|
|
19259
|
-
nextStep: "Nobody approved it in time. Run it again."
|
|
19260
|
-
});
|
|
19261
|
-
yield* Effect.sleep(pollMillis$1);
|
|
19262
|
-
}
|
|
19263
|
-
});
|
|
19264
|
-
var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
19265
|
-
static layer = Layer.effect(Ref0, Effect.gen(function* () {
|
|
19266
|
-
const config = yield* Ref0Config;
|
|
19267
|
-
const session = yield* SessionToken;
|
|
19268
|
-
const http = yield* HttpClient.HttpClient;
|
|
19269
|
-
const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
|
|
19270
|
-
const timeout = timeoutOf(request.command.endpoint);
|
|
19271
|
-
const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
|
|
19272
|
-
const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
|
|
19273
|
-
const change = request.method !== "GET";
|
|
19274
|
-
const lost = change ? unanswered$1 : unreachable;
|
|
19275
|
-
const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
|
|
19276
|
-
response,
|
|
19277
|
-
text
|
|
19278
|
-
}))), Effect.timeoutOrElse({
|
|
19279
|
-
duration: timeout,
|
|
19280
|
-
orElse: () => Effect.fail(lost)
|
|
19281
|
-
}), Effect.mapError(() => lost));
|
|
19282
|
-
const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
|
|
19283
|
-
if (response.status === 202) {
|
|
19284
|
-
const needed = decodeNeeded(body);
|
|
19285
|
-
if (Option.isNone(needed)) return yield* failedWith$15(202, body, change);
|
|
19286
|
-
yield* Console.error(`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`);
|
|
19287
|
-
return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
|
|
19288
|
-
}
|
|
19289
|
-
return response.status < 300 ? body : yield* failedWith$15(response.status, body, change);
|
|
19290
|
-
});
|
|
19291
|
-
const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
|
|
19292
|
-
const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
|
|
19293
|
-
const refused = (status) => (error) => Effect.fail(new CallFailed({
|
|
19294
|
-
status,
|
|
19295
|
-
kind: error._tag,
|
|
19296
|
-
nextStep: error.nextStep
|
|
19297
|
-
}));
|
|
19298
|
-
return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19299
|
-
duration: requestTimeout$1,
|
|
19300
|
-
orElse: () => Effect.succeedNone
|
|
19301
|
-
}), Effect.catchTags({
|
|
19302
|
-
ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
|
|
19303
|
-
Unauthenticated: refused(401),
|
|
19304
|
-
SessionHasEnded: refused(410)
|
|
19305
|
-
}), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19306
|
-
});
|
|
19307
|
-
const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
|
|
19308
|
-
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
19309
|
-
const { secret, hash } = yield* freshSecret;
|
|
19310
|
-
const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
|
|
19311
|
-
const started = yield* client.terminalApprovals.start({
|
|
19312
|
-
headers: { "idempotency-key": yield* newRequestId },
|
|
19313
|
-
payload: {
|
|
19314
|
-
command,
|
|
19315
|
-
secretHash
|
|
19316
|
-
}
|
|
19317
|
-
}).pipe(Effect.timeoutOrElse({
|
|
19318
|
-
duration: requestTimeout$1,
|
|
19319
|
-
orElse: () => Effect.fail(unreachable)
|
|
19320
|
-
}), Effect.catchTags({
|
|
19321
|
-
CommandRefused: (refused) => Effect.fail(new CallFailed({
|
|
19322
|
-
status: 400,
|
|
19323
|
-
kind: refused._tag,
|
|
19324
|
-
nextStep: refused.nextStep
|
|
19325
|
-
})),
|
|
19326
|
-
SlowDown: (slow) => Effect.fail(new CallFailed({
|
|
19327
|
-
status: 429,
|
|
19328
|
-
kind: slow._tag,
|
|
19329
|
-
nextStep: slow.nextStep
|
|
19330
|
-
}))
|
|
19331
|
-
}), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
|
|
19332
|
-
yield* Console.error(`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`);
|
|
19333
|
-
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
|
|
19334
|
-
const collect = (yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http))).terminalApprovals.result({ params: { approval: started.approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19335
|
-
duration: requestTimeout$1,
|
|
19336
|
-
orElse: () => Effect.succeedNone
|
|
19337
|
-
}), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19338
|
-
return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
|
|
19339
|
-
});
|
|
19340
|
-
const call = Effect.fn("Ref0.call")(function* (command) {
|
|
19341
|
-
const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
|
|
19342
|
-
status: 0,
|
|
19343
|
-
kind: error._tag,
|
|
19344
|
-
nextStep: error.nextStep
|
|
19345
|
-
})));
|
|
19346
|
-
const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
|
|
19347
|
-
status: 400,
|
|
19348
|
-
kind: refused._tag,
|
|
19349
|
-
nextStep: refused.nextStep
|
|
19350
|
-
})));
|
|
19351
|
-
const change = request.method !== "GET";
|
|
19352
|
-
const answer = yield* Option.match(token, {
|
|
19353
|
-
onNone: () => asPerson(request.command, change, command.endpoint),
|
|
19354
|
-
onSome: (bearer) => asSession(bearer, request, command.endpoint)
|
|
19355
|
-
});
|
|
19356
|
-
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
|
|
19357
|
-
return answer;
|
|
19358
|
-
});
|
|
19359
|
-
return Ref0.of({ call });
|
|
19360
|
-
}));
|
|
19361
|
-
};
|
|
19362
|
-
/**
|
|
19363
|
-
* One endpoint's call, typed from the API: the compiler checks its command, and its answer is
|
|
19364
|
-
* read by the endpoint's own success schemas, so what a command shows of it is checked too.
|
|
19365
|
-
*/
|
|
19366
|
-
const callEndpoint = (command) => Effect.gen(function* () {
|
|
19367
|
-
const json = yield* (yield* Ref0).call(command);
|
|
19368
|
-
return {
|
|
19369
|
-
json,
|
|
19370
|
-
value: yield* Option.match(successOf(command.endpoint, json), {
|
|
19371
|
-
onNone: () => Effect.fail(unreadable(command.endpoint, false)),
|
|
19372
|
-
onSome: Effect.succeed
|
|
19373
|
-
})
|
|
19374
|
-
};
|
|
19375
|
-
});
|
|
19376
|
-
//#endregion
|
|
19377
19114
|
//#region src/output.ts
|
|
19378
19115
|
/**
|
|
19379
19116
|
* What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
|
|
@@ -19399,12 +19136,88 @@ const renderLine = (line, terminal) => {
|
|
|
19399
19136
|
if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
|
|
19400
19137
|
return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
|
|
19401
19138
|
};
|
|
19402
|
-
|
|
19139
|
+
/** The most columns a printed line takes, however wide the terminal. */
|
|
19140
|
+
const mostColumns = 100;
|
|
19141
|
+
/** A printed text without the styles this printer adds. */
|
|
19142
|
+
const unstyled = (text) => text.replaceAll(/\u001b\[\d+m/g, "");
|
|
19143
|
+
const graphemes = new Intl.Segmenter();
|
|
19144
|
+
/** The columns a printed text takes, a character each as the person sees it: styles take none. */
|
|
19145
|
+
const columnsOf = (text) => [...graphemes.segment(unstyled(text))].length;
|
|
19146
|
+
/** A word that says an inline command follows: `run ref0 connect`, `Read a text: ref0 text …`. */
|
|
19147
|
+
const introducesCommand = (word) => word === "run" || word === "Run" || word.endsWith(":");
|
|
19148
|
+
/**
|
|
19149
|
+
* Inline `ref0` commands made single words while they fit in `room`: `ref0` and the word after it
|
|
19150
|
+
* always, and a command after `run` or a colon to its clause's end, so no line breaks inside one.
|
|
19151
|
+
*/
|
|
19152
|
+
const withCommandsWhole = (words, room) => {
|
|
19153
|
+
const joined = [];
|
|
19154
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
19155
|
+
const first = words[at];
|
|
19156
|
+
if (first === void 0) continue;
|
|
19157
|
+
if (unstyled(first.word) !== "ref0" || at + 1 >= words.length) {
|
|
19158
|
+
joined.push(first);
|
|
19159
|
+
continue;
|
|
19160
|
+
}
|
|
19161
|
+
const before = words[at - 1];
|
|
19162
|
+
let end = at + 1;
|
|
19163
|
+
if (before !== void 0 && introducesCommand(unstyled(before.word))) while (end + 1 < words.length && !/[.,;:]$/.test(unstyled(words[end]?.word ?? ""))) end += 1;
|
|
19164
|
+
const word = words.slice(at, end + 1).map((each, index) => index === 0 ? each.word : `${each.space}${each.word}`).join("");
|
|
19165
|
+
if (columnsOf(word) > room) {
|
|
19166
|
+
joined.push(first);
|
|
19167
|
+
continue;
|
|
19168
|
+
}
|
|
19169
|
+
joined.push({
|
|
19170
|
+
space: first.space,
|
|
19171
|
+
word
|
|
19172
|
+
});
|
|
19173
|
+
at = end;
|
|
19174
|
+
}
|
|
19175
|
+
return joined;
|
|
19176
|
+
};
|
|
19177
|
+
/**
|
|
19178
|
+
* A line broken between words to fit `width` columns, each line after the first indented as the
|
|
19179
|
+
* first is. A line that fits stays as it is, and a word wider than the width stays whole, on a
|
|
19180
|
+
* line of its own.
|
|
19181
|
+
*/
|
|
19182
|
+
const wrapLine = (line, width) => {
|
|
19183
|
+
if (columnsOf(line) <= width) return line;
|
|
19184
|
+
const indent = /^\s*/.exec(line)?.[0] ?? "";
|
|
19185
|
+
const room = Math.max(1, width - columnsOf(indent));
|
|
19186
|
+
const words = withCommandsWhole(Array.from(line.slice(indent.length).matchAll(/(\s*)(\S+)/g), ([, space = "", word = ""]) => ({
|
|
19187
|
+
space,
|
|
19188
|
+
word
|
|
19189
|
+
})), room);
|
|
19190
|
+
const lines = [];
|
|
19191
|
+
let current = "";
|
|
19192
|
+
for (const { space, word } of words) if (current !== "" && columnsOf(current) + columnsOf(space) + columnsOf(word) > room) {
|
|
19193
|
+
lines.push(current);
|
|
19194
|
+
current = word;
|
|
19195
|
+
} else current += current === "" ? word : `${space}${word}`;
|
|
19196
|
+
return [...lines, current].map((each) => `${indent}${each}`).join("\n");
|
|
19197
|
+
};
|
|
19198
|
+
/**
|
|
19199
|
+
* How wide a printed line may be: the terminal's width, at most 100 columns, or none when the
|
|
19200
|
+
* stream isn't a terminal, as for an agent or a script, whose own reader wraps.
|
|
19201
|
+
*/
|
|
19202
|
+
const widthOf = (stream) => {
|
|
19203
|
+
if (process[stream].isTTY !== true) return void 0;
|
|
19204
|
+
const columns = process[stream].columns;
|
|
19205
|
+
return columns > 0 ? Math.min(columns, mostColumns) : mostColumns;
|
|
19206
|
+
};
|
|
19207
|
+
const renderBlocks = (blocks, stream, width) => {
|
|
19403
19208
|
const terminal = process[stream].isTTY === true && process.env["NO_COLOR"] === void 0;
|
|
19404
|
-
|
|
19209
|
+
const shown = (line) => {
|
|
19210
|
+
const rendered = renderLine(line, terminal);
|
|
19211
|
+
if (width === void 0 || !Predicate.isString(line) && "command" in line) return rendered;
|
|
19212
|
+
return rendered.split("\n").map((each) => wrapLine(each, width)).join("\n");
|
|
19213
|
+
};
|
|
19214
|
+
return blocks.flatMap((block) => block.length === 0 ? [] : [block.map(shown).join("\n")]).join("\n\n");
|
|
19405
19215
|
};
|
|
19406
|
-
/**
|
|
19407
|
-
|
|
19216
|
+
/**
|
|
19217
|
+
* Words go to stderr by default; a command's answer goes to stdout. In a terminal, each line but a
|
|
19218
|
+
* command's is broken between words to fit it.
|
|
19219
|
+
*/
|
|
19220
|
+
const printBlocks = (blocks, stream = "stderr") => Effect.sync(() => renderBlocks(blocks, stream, widthOf(stream))).pipe(Effect.flatMap((text) => stream === "stdout" ? Console.log(text) : Console.error(text)));
|
|
19408
19221
|
/**
|
|
19409
19222
|
* A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
|
|
19410
19223
|
* yes. The cursor stays after it, for their answer.
|
|
@@ -19474,6 +19287,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
|
|
|
19474
19287
|
};
|
|
19475
19288
|
/** Says what went wrong, once, on stderr, and fails the command. */
|
|
19476
19289
|
const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] : words).pipe(Effect.andThen(Effect.fail(new Said())));
|
|
19290
|
+
//#endregion
|
|
19291
|
+
//#region src/session.ts
|
|
19292
|
+
/**
|
|
19293
|
+
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
19294
|
+
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
19295
|
+
* person instead.
|
|
19296
|
+
*/
|
|
19297
|
+
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
19298
|
+
/**
|
|
19299
|
+
* The token of the session this command runs in, if it runs in one: an agent's, which its
|
|
19300
|
+
* harness bound when the session started. Where it's found is the harness's to say. Outside a
|
|
19301
|
+
* session, there's none, and every command is the person's, asked by approval.
|
|
19302
|
+
*/
|
|
19303
|
+
var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
|
|
19304
|
+
/** No session: until a harness binds one, the person asks, by approval. */
|
|
19305
|
+
static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
|
|
19306
|
+
};
|
|
19307
|
+
/**
|
|
19308
|
+
* The thread the session this command runs in is on, as its start kept it beside the session's
|
|
19309
|
+
* token. Outside a session there's none, and in one whose start kept none.
|
|
19310
|
+
*/
|
|
19311
|
+
var SessionThread = class SessionThread extends Context.Service()("ref0/cli/SessionThread") {
|
|
19312
|
+
/** No session, so no thread. */
|
|
19313
|
+
static none = Layer.succeed(SessionThread, SessionThread.of({ current: Effect.succeedNone }));
|
|
19314
|
+
};
|
|
19315
|
+
//#endregion
|
|
19316
|
+
//#region src/call.ts
|
|
19317
|
+
/**
|
|
19318
|
+
* Every command's one way to ref0: an endpoint's request, sent with the session's token when
|
|
19319
|
+
* the command runs in a session, or else asked of the person, by approval, from this terminal.
|
|
19320
|
+
* Either way the answer is the endpoint's own, as JSON.
|
|
19321
|
+
*/
|
|
19322
|
+
/** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
|
|
19323
|
+
var CallFailed = class extends Schema.TaggedError()("CallFailed", {
|
|
19324
|
+
status: Schema.Int,
|
|
19325
|
+
kind: Schema.String,
|
|
19326
|
+
nextStep: Schema.String
|
|
19327
|
+
}) {};
|
|
19328
|
+
const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
19329
|
+
const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
|
|
19330
|
+
const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
|
|
19331
|
+
const decodeRefusal = Schema.decodeUnknownOption(AnyError);
|
|
19332
|
+
/** How often a terminal asks whether its person has answered. */
|
|
19333
|
+
const pollMillis$1 = 2e3;
|
|
19334
|
+
/** How long one request may take before it's asked again, or the call gives up. */
|
|
19335
|
+
const requestTimeout$1 = Duration.seconds(15);
|
|
19336
|
+
/**
|
|
19337
|
+
* How long a session's request may take for the commands that read and compile before they
|
|
19338
|
+
* answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
|
|
19339
|
+
* lacks the check, one paced read at a time, up to 256 files each.
|
|
19340
|
+
*/
|
|
19341
|
+
const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
|
|
19342
|
+
/** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
|
|
19343
|
+
const giveUpAfterMillis$1 = 3e5;
|
|
19344
|
+
/** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
|
|
19345
|
+
const outageMillis = 3e5;
|
|
19346
|
+
/** What a change that failed partway says: ref0 may have made it all the same. */
|
|
19347
|
+
const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
|
|
19348
|
+
/**
|
|
19349
|
+
* An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
|
|
19350
|
+
* 5xx, may have been made before it failed, so it isn't said to try again.
|
|
19351
|
+
*/
|
|
19352
|
+
const failedWith$16 = (status, body, change) => Option.match(decodeRefusal(body), {
|
|
19353
|
+
onNone: () => new CallFailed({
|
|
19354
|
+
status,
|
|
19355
|
+
kind: "Unexpected",
|
|
19356
|
+
nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
|
|
19357
|
+
}),
|
|
19358
|
+
onSome: (refusal) => new CallFailed({
|
|
19359
|
+
status,
|
|
19360
|
+
kind: refusal._tag,
|
|
19361
|
+
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19362
|
+
})
|
|
19363
|
+
});
|
|
19364
|
+
/**
|
|
19365
|
+
* A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
|
|
19366
|
+
* command's failure: a change that failed at a 5xx may have been made all the same.
|
|
19367
|
+
*/
|
|
19368
|
+
const refusedBy = (status, refusal, change) => new CallFailed({
|
|
19369
|
+
status,
|
|
19370
|
+
kind: refusal._tag,
|
|
19371
|
+
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19372
|
+
});
|
|
19373
|
+
/**
|
|
19374
|
+
* An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
|
|
19375
|
+
* the CLI made. A change it answered may have run.
|
|
19376
|
+
*/
|
|
19377
|
+
const unreadable = (endpoint, change) => new CallFailed({
|
|
19378
|
+
status: 0,
|
|
19379
|
+
kind: "Unexpected",
|
|
19380
|
+
nextStep: change ? `ref0’s answer to ${endpoint} isn’t one that endpoint gives, so it may have run. Check before you run it again.` : `ref0’s answer to ${endpoint} isn’t one that endpoint gives. Try again.`
|
|
19381
|
+
});
|
|
19382
|
+
const unreachable = new CallFailed({
|
|
19383
|
+
status: 0,
|
|
19384
|
+
kind: "Unreachable",
|
|
19385
|
+
nextStep: "Couldn’t reach ref0. Try again."
|
|
19386
|
+
});
|
|
19387
|
+
/**
|
|
19388
|
+
* An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
|
|
19389
|
+
* change may have run, and asking again could make it twice.
|
|
19390
|
+
*/
|
|
19391
|
+
const gone = (change, nextStep) => new CallFailed({
|
|
19392
|
+
status: 404,
|
|
19393
|
+
kind: "ApprovalNotFound",
|
|
19394
|
+
nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
|
|
19395
|
+
});
|
|
19396
|
+
/** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
|
|
19397
|
+
const unanswered$1 = new CallFailed({
|
|
19398
|
+
status: 0,
|
|
19399
|
+
kind: "Unreachable",
|
|
19400
|
+
nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
|
|
19401
|
+
});
|
|
19402
|
+
/**
|
|
19403
|
+
* Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
|
|
19404
|
+
* stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
|
|
19405
|
+
* What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
|
|
19406
|
+
*/
|
|
19407
|
+
const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
|
|
19408
|
+
const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
|
|
19409
|
+
let approved = false;
|
|
19410
|
+
let heardAt = yield* Clock.currentTimeMillis;
|
|
19411
|
+
while (true) {
|
|
19412
|
+
const answered = yield* collect;
|
|
19413
|
+
const now = yield* Clock.currentTimeMillis;
|
|
19414
|
+
if (Option.isNone(answered)) {
|
|
19415
|
+
if (now - heardAt > outageMillis) return yield* new CallFailed({
|
|
19416
|
+
status: 0,
|
|
19417
|
+
kind: "Unreachable",
|
|
19418
|
+
nextStep: approved ? "It was approved, and ref0 stopped answering before it said it ran. Check whether it did before you run it again." : "ref0 stopped answering before it said how it ended. Check whether it ran before you run it again."
|
|
19419
|
+
});
|
|
19420
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19421
|
+
continue;
|
|
19422
|
+
}
|
|
19423
|
+
const state = answered.value;
|
|
19424
|
+
heardAt = now;
|
|
19425
|
+
if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
|
|
19426
|
+
onNone: () => Effect.fail(unreadable(endpoint, change)),
|
|
19427
|
+
onSome: Result.match({
|
|
19428
|
+
onSuccess: () => Effect.succeed(state.result),
|
|
19429
|
+
onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
|
|
19430
|
+
})
|
|
19431
|
+
});
|
|
19432
|
+
if (state.state === "refused") return yield* new CallFailed({
|
|
19433
|
+
status: 403,
|
|
19434
|
+
kind: "Refused",
|
|
19435
|
+
nextStep: "It was refused."
|
|
19436
|
+
});
|
|
19437
|
+
if (state.state === "approved") {
|
|
19438
|
+
if (!approved) yield* printBlocks([["Approved. Waiting for it to run."]]);
|
|
19439
|
+
approved = true;
|
|
19440
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19441
|
+
continue;
|
|
19442
|
+
}
|
|
19443
|
+
if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
|
|
19444
|
+
status: 408,
|
|
19445
|
+
kind: "Expired",
|
|
19446
|
+
nextStep: "Nobody approved it in time. Run it again."
|
|
19447
|
+
});
|
|
19448
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19449
|
+
}
|
|
19450
|
+
});
|
|
19451
|
+
var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
19452
|
+
static layer = Layer.effect(Ref0, Effect.gen(function* () {
|
|
19453
|
+
const config = yield* Ref0Config;
|
|
19454
|
+
const session = yield* SessionToken;
|
|
19455
|
+
const http = yield* HttpClient.HttpClient;
|
|
19456
|
+
const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
|
|
19457
|
+
const timeout = timeoutOf(request.command.endpoint);
|
|
19458
|
+
const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
|
|
19459
|
+
const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
|
|
19460
|
+
const change = request.method !== "GET";
|
|
19461
|
+
const lost = change ? unanswered$1 : unreachable;
|
|
19462
|
+
const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
|
|
19463
|
+
response,
|
|
19464
|
+
text
|
|
19465
|
+
}))), Effect.timeoutOrElse({
|
|
19466
|
+
duration: timeout,
|
|
19467
|
+
orElse: () => Effect.fail(lost)
|
|
19468
|
+
}), Effect.mapError(() => lost));
|
|
19469
|
+
const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
|
|
19470
|
+
if (response.status === 202) {
|
|
19471
|
+
const needed = decodeNeeded(body);
|
|
19472
|
+
if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
|
|
19473
|
+
yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
|
|
19474
|
+
return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
|
|
19475
|
+
}
|
|
19476
|
+
return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
|
|
19477
|
+
});
|
|
19478
|
+
const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
|
|
19479
|
+
const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
|
|
19480
|
+
const refused = (status) => (error) => Effect.fail(new CallFailed({
|
|
19481
|
+
status,
|
|
19482
|
+
kind: error._tag,
|
|
19483
|
+
nextStep: error.nextStep
|
|
19484
|
+
}));
|
|
19485
|
+
return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19486
|
+
duration: requestTimeout$1,
|
|
19487
|
+
orElse: () => Effect.succeedNone
|
|
19488
|
+
}), Effect.catchTags({
|
|
19489
|
+
ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
|
|
19490
|
+
Unauthenticated: refused(401),
|
|
19491
|
+
SessionHasEnded: refused(410)
|
|
19492
|
+
}), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19493
|
+
});
|
|
19494
|
+
const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
|
|
19495
|
+
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
19496
|
+
const { secret, hash } = yield* freshSecret;
|
|
19497
|
+
const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
|
|
19498
|
+
const started = yield* client.terminalApprovals.start({
|
|
19499
|
+
headers: { "idempotency-key": yield* newRequestId },
|
|
19500
|
+
payload: {
|
|
19501
|
+
command,
|
|
19502
|
+
secretHash
|
|
19503
|
+
}
|
|
19504
|
+
}).pipe(Effect.timeoutOrElse({
|
|
19505
|
+
duration: requestTimeout$1,
|
|
19506
|
+
orElse: () => Effect.fail(unreachable)
|
|
19507
|
+
}), Effect.catchTags({
|
|
19508
|
+
CommandRefused: (refused) => Effect.fail(new CallFailed({
|
|
19509
|
+
status: 400,
|
|
19510
|
+
kind: refused._tag,
|
|
19511
|
+
nextStep: refused.nextStep
|
|
19512
|
+
})),
|
|
19513
|
+
SlowDown: (slow) => Effect.fail(new CallFailed({
|
|
19514
|
+
status: 429,
|
|
19515
|
+
kind: slow._tag,
|
|
19516
|
+
nextStep: slow.nextStep
|
|
19517
|
+
}))
|
|
19518
|
+
}), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
|
|
19519
|
+
yield* printBlocks([[`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`]]);
|
|
19520
|
+
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
|
|
19521
|
+
const collect = (yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http))).terminalApprovals.result({ params: { approval: started.approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19522
|
+
duration: requestTimeout$1,
|
|
19523
|
+
orElse: () => Effect.succeedNone
|
|
19524
|
+
}), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19525
|
+
return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
|
|
19526
|
+
});
|
|
19527
|
+
const call = Effect.fn("Ref0.call")(function* (command) {
|
|
19528
|
+
const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
|
|
19529
|
+
status: 0,
|
|
19530
|
+
kind: error._tag,
|
|
19531
|
+
nextStep: error.nextStep
|
|
19532
|
+
})));
|
|
19533
|
+
const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
|
|
19534
|
+
status: 400,
|
|
19535
|
+
kind: refused._tag,
|
|
19536
|
+
nextStep: refused.nextStep
|
|
19537
|
+
})));
|
|
19538
|
+
const change = request.method !== "GET";
|
|
19539
|
+
const answer = yield* Option.match(token, {
|
|
19540
|
+
onNone: () => asPerson(request.command, change, command.endpoint),
|
|
19541
|
+
onSome: (bearer) => asSession(bearer, request, command.endpoint)
|
|
19542
|
+
});
|
|
19543
|
+
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
|
|
19544
|
+
return answer;
|
|
19545
|
+
});
|
|
19546
|
+
return Ref0.of({ call });
|
|
19547
|
+
}));
|
|
19548
|
+
};
|
|
19549
|
+
/**
|
|
19550
|
+
* One endpoint's call, typed from the API: the compiler checks its command, and its answer is
|
|
19551
|
+
* read by the endpoint's own success schemas, so what a command shows of it is checked too.
|
|
19552
|
+
*/
|
|
19553
|
+
const callEndpoint = (command) => Effect.gen(function* () {
|
|
19554
|
+
const json = yield* (yield* Ref0).call(command);
|
|
19555
|
+
return {
|
|
19556
|
+
json,
|
|
19557
|
+
value: yield* Option.match(successOf(command.endpoint, json), {
|
|
19558
|
+
onNone: () => Effect.fail(unreadable(command.endpoint, false)),
|
|
19559
|
+
onSome: Effect.succeed
|
|
19560
|
+
})
|
|
19561
|
+
};
|
|
19562
|
+
});
|
|
19477
19563
|
/** The account's commands. */
|
|
19478
19564
|
const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
19479
19565
|
const answer = yield* callEndpoint({ endpoint: "me.get" });
|
|
@@ -20417,7 +20503,7 @@ const waitsFor = (each) => {
|
|
|
20417
20503
|
* A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
|
|
20418
20504
|
* when it's in another of `owner`'s, then its title and stage.
|
|
20419
20505
|
*/
|
|
20420
|
-
const threadWords = (thread, owner = "") => {
|
|
20506
|
+
const threadWords$1 = (thread, owner = "") => {
|
|
20421
20507
|
return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
|
|
20422
20508
|
};
|
|
20423
20509
|
/** What holds a stack back below the thread, as a line says it, to follow "and". */
|
|
@@ -20435,13 +20521,13 @@ const stackHeld = (stack) => {
|
|
|
20435
20521
|
*/
|
|
20436
20522
|
const planLine = (wait, number, owner) => {
|
|
20437
20523
|
if (wait.kind === "stack") {
|
|
20438
|
-
const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
|
|
20524
|
+
const line = `stacked on ${wait.on === void 0 ? "a thread you can’t see" : threadWords$1(wait.on)}: #${number} may merge once it may${stackHeld(wait)}`;
|
|
20439
20525
|
return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
|
|
20440
20526
|
}
|
|
20441
20527
|
if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
|
|
20442
20528
|
if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
|
|
20443
20529
|
const holds = wait.child.project === void 0 || wait.child.stage !== "settled" ? `#${number} is settled once it is` : `#${number} is settled once a settle check reads it so`;
|
|
20444
|
-
return `${wait.child.thread} required child ${threadWords(wait.child, owner)}: ${holds}`;
|
|
20530
|
+
return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
|
|
20445
20531
|
};
|
|
20446
20532
|
/** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
|
|
20447
20533
|
const blockedBy$1 = (list) => {
|
|
@@ -20646,7 +20732,7 @@ const browserOpener = Effect.gen(function* () {
|
|
|
20646
20732
|
});
|
|
20647
20733
|
/** Starts the browser independently; a missing or failing desktop never stops the paste. */
|
|
20648
20734
|
const openBrowser = (program, link) => Effect.sync(() => {
|
|
20649
|
-
const failed = () => Effect.runFork(
|
|
20735
|
+
const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
|
|
20650
20736
|
try {
|
|
20651
20737
|
const child = spawn(program, [link], {
|
|
20652
20738
|
detached: true,
|
|
@@ -21317,7 +21403,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
21317
21403
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21318
21404
|
* and only as a default export.
|
|
21319
21405
|
*/
|
|
21320
|
-
const cliVersion = "0.
|
|
21406
|
+
const cliVersion = "0.12.1";
|
|
21321
21407
|
//#endregion
|
|
21322
21408
|
//#region src/harness/hook-protocol.ts
|
|
21323
21409
|
/**
|
|
@@ -21635,7 +21721,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21635
21721
|
const link = yield* freshLinkSecret;
|
|
21636
21722
|
const pasted = yield* Ref.make(void 0);
|
|
21637
21723
|
const opener = yield* browserOpener;
|
|
21638
|
-
const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(
|
|
21724
|
+
const listener = opener === void 0 ? void 0 : yield* listenForAnswer(new URL(config.api).origin).pipe(Effect.catchTag("ListenerUnavailable", () => Effect.as(printBlocks([["Couldn’t listen for the browser. Paste the code instead."]]), void 0)));
|
|
21639
21725
|
const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
|
|
21640
21726
|
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
21641
21727
|
const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
|
|
@@ -21646,7 +21732,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21646
21732
|
...folder,
|
|
21647
21733
|
shown: shownConfigFolder(home, folder.configFolder),
|
|
21648
21734
|
grant: yield* freshTokenSecret,
|
|
21649
|
-
replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
|
|
21735
|
+
replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said)),
|
|
21736
|
+
before: Option.isSome(held) ? yield* hostGrant.get(folder.configFolder).pipe(Effect.catchTag("KeychainError", (error) => error.unreadable === true ? Effect.succeedNone : said(error))) : Option.none()
|
|
21650
21737
|
};
|
|
21651
21738
|
}));
|
|
21652
21739
|
const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
|
|
@@ -21701,10 +21788,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21701
21788
|
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
|
|
21702
21789
|
const collector = yield* ref0Client(config.api).pipe(Effect.provide(Layer.merge(anonymousCredential, withSecret)), Effect.provideService(HttpClient.HttpClient, http));
|
|
21703
21790
|
const params = { approval: started.approval };
|
|
21704
|
-
const collect =
|
|
21791
|
+
const collect = (answerCode) => collector.machineAttachments.collect({
|
|
21705
21792
|
params,
|
|
21706
21793
|
payload: answerCode === void 0 ? {} : { answerCode }
|
|
21707
|
-
})
|
|
21794
|
+
}).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
21708
21795
|
duration: requestTimeout,
|
|
21709
21796
|
orElse: () => Effect.succeedNone
|
|
21710
21797
|
}), Effect.catchTags({
|
|
@@ -21714,29 +21801,28 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21714
21801
|
HttpClientError: () => Effect.succeedNone,
|
|
21715
21802
|
SchemaError: () => Effect.succeedNone
|
|
21716
21803
|
}));
|
|
21804
|
+
const lapsing = asking.some((folder) => Option.isSome(folder.replaces)) ? "Its new grants expire unconfirmed, and each folder’s connection from before still works." : "Its new grants expire unconfirmed.";
|
|
21717
21805
|
const giveUpAt = DateTime.toEpochMillis(started.expiresAt) + giveUpAfterMillis;
|
|
21806
|
+
let confirmBy;
|
|
21718
21807
|
const answered = Effect.gen(function* () {
|
|
21719
21808
|
while (true) {
|
|
21720
|
-
const
|
|
21721
|
-
|
|
21722
|
-
if (
|
|
21809
|
+
const sent = yield* Ref.get(pasted);
|
|
21810
|
+
const state = Option.getOrUndefined(yield* collect(sent));
|
|
21811
|
+
if (state !== void 0 && isEnded(state)) return state;
|
|
21812
|
+
if (state?.confirmBy !== void 0) confirmBy = DateTime.toEpochMillis(state.confirmBy);
|
|
21813
|
+
if ((yield* Ref.get(pasted)) !== sent) continue;
|
|
21814
|
+
if ((yield* Clock.currentTimeMillis) > (confirmBy ?? giveUpAt)) {
|
|
21723
21815
|
yield* endWait;
|
|
21816
|
+
if (confirmBy !== void 0 && sent === void 0) return yield* sayFailed(`It was approved, but no code was pasted in time, so this machine kept nothing. ${lapsing} To connect this machine, run: ${again$1}`);
|
|
21724
21817
|
return yield* sayFailed(`ref0 hasn’t said how it ended, so this machine kept nothing. If it was approved, ref0 revokes its grants 10 minutes after it connected. To connect this machine, run: ${again$1}`);
|
|
21725
21818
|
}
|
|
21726
21819
|
yield* Effect.sleep(pollMillis);
|
|
21727
21820
|
}
|
|
21728
21821
|
});
|
|
21729
|
-
const
|
|
21822
|
+
const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
|
|
21730
21823
|
const left = keepBy - (yield* Clock.currentTimeMillis);
|
|
21731
|
-
if (left <= 0) return Option.some(
|
|
21732
|
-
const
|
|
21733
|
-
const kept = yield* hostGrant.keep(folder.configFolder, {
|
|
21734
|
-
harness: folder.harness,
|
|
21735
|
-
owner: connection.owner,
|
|
21736
|
-
agent: connection.handle,
|
|
21737
|
-
agentId: connection.agent.id,
|
|
21738
|
-
token: Redacted.make(token)
|
|
21739
|
-
}).pipe(Effect.timeoutOrElse({
|
|
21824
|
+
if (left <= 0) return Option.some(late);
|
|
21825
|
+
const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
|
|
21740
21826
|
duration: Duration.min(keepTimeout, Duration.millis(left)),
|
|
21741
21827
|
orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
|
|
21742
21828
|
}), Effect.retry({
|
|
@@ -21745,10 +21831,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21745
21831
|
}), Effect.result);
|
|
21746
21832
|
return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
|
|
21747
21833
|
});
|
|
21834
|
+
const keepOne = (folder, connection, keepBy) => keepTrying(folder.configFolder, {
|
|
21835
|
+
harness: folder.harness,
|
|
21836
|
+
owner: connection.owner,
|
|
21837
|
+
agent: connection.handle,
|
|
21838
|
+
agentId: connection.agent.id,
|
|
21839
|
+
token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
|
|
21840
|
+
}, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
|
|
21748
21841
|
const finish = Effect.fnUntraced(function* (state) {
|
|
21749
21842
|
if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
|
|
21750
21843
|
const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
|
|
21751
21844
|
const kept = [];
|
|
21845
|
+
const keptFrom = [];
|
|
21752
21846
|
const grants = [];
|
|
21753
21847
|
const notKept = [];
|
|
21754
21848
|
for (const folder of asking) {
|
|
@@ -21778,9 +21872,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21778
21872
|
handle: connection.handle,
|
|
21779
21873
|
organizations: connection.organizations
|
|
21780
21874
|
});
|
|
21875
|
+
keptFrom.push(folder);
|
|
21781
21876
|
grants.push(connection.credential.id);
|
|
21782
21877
|
}
|
|
21783
|
-
yield* oldGrants.retire(kept);
|
|
21784
21878
|
const [first, ...rest] = grants;
|
|
21785
21879
|
if (first === void 0) return yield* sayFailed([[
|
|
21786
21880
|
"It connected, but this machine couldn’t keep what it needs to act:",
|
|
@@ -21813,9 +21907,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21813
21907
|
times: 2,
|
|
21814
21908
|
schedule: Schedule.spaced("1 second")
|
|
21815
21909
|
}), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
|
|
21816
|
-
|
|
21817
|
-
|
|
21910
|
+
const backBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
|
|
21911
|
+
const notBack = [];
|
|
21912
|
+
for (const folder of keptFrom) {
|
|
21913
|
+
if (Option.isNone(folder.before)) {
|
|
21914
|
+
yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
|
|
21915
|
+
continue;
|
|
21916
|
+
}
|
|
21917
|
+
const failed = yield* keepTrying(folder.configFolder, folder.before.value, backBy, "The keychain took too long with the folders before it.");
|
|
21918
|
+
if (Option.isSome(failed)) notBack.push(`${shownConfigFolder(home, folder.configFolder)}: ${failed.value}`);
|
|
21919
|
+
}
|
|
21920
|
+
return yield* sayFailed([[
|
|
21921
|
+
"ref0 revoked the new grants, since this machine didn’t confirm in time that it kept them.",
|
|
21922
|
+
...notBack.length === 0 ? ["Each folder has its connection from before back, if it had one. To connect this machine, run:"] : [
|
|
21923
|
+
"These folders hold a grant that works nowhere, since the keychain didn’t take back their records:",
|
|
21924
|
+
...notBack,
|
|
21925
|
+
"Each other folder has its connection from before back, if it had one. To connect this machine, run:"
|
|
21926
|
+
],
|
|
21927
|
+
{ command: again$1 }
|
|
21928
|
+
]]);
|
|
21818
21929
|
}
|
|
21930
|
+
yield* oldGrants.retire(kept);
|
|
21819
21931
|
return {
|
|
21820
21932
|
owner: state.owner,
|
|
21821
21933
|
kept,
|
|
@@ -21827,7 +21939,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21827
21939
|
};
|
|
21828
21940
|
});
|
|
21829
21941
|
const stopped = Effect.gen(function* () {
|
|
21830
|
-
yield*
|
|
21942
|
+
yield* printBlocks([["Stopping: aborting the request."]]);
|
|
21831
21943
|
const abortId = yield* newRequestId;
|
|
21832
21944
|
const state = yield* collector.machineAttachments.abort({
|
|
21833
21945
|
params,
|
|
@@ -21846,18 +21958,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21846
21958
|
times: 2
|
|
21847
21959
|
}), Effect.catchTag("Unanswered", () => sayFailed("Couldn’t reach ref0 to abort it. If it’s approved anyway, ref0 revokes its grants 10 minutes after they connect, since this machine never kept them.")));
|
|
21848
21960
|
if (!isEnded(state)) {
|
|
21849
|
-
if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed("It was already answered, but no code was pasted
|
|
21850
|
-
yield*
|
|
21961
|
+
if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", lapsing]]);
|
|
21962
|
+
yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
|
|
21851
21963
|
}
|
|
21852
21964
|
const connected = yield* finish(isEnded(state) ? state : yield* answered);
|
|
21853
21965
|
yield* printBlocks([[{ label: `Connected as ${connected.owner}’s agents` }, ...connected.kept.map((folder) => `${shownConfigFolder(home, folder.configFolder)} ${folder.handle}`)], ["Stopped, so nothing was installed. To install ref0’s plugin into each, run:", ...connected.kept.map((folder) => ({ command: installLine(folder.configFolder) }))]]);
|
|
21854
21966
|
});
|
|
21855
|
-
const
|
|
21967
|
+
const readAnswer = Effect.gen(function* () {
|
|
21968
|
+
if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21856
21969
|
const keys = yield* terminal.readInput;
|
|
21857
21970
|
let typed = "";
|
|
21858
21971
|
while (true) {
|
|
21859
21972
|
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21860
21973
|
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21974
|
+
if ((yield* Ref.get(pasted)) !== void 0) continue;
|
|
21861
21975
|
const { input, key } = pressed.value;
|
|
21862
21976
|
if (key.name === "backspace") {
|
|
21863
21977
|
typed = takeBack(typed);
|
|
@@ -21871,17 +21985,14 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21871
21985
|
const line = typed.trim();
|
|
21872
21986
|
typed = "";
|
|
21873
21987
|
if (line === "") continue;
|
|
21874
|
-
if (isAnswerCode(line))
|
|
21875
|
-
|
|
21988
|
+
if (!isAnswerCode(line)) {
|
|
21989
|
+
yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
|
|
21990
|
+
continue;
|
|
21991
|
+
}
|
|
21992
|
+
yield* Ref.update(pasted, (first) => first ?? line);
|
|
21993
|
+
if (listener !== void 0) yield* listener.stop;
|
|
21876
21994
|
}
|
|
21877
21995
|
}).pipe(Effect.scoped);
|
|
21878
|
-
const readAnswer = Effect.gen(function* () {
|
|
21879
|
-
if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21880
|
-
const code = yield* typedCode;
|
|
21881
|
-
yield* Ref.update(pasted, (first) => first ?? code);
|
|
21882
|
-
if (listener !== void 0) yield* listener.stop;
|
|
21883
|
-
return yield* Effect.never;
|
|
21884
|
-
});
|
|
21885
21996
|
const receiveAnswer = listener === void 0 ? readAnswer : Effect.raceFirst(readAnswer, listener.answer.pipe(Effect.flatMap((code) => Ref.update(pasted, (first) => first ?? code)), Effect.andThen(Effect.never)));
|
|
21886
21997
|
return yield* Effect.uninterruptibleMask((restore) => restore(Effect.raceFirst(answered, receiveAnswer)).pipe(Effect.ensuring(listener === void 0 ? Effect.void : listener.stop), Effect.ensuring(endWait), Effect.onInterrupt(() => Effect.ignore(stopped)), Effect.flatMap(finish)));
|
|
21887
21998
|
}, Effect.scoped);
|
|
@@ -22861,6 +22972,111 @@ function parse(toml, options = {}) {
|
|
|
22861
22972
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22862
22973
|
*/
|
|
22863
22974
|
//#endregion
|
|
22975
|
+
//#region src/harness/empty-folder.ts
|
|
22976
|
+
/**
|
|
22977
|
+
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22978
|
+
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22979
|
+
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22980
|
+
*
|
|
22981
|
+
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22982
|
+
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22983
|
+
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22984
|
+
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22985
|
+
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22986
|
+
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22987
|
+
*/
|
|
22988
|
+
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22989
|
+
//#endregion
|
|
22990
|
+
//#region src/harness/codex-app-server.ts
|
|
22991
|
+
/**
|
|
22992
|
+
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22993
|
+
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22994
|
+
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
22995
|
+
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
22996
|
+
* didn't send, is passed over.
|
|
22997
|
+
*/
|
|
22998
|
+
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
22999
|
+
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
23000
|
+
/** Codex's app server answered a request with an error. */
|
|
23001
|
+
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
23002
|
+
method: Schema.String,
|
|
23003
|
+
error: CodexError
|
|
23004
|
+
}) {};
|
|
23005
|
+
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
23006
|
+
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
23007
|
+
method: Schema.String,
|
|
23008
|
+
issue: Schema.String
|
|
23009
|
+
}) {};
|
|
23010
|
+
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
23011
|
+
const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
23012
|
+
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
23013
|
+
const initializeId = "ref0-initialize";
|
|
23014
|
+
const askedId = "ref0-asked";
|
|
23015
|
+
/** The exchange's opening: who's asking, then that it's ready. */
|
|
23016
|
+
const opening = [{
|
|
23017
|
+
jsonrpc: "2.0",
|
|
23018
|
+
id: initializeId,
|
|
23019
|
+
method: codexMethods.initialize.method,
|
|
23020
|
+
params: { clientInfo: {
|
|
23021
|
+
name: "ref0",
|
|
23022
|
+
title: null,
|
|
23023
|
+
version: cliVersion
|
|
23024
|
+
} }
|
|
23025
|
+
}, {
|
|
23026
|
+
jsonrpc: "2.0",
|
|
23027
|
+
method: codexMethods.initialized.method
|
|
23028
|
+
}];
|
|
23029
|
+
/**
|
|
23030
|
+
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
23031
|
+
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
23032
|
+
*/
|
|
23033
|
+
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
23034
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23035
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23036
|
+
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
23037
|
+
cwd: yield* emptyFolder(fs),
|
|
23038
|
+
env,
|
|
23039
|
+
extendEnv: true,
|
|
23040
|
+
forceKillAfter: "2 seconds",
|
|
23041
|
+
stdin: {
|
|
23042
|
+
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
23043
|
+
endOnDone: false
|
|
23044
|
+
}
|
|
23045
|
+
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
23046
|
+
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
23047
|
+
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
23048
|
+
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
23049
|
+
const resultOf = (method, answer) => {
|
|
23050
|
+
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
23051
|
+
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
23052
|
+
method: method.method,
|
|
23053
|
+
error: answer.error
|
|
23054
|
+
}));
|
|
23055
|
+
const { result } = method;
|
|
23056
|
+
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
23057
|
+
method: method.method,
|
|
23058
|
+
issue: error.message
|
|
23059
|
+
})));
|
|
23060
|
+
};
|
|
23061
|
+
/**
|
|
23062
|
+
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
23063
|
+
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
23064
|
+
* its request's ID, and its result is read by the method's result schema.
|
|
23065
|
+
*/
|
|
23066
|
+
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
23067
|
+
const asked = {
|
|
23068
|
+
jsonrpc: "2.0",
|
|
23069
|
+
id: askedId,
|
|
23070
|
+
method: method.method,
|
|
23071
|
+
params
|
|
23072
|
+
};
|
|
23073
|
+
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
23074
|
+
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
23075
|
+
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
23076
|
+
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
23077
|
+
return yield* resultOf(method, answers.value.get(askedId));
|
|
23078
|
+
});
|
|
23079
|
+
//#endregion
|
|
22864
23080
|
//#region src/harness/codex-checkouts.ts
|
|
22865
23081
|
const codexGitFolder = (configFolder, pluginId) => {
|
|
22866
23082
|
const at = pluginId.indexOf("@");
|
|
@@ -22977,111 +23193,6 @@ const codexSignOff = [
|
|
|
22977
23193
|
`perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
|
|
22978
23194
|
];
|
|
22979
23195
|
//#endregion
|
|
22980
|
-
//#region src/harness/empty-folder.ts
|
|
22981
|
-
/**
|
|
22982
|
-
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22983
|
-
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22984
|
-
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22985
|
-
*
|
|
22986
|
-
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22987
|
-
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22988
|
-
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22989
|
-
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22990
|
-
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22991
|
-
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22992
|
-
*/
|
|
22993
|
-
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22994
|
-
//#endregion
|
|
22995
|
-
//#region src/harness/codex-app-server.ts
|
|
22996
|
-
/**
|
|
22997
|
-
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22998
|
-
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22999
|
-
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
23000
|
-
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
23001
|
-
* didn't send, is passed over.
|
|
23002
|
-
*/
|
|
23003
|
-
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
23004
|
-
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
23005
|
-
/** Codex's app server answered a request with an error. */
|
|
23006
|
-
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
23007
|
-
method: Schema.String,
|
|
23008
|
-
error: CodexError
|
|
23009
|
-
}) {};
|
|
23010
|
-
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
23011
|
-
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
23012
|
-
method: Schema.String,
|
|
23013
|
-
issue: Schema.String
|
|
23014
|
-
}) {};
|
|
23015
|
-
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
23016
|
-
const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
23017
|
-
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
23018
|
-
const initializeId = "ref0-initialize";
|
|
23019
|
-
const askedId = "ref0-asked";
|
|
23020
|
-
/** The exchange's opening: who's asking, then that it's ready. */
|
|
23021
|
-
const opening = [{
|
|
23022
|
-
jsonrpc: "2.0",
|
|
23023
|
-
id: initializeId,
|
|
23024
|
-
method: codexMethods.initialize.method,
|
|
23025
|
-
params: { clientInfo: {
|
|
23026
|
-
name: "ref0",
|
|
23027
|
-
title: null,
|
|
23028
|
-
version: cliVersion
|
|
23029
|
-
} }
|
|
23030
|
-
}, {
|
|
23031
|
-
jsonrpc: "2.0",
|
|
23032
|
-
method: codexMethods.initialized.method
|
|
23033
|
-
}];
|
|
23034
|
-
/**
|
|
23035
|
-
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
23036
|
-
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
23037
|
-
*/
|
|
23038
|
-
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
23039
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23040
|
-
const fs = yield* FileSystem.FileSystem;
|
|
23041
|
-
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
23042
|
-
cwd: yield* emptyFolder(fs),
|
|
23043
|
-
env,
|
|
23044
|
-
extendEnv: true,
|
|
23045
|
-
forceKillAfter: "2 seconds",
|
|
23046
|
-
stdin: {
|
|
23047
|
-
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
23048
|
-
endOnDone: false
|
|
23049
|
-
}
|
|
23050
|
-
}))).stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.map((line) => decodeAnswer$2(line)), Stream.filter(Option.isSome), Stream.scan(() => /* @__PURE__ */ new Map(), (answers, { value: answer }) => ids.includes(answer.id) && !answers.has(answer.id) ? new Map(answers).set(answer.id, answer) : answers), Stream.takeUntil((answers) => answers.size === ids.length), Stream.runLast);
|
|
23051
|
-
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
23052
|
-
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
23053
|
-
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
23054
|
-
const resultOf = (method, answer) => {
|
|
23055
|
-
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
23056
|
-
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
23057
|
-
method: method.method,
|
|
23058
|
-
error: answer.error
|
|
23059
|
-
}));
|
|
23060
|
-
const { result } = method;
|
|
23061
|
-
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
23062
|
-
method: method.method,
|
|
23063
|
-
issue: error.message
|
|
23064
|
-
})));
|
|
23065
|
-
};
|
|
23066
|
-
/**
|
|
23067
|
-
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
23068
|
-
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
23069
|
-
* its request's ID, and its result is read by the method's result schema.
|
|
23070
|
-
*/
|
|
23071
|
-
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
23072
|
-
const asked = {
|
|
23073
|
-
jsonrpc: "2.0",
|
|
23074
|
-
id: askedId,
|
|
23075
|
-
method: method.method,
|
|
23076
|
-
params
|
|
23077
|
-
};
|
|
23078
|
-
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
23079
|
-
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
23080
|
-
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
23081
|
-
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
23082
|
-
return yield* resultOf(method, answers.value.get(askedId));
|
|
23083
|
-
});
|
|
23084
|
-
//#endregion
|
|
23085
23196
|
//#region src/harness/commit-hooks.ts
|
|
23086
23197
|
/**
|
|
23087
23198
|
* ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
|
|
@@ -23391,21 +23502,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
|
|
|
23391
23502
|
parameters: shellWord(`include.path=${file}`)
|
|
23392
23503
|
};
|
|
23393
23504
|
});
|
|
23505
|
+
/** Only the single ref0 include installed by this CLI belongs to ref0. */
|
|
23506
|
+
const selectedInclude = (set) => {
|
|
23507
|
+
const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
|
|
23508
|
+
const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
|
|
23509
|
+
return set["REF0_HARNESS"] === "codex" && shellWord(included) === parameters && /^include\.path=[\s\S]*\/plugins\/cache\/[^/]+\/[^/]+\/@git\/agents\/[a-z0-9_-]+\.gitconfig$/.test(included) ? included.slice(13) : void 0;
|
|
23510
|
+
};
|
|
23511
|
+
const decodePolicy = Schema.decodeUnknownEffect(Schema.Struct({ shell_environment_policy: CodexConfigRead.fields.config.fields.shell_environment_policy }));
|
|
23512
|
+
/**
|
|
23513
|
+
* The agent's include a Codex config folder's `config.toml` selects, if it's ref0's: its file, read
|
|
23514
|
+
* as TOML for its `shell_environment_policy` alone.
|
|
23515
|
+
*/
|
|
23516
|
+
const selectedCodexInclude = Effect.fnUntraced(function* (configFolder) {
|
|
23517
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23518
|
+
const configFile = `${configFolder}/config.toml`;
|
|
23519
|
+
if (!(yield* fs.exists(configFile))) return Option.none();
|
|
23520
|
+
const text = yield* fs.readFileString(configFile);
|
|
23521
|
+
const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
|
|
23522
|
+
const read = yield* decodePolicy(document);
|
|
23523
|
+
return Option.fromNullishOr(selectedInclude(read.shell_environment_policy?.set ?? {}));
|
|
23524
|
+
});
|
|
23525
|
+
/**
|
|
23526
|
+
* Takes the author out of `selected`, the include a folder's `config.toml` selects, unless it's
|
|
23527
|
+
* this origin's include for the folder's agent, so a folder switched to another agent never names
|
|
23528
|
+
* the one before. Its commits name the person until an install selects the agent's own. Whether it
|
|
23529
|
+
* took one out.
|
|
23530
|
+
*/
|
|
23531
|
+
const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, selected, options) {
|
|
23532
|
+
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23533
|
+
if (selected === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return false;
|
|
23534
|
+
const oldRoot = selected.slice(0, selected.lastIndexOf("/agents/"));
|
|
23535
|
+
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home);
|
|
23536
|
+
yield* writeCodexCheckout({
|
|
23537
|
+
gitFolder: oldRoot,
|
|
23538
|
+
common: "",
|
|
23539
|
+
folder: "",
|
|
23540
|
+
signsOff: false,
|
|
23541
|
+
clearAuthor: selected,
|
|
23542
|
+
author: fallback
|
|
23543
|
+
});
|
|
23544
|
+
return true;
|
|
23545
|
+
});
|
|
23546
|
+
/** What the agent reads at its session's start when its folder's `config.toml` names another. */
|
|
23547
|
+
const otherAgentWords = (configFolder) => `ref0: The config.toml of this Codex config folder, ${configFolder}, names another agent than the one this folder is connected as, so your commits name the person instead of you until they run, in their own terminal: ${installLine(configFolder)}`;
|
|
23394
23548
|
/**
|
|
23395
23549
|
* Codex has no environment file: its install selects a file; session start fills its checkout,
|
|
23396
|
-
* for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`.
|
|
23550
|
+
* for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`. An include
|
|
23551
|
+
* `config.toml` selects for another agent, after a switch whose install never finished or another
|
|
23552
|
+
* origin's install, has its author taken out first, and the agent hears so.
|
|
23397
23553
|
*/
|
|
23398
23554
|
const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
|
|
23399
23555
|
const folders = yield* ConfigFolders;
|
|
23400
23556
|
const fs = yield* FileSystem.FileSystem;
|
|
23401
23557
|
const path = yield* Path.Path;
|
|
23402
23558
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23403
|
-
const
|
|
23559
|
+
const configFolder = yield* folders.folder("codex");
|
|
23560
|
+
const selected = yield* selectedCodexInclude(configFolder).pipe(Effect.mapError(() => new CodexCommitFailed({ detail: `Couldn’t read Codex’s config, ${configFolder}/config.toml.` })), Effect.result);
|
|
23561
|
+
const agent = yield* (yield* HostGrant).get(configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
|
|
23404
23562
|
if (Option.isNone(agent)) return false;
|
|
23563
|
+
const home = yield* Config.String("HOME");
|
|
23564
|
+
if (yield* Option.match(yield* Effect.fromResult(selected), {
|
|
23565
|
+
onNone: () => Effect.succeed(false),
|
|
23566
|
+
onSome: (file) => clearOtherCodexAuthor(agent.value, file, {
|
|
23567
|
+
configFolder,
|
|
23568
|
+
home
|
|
23569
|
+
})
|
|
23570
|
+
})) yield* Console.log(otherAgentWords(configFolder));
|
|
23405
23571
|
const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
|
|
23406
23572
|
if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
|
|
23407
23573
|
const gitFolder = path.join(path.dirname(pluginRoot), "@git");
|
|
23408
|
-
const home = yield* Config.String("HOME");
|
|
23409
23574
|
const author = yield* codexAgentConfig(agent.value, gitFolder, home);
|
|
23410
23575
|
const common = (yield* spawner.string(ChildProcess.make("git", [
|
|
23411
23576
|
"-C",
|
|
@@ -23761,35 +23926,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
23761
23926
|
//#region src/harness/codex-install-commits.ts
|
|
23762
23927
|
const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
|
|
23763
23928
|
const sameJson = Schema.toEquivalence(Schema.Json);
|
|
23764
|
-
/**
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
23929
|
+
/**
|
|
23930
|
+
* What the person reads when an install that never finished took the agent before out of a
|
|
23931
|
+
* folder's commits: what stopped it, in its own words, a conflict only when there is one, then
|
|
23932
|
+
* what to run.
|
|
23933
|
+
*/
|
|
23934
|
+
const unfinishedWords = (stopped, configFolder) => {
|
|
23935
|
+
return `ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. ${Option.match(stopped, {
|
|
23936
|
+
onNone: () => `The install stopped before it finished. To finish it, run: ${installLine(configFolder)}`,
|
|
23937
|
+
onSome: (failed) => `${failed.says} Then run: ${repairOf(failed, configFolder)}`
|
|
23938
|
+
})}`;
|
|
23769
23939
|
};
|
|
23770
|
-
/**
|
|
23771
|
-
|
|
23772
|
-
|
|
23773
|
-
|
|
23774
|
-
|
|
23775
|
-
|
|
23776
|
-
const
|
|
23777
|
-
|
|
23778
|
-
|
|
23779
|
-
|
|
23780
|
-
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23781
|
-
if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
|
|
23782
|
-
const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
|
|
23783
|
-
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
|
|
23784
|
-
yield* writeCodexCheckout({
|
|
23785
|
-
gitFolder: oldRoot,
|
|
23786
|
-
common: "",
|
|
23787
|
-
folder: "",
|
|
23788
|
-
signsOff: false,
|
|
23789
|
-
clearAuthor: file,
|
|
23790
|
-
author: fallback
|
|
23791
|
-
});
|
|
23792
|
-
yield* Console.error(`ref0: Commits now name you instead of the previously connected agent, including its running sessions; ref0’s Git hooks still run. Fix the shell_environment_policy conflict in Codex’s config or profile named above: remove conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded. Then run: ${installLine(options.configFolder)}`);
|
|
23940
|
+
/**
|
|
23941
|
+
* After an install into a Codex folder fails or is interrupted, the agent before is taken out of
|
|
23942
|
+
* the include `config.toml` still selects, so the folder's commits never name an agent other than
|
|
23943
|
+
* its grant's, and the person hears what stopped the install: `stopped`, or none when interrupted.
|
|
23944
|
+
*/
|
|
23945
|
+
const clearUnfinishedCodexInstall = Effect.fnUntraced(function* (agent, stopped, options) {
|
|
23946
|
+
const selected = yield* selectedCodexInclude(options.configFolder);
|
|
23947
|
+
if (Option.isNone(selected)) return;
|
|
23948
|
+
if (!(yield* clearOtherCodexAuthor(agent, selected.value, options))) return;
|
|
23949
|
+
yield* printBlocks([[unfinishedWords(stopped, options.configFolder)]]);
|
|
23793
23950
|
});
|
|
23794
23951
|
/** Install the connected agent's include and shell policy. */
|
|
23795
23952
|
const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
@@ -23798,6 +23955,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23798
23955
|
CODEX_HOME: options.configFolder
|
|
23799
23956
|
};
|
|
23800
23957
|
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23958
|
+
const conflicted = new InstallFailed({ says: "Codex’s shell_environment_policy conflicts with ref0’s commit environment, in the config or profile named above. Remove the conflicting set values or allowlists, and keep GIT_AUTHOR_NAME and GIT_AUTHOR_EMAIL excluded." });
|
|
23801
23959
|
return yield* Effect.gen(function* () {
|
|
23802
23960
|
const fs = yield* FileSystem.FileSystem;
|
|
23803
23961
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -23825,8 +23983,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23825
23983
|
].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
|
|
23826
23984
|
const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
|
|
23827
23985
|
if (collision !== void 0 || allowlist) {
|
|
23828
|
-
yield*
|
|
23829
|
-
if (own) return yield*
|
|
23986
|
+
yield* printBlocks([[`ref0: Codex’s shell_environment_policy ${collision === void 0 ? "has an allowlist" : `already sets ${collision}`}, so ref0 left its commit environment as it was.`]]);
|
|
23987
|
+
if (own) return yield* conflicted;
|
|
23830
23988
|
return;
|
|
23831
23989
|
}
|
|
23832
23990
|
const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
|
|
@@ -23851,8 +24009,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23851
24009
|
const losesAuthors = excluded !== void 0 && excluded !== null && ["GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL"].some((name) => !excluded.some((pattern) => pathMatchesWithin(pattern.toLowerCase(), name.toLowerCase(), { left: 1e4 })));
|
|
23852
24010
|
const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
|
|
23853
24011
|
if (key === void 0 && !losesAuthors && !allowlist) continue;
|
|
23854
|
-
yield*
|
|
23855
|
-
if (own) return yield*
|
|
24012
|
+
yield* printBlocks([[`ref0: Codex’s profile ${file} ${key === void 0 ? "overrides the author exclusions or has an allowlist" : `already sets ${key}`}, so ref0 left its commit environment as it was.`]]);
|
|
24013
|
+
if (own) return yield* conflicted;
|
|
23856
24014
|
return;
|
|
23857
24015
|
}
|
|
23858
24016
|
if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
|
|
@@ -23940,9 +24098,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23940
24098
|
}, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
|
|
23941
24099
|
}).pipe(Effect.onError(() => Effect.gen(function* () {
|
|
23942
24100
|
if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
|
|
23943
|
-
else yield*
|
|
24101
|
+
else yield* printBlocks([["ref0: Could not safely restore Codex’s commit policy after the failed setup. Its author file was kept so the include remains valid. Check your Codex config, then run ref0 install again."]]);
|
|
23944
24102
|
}).pipe(Effect.orDie)));
|
|
23945
|
-
}).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
|
|
24103
|
+
}).pipe(Effect.scoped, Effect.mapError((error) => error instanceof InstallFailed ? error : failed("set Codex’s commit environment")));
|
|
23946
24104
|
}, Effect.scoped);
|
|
23947
24105
|
//#endregion
|
|
23948
24106
|
//#region src/harness/install-claude-code.ts
|
|
@@ -24106,7 +24264,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
|
|
|
24106
24264
|
trusted,
|
|
24107
24265
|
profile
|
|
24108
24266
|
};
|
|
24109
|
-
}).pipe(Effect.onError(() =>
|
|
24267
|
+
}).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
|
|
24110
24268
|
});
|
|
24111
24269
|
//#endregion
|
|
24112
24270
|
//#region src/harness/install.ts
|
|
@@ -24212,7 +24370,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24212
24370
|
for (const folder of into) answers.set(folder, Result.fail(failed));
|
|
24213
24371
|
};
|
|
24214
24372
|
const started = /* @__PURE__ */ new Set();
|
|
24215
|
-
const clearUnstarted = (into) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) =>
|
|
24373
|
+
const clearUnstarted = (into, stopped) => Effect.forEach(into.filter((folder) => folder.harness === "codex" && !started.has(folder)), (folder) => codexAgentOf(folder.configFolder).pipe(Effect.flatMap((agent) => clearUnfinishedCodexInstall(agent, stopped, {
|
|
24216
24374
|
configFolder: folder.configFolder,
|
|
24217
24375
|
home
|
|
24218
24376
|
}).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
|
|
@@ -24227,8 +24385,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24227
24385
|
if (into.length === 0) continue;
|
|
24228
24386
|
const command = yield* onPath(harness === "codex" ? "codex" : "claude");
|
|
24229
24387
|
if (Option.isNone(command)) {
|
|
24230
|
-
|
|
24231
|
-
|
|
24388
|
+
const missing = new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` });
|
|
24389
|
+
failAll(into, missing);
|
|
24390
|
+
yield* clearUnstarted(into, Option.some(missing));
|
|
24232
24391
|
continue;
|
|
24233
24392
|
}
|
|
24234
24393
|
yield* Effect.scoped(Effect.gen(function* () {
|
|
@@ -24249,9 +24408,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24249
24408
|
})).pipe(Effect.result);
|
|
24250
24409
|
answers.set(folder, installed);
|
|
24251
24410
|
}
|
|
24252
|
-
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
|
|
24411
|
+
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into, Option.some(failed)))))));
|
|
24253
24412
|
}
|
|
24254
|
-
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
|
|
24413
|
+
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
|
|
24255
24414
|
return folders.map((folder) => ({
|
|
24256
24415
|
...folder,
|
|
24257
24416
|
installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
|
|
@@ -24343,16 +24502,13 @@ const mostFolders = 8;
|
|
|
24343
24502
|
const isConfigFolderPath = Schema.is(ConfigFolderPath);
|
|
24344
24503
|
/** The line that asks again. */
|
|
24345
24504
|
const again = "ref0 connect";
|
|
24346
|
-
/** The widest a line of ref0's own words runs, as its help does, in a terminal at least as wide. */
|
|
24347
|
-
const mostColumns = 100;
|
|
24348
24505
|
/**
|
|
24349
24506
|
* Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. The
|
|
24350
|
-
* question is said first,
|
|
24351
|
-
*
|
|
24352
|
-
*
|
|
24507
|
+
* question is said first, and the chooser shows only the folders: once Enter chooses, it leaves
|
|
24508
|
+
* the ticked ones on one line after any question it was given, which the terminal would break
|
|
24509
|
+
* mid-word.
|
|
24353
24510
|
*/
|
|
24354
24511
|
const choose = Effect.fnUntraced(function* (found, home) {
|
|
24355
|
-
const columns = yield* (yield* Terminal.Terminal).columns;
|
|
24356
24512
|
const choices = found.map((folder) => {
|
|
24357
24513
|
const shown = shownConfigFolder(home, folder.configFolder);
|
|
24358
24514
|
const sendable = isConfigFolderPath(shown);
|
|
@@ -24369,7 +24525,7 @@ const choose = Effect.fnUntraced(function* (found, home) {
|
|
|
24369
24525
|
});
|
|
24370
24526
|
const ticked = choices.filter((choice) => choice.selected).length;
|
|
24371
24527
|
const keys = "Space ticks or unticks one; Enter connects those ticked.";
|
|
24372
|
-
yield* printBlocks([[{ label: "Connect which config folders?" },
|
|
24528
|
+
yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
|
|
24373
24529
|
return yield* Prompt.run(Prompt.MultiSelect({
|
|
24374
24530
|
choices,
|
|
24375
24531
|
max: mostFolders
|
|
@@ -24562,7 +24718,7 @@ const signalLines = (answer) => {
|
|
|
24562
24718
|
* credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
|
|
24563
24719
|
* thread in the project sends a signal to one of its connections by name.
|
|
24564
24720
|
*/
|
|
24565
|
-
const failedWith$
|
|
24721
|
+
const failedWith$15 = (failed) => sayFailed(failed.nextStep);
|
|
24566
24722
|
const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
|
|
24567
24723
|
const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
|
|
24568
24724
|
const list$5 = Command.make("list", {
|
|
@@ -24574,7 +24730,7 @@ const list$5 = Command.make("list", {
|
|
|
24574
24730
|
params: projectParts(address)
|
|
24575
24731
|
});
|
|
24576
24732
|
yield* printAnswer(answer.json, asJson, connectionLines(answer.value));
|
|
24577
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
24733
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("The project’s connections, their last signal and what they dropped"), Command.withShortDescription("List a project’s connections and their last signals"));
|
|
24578
24734
|
/** A change made on the project's page: its link, and what to do there. */
|
|
24579
24735
|
const onThePage = (address, change) => Effect.gen(function* () {
|
|
24580
24736
|
const config = yield* Ref0Config;
|
|
@@ -24650,14 +24806,14 @@ const send = Command.make("send", {
|
|
|
24650
24806
|
})
|
|
24651
24807
|
});
|
|
24652
24808
|
yield* printAnswer(answer.json, asJson, signalLines(answer.value));
|
|
24653
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
24809
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
|
|
24654
24810
|
/** A project's connections, and sending a signal by one. */
|
|
24655
24811
|
const connectionCommands = [connectionCommand, Command.make("signal").pipe(Command.withDescription("A project’s signals: send one by a connection"), Command.withShortDescription("Send a signal to a project"), Command.withSubcommands([send]))];
|
|
24656
24812
|
//#endregion
|
|
24657
24813
|
//#region src/harness/checkout.ts
|
|
24658
24814
|
const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
|
|
24659
24815
|
const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
|
|
24660
|
-
const decodeThread$
|
|
24816
|
+
const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
|
|
24661
24817
|
const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
|
|
24662
24818
|
var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
24663
24819
|
static layer = Layer.effect(Checkout, Effect.gen(function* () {
|
|
@@ -24696,7 +24852,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
|
24696
24852
|
"config",
|
|
24697
24853
|
"--get",
|
|
24698
24854
|
"ref0.thread"
|
|
24699
|
-
]), (written) => decodeThread$
|
|
24855
|
+
]), (written) => decodeThread$2(written.trim()));
|
|
24700
24856
|
const repoAt = (folder) => Effect.map(git(folder, [
|
|
24701
24857
|
"config",
|
|
24702
24858
|
"--get",
|
|
@@ -25699,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25699
25855
|
})).value;
|
|
25700
25856
|
if (code.compiled.entry.reaches?.browser === true) return yield* sayFailed(`${code.check} browses, and only a run on ref0 browses: run it there, with ref0 run ${check} --thread ${thread.value}.`);
|
|
25701
25857
|
if (code.runs.length === 0) {
|
|
25702
|
-
yield*
|
|
25858
|
+
yield* printBlocks([[`${code.check} has no part left for a local run on version ${code.version}: each is met, or failed on ref0. See ref0 gate.`]]);
|
|
25703
25859
|
return;
|
|
25704
25860
|
}
|
|
25705
25861
|
yield* holdsVersion(root.value, code);
|
|
@@ -25708,7 +25864,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25708
25864
|
thread: `${thread.value}’s own .ref0/`,
|
|
25709
25865
|
project: "the project’s checks"
|
|
25710
25866
|
}[code.code.origin];
|
|
25711
|
-
yield*
|
|
25867
|
+
yield* printBlocks([[`Running ${code.check} from ${from}, as ref0 compiled it for version ${code.version}: its code and its sandboxes’ setup run on this machine, as you, outside any container.`]]);
|
|
25712
25868
|
const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
|
|
25713
25869
|
const sandboxes = code.compiled.entry.sandboxes ?? [];
|
|
25714
25870
|
const held = /* @__PURE__ */ new Set();
|
|
@@ -25720,7 +25876,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25720
25876
|
const run = yield* newRunId;
|
|
25721
25877
|
const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
|
|
25722
25878
|
const sandbox = yield* localOpener(root.value, held, sandboxes, deadline);
|
|
25723
|
-
yield*
|
|
25879
|
+
yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
|
|
25724
25880
|
const ran = yield* runPart(own, {
|
|
25725
25881
|
params,
|
|
25726
25882
|
host: treeHost(root.value, code.tree),
|
|
@@ -25783,7 +25939,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
|
|
|
25783
25939
|
/** Says a waiting start's tree on stderr, as it stands now. */
|
|
25784
25940
|
const sayTree = Effect.fnUntraced(function* (wait) {
|
|
25785
25941
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25786
|
-
yield*
|
|
25942
|
+
yield* printBlocks([[waitingLines$1(wait, now)]]);
|
|
25787
25943
|
});
|
|
25788
25944
|
/**
|
|
25789
25945
|
* Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
|
|
@@ -25829,7 +25985,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
|
|
|
25829
25985
|
* reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
|
|
25830
25986
|
* at a time.
|
|
25831
25987
|
*/
|
|
25832
|
-
const failedWith$
|
|
25988
|
+
const failedWith$14 = (failed) => sayFailed(failed.nextStep);
|
|
25833
25989
|
const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
|
|
25834
25990
|
const decodeTraced = Schema.decodeUnknownOption(TracedRun);
|
|
25835
25991
|
/** A run named by a flag, or a failure that says how to name one. */
|
|
@@ -25931,7 +26087,7 @@ const run = Command.make("run", {
|
|
|
25931
26087
|
}, asJson, rerunCommand);
|
|
25932
26088
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25933
26089
|
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
25934
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26090
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Run a check on main, or on a thread’s head: a run it ran already is answered, not run again, unless the check says reuse: false. A start its check’s start rule makes wait shows what it waits on, and waits up to 9 minutes for it, unless --no-wait. With --local, run a check of the thread’s open version on this machine, and report it"), Command.withShortDescription("Run a check on main or a thread’s head"));
|
|
25935
26091
|
const trace = Command.make("trace", {
|
|
25936
26092
|
run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
|
|
25937
26093
|
repo: repoFlag,
|
|
@@ -25974,7 +26130,7 @@ const trace = Command.make("trace", {
|
|
|
25974
26130
|
...narrowed,
|
|
25975
26131
|
`--cursor ${next}`
|
|
25976
26132
|
].join(" ")));
|
|
25977
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26133
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A run’s trace, a check run’s or a workflow run’s: its steps and its log lines, a page at a time"), Command.withShortDescription("Show a run’s trace and log lines"));
|
|
25978
26134
|
const waiting$1 = Command.make("waiting", {
|
|
25979
26135
|
start: startArgument,
|
|
25980
26136
|
repo: repoFlag,
|
|
@@ -26009,7 +26165,7 @@ const waiting$1 = Command.make("waiting", {
|
|
|
26009
26165
|
}, asJson, rerunCommand);
|
|
26010
26166
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
26011
26167
|
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
26012
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26168
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A start of yours its check’s start rule made wait: what it waits on, as a tree, waiting up to 9 minutes for it to go on or end, unless --no-wait"), Command.withShortDescription("Show what a waiting start waits on"));
|
|
26013
26169
|
/** What a cancel did, in words. */
|
|
26014
26170
|
const cancelledText = {
|
|
26015
26171
|
start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
|
|
@@ -26035,7 +26191,7 @@ const cancel = Command.make("cancel", {
|
|
|
26035
26191
|
})
|
|
26036
26192
|
});
|
|
26037
26193
|
yield* printAnswer(answer.json, asJson, cancelledText[answer.value.cancelled]);
|
|
26038
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26194
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("Cancel a start of yours that waits: yours ends it and the workflow runs it waits on; one that joined another’s only leaves it"), Command.withShortDescription("Cancel a waiting start of yours"));
|
|
26039
26195
|
/** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
|
|
26040
26196
|
const runCommands = [run, Command.make("runs", {
|
|
26041
26197
|
repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
|
|
@@ -26077,14 +26233,14 @@ const runCommands = [run, Command.make("runs", {
|
|
|
26077
26233
|
...narrowed,
|
|
26078
26234
|
`--before ${next}`
|
|
26079
26235
|
].join(" ")));
|
|
26080
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26236
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$14))).pipe(Command.withDescription("A repo’s check runs, main’s or a thread’s, the newest first, a page at a time"), Command.withShortDescription("List a repo’s check runs"), Command.withSubcommands([
|
|
26081
26237
|
trace,
|
|
26082
26238
|
waiting$1,
|
|
26083
26239
|
cancel
|
|
26084
26240
|
]))];
|
|
26085
26241
|
//#endregion
|
|
26086
26242
|
//#region src/commands/checks.ts
|
|
26087
|
-
const failedWith$
|
|
26243
|
+
const failedWith$13 = (failed) => sayFailed(failed.nextStep);
|
|
26088
26244
|
const decodeCheck = Schema.decodeUnknownOption(CheckKey);
|
|
26089
26245
|
const decodeToken = Schema.decodeUnknownOption(TokenId);
|
|
26090
26246
|
const decodePerson = Schema.decodeUnknownOption(OwnerName);
|
|
@@ -26120,7 +26276,7 @@ const restore = Command.make("restore", {
|
|
|
26120
26276
|
}
|
|
26121
26277
|
});
|
|
26122
26278
|
yield* printAnswer(answer.json, asJson, restoredWords(answer.value, repo));
|
|
26123
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26279
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$13))).pipe(Command.withDescription("Restore a signer a miss revoked from answering a check, as an admin: its local runs, or their yes, count again"), Command.withShortDescription("Restore a revoked check signer"));
|
|
26124
26280
|
const signers = Command.make("signers").pipe(Command.withDescription("Who signs a check’s local runs or says yes over its runs: restore one a miss revoked"), Command.withShortDescription("Manage a check’s signers"), Command.withSubcommands([restore]));
|
|
26125
26281
|
/** `ref0 checks`: a repo's checks. */
|
|
26126
26282
|
const checksCommands = [Command.make("checks").pipe(Command.withDescription("A repo’s checks: the signers of their local runs and yeses"), Command.withShortDescription("Manage a repo’s check signers"), Command.withSubcommands([signers]))];
|
|
@@ -26452,7 +26608,7 @@ const projectGateLines = (view, repo) => {
|
|
|
26452
26608
|
};
|
|
26453
26609
|
//#endregion
|
|
26454
26610
|
//#region src/commands/gate.ts
|
|
26455
|
-
const failedWith$
|
|
26611
|
+
const failedWith$12 = (failed) => sayFailed(failed.nextStep);
|
|
26456
26612
|
const project$4 = Flag.Boolean("project").pipe(Flag.withDescription("The project gate instead: its active revision, and a pending one with each latch it waits on"), Flag.withDefault(false));
|
|
26457
26613
|
/** A pull request's gate. */
|
|
26458
26614
|
const gateCommands = [Command.make("gate", {
|
|
@@ -26478,7 +26634,7 @@ const gateCommands = [Command.make("gate", {
|
|
|
26478
26634
|
}
|
|
26479
26635
|
});
|
|
26480
26636
|
yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
|
|
26481
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26637
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$12))).pipe(Command.withDescription("A pull request’s needs, each with where it came from, ref0’s floor or a line of .ref0/gate.ts, and whether it’s met or on whom it waits; with --project, the project gate"), Command.withShortDescription("Show a pull request’s or project’s gate"))];
|
|
26482
26638
|
//#endregion
|
|
26483
26639
|
//#region src/clone.ts
|
|
26484
26640
|
/** The remote a clone pushes to: its thread's fork path, named as the thread is. */
|
|
@@ -26836,7 +26992,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
|
|
|
26836
26992
|
* with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
|
|
26837
26993
|
* thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
|
|
26838
26994
|
*/
|
|
26839
|
-
const failedWith$
|
|
26995
|
+
const failedWith$11 = (failed) => sayFailed(failed.nextStep);
|
|
26840
26996
|
/** `ref0 git-credential`, as Git runs it: see `answerGit`. */
|
|
26841
26997
|
const credential = Command.make("git-credential", { operation: Argument.String("operation").pipe(Argument.withDescription("What Git asks: get, store or erase")) }, ({ operation }) => Effect.gen(function* () {
|
|
26842
26998
|
const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
|
|
@@ -26890,7 +27046,7 @@ const gitCommands = [
|
|
|
26890
27046
|
value
|
|
26891
27047
|
])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
|
|
26892
27048
|
yield* printAnswer(fork.json, asJson, `Cloned ${owner}/${name} into ${into}. origin is read-only to agents: git push goes to ${forkRemote(fork.value)}, the fork of whichever session on the thread pushes.`);
|
|
26893
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27049
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Clone a repo of the session’s project, to push to its fork"), Command.withShortDescription("Clone a repo to work in your session’s fork")),
|
|
26894
27050
|
Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
26895
27051
|
yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
|
|
26896
27052
|
const synced = yield* syncBase(".");
|
|
@@ -26906,7 +27062,7 @@ const gitCommands = [
|
|
|
26906
27062
|
on,
|
|
26907
27063
|
commit
|
|
26908
27064
|
}, asJson, `Merged ${at}. Push it when you’re ready: git push.`);
|
|
26909
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27065
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$11))).pipe(Command.withDescription("Merge in main, or the thread this one is stacked on, as ref0 holds it now. ref0 mcp says to when what merged touches your work"), Command.withShortDescription("Merge in main or the thread this one is stacked on")),
|
|
26910
27066
|
credential
|
|
26911
27067
|
];
|
|
26912
27068
|
//#endregion
|
|
@@ -27319,7 +27475,7 @@ const usageCommands = [personUsage, personMeters];
|
|
|
27319
27475
|
* invitations. Each is a person's, asked of them by approval, as every terminal command of
|
|
27320
27476
|
* theirs is; agents never change members.
|
|
27321
27477
|
*/
|
|
27322
|
-
const failedWith$
|
|
27478
|
+
const failedWith$10 = (failed) => sayFailed(failed.nextStep);
|
|
27323
27479
|
const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
|
|
27324
27480
|
const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
|
|
27325
27481
|
const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
|
|
@@ -27349,7 +27505,7 @@ const create$1 = Command.make("create", {
|
|
|
27349
27505
|
payload
|
|
27350
27506
|
});
|
|
27351
27507
|
yield* printAnswer(answer.json, asJson, answer.value.state === "ready" ? `Made ${answer.value.name}.` : `Made ${answer.value.name}; it’s being set up.`);
|
|
27352
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27508
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
|
|
27353
27509
|
const leave = Command.make("leave", {
|
|
27354
27510
|
organization: organization$2,
|
|
27355
27511
|
json
|
|
@@ -27359,7 +27515,7 @@ const leave = Command.make("leave", {
|
|
|
27359
27515
|
params: { owner: org }
|
|
27360
27516
|
});
|
|
27361
27517
|
yield* printAnswer(answer.json, asJson, `Left ${org}.`);
|
|
27362
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27518
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
|
|
27363
27519
|
const orgCommand = Command.make("org").pipe(Command.withDescription("Make an organization, leave one, or read its usage and what ref0 ran and kept for it"), Command.withShortDescription("Manage an organization and read its usage"), Command.withSubcommands([
|
|
27364
27520
|
create$1,
|
|
27365
27521
|
leave,
|
|
@@ -27376,7 +27532,7 @@ const orgs = Command.make("orgs", {
|
|
|
27376
27532
|
});
|
|
27377
27533
|
const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
|
|
27378
27534
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You’re in no organization yet." : [...lines, ...more(answer.value.next)].join("\n"));
|
|
27379
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27535
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
|
|
27380
27536
|
const members = Command.make("members", {
|
|
27381
27537
|
organization: organization$2,
|
|
27382
27538
|
cursor: cursor$3,
|
|
@@ -27389,7 +27545,7 @@ const members = Command.make("members", {
|
|
|
27389
27545
|
});
|
|
27390
27546
|
const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
|
|
27391
27547
|
yield* printAnswer(answer.json, asJson, [...lines, ...more(answer.value.next)].join("\n"));
|
|
27392
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27548
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s members, people and its own agents"), Command.withShortDescription("List an organization’s members"));
|
|
27393
27549
|
const role = Command.make("role", {
|
|
27394
27550
|
organization: organization$2,
|
|
27395
27551
|
person,
|
|
@@ -27405,7 +27561,7 @@ const role = Command.make("role", {
|
|
|
27405
27561
|
payload: { role: chosen }
|
|
27406
27562
|
});
|
|
27407
27563
|
yield* printAnswer(answer.json, asJson, `${named} is ${answer.value.role} of ${org} now.`);
|
|
27408
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27564
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
|
|
27409
27565
|
const remove$1 = Command.make("remove", {
|
|
27410
27566
|
organization: organization$2,
|
|
27411
27567
|
person,
|
|
@@ -27419,7 +27575,7 @@ const remove$1 = Command.make("remove", {
|
|
|
27419
27575
|
}
|
|
27420
27576
|
});
|
|
27421
27577
|
yield* printAnswer(answer.json, asJson, `Removed ${named} from ${org}.`);
|
|
27422
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27578
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
|
|
27423
27579
|
const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
|
|
27424
27580
|
const listInvitations = Command.make("list", {
|
|
27425
27581
|
organization: organization$2,
|
|
@@ -27433,7 +27589,7 @@ const listInvitations = Command.make("list", {
|
|
|
27433
27589
|
});
|
|
27434
27590
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
|
|
27435
27591
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? `${org} has no open invitations.` : [...lines, ...more(answer.value.next)].join("\n"));
|
|
27436
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27592
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
|
|
27437
27593
|
const withdraw = Command.make("withdraw", {
|
|
27438
27594
|
organization: organization$2,
|
|
27439
27595
|
invitation,
|
|
@@ -27447,7 +27603,7 @@ const withdraw = Command.make("withdraw", {
|
|
|
27447
27603
|
}
|
|
27448
27604
|
});
|
|
27449
27605
|
yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
|
|
27450
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27606
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
|
|
27451
27607
|
const invite = Command.make("invite", {
|
|
27452
27608
|
organization: organization$2,
|
|
27453
27609
|
person,
|
|
@@ -27467,7 +27623,7 @@ const invite = Command.make("invite", {
|
|
|
27467
27623
|
payload
|
|
27468
27624
|
});
|
|
27469
27625
|
yield* printAnswer(answer.json, asJson, `Invited ${answer.value.invitee.name} to ${org} as ${answer.value.role}: ${answer.value.invitation}.`);
|
|
27470
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27626
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
|
|
27471
27627
|
const invitations = Command.make("invitations", {
|
|
27472
27628
|
cursor: cursor$3,
|
|
27473
27629
|
json
|
|
@@ -27478,7 +27634,7 @@ const invitations = Command.make("invitations", {
|
|
|
27478
27634
|
});
|
|
27479
27635
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
|
|
27480
27636
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You have no open invitations." : [...lines, ...more(answer.value.next)].join("\n"));
|
|
27481
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27637
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
|
|
27482
27638
|
const accept = Command.make("accept", {
|
|
27483
27639
|
invitation,
|
|
27484
27640
|
json
|
|
@@ -27488,7 +27644,7 @@ const accept = Command.make("accept", {
|
|
|
27488
27644
|
params: { invitation: id }
|
|
27489
27645
|
});
|
|
27490
27646
|
yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
|
|
27491
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27647
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
|
|
27492
27648
|
const decline = Command.make("decline", {
|
|
27493
27649
|
invitation,
|
|
27494
27650
|
json
|
|
@@ -27498,7 +27654,7 @@ const decline = Command.make("decline", {
|
|
|
27498
27654
|
params: { invitation: id }
|
|
27499
27655
|
});
|
|
27500
27656
|
yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
|
|
27501
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27657
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
|
|
27502
27658
|
/** Organizations, their members and invitations. */
|
|
27503
27659
|
const organizationCommands = [
|
|
27504
27660
|
orgCommand,
|
|
@@ -27511,7 +27667,7 @@ const organizationCommands = [
|
|
|
27511
27667
|
];
|
|
27512
27668
|
//#endregion
|
|
27513
27669
|
//#region src/commands/grants.ts
|
|
27514
|
-
const failedWith$
|
|
27670
|
+
const failedWith$9 = (failed) => sayFailed(failed.nextStep);
|
|
27515
27671
|
const isOwnerRef = Schema.is(OwnerRef);
|
|
27516
27672
|
const isProject$1 = Schema.is(WrittenProject);
|
|
27517
27673
|
const isThread = Schema.is(WrittenThread);
|
|
@@ -27633,7 +27789,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
|
|
|
27633
27789
|
onSome: (on) => [on]
|
|
27634
27790
|
})];
|
|
27635
27791
|
yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
|
|
27636
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27792
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$9)));
|
|
27637
27793
|
const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
|
|
27638
27794
|
const grant = changeCommand("grant", "grants.set").pipe(Command.withDescription("Give a grant in an organization, as an admin: ref0 grant acme/billing pip/claude write"), Command.withShortDescription("Give a grant in an organization"), Command.withSubcommands([revoke]));
|
|
27639
27795
|
/** An organization's grants. */
|
|
@@ -27653,7 +27809,41 @@ const grantCommands = [Command.make("grants", {
|
|
|
27653
27809
|
const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
|
|
27654
27810
|
const next = answer.value.nextAfter;
|
|
27655
27811
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? `You see no grants in ${org}.` : [...lines, ...next === void 0 ? [] : [`More: --cursor ${next}`]].join("\n"));
|
|
27656
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27812
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$9))).pipe(Command.withDescription("The grants in an organization you may see"), Command.withShortDescription("List an organization’s grants")), grant];
|
|
27813
|
+
//#endregion
|
|
27814
|
+
//#region src/commands/identity-token.ts
|
|
27815
|
+
/**
|
|
27816
|
+
* `ref0 identity-token --audience <origin>`: an identity token for the agent's session it runs
|
|
27817
|
+
* in, for one outside service, which verifies it against ref0's published keys. One request, by
|
|
27818
|
+
* the session's own token, which never starts or renews a session. The token alone goes to
|
|
27819
|
+
* stdout, so it pipes into a verifier; anything else goes to stderr, with no token, and fails.
|
|
27820
|
+
*/
|
|
27821
|
+
const decodeAudience = Schema.decodeUnknownOption(IdentityAudience);
|
|
27822
|
+
const outside = "ref0 identity-token runs only in an agent’s ref0 session, and none runs here. In Claude Code or Codex, one starts at each prompt once ref0 connect has connected its config folder: ask the agent there.";
|
|
27823
|
+
const audienceOnce = "Name the audience once, as --audience https://verifier.example: the outside service’s HTTPS origin.";
|
|
27824
|
+
const notAnAudience = "Send the outside service’s HTTPS origin as the audience, such as https://verifier.example, with no path.";
|
|
27825
|
+
const mayHaveIssued = "ref0’s answer was lost or failed partway, so it may have made a token that never reached you. Ask again for a fresh one in a moment.";
|
|
27826
|
+
/**
|
|
27827
|
+
* What went wrong, said once, on stderr. An answer lost or failed partway may still have made a
|
|
27828
|
+
* token, as ref0's log records, but nothing to check or undo: a fresh request is the way on.
|
|
27829
|
+
*/
|
|
27830
|
+
const failedWith$8 = (failed) => sayFailed(failed.status >= 500 || failed.status === 0 && (failed.kind === "Unreachable" || failed.kind === "Unexpected") ? mayHaveIssued : failed.nextStep);
|
|
27831
|
+
/** Identity tokens, for outside services. */
|
|
27832
|
+
const identityTokenCommands = [Command.make("identity-token", {
|
|
27833
|
+
audience: Flag.String("audience").pipe(Flag.withDescription("The outside service that verifies the token, as its HTTPS origin, such as https://verifier.example"), Flag.atLeast(0)),
|
|
27834
|
+
json
|
|
27835
|
+
}, ({ audience: named, json: asJson }) => Effect.gen(function* () {
|
|
27836
|
+
if (named.length !== 1) return yield* sayFailed(audienceOnce);
|
|
27837
|
+
const audience = decodeAudience(named[0]);
|
|
27838
|
+
if (Option.isNone(audience)) return yield* sayFailed(notAnAudience);
|
|
27839
|
+
yield* ownSession(outside);
|
|
27840
|
+
const issued = yield* callEndpoint({
|
|
27841
|
+
endpoint: "me.identityToken",
|
|
27842
|
+
payload: { audience: audience.value }
|
|
27843
|
+
});
|
|
27844
|
+
if (asJson) return yield* printAnswer(issued.json, true, "");
|
|
27845
|
+
yield* Console.log(issued.value.token);
|
|
27846
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$8))).pipe(Command.withDescription("Print a short-lived identity token, a signed JWT naming your agent, for one outside service"), Command.withShortDescription("Print an identity token for one outside service"))];
|
|
27657
27847
|
//#endregion
|
|
27658
27848
|
//#region src/commands/agents.ts
|
|
27659
27849
|
/**
|
|
@@ -27928,27 +28118,39 @@ const repoCommands = [Command.make("repos", {
|
|
|
27928
28118
|
//#endregion
|
|
27929
28119
|
//#region src/decision-lines.ts
|
|
27930
28120
|
/** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
|
|
27931
|
-
/**
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
|
|
28121
|
+
/**
|
|
28122
|
+
* A thread as a line read in `scope` names it: `#3` when it's in that scope, else in full, as
|
|
28123
|
+
* `pip/default#3`.
|
|
28124
|
+
*/
|
|
28125
|
+
const threadWords = (thread, scope) => thread.startsWith(`${scope}#`) ? thread.slice(scope.length) : thread;
|
|
28126
|
+
/**
|
|
28127
|
+
* What waits, in words: a gate's pull request and version, and the thread it's from, or the thread
|
|
28128
|
+
* an ask is on.
|
|
28129
|
+
*/
|
|
28130
|
+
const subjectWords = ({ subject, names }, scope) => subject.kind === "version" && names.pullRequest !== void 0 ? `${names.pullRequest}, version ${subject.version}, from ${threadWords(names.thread, scope)}` : names.thread;
|
|
28131
|
+
/**
|
|
28132
|
+
* A decision in a line, read in `scope`: what waits, its answers or how it ended, then its ID, for
|
|
28133
|
+
* `ref0 decision` and `ref0 answer`.
|
|
28134
|
+
*/
|
|
28135
|
+
const decisionLines = (decision, scope) => {
|
|
28136
|
+
const on = subjectWords(decision, scope);
|
|
27936
28137
|
const answers = decision.options.map((option) => option.key).join(" or ");
|
|
27937
|
-
return decision.state === "open" ?
|
|
28138
|
+
return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
|
|
27938
28139
|
};
|
|
27939
|
-
const nothing = "Nothing waits on you here.";
|
|
27940
28140
|
/**
|
|
27941
|
-
* What waits
|
|
27942
|
-
* waits on its person, which they answer on their own page.
|
|
28141
|
+
* What waits in `scope`, as `ref0 waiting` prints it: what waits on the caller, then, for an agent,
|
|
28142
|
+
* what waits on its person, which only they answer, on their own page.
|
|
27943
28143
|
*/
|
|
27944
|
-
const waitingLines = (decisions) => {
|
|
27945
|
-
const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision)] : []);
|
|
27946
|
-
const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision)}`] : []);
|
|
27947
|
-
if (theirs.length === 0) return yours.length === 0 ?
|
|
28144
|
+
const waitingLines = (decisions, scope) => {
|
|
28145
|
+
const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision, scope)] : []);
|
|
28146
|
+
const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision, scope)}`] : []);
|
|
28147
|
+
if (theirs.length === 0) return yours.length === 0 ? `Nothing waits on you in ${scope}.` : yours.join("\n");
|
|
28148
|
+
const them = theirs.length === 1 ? "it" : "them";
|
|
27948
28149
|
return [
|
|
27949
28150
|
...yours,
|
|
27950
|
-
|
|
27951
|
-
...theirs
|
|
28151
|
+
`Waits on your person, who alone can answer ${them}, under Waiting on you on their page:`,
|
|
28152
|
+
...theirs,
|
|
28153
|
+
`ref0 refuses an agent’s answer to ${them}.`
|
|
27952
28154
|
].join("\n");
|
|
27953
28155
|
};
|
|
27954
28156
|
//#endregion
|
|
@@ -27964,7 +28166,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
|
|
|
27964
28166
|
const encodePlan = Schema.encodeEffect(PlanInput);
|
|
27965
28167
|
const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
|
|
27966
28168
|
const thread$4 = Argument.String("thread").pipe(Argument.withDescription("The thread, as owner/project#number, or an org thread, as acme#7"), Argument.withSchema(WrittenThread));
|
|
27967
|
-
|
|
28169
|
+
/** A project, `acme/billing`, or an organization, `acme`, for its org threads. */
|
|
28170
|
+
const Scope$1 = Schema.Union([WrittenProject, OwnerName]);
|
|
28171
|
+
const decodeScope = Schema.decodeUnknownOption(Scope$1);
|
|
28172
|
+
const project$2 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads"), Argument.withSchema(Scope$1));
|
|
27968
28173
|
const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
|
|
27969
28174
|
const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
|
|
27970
28175
|
/** Where a version's approval stands: who accepted it, or its check. */
|
|
@@ -28125,25 +28330,37 @@ const comments = Command.make("comments", {
|
|
|
28125
28330
|
].filter((line) => line !== "").join("\n");
|
|
28126
28331
|
yield* printAnswer(answer.json, asJson, words);
|
|
28127
28332
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("A plan version’s comments, oldest first"), Command.withShortDescription("List a plan version’s comments"));
|
|
28333
|
+
const plan = Command.make("plan").pipe(Command.withDescription("A thread’s plan: write, propose and read its versions, and comment"), Command.withShortDescription("Write, propose and read a thread’s plan"), Command.withSubcommands([
|
|
28334
|
+
list$4,
|
|
28335
|
+
write,
|
|
28336
|
+
onVersion("show"),
|
|
28337
|
+
onVersion("propose"),
|
|
28338
|
+
comment,
|
|
28339
|
+
comments
|
|
28340
|
+
]));
|
|
28341
|
+
/**
|
|
28342
|
+
* The project of the thread the session runs on, or its organization for an org thread's. Outside
|
|
28343
|
+
* a session, it says how to name one, and fails before anything's sent.
|
|
28344
|
+
*/
|
|
28345
|
+
const sessionScope = Effect.gen(function* () {
|
|
28346
|
+
const thread = yield* (yield* SessionThread).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
|
|
28347
|
+
const scope = Option.flatMap(thread, (written) => decodeScope(projectOf$2(written)));
|
|
28348
|
+
if (Option.isNone(scope)) return yield* sayFailed("Name the project: ref0 waiting pip/shop. Or, for an organization’s org threads: ref0 waiting acme.");
|
|
28349
|
+
return scope.value;
|
|
28350
|
+
});
|
|
28128
28351
|
/** Plans' and decisions' commands. */
|
|
28129
28352
|
const planCommands = [
|
|
28130
|
-
|
|
28131
|
-
list$4,
|
|
28132
|
-
write,
|
|
28133
|
-
onVersion("show"),
|
|
28134
|
-
onVersion("propose"),
|
|
28135
|
-
comment,
|
|
28136
|
-
comments
|
|
28137
|
-
])),
|
|
28353
|
+
plan,
|
|
28138
28354
|
Command.make("waiting", {
|
|
28139
|
-
project: project$
|
|
28355
|
+
project: Argument.String("project").pipe(Argument.withDescription("The project, as owner/project, or the organization, as acme, for its org threads; in an agent’s session, its own if not named"), Argument.withSchema(Scope$1), Argument.optional),
|
|
28140
28356
|
json
|
|
28141
|
-
}, ({ project:
|
|
28357
|
+
}, ({ project: named, json: asJson }) => Effect.gen(function* () {
|
|
28358
|
+
const address = Option.isSome(named) ? named.value : yield* sessionScope;
|
|
28142
28359
|
const answer = yield* callEndpoint(routed(address, {
|
|
28143
28360
|
endpoint: "decisions.waiting",
|
|
28144
28361
|
params: scopeParams(address)
|
|
28145
28362
|
}));
|
|
28146
|
-
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
|
|
28363
|
+
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
|
|
28147
28364
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("What in a project, or in an organization’s org threads, waits on you"), Command.withShortDescription("Show what waits on you")),
|
|
28148
28365
|
Command.make("decision", {
|
|
28149
28366
|
project: project$2,
|
|
@@ -28157,7 +28374,7 @@ const planCommands = [
|
|
|
28157
28374
|
decision: id
|
|
28158
28375
|
}
|
|
28159
28376
|
}));
|
|
28160
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28377
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
28161
28378
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("One decision, as it stands"), Command.withShortDescription("Show a decision and where it stands")),
|
|
28162
28379
|
Command.make("answer", {
|
|
28163
28380
|
project: project$2,
|
|
@@ -28180,7 +28397,7 @@ const planCommands = [
|
|
|
28180
28397
|
})
|
|
28181
28398
|
})
|
|
28182
28399
|
}));
|
|
28183
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28400
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
28184
28401
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
|
|
28185
28402
|
];
|
|
28186
28403
|
//#endregion
|
|
@@ -28220,9 +28437,14 @@ const endedLines = (view) => {
|
|
|
28220
28437
|
const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
|
|
28221
28438
|
return [` ${view.state.padEnd(8)} ${at}${why}`];
|
|
28222
28439
|
};
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
28440
|
+
/**
|
|
28441
|
+
* When it opened, and by what: a workflow's run, or an agent's session, whose IDs go on a line of
|
|
28442
|
+
* their own, since ref0 answers no name for either.
|
|
28443
|
+
*/
|
|
28444
|
+
const openedLines = (view) => {
|
|
28445
|
+
const at = minute(view.openedAt);
|
|
28446
|
+
if ("workflow" in view.openedBy) return [` opened ${at} by workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`];
|
|
28447
|
+
return [` opened ${at} by an agent’s session:`, ` ${view.openedBy.session} of ${view.openedBy.agent.id}`];
|
|
28226
28448
|
};
|
|
28227
28449
|
/** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
|
|
28228
28450
|
const pullRequestLines = (view, repo) => {
|
|
@@ -28233,7 +28455,7 @@ const pullRequestLines = (view, repo) => {
|
|
|
28233
28455
|
`${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
|
|
28234
28456
|
` head ${headWords(head)}`,
|
|
28235
28457
|
` current ${versionWords(current)}, on ${base}`,
|
|
28236
|
-
|
|
28458
|
+
...openedLines(view),
|
|
28237
28459
|
...endedLines(view)
|
|
28238
28460
|
].join("\n");
|
|
28239
28461
|
};
|
|
@@ -29219,7 +29441,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
|
|
|
29219
29441
|
const { thread, blockers } = first.value;
|
|
29220
29442
|
if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
|
|
29221
29443
|
const them = blockers.length === 1 ? "it" : "them";
|
|
29222
|
-
yield*
|
|
29444
|
+
yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
|
|
29223
29445
|
let renewedAt = Number.NEGATIVE_INFINITY;
|
|
29224
29446
|
for (;;) {
|
|
29225
29447
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -30112,7 +30334,7 @@ const enable = Command.make("enable", {
|
|
|
30112
30334
|
const why = found.why === void 0 ? "" : ` (${found.why})`;
|
|
30113
30335
|
return yield* sayFailed(found.why === "activation failed" ? `ref0 couldn't read main's .ref0/ for a day, and reads it again each hour. Once ref0 workflows list ${address} shows a version of ${workflow}, turn it on.` : `main has no version of ${workflow} ref0 can run${why}. Fix its code in .ref0/ and merge it, so ref0 compiles a new version, then turn that one on.`);
|
|
30114
30336
|
}
|
|
30115
|
-
yield*
|
|
30337
|
+
yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
|
|
30116
30338
|
const answer = yield* callEndpoint({
|
|
30117
30339
|
endpoint: "workflows.enable",
|
|
30118
30340
|
params: repoParts(address),
|
|
@@ -30145,7 +30367,7 @@ const start = Command.make("start", {
|
|
|
30145
30367
|
source: source.value
|
|
30146
30368
|
}
|
|
30147
30369
|
});
|
|
30148
|
-
yield*
|
|
30370
|
+
yield* printBlocks([[attachingLines(written, compiled.value)]]);
|
|
30149
30371
|
const answer = yield* callEndpoint({
|
|
30150
30372
|
endpoint: "threadWorkflows.start",
|
|
30151
30373
|
params,
|
|
@@ -33105,11 +33327,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
33105
33327
|
* started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
|
|
33106
33328
|
* someone else's words, and nothing here reads it.
|
|
33107
33329
|
*/
|
|
33108
|
-
const decodeThread = Schema.decodeUnknownOption(WrittenThread);
|
|
33330
|
+
const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
|
|
33109
33331
|
const decodeEvent = Schema.decodeUnknownOption(EventId);
|
|
33110
33332
|
const optional = (name) => Config.String(name).pipe(Config.option);
|
|
33111
33333
|
const watchedHere = Effect.gen(function* () {
|
|
33112
|
-
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
|
|
33334
|
+
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
|
|
33113
33335
|
const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
|
|
33114
33336
|
return Option.zipWith(thread, event, (on, by) => ({
|
|
33115
33337
|
thread: on,
|
|
@@ -34487,6 +34709,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
|
34487
34709
|
});
|
|
34488
34710
|
return SessionToken.of({ current });
|
|
34489
34711
|
}));
|
|
34712
|
+
const decodeThread = Schema.decodeUnknownOption(WrittenThread);
|
|
34713
|
+
/** `SessionThread` from the keychain, as the start of the harness session this runs in kept it. */
|
|
34714
|
+
const harnessSessionThread = Layer.effect(SessionThread, Effect.gen(function* () {
|
|
34715
|
+
const keychain = yield* Keychain;
|
|
34716
|
+
const harness = yield* HarnessProcess;
|
|
34717
|
+
const current = Effect.gen(function* () {
|
|
34718
|
+
const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
|
|
34719
|
+
if (Option.isNone(binding)) return Option.none();
|
|
34720
|
+
const kept = yield* keychain.get(threadAccount(binding.value));
|
|
34721
|
+
return Option.flatMap(kept, (thread) => decodeThread(Redacted.value(thread)));
|
|
34722
|
+
});
|
|
34723
|
+
return SessionThread.of({ current });
|
|
34724
|
+
}));
|
|
34490
34725
|
//#endregion
|
|
34491
34726
|
//#region src/harness/server-check.ts
|
|
34492
34727
|
/**
|
|
@@ -34848,7 +35083,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
|
|
|
34848
35083
|
...organizationCommands,
|
|
34849
35084
|
...grantCommands,
|
|
34850
35085
|
...agentCommands,
|
|
34851
|
-
...attachmentCommands
|
|
35086
|
+
...attachmentCommands,
|
|
35087
|
+
...identityTokenCommands
|
|
34852
35088
|
]));
|
|
34853
35089
|
const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
|
|
34854
35090
|
const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
|
|
@@ -34863,7 +35099,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
|
|
|
34863
35099
|
Ref0Config.layer,
|
|
34864
35100
|
sessionToken,
|
|
34865
35101
|
FetchHttpClient.layer
|
|
34866
|
-
])), sessionToken, FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
|
|
35102
|
+
])), sessionToken, harnessSessionThread.pipe(Layer.provide([keychain, harnessProcess])), FetchHttpClient.layer, keychain, HostGrant.layer.pipe(Layer.provide(keychain)), OldHostGrants.layer, ConnectLock.layer.pipe(Layer.provide(Processes.layer)), Machine.layer, Tty.layer, harnessProcess, Checkout.layer.pipe(Layer.provide(NodeServices.layer)), BaseChecks.layer.pipe(Layer.provide([Ref0Config.layer, FetchHttpClient.layer])), Attribution.layer.pipe(Layer.provide([Commits.layer, Checkout.layer])), HarnessTasks.layer, Transcripts.layer, Waker.layer, LiveHints.layer).pipe(Layer.provideMerge(ConfigFolders.layer), Layer.provideMerge(Ref0Config.layer), Layer.provideMerge(NodeServices.layer));
|
|
34867
35103
|
//#endregion
|
|
34868
35104
|
//#region src/main.ts
|
|
34869
35105
|
ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
|