@ref0/cli 0.11.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +144 -2
- package/dist/main.mjs +775 -548
- 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
|
/**
|
|
@@ -13242,7 +13241,7 @@ Schema.Struct({
|
|
|
13242
13241
|
actor: ActorRef,
|
|
13243
13242
|
/** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
|
|
13244
13243
|
attachment: Schema.NullOr(CredentialRef),
|
|
13245
|
-
scopes: Schema.Array(Scope$
|
|
13244
|
+
scopes: Schema.Array(Scope$2),
|
|
13246
13245
|
ownerName: OwnerName,
|
|
13247
13246
|
number: ThreadNumber
|
|
13248
13247
|
});
|
|
@@ -19109,271 +19108,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
|
|
|
19109
19108
|
/** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
|
|
19110
19109
|
const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
|
|
19111
19110
|
//#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
19111
|
//#region src/output.ts
|
|
19378
19112
|
/**
|
|
19379
19113
|
* What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
|
|
@@ -19399,12 +19133,88 @@ const renderLine = (line, terminal) => {
|
|
|
19399
19133
|
if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
|
|
19400
19134
|
return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
|
|
19401
19135
|
};
|
|
19402
|
-
|
|
19136
|
+
/** The most columns a printed line takes, however wide the terminal. */
|
|
19137
|
+
const mostColumns = 100;
|
|
19138
|
+
/** A printed text without the styles this printer adds. */
|
|
19139
|
+
const unstyled = (text) => text.replaceAll(/\u001b\[\d+m/g, "");
|
|
19140
|
+
const graphemes = new Intl.Segmenter();
|
|
19141
|
+
/** The columns a printed text takes, a character each as the person sees it: styles take none. */
|
|
19142
|
+
const columnsOf = (text) => [...graphemes.segment(unstyled(text))].length;
|
|
19143
|
+
/** A word that says an inline command follows: `run ref0 connect`, `Read a text: ref0 text …`. */
|
|
19144
|
+
const introducesCommand = (word) => word === "run" || word === "Run" || word.endsWith(":");
|
|
19145
|
+
/**
|
|
19146
|
+
* Inline `ref0` commands made single words while they fit in `room`: `ref0` and the word after it
|
|
19147
|
+
* always, and a command after `run` or a colon to its clause's end, so no line breaks inside one.
|
|
19148
|
+
*/
|
|
19149
|
+
const withCommandsWhole = (words, room) => {
|
|
19150
|
+
const joined = [];
|
|
19151
|
+
for (let at = 0; at < words.length; at += 1) {
|
|
19152
|
+
const first = words[at];
|
|
19153
|
+
if (first === void 0) continue;
|
|
19154
|
+
if (unstyled(first.word) !== "ref0" || at + 1 >= words.length) {
|
|
19155
|
+
joined.push(first);
|
|
19156
|
+
continue;
|
|
19157
|
+
}
|
|
19158
|
+
const before = words[at - 1];
|
|
19159
|
+
let end = at + 1;
|
|
19160
|
+
if (before !== void 0 && introducesCommand(unstyled(before.word))) while (end + 1 < words.length && !/[.,;:]$/.test(unstyled(words[end]?.word ?? ""))) end += 1;
|
|
19161
|
+
const word = words.slice(at, end + 1).map((each, index) => index === 0 ? each.word : `${each.space}${each.word}`).join("");
|
|
19162
|
+
if (columnsOf(word) > room) {
|
|
19163
|
+
joined.push(first);
|
|
19164
|
+
continue;
|
|
19165
|
+
}
|
|
19166
|
+
joined.push({
|
|
19167
|
+
space: first.space,
|
|
19168
|
+
word
|
|
19169
|
+
});
|
|
19170
|
+
at = end;
|
|
19171
|
+
}
|
|
19172
|
+
return joined;
|
|
19173
|
+
};
|
|
19174
|
+
/**
|
|
19175
|
+
* A line broken between words to fit `width` columns, each line after the first indented as the
|
|
19176
|
+
* first is. A line that fits stays as it is, and a word wider than the width stays whole, on a
|
|
19177
|
+
* line of its own.
|
|
19178
|
+
*/
|
|
19179
|
+
const wrapLine = (line, width) => {
|
|
19180
|
+
if (columnsOf(line) <= width) return line;
|
|
19181
|
+
const indent = /^\s*/.exec(line)?.[0] ?? "";
|
|
19182
|
+
const room = Math.max(1, width - columnsOf(indent));
|
|
19183
|
+
const words = withCommandsWhole(Array.from(line.slice(indent.length).matchAll(/(\s*)(\S+)/g), ([, space = "", word = ""]) => ({
|
|
19184
|
+
space,
|
|
19185
|
+
word
|
|
19186
|
+
})), room);
|
|
19187
|
+
const lines = [];
|
|
19188
|
+
let current = "";
|
|
19189
|
+
for (const { space, word } of words) if (current !== "" && columnsOf(current) + columnsOf(space) + columnsOf(word) > room) {
|
|
19190
|
+
lines.push(current);
|
|
19191
|
+
current = word;
|
|
19192
|
+
} else current += current === "" ? word : `${space}${word}`;
|
|
19193
|
+
return [...lines, current].map((each) => `${indent}${each}`).join("\n");
|
|
19194
|
+
};
|
|
19195
|
+
/**
|
|
19196
|
+
* How wide a printed line may be: the terminal's width, at most 100 columns, or none when the
|
|
19197
|
+
* stream isn't a terminal, as for an agent or a script, whose own reader wraps.
|
|
19198
|
+
*/
|
|
19199
|
+
const widthOf = (stream) => {
|
|
19200
|
+
if (process[stream].isTTY !== true) return void 0;
|
|
19201
|
+
const columns = process[stream].columns;
|
|
19202
|
+
return columns > 0 ? Math.min(columns, mostColumns) : mostColumns;
|
|
19203
|
+
};
|
|
19204
|
+
const renderBlocks = (blocks, stream, width) => {
|
|
19403
19205
|
const terminal = process[stream].isTTY === true && process.env["NO_COLOR"] === void 0;
|
|
19404
|
-
|
|
19206
|
+
const shown = (line) => {
|
|
19207
|
+
const rendered = renderLine(line, terminal);
|
|
19208
|
+
if (width === void 0 || !Predicate.isString(line) && "command" in line) return rendered;
|
|
19209
|
+
return rendered.split("\n").map((each) => wrapLine(each, width)).join("\n");
|
|
19210
|
+
};
|
|
19211
|
+
return blocks.flatMap((block) => block.length === 0 ? [] : [block.map(shown).join("\n")]).join("\n\n");
|
|
19405
19212
|
};
|
|
19406
|
-
/**
|
|
19407
|
-
|
|
19213
|
+
/**
|
|
19214
|
+
* Words go to stderr by default; a command's answer goes to stdout. In a terminal, each line but a
|
|
19215
|
+
* command's is broken between words to fit it.
|
|
19216
|
+
*/
|
|
19217
|
+
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
19218
|
/**
|
|
19409
19219
|
* A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
|
|
19410
19220
|
* yes. The cursor stays after it, for their answer.
|
|
@@ -19474,6 +19284,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
|
|
|
19474
19284
|
};
|
|
19475
19285
|
/** Says what went wrong, once, on stderr, and fails the command. */
|
|
19476
19286
|
const sayFailed = (words) => printBlocks(Predicate.isString(words) ? [[words]] : words).pipe(Effect.andThen(Effect.fail(new Said())));
|
|
19287
|
+
//#endregion
|
|
19288
|
+
//#region src/session.ts
|
|
19289
|
+
/**
|
|
19290
|
+
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
19291
|
+
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
19292
|
+
* person instead.
|
|
19293
|
+
*/
|
|
19294
|
+
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
19295
|
+
/**
|
|
19296
|
+
* The token of the session this command runs in, if it runs in one: an agent's, which its
|
|
19297
|
+
* harness bound when the session started. Where it's found is the harness's to say. Outside a
|
|
19298
|
+
* session, there's none, and every command is the person's, asked by approval.
|
|
19299
|
+
*/
|
|
19300
|
+
var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
|
|
19301
|
+
/** No session: until a harness binds one, the person asks, by approval. */
|
|
19302
|
+
static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
|
|
19303
|
+
};
|
|
19304
|
+
/**
|
|
19305
|
+
* The thread the session this command runs in is on, as its start kept it beside the session's
|
|
19306
|
+
* token. Outside a session there's none, and in one whose start kept none.
|
|
19307
|
+
*/
|
|
19308
|
+
var SessionThread = class SessionThread extends Context.Service()("ref0/cli/SessionThread") {
|
|
19309
|
+
/** No session, so no thread. */
|
|
19310
|
+
static none = Layer.succeed(SessionThread, SessionThread.of({ current: Effect.succeedNone }));
|
|
19311
|
+
};
|
|
19312
|
+
//#endregion
|
|
19313
|
+
//#region src/call.ts
|
|
19314
|
+
/**
|
|
19315
|
+
* Every command's one way to ref0: an endpoint's request, sent with the session's token when
|
|
19316
|
+
* the command runs in a session, or else asked of the person, by approval, from this terminal.
|
|
19317
|
+
* Either way the answer is the endpoint's own, as JSON.
|
|
19318
|
+
*/
|
|
19319
|
+
/** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
|
|
19320
|
+
var CallFailed = class extends Schema.TaggedError()("CallFailed", {
|
|
19321
|
+
status: Schema.Int,
|
|
19322
|
+
kind: Schema.String,
|
|
19323
|
+
nextStep: Schema.String
|
|
19324
|
+
}) {};
|
|
19325
|
+
const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
19326
|
+
const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
|
|
19327
|
+
const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
|
|
19328
|
+
const decodeRefusal = Schema.decodeUnknownOption(AnyError);
|
|
19329
|
+
/** How often a terminal asks whether its person has answered. */
|
|
19330
|
+
const pollMillis$1 = 2e3;
|
|
19331
|
+
/** How long one request may take before it's asked again, or the call gives up. */
|
|
19332
|
+
const requestTimeout$1 = Duration.seconds(15);
|
|
19333
|
+
/**
|
|
19334
|
+
* How long a session's request may take for the commands that read and compile before they
|
|
19335
|
+
* answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
|
|
19336
|
+
* lacks the check, one paced read at a time, up to 256 files each.
|
|
19337
|
+
*/
|
|
19338
|
+
const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
|
|
19339
|
+
/** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
|
|
19340
|
+
const giveUpAfterMillis$1 = 3e5;
|
|
19341
|
+
/** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
|
|
19342
|
+
const outageMillis = 3e5;
|
|
19343
|
+
/** What a change that failed partway says: ref0 may have made it all the same. */
|
|
19344
|
+
const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
|
|
19345
|
+
/**
|
|
19346
|
+
* An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
|
|
19347
|
+
* 5xx, may have been made before it failed, so it isn't said to try again.
|
|
19348
|
+
*/
|
|
19349
|
+
const failedWith$16 = (status, body, change) => Option.match(decodeRefusal(body), {
|
|
19350
|
+
onNone: () => new CallFailed({
|
|
19351
|
+
status,
|
|
19352
|
+
kind: "Unexpected",
|
|
19353
|
+
nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
|
|
19354
|
+
}),
|
|
19355
|
+
onSome: (refusal) => new CallFailed({
|
|
19356
|
+
status,
|
|
19357
|
+
kind: refusal._tag,
|
|
19358
|
+
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19359
|
+
})
|
|
19360
|
+
});
|
|
19361
|
+
/**
|
|
19362
|
+
* A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
|
|
19363
|
+
* command's failure: a change that failed at a 5xx may have been made all the same.
|
|
19364
|
+
*/
|
|
19365
|
+
const refusedBy = (status, refusal, change) => new CallFailed({
|
|
19366
|
+
status,
|
|
19367
|
+
kind: refusal._tag,
|
|
19368
|
+
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
19369
|
+
});
|
|
19370
|
+
/**
|
|
19371
|
+
* An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
|
|
19372
|
+
* the CLI made. A change it answered may have run.
|
|
19373
|
+
*/
|
|
19374
|
+
const unreadable = (endpoint, change) => new CallFailed({
|
|
19375
|
+
status: 0,
|
|
19376
|
+
kind: "Unexpected",
|
|
19377
|
+
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.`
|
|
19378
|
+
});
|
|
19379
|
+
const unreachable = new CallFailed({
|
|
19380
|
+
status: 0,
|
|
19381
|
+
kind: "Unreachable",
|
|
19382
|
+
nextStep: "Couldn’t reach ref0. Try again."
|
|
19383
|
+
});
|
|
19384
|
+
/**
|
|
19385
|
+
* An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
|
|
19386
|
+
* change may have run, and asking again could make it twice.
|
|
19387
|
+
*/
|
|
19388
|
+
const gone = (change, nextStep) => new CallFailed({
|
|
19389
|
+
status: 404,
|
|
19390
|
+
kind: "ApprovalNotFound",
|
|
19391
|
+
nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
|
|
19392
|
+
});
|
|
19393
|
+
/** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
|
|
19394
|
+
const unanswered$1 = new CallFailed({
|
|
19395
|
+
status: 0,
|
|
19396
|
+
kind: "Unreachable",
|
|
19397
|
+
nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
|
|
19398
|
+
});
|
|
19399
|
+
/**
|
|
19400
|
+
* Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
|
|
19401
|
+
* stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
|
|
19402
|
+
* What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
|
|
19403
|
+
*/
|
|
19404
|
+
const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
|
|
19405
|
+
const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
|
|
19406
|
+
let approved = false;
|
|
19407
|
+
let heardAt = yield* Clock.currentTimeMillis;
|
|
19408
|
+
while (true) {
|
|
19409
|
+
const answered = yield* collect;
|
|
19410
|
+
const now = yield* Clock.currentTimeMillis;
|
|
19411
|
+
if (Option.isNone(answered)) {
|
|
19412
|
+
if (now - heardAt > outageMillis) return yield* new CallFailed({
|
|
19413
|
+
status: 0,
|
|
19414
|
+
kind: "Unreachable",
|
|
19415
|
+
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."
|
|
19416
|
+
});
|
|
19417
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19418
|
+
continue;
|
|
19419
|
+
}
|
|
19420
|
+
const state = answered.value;
|
|
19421
|
+
heardAt = now;
|
|
19422
|
+
if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
|
|
19423
|
+
onNone: () => Effect.fail(unreadable(endpoint, change)),
|
|
19424
|
+
onSome: Result.match({
|
|
19425
|
+
onSuccess: () => Effect.succeed(state.result),
|
|
19426
|
+
onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
|
|
19427
|
+
})
|
|
19428
|
+
});
|
|
19429
|
+
if (state.state === "refused") return yield* new CallFailed({
|
|
19430
|
+
status: 403,
|
|
19431
|
+
kind: "Refused",
|
|
19432
|
+
nextStep: "It was refused."
|
|
19433
|
+
});
|
|
19434
|
+
if (state.state === "approved") {
|
|
19435
|
+
if (!approved) yield* printBlocks([["Approved. Waiting for it to run."]]);
|
|
19436
|
+
approved = true;
|
|
19437
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19438
|
+
continue;
|
|
19439
|
+
}
|
|
19440
|
+
if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
|
|
19441
|
+
status: 408,
|
|
19442
|
+
kind: "Expired",
|
|
19443
|
+
nextStep: "Nobody approved it in time. Run it again."
|
|
19444
|
+
});
|
|
19445
|
+
yield* Effect.sleep(pollMillis$1);
|
|
19446
|
+
}
|
|
19447
|
+
});
|
|
19448
|
+
var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
19449
|
+
static layer = Layer.effect(Ref0, Effect.gen(function* () {
|
|
19450
|
+
const config = yield* Ref0Config;
|
|
19451
|
+
const session = yield* SessionToken;
|
|
19452
|
+
const http = yield* HttpClient.HttpClient;
|
|
19453
|
+
const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
|
|
19454
|
+
const timeout = timeoutOf(request.command.endpoint);
|
|
19455
|
+
const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
|
|
19456
|
+
const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
|
|
19457
|
+
const change = request.method !== "GET";
|
|
19458
|
+
const lost = change ? unanswered$1 : unreachable;
|
|
19459
|
+
const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
|
|
19460
|
+
response,
|
|
19461
|
+
text
|
|
19462
|
+
}))), Effect.timeoutOrElse({
|
|
19463
|
+
duration: timeout,
|
|
19464
|
+
orElse: () => Effect.fail(lost)
|
|
19465
|
+
}), Effect.mapError(() => lost));
|
|
19466
|
+
const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
|
|
19467
|
+
if (response.status === 202) {
|
|
19468
|
+
const needed = decodeNeeded(body);
|
|
19469
|
+
if (Option.isNone(needed)) return yield* failedWith$16(202, body, change);
|
|
19470
|
+
yield* printBlocks([[`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`]]);
|
|
19471
|
+
return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
|
|
19472
|
+
}
|
|
19473
|
+
return response.status < 300 ? body : yield* failedWith$16(response.status, body, change);
|
|
19474
|
+
});
|
|
19475
|
+
const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
|
|
19476
|
+
const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
|
|
19477
|
+
const refused = (status) => (error) => Effect.fail(new CallFailed({
|
|
19478
|
+
status,
|
|
19479
|
+
kind: error._tag,
|
|
19480
|
+
nextStep: error.nextStep
|
|
19481
|
+
}));
|
|
19482
|
+
return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19483
|
+
duration: requestTimeout$1,
|
|
19484
|
+
orElse: () => Effect.succeedNone
|
|
19485
|
+
}), Effect.catchTags({
|
|
19486
|
+
ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
|
|
19487
|
+
Unauthenticated: refused(401),
|
|
19488
|
+
SessionHasEnded: refused(410)
|
|
19489
|
+
}), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19490
|
+
});
|
|
19491
|
+
const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
|
|
19492
|
+
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
19493
|
+
const { secret, hash } = yield* freshSecret;
|
|
19494
|
+
const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
|
|
19495
|
+
const started = yield* client.terminalApprovals.start({
|
|
19496
|
+
headers: { "idempotency-key": yield* newRequestId },
|
|
19497
|
+
payload: {
|
|
19498
|
+
command,
|
|
19499
|
+
secretHash
|
|
19500
|
+
}
|
|
19501
|
+
}).pipe(Effect.timeoutOrElse({
|
|
19502
|
+
duration: requestTimeout$1,
|
|
19503
|
+
orElse: () => Effect.fail(unreachable)
|
|
19504
|
+
}), Effect.catchTags({
|
|
19505
|
+
CommandRefused: (refused) => Effect.fail(new CallFailed({
|
|
19506
|
+
status: 400,
|
|
19507
|
+
kind: refused._tag,
|
|
19508
|
+
nextStep: refused.nextStep
|
|
19509
|
+
})),
|
|
19510
|
+
SlowDown: (slow) => Effect.fail(new CallFailed({
|
|
19511
|
+
status: 429,
|
|
19512
|
+
kind: slow._tag,
|
|
19513
|
+
nextStep: slow.nextStep
|
|
19514
|
+
}))
|
|
19515
|
+
}), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
|
|
19516
|
+
yield* printBlocks([[`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`]]);
|
|
19517
|
+
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
|
|
19518
|
+
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({
|
|
19519
|
+
duration: requestTimeout$1,
|
|
19520
|
+
orElse: () => Effect.succeedNone
|
|
19521
|
+
}), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19522
|
+
return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
|
|
19523
|
+
});
|
|
19524
|
+
const call = Effect.fn("Ref0.call")(function* (command) {
|
|
19525
|
+
const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
|
|
19526
|
+
status: 0,
|
|
19527
|
+
kind: error._tag,
|
|
19528
|
+
nextStep: error.nextStep
|
|
19529
|
+
})));
|
|
19530
|
+
const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
|
|
19531
|
+
status: 400,
|
|
19532
|
+
kind: refused._tag,
|
|
19533
|
+
nextStep: refused.nextStep
|
|
19534
|
+
})));
|
|
19535
|
+
const change = request.method !== "GET";
|
|
19536
|
+
const answer = yield* Option.match(token, {
|
|
19537
|
+
onNone: () => asPerson(request.command, change, command.endpoint),
|
|
19538
|
+
onSome: (bearer) => asSession(bearer, request, command.endpoint)
|
|
19539
|
+
});
|
|
19540
|
+
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
|
|
19541
|
+
return answer;
|
|
19542
|
+
});
|
|
19543
|
+
return Ref0.of({ call });
|
|
19544
|
+
}));
|
|
19545
|
+
};
|
|
19546
|
+
/**
|
|
19547
|
+
* One endpoint's call, typed from the API: the compiler checks its command, and its answer is
|
|
19548
|
+
* read by the endpoint's own success schemas, so what a command shows of it is checked too.
|
|
19549
|
+
*/
|
|
19550
|
+
const callEndpoint = (command) => Effect.gen(function* () {
|
|
19551
|
+
const json = yield* (yield* Ref0).call(command);
|
|
19552
|
+
return {
|
|
19553
|
+
json,
|
|
19554
|
+
value: yield* Option.match(successOf(command.endpoint, json), {
|
|
19555
|
+
onNone: () => Effect.fail(unreadable(command.endpoint, false)),
|
|
19556
|
+
onSome: Effect.succeed
|
|
19557
|
+
})
|
|
19558
|
+
};
|
|
19559
|
+
});
|
|
19477
19560
|
/** The account's commands. */
|
|
19478
19561
|
const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
19479
19562
|
const answer = yield* callEndpoint({ endpoint: "me.get" });
|
|
@@ -20417,7 +20500,7 @@ const waitsFor = (each) => {
|
|
|
20417
20500
|
* A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
|
|
20418
20501
|
* when it's in another of `owner`'s, then its title and stage.
|
|
20419
20502
|
*/
|
|
20420
|
-
const threadWords = (thread, owner = "") => {
|
|
20503
|
+
const threadWords$1 = (thread, owner = "") => {
|
|
20421
20504
|
return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
|
|
20422
20505
|
};
|
|
20423
20506
|
/** What holds a stack back below the thread, as a line says it, to follow "and". */
|
|
@@ -20435,13 +20518,13 @@ const stackHeld = (stack) => {
|
|
|
20435
20518
|
*/
|
|
20436
20519
|
const planLine = (wait, number, owner) => {
|
|
20437
20520
|
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)}`;
|
|
20521
|
+
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
20522
|
return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
|
|
20440
20523
|
}
|
|
20441
20524
|
if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
|
|
20442
20525
|
if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
|
|
20443
20526
|
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}`;
|
|
20527
|
+
return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
|
|
20445
20528
|
};
|
|
20446
20529
|
/** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
|
|
20447
20530
|
const blockedBy$1 = (list) => {
|
|
@@ -20646,7 +20729,7 @@ const browserOpener = Effect.gen(function* () {
|
|
|
20646
20729
|
});
|
|
20647
20730
|
/** Starts the browser independently; a missing or failing desktop never stops the paste. */
|
|
20648
20731
|
const openBrowser = (program, link) => Effect.sync(() => {
|
|
20649
|
-
const failed = () => Effect.runFork(
|
|
20732
|
+
const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
|
|
20650
20733
|
try {
|
|
20651
20734
|
const child = spawn(program, [link], {
|
|
20652
20735
|
detached: true,
|
|
@@ -21317,7 +21400,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
21317
21400
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21318
21401
|
* and only as a default export.
|
|
21319
21402
|
*/
|
|
21320
|
-
const cliVersion = "0.
|
|
21403
|
+
const cliVersion = "0.12.0";
|
|
21321
21404
|
//#endregion
|
|
21322
21405
|
//#region src/harness/hook-protocol.ts
|
|
21323
21406
|
/**
|
|
@@ -21635,7 +21718,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21635
21718
|
const link = yield* freshLinkSecret;
|
|
21636
21719
|
const pasted = yield* Ref.make(void 0);
|
|
21637
21720
|
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(
|
|
21721
|
+
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
21722
|
const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
|
|
21640
21723
|
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
21641
21724
|
const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
|
|
@@ -21646,7 +21729,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21646
21729
|
...folder,
|
|
21647
21730
|
shown: shownConfigFolder(home, folder.configFolder),
|
|
21648
21731
|
grant: yield* freshTokenSecret,
|
|
21649
|
-
replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said))
|
|
21732
|
+
replaces: Option.isSome(held) ? grantOf(held.value) : yield* oldGrants.replaces(folder).pipe(Effect.catchTag("KeychainError", said)),
|
|
21733
|
+
before: Option.isSome(held) ? yield* hostGrant.get(folder.configFolder).pipe(Effect.catchTag("KeychainError", (error) => error.unreadable === true ? Effect.succeedNone : said(error))) : Option.none()
|
|
21650
21734
|
};
|
|
21651
21735
|
}));
|
|
21652
21736
|
const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
|
|
@@ -21726,17 +21810,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21726
21810
|
yield* Effect.sleep(pollMillis);
|
|
21727
21811
|
}
|
|
21728
21812
|
});
|
|
21729
|
-
const
|
|
21813
|
+
const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
|
|
21730
21814
|
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({
|
|
21815
|
+
if (left <= 0) return Option.some(late);
|
|
21816
|
+
const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
|
|
21740
21817
|
duration: Duration.min(keepTimeout, Duration.millis(left)),
|
|
21741
21818
|
orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
|
|
21742
21819
|
}), Effect.retry({
|
|
@@ -21745,10 +21822,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21745
21822
|
}), Effect.result);
|
|
21746
21823
|
return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
|
|
21747
21824
|
});
|
|
21825
|
+
const keepOne = (folder, connection, keepBy) => keepTrying(folder.configFolder, {
|
|
21826
|
+
harness: folder.harness,
|
|
21827
|
+
owner: connection.owner,
|
|
21828
|
+
agent: connection.handle,
|
|
21829
|
+
agentId: connection.agent.id,
|
|
21830
|
+
token: Redacted.make(formatToken(connection.credential.person, connection.credential.id, folder.grant.secret))
|
|
21831
|
+
}, keepBy, "The keychain took too long with the folders before it, so this machine didn’t keep this one’s grant.");
|
|
21748
21832
|
const finish = Effect.fnUntraced(function* (state) {
|
|
21749
21833
|
if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
|
|
21750
21834
|
const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
|
|
21751
21835
|
const kept = [];
|
|
21836
|
+
const keptFrom = [];
|
|
21752
21837
|
const grants = [];
|
|
21753
21838
|
const notKept = [];
|
|
21754
21839
|
for (const folder of asking) {
|
|
@@ -21778,9 +21863,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21778
21863
|
handle: connection.handle,
|
|
21779
21864
|
organizations: connection.organizations
|
|
21780
21865
|
});
|
|
21866
|
+
keptFrom.push(folder);
|
|
21781
21867
|
grants.push(connection.credential.id);
|
|
21782
21868
|
}
|
|
21783
|
-
yield* oldGrants.retire(kept);
|
|
21784
21869
|
const [first, ...rest] = grants;
|
|
21785
21870
|
if (first === void 0) return yield* sayFailed([[
|
|
21786
21871
|
"It connected, but this machine couldn’t keep what it needs to act:",
|
|
@@ -21813,9 +21898,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21813
21898
|
times: 2,
|
|
21814
21899
|
schedule: Schedule.spaced("1 second")
|
|
21815
21900
|
}), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
|
|
21816
|
-
|
|
21817
|
-
|
|
21901
|
+
const backBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
|
|
21902
|
+
const notBack = [];
|
|
21903
|
+
for (const folder of keptFrom) {
|
|
21904
|
+
if (Option.isNone(folder.before)) {
|
|
21905
|
+
yield* hostGrant.remove(folder.configFolder).pipe(Effect.ignore);
|
|
21906
|
+
continue;
|
|
21907
|
+
}
|
|
21908
|
+
const failed = yield* keepTrying(folder.configFolder, folder.before.value, backBy, "The keychain took too long with the folders before it.");
|
|
21909
|
+
if (Option.isSome(failed)) notBack.push(`${shownConfigFolder(home, folder.configFolder)}: ${failed.value}`);
|
|
21910
|
+
}
|
|
21911
|
+
return yield* sayFailed([[
|
|
21912
|
+
"ref0 revoked the new grants, since this machine didn’t confirm in time that it kept them.",
|
|
21913
|
+
...notBack.length === 0 ? ["Each folder has its connection from before back, if it had one. To connect this machine, run:"] : [
|
|
21914
|
+
"These folders hold a grant that works nowhere, since the keychain didn’t take back their records:",
|
|
21915
|
+
...notBack,
|
|
21916
|
+
"Each other folder has its connection from before back, if it had one. To connect this machine, run:"
|
|
21917
|
+
],
|
|
21918
|
+
{ command: again$1 }
|
|
21919
|
+
]]);
|
|
21818
21920
|
}
|
|
21921
|
+
yield* oldGrants.retire(kept);
|
|
21819
21922
|
return {
|
|
21820
21923
|
owner: state.owner,
|
|
21821
21924
|
kept,
|
|
@@ -21827,7 +21930,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21827
21930
|
};
|
|
21828
21931
|
});
|
|
21829
21932
|
const stopped = Effect.gen(function* () {
|
|
21830
|
-
yield*
|
|
21933
|
+
yield* printBlocks([["Stopping: aborting the request."]]);
|
|
21831
21934
|
const abortId = yield* newRequestId;
|
|
21832
21935
|
const state = yield* collector.machineAttachments.abort({
|
|
21833
21936
|
params,
|
|
@@ -21846,18 +21949,20 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21846
21949
|
times: 2
|
|
21847
21950
|
}), 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
21951
|
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*
|
|
21952
|
+
if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed([["It was already answered, but no code was pasted, so this machine kept nothing.", 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."]]);
|
|
21953
|
+
yield* printBlocks([["It was already answered, so this machine waits to hear how."]]);
|
|
21851
21954
|
}
|
|
21852
21955
|
const connected = yield* finish(isEnded(state) ? state : yield* answered);
|
|
21853
21956
|
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
21957
|
});
|
|
21855
|
-
const
|
|
21958
|
+
const readAnswer = Effect.gen(function* () {
|
|
21959
|
+
if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21856
21960
|
const keys = yield* terminal.readInput;
|
|
21857
21961
|
let typed = "";
|
|
21858
21962
|
while (true) {
|
|
21859
21963
|
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21860
21964
|
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21965
|
+
if ((yield* Ref.get(pasted)) !== void 0) continue;
|
|
21861
21966
|
const { input, key } = pressed.value;
|
|
21862
21967
|
if (key.name === "backspace") {
|
|
21863
21968
|
typed = takeBack(typed);
|
|
@@ -21871,17 +21976,14 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21871
21976
|
const line = typed.trim();
|
|
21872
21977
|
typed = "";
|
|
21873
21978
|
if (line === "") continue;
|
|
21874
|
-
if (isAnswerCode(line))
|
|
21875
|
-
|
|
21979
|
+
if (!isAnswerCode(line)) {
|
|
21980
|
+
yield* printBlocks([[`Paste the 8-character code from your own page on ${config.api}.`]]);
|
|
21981
|
+
continue;
|
|
21982
|
+
}
|
|
21983
|
+
yield* Ref.update(pasted, (first) => first ?? line);
|
|
21984
|
+
if (listener !== void 0) yield* listener.stop;
|
|
21876
21985
|
}
|
|
21877
21986
|
}).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
21987
|
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
21988
|
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
21989
|
}, Effect.scoped);
|
|
@@ -22861,6 +22963,111 @@ function parse(toml, options = {}) {
|
|
|
22861
22963
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22862
22964
|
*/
|
|
22863
22965
|
//#endregion
|
|
22966
|
+
//#region src/harness/empty-folder.ts
|
|
22967
|
+
/**
|
|
22968
|
+
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22969
|
+
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22970
|
+
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22971
|
+
*
|
|
22972
|
+
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22973
|
+
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22974
|
+
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22975
|
+
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22976
|
+
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22977
|
+
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22978
|
+
*/
|
|
22979
|
+
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22980
|
+
//#endregion
|
|
22981
|
+
//#region src/harness/codex-app-server.ts
|
|
22982
|
+
/**
|
|
22983
|
+
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22984
|
+
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22985
|
+
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
22986
|
+
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
22987
|
+
* didn't send, is passed over.
|
|
22988
|
+
*/
|
|
22989
|
+
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
22990
|
+
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
22991
|
+
/** Codex's app server answered a request with an error. */
|
|
22992
|
+
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
22993
|
+
method: Schema.String,
|
|
22994
|
+
error: CodexError
|
|
22995
|
+
}) {};
|
|
22996
|
+
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
22997
|
+
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
22998
|
+
method: Schema.String,
|
|
22999
|
+
issue: Schema.String
|
|
23000
|
+
}) {};
|
|
23001
|
+
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
23002
|
+
const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
23003
|
+
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
23004
|
+
const initializeId = "ref0-initialize";
|
|
23005
|
+
const askedId = "ref0-asked";
|
|
23006
|
+
/** The exchange's opening: who's asking, then that it's ready. */
|
|
23007
|
+
const opening = [{
|
|
23008
|
+
jsonrpc: "2.0",
|
|
23009
|
+
id: initializeId,
|
|
23010
|
+
method: codexMethods.initialize.method,
|
|
23011
|
+
params: { clientInfo: {
|
|
23012
|
+
name: "ref0",
|
|
23013
|
+
title: null,
|
|
23014
|
+
version: cliVersion
|
|
23015
|
+
} }
|
|
23016
|
+
}, {
|
|
23017
|
+
jsonrpc: "2.0",
|
|
23018
|
+
method: codexMethods.initialized.method
|
|
23019
|
+
}];
|
|
23020
|
+
/**
|
|
23021
|
+
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
23022
|
+
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
23023
|
+
*/
|
|
23024
|
+
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
23025
|
+
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23026
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23027
|
+
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
23028
|
+
cwd: yield* emptyFolder(fs),
|
|
23029
|
+
env,
|
|
23030
|
+
extendEnv: true,
|
|
23031
|
+
forceKillAfter: "2 seconds",
|
|
23032
|
+
stdin: {
|
|
23033
|
+
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
23034
|
+
endOnDone: false
|
|
23035
|
+
}
|
|
23036
|
+
}))).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);
|
|
23037
|
+
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
23038
|
+
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
23039
|
+
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
23040
|
+
const resultOf = (method, answer) => {
|
|
23041
|
+
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
23042
|
+
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
23043
|
+
method: method.method,
|
|
23044
|
+
error: answer.error
|
|
23045
|
+
}));
|
|
23046
|
+
const { result } = method;
|
|
23047
|
+
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
23048
|
+
method: method.method,
|
|
23049
|
+
issue: error.message
|
|
23050
|
+
})));
|
|
23051
|
+
};
|
|
23052
|
+
/**
|
|
23053
|
+
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
23054
|
+
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
23055
|
+
* its request's ID, and its result is read by the method's result schema.
|
|
23056
|
+
*/
|
|
23057
|
+
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
23058
|
+
const asked = {
|
|
23059
|
+
jsonrpc: "2.0",
|
|
23060
|
+
id: askedId,
|
|
23061
|
+
method: method.method,
|
|
23062
|
+
params
|
|
23063
|
+
};
|
|
23064
|
+
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
23065
|
+
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
23066
|
+
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
23067
|
+
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
23068
|
+
return yield* resultOf(method, answers.value.get(askedId));
|
|
23069
|
+
});
|
|
23070
|
+
//#endregion
|
|
22864
23071
|
//#region src/harness/codex-checkouts.ts
|
|
22865
23072
|
const codexGitFolder = (configFolder, pluginId) => {
|
|
22866
23073
|
const at = pluginId.indexOf("@");
|
|
@@ -22977,111 +23184,6 @@ const codexSignOff = [
|
|
|
22977
23184
|
`perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
|
|
22978
23185
|
];
|
|
22979
23186
|
//#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
23187
|
//#region src/harness/commit-hooks.ts
|
|
23086
23188
|
/**
|
|
23087
23189
|
* ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
|
|
@@ -23391,21 +23493,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
|
|
|
23391
23493
|
parameters: shellWord(`include.path=${file}`)
|
|
23392
23494
|
};
|
|
23393
23495
|
});
|
|
23496
|
+
/** Only the single ref0 include installed by this CLI belongs to ref0. */
|
|
23497
|
+
const selectedInclude = (set) => {
|
|
23498
|
+
const parameters = set["GIT_CONFIG_PARAMETERS"] ?? "";
|
|
23499
|
+
const included = parameters.slice(1, -1).replaceAll("'\\''", "'");
|
|
23500
|
+
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;
|
|
23501
|
+
};
|
|
23502
|
+
const decodePolicy = Schema.decodeUnknownEffect(Schema.Struct({ shell_environment_policy: CodexConfigRead.fields.config.fields.shell_environment_policy }));
|
|
23503
|
+
/**
|
|
23504
|
+
* The agent's include a Codex config folder's `config.toml` selects, if it's ref0's: its file, read
|
|
23505
|
+
* as TOML for its `shell_environment_policy` alone.
|
|
23506
|
+
*/
|
|
23507
|
+
const selectedCodexInclude = Effect.fnUntraced(function* (configFolder) {
|
|
23508
|
+
const fs = yield* FileSystem.FileSystem;
|
|
23509
|
+
const configFile = `${configFolder}/config.toml`;
|
|
23510
|
+
if (!(yield* fs.exists(configFile))) return Option.none();
|
|
23511
|
+
const text = yield* fs.readFileString(configFile);
|
|
23512
|
+
const document = yield* Effect.try(() => parse(text, { integersAsBigInt: "asNeeded" }));
|
|
23513
|
+
const read = yield* decodePolicy(document);
|
|
23514
|
+
return Option.fromNullishOr(selectedInclude(read.shell_environment_policy?.set ?? {}));
|
|
23515
|
+
});
|
|
23516
|
+
/**
|
|
23517
|
+
* Takes the author out of `selected`, the include a folder's `config.toml` selects, unless it's
|
|
23518
|
+
* this origin's include for the folder's agent, so a folder switched to another agent never names
|
|
23519
|
+
* the one before. Its commits name the person until an install selects the agent's own. Whether it
|
|
23520
|
+
* took one out.
|
|
23521
|
+
*/
|
|
23522
|
+
const clearOtherCodexAuthor = Effect.fnUntraced(function* (agent, selected, options) {
|
|
23523
|
+
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23524
|
+
if (selected === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return false;
|
|
23525
|
+
const oldRoot = selected.slice(0, selected.lastIndexOf("/agents/"));
|
|
23526
|
+
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home);
|
|
23527
|
+
yield* writeCodexCheckout({
|
|
23528
|
+
gitFolder: oldRoot,
|
|
23529
|
+
common: "",
|
|
23530
|
+
folder: "",
|
|
23531
|
+
signsOff: false,
|
|
23532
|
+
clearAuthor: selected,
|
|
23533
|
+
author: fallback
|
|
23534
|
+
});
|
|
23535
|
+
return true;
|
|
23536
|
+
});
|
|
23537
|
+
/** What the agent reads at its session's start when its folder's `config.toml` names another. */
|
|
23538
|
+
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
23539
|
/**
|
|
23395
23540
|
* 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`.
|
|
23541
|
+
* for the agent of the config folder `CODEX_HOME` names, or the default `~/.codex`. An include
|
|
23542
|
+
* `config.toml` selects for another agent, after a switch whose install never finished or another
|
|
23543
|
+
* origin's install, has its author taken out first, and the agent hears so.
|
|
23397
23544
|
*/
|
|
23398
23545
|
const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
|
|
23399
23546
|
const folders = yield* ConfigFolders;
|
|
23400
23547
|
const fs = yield* FileSystem.FileSystem;
|
|
23401
23548
|
const path = yield* Path.Path;
|
|
23402
23549
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23403
|
-
const
|
|
23550
|
+
const configFolder = yield* folders.folder("codex");
|
|
23551
|
+
const selected = yield* selectedCodexInclude(configFolder).pipe(Effect.mapError(() => new CodexCommitFailed({ detail: `Couldn’t read Codex’s config, ${configFolder}/config.toml.` })), Effect.result);
|
|
23552
|
+
const agent = yield* (yield* HostGrant).get(configFolder).pipe(Effect.catchIf((failed) => failed.unreadable === true, () => Effect.succeedNone));
|
|
23404
23553
|
if (Option.isNone(agent)) return false;
|
|
23554
|
+
const home = yield* Config.String("HOME");
|
|
23555
|
+
if (yield* Option.match(yield* Effect.fromResult(selected), {
|
|
23556
|
+
onNone: () => Effect.succeed(false),
|
|
23557
|
+
onSome: (file) => clearOtherCodexAuthor(agent.value, file, {
|
|
23558
|
+
configFolder,
|
|
23559
|
+
home
|
|
23560
|
+
})
|
|
23561
|
+
})) yield* Console.log(otherAgentWords(configFolder));
|
|
23405
23562
|
const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
|
|
23406
23563
|
if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
|
|
23407
23564
|
const gitFolder = path.join(path.dirname(pluginRoot), "@git");
|
|
23408
|
-
const home = yield* Config.String("HOME");
|
|
23409
23565
|
const author = yield* codexAgentConfig(agent.value, gitFolder, home);
|
|
23410
23566
|
const common = (yield* spawner.string(ChildProcess.make("git", [
|
|
23411
23567
|
"-C",
|
|
@@ -23761,35 +23917,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
23761
23917
|
//#region src/harness/codex-install-commits.ts
|
|
23762
23918
|
const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
|
|
23763
23919
|
const sameJson = Schema.toEquivalence(Schema.Json);
|
|
23764
|
-
/**
|
|
23765
|
-
|
|
23766
|
-
|
|
23767
|
-
|
|
23768
|
-
|
|
23920
|
+
/**
|
|
23921
|
+
* What the person reads when an install that never finished took the agent before out of a
|
|
23922
|
+
* folder's commits: what stopped it, in its own words, a conflict only when there is one, then
|
|
23923
|
+
* what to run.
|
|
23924
|
+
*/
|
|
23925
|
+
const unfinishedWords = (stopped, configFolder) => {
|
|
23926
|
+
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, {
|
|
23927
|
+
onNone: () => `The install stopped before it finished. To finish it, run: ${installLine(configFolder)}`,
|
|
23928
|
+
onSome: (failed) => `${failed.says} Then run: ${repairOf(failed, configFolder)}`
|
|
23929
|
+
})}`;
|
|
23769
23930
|
};
|
|
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)}`);
|
|
23931
|
+
/**
|
|
23932
|
+
* After an install into a Codex folder fails or is interrupted, the agent before is taken out of
|
|
23933
|
+
* the include `config.toml` still selects, so the folder's commits never name an agent other than
|
|
23934
|
+
* its grant's, and the person hears what stopped the install: `stopped`, or none when interrupted.
|
|
23935
|
+
*/
|
|
23936
|
+
const clearUnfinishedCodexInstall = Effect.fnUntraced(function* (agent, stopped, options) {
|
|
23937
|
+
const selected = yield* selectedCodexInclude(options.configFolder);
|
|
23938
|
+
if (Option.isNone(selected)) return;
|
|
23939
|
+
if (!(yield* clearOtherCodexAuthor(agent, selected.value, options))) return;
|
|
23940
|
+
yield* printBlocks([[unfinishedWords(stopped, options.configFolder)]]);
|
|
23793
23941
|
});
|
|
23794
23942
|
/** Install the connected agent's include and shell policy. */
|
|
23795
23943
|
const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
@@ -23798,6 +23946,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23798
23946
|
CODEX_HOME: options.configFolder
|
|
23799
23947
|
};
|
|
23800
23948
|
const failed = (what) => new InstallFailed({ says: `Couldn’t ${what}. Check that Codex is installed.` });
|
|
23949
|
+
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
23950
|
return yield* Effect.gen(function* () {
|
|
23802
23951
|
const fs = yield* FileSystem.FileSystem;
|
|
23803
23952
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -23825,8 +23974,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23825
23974
|
].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
|
|
23826
23975
|
const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
|
|
23827
23976
|
if (collision !== void 0 || allowlist) {
|
|
23828
|
-
yield*
|
|
23829
|
-
if (own) return yield*
|
|
23977
|
+
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.`]]);
|
|
23978
|
+
if (own) return yield* conflicted;
|
|
23830
23979
|
return;
|
|
23831
23980
|
}
|
|
23832
23981
|
const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
|
|
@@ -23851,8 +24000,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23851
24000
|
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
24001
|
const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
|
|
23853
24002
|
if (key === void 0 && !losesAuthors && !allowlist) continue;
|
|
23854
|
-
yield*
|
|
23855
|
-
if (own) return yield*
|
|
24003
|
+
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.`]]);
|
|
24004
|
+
if (own) return yield* conflicted;
|
|
23856
24005
|
return;
|
|
23857
24006
|
}
|
|
23858
24007
|
if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
|
|
@@ -23940,9 +24089,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23940
24089
|
}, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
|
|
23941
24090
|
}).pipe(Effect.onError(() => Effect.gen(function* () {
|
|
23942
24091
|
if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
|
|
23943
|
-
else yield*
|
|
24092
|
+
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
24093
|
}).pipe(Effect.orDie)));
|
|
23945
|
-
}).pipe(Effect.scoped, Effect.mapError(() => failed("set Codex’s commit environment")));
|
|
24094
|
+
}).pipe(Effect.scoped, Effect.mapError((error) => error instanceof InstallFailed ? error : failed("set Codex’s commit environment")));
|
|
23946
24095
|
}, Effect.scoped);
|
|
23947
24096
|
//#endregion
|
|
23948
24097
|
//#region src/harness/install-claude-code.ts
|
|
@@ -24106,7 +24255,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
|
|
|
24106
24255
|
trusted,
|
|
24107
24256
|
profile
|
|
24108
24257
|
};
|
|
24109
|
-
}).pipe(Effect.onError(() =>
|
|
24258
|
+
}).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
|
|
24110
24259
|
});
|
|
24111
24260
|
//#endregion
|
|
24112
24261
|
//#region src/harness/install.ts
|
|
@@ -24212,7 +24361,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24212
24361
|
for (const folder of into) answers.set(folder, Result.fail(failed));
|
|
24213
24362
|
};
|
|
24214
24363
|
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) =>
|
|
24364
|
+
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
24365
|
configFolder: folder.configFolder,
|
|
24217
24366
|
home
|
|
24218
24367
|
}).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
|
|
@@ -24227,8 +24376,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24227
24376
|
if (into.length === 0) continue;
|
|
24228
24377
|
const command = yield* onPath(harness === "codex" ? "codex" : "claude");
|
|
24229
24378
|
if (Option.isNone(command)) {
|
|
24230
|
-
|
|
24231
|
-
|
|
24379
|
+
const missing = new InstallFailed({ says: `${configFolders[harness].name} isn’t on this machine’s PATH. Install it.` });
|
|
24380
|
+
failAll(into, missing);
|
|
24381
|
+
yield* clearUnstarted(into, Option.some(missing));
|
|
24232
24382
|
continue;
|
|
24233
24383
|
}
|
|
24234
24384
|
yield* Effect.scoped(Effect.gen(function* () {
|
|
@@ -24249,9 +24399,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
24249
24399
|
})).pipe(Effect.result);
|
|
24250
24400
|
answers.set(folder, installed);
|
|
24251
24401
|
}
|
|
24252
|
-
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into))))));
|
|
24402
|
+
}).pipe(Effect.catchTag("InstallFailed", (failed) => Effect.sync(() => failAll(into, failed)).pipe(Effect.andThen(clearUnstarted(into, Option.some(failed)))))));
|
|
24253
24403
|
}
|
|
24254
|
-
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
|
|
24404
|
+
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
|
|
24255
24405
|
return folders.map((folder) => ({
|
|
24256
24406
|
...folder,
|
|
24257
24407
|
installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
|
|
@@ -24343,16 +24493,13 @@ const mostFolders = 8;
|
|
|
24343
24493
|
const isConfigFolderPath = Schema.is(ConfigFolderPath);
|
|
24344
24494
|
/** The line that asks again. */
|
|
24345
24495
|
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
24496
|
/**
|
|
24349
24497
|
* 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
|
-
*
|
|
24498
|
+
* question is said first, and the chooser shows only the folders: once Enter chooses, it leaves
|
|
24499
|
+
* the ticked ones on one line after any question it was given, which the terminal would break
|
|
24500
|
+
* mid-word.
|
|
24353
24501
|
*/
|
|
24354
24502
|
const choose = Effect.fnUntraced(function* (found, home) {
|
|
24355
|
-
const columns = yield* (yield* Terminal.Terminal).columns;
|
|
24356
24503
|
const choices = found.map((folder) => {
|
|
24357
24504
|
const shown = shownConfigFolder(home, folder.configFolder);
|
|
24358
24505
|
const sendable = isConfigFolderPath(shown);
|
|
@@ -24369,7 +24516,7 @@ const choose = Effect.fnUntraced(function* (found, home) {
|
|
|
24369
24516
|
});
|
|
24370
24517
|
const ticked = choices.filter((choice) => choice.selected).length;
|
|
24371
24518
|
const keys = "Space ticks or unticks one; Enter connects those ticked.";
|
|
24372
|
-
yield* printBlocks([[{ label: "Connect which config folders?" },
|
|
24519
|
+
yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
|
|
24373
24520
|
return yield* Prompt.run(Prompt.MultiSelect({
|
|
24374
24521
|
choices,
|
|
24375
24522
|
max: mostFolders
|
|
@@ -24562,7 +24709,7 @@ const signalLines = (answer) => {
|
|
|
24562
24709
|
* credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
|
|
24563
24710
|
* thread in the project sends a signal to one of its connections by name.
|
|
24564
24711
|
*/
|
|
24565
|
-
const failedWith$
|
|
24712
|
+
const failedWith$15 = (failed) => sayFailed(failed.nextStep);
|
|
24566
24713
|
const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
|
|
24567
24714
|
const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
|
|
24568
24715
|
const list$5 = Command.make("list", {
|
|
@@ -24574,7 +24721,7 @@ const list$5 = Command.make("list", {
|
|
|
24574
24721
|
params: projectParts(address)
|
|
24575
24722
|
});
|
|
24576
24723
|
yield* printAnswer(answer.json, asJson, connectionLines(answer.value));
|
|
24577
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
24724
|
+
}).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
24725
|
/** A change made on the project's page: its link, and what to do there. */
|
|
24579
24726
|
const onThePage = (address, change) => Effect.gen(function* () {
|
|
24580
24727
|
const config = yield* Ref0Config;
|
|
@@ -24650,14 +24797,14 @@ const send = Command.make("send", {
|
|
|
24650
24797
|
})
|
|
24651
24798
|
});
|
|
24652
24799
|
yield* printAnswer(answer.json, asJson, signalLines(answer.value));
|
|
24653
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
24800
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$15))).pipe(Command.withDescription("Send a signal to one of the project’s connections"));
|
|
24654
24801
|
/** A project's connections, and sending a signal by one. */
|
|
24655
24802
|
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
24803
|
//#endregion
|
|
24657
24804
|
//#region src/harness/checkout.ts
|
|
24658
24805
|
const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
|
|
24659
24806
|
const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
|
|
24660
|
-
const decodeThread$
|
|
24807
|
+
const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
|
|
24661
24808
|
const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
|
|
24662
24809
|
var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
24663
24810
|
static layer = Layer.effect(Checkout, Effect.gen(function* () {
|
|
@@ -24696,7 +24843,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
|
24696
24843
|
"config",
|
|
24697
24844
|
"--get",
|
|
24698
24845
|
"ref0.thread"
|
|
24699
|
-
]), (written) => decodeThread$
|
|
24846
|
+
]), (written) => decodeThread$2(written.trim()));
|
|
24700
24847
|
const repoAt = (folder) => Effect.map(git(folder, [
|
|
24701
24848
|
"config",
|
|
24702
24849
|
"--get",
|
|
@@ -25699,7 +25846,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25699
25846
|
})).value;
|
|
25700
25847
|
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
25848
|
if (code.runs.length === 0) {
|
|
25702
|
-
yield*
|
|
25849
|
+
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
25850
|
return;
|
|
25704
25851
|
}
|
|
25705
25852
|
yield* holdsVersion(root.value, code);
|
|
@@ -25708,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25708
25855
|
thread: `${thread.value}’s own .ref0/`,
|
|
25709
25856
|
project: "the project’s checks"
|
|
25710
25857
|
}[code.code.origin];
|
|
25711
|
-
yield*
|
|
25858
|
+
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
25859
|
const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
|
|
25713
25860
|
const sandboxes = code.compiled.entry.sandboxes ?? [];
|
|
25714
25861
|
const held = /* @__PURE__ */ new Set();
|
|
@@ -25720,7 +25867,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25720
25867
|
const run = yield* newRunId;
|
|
25721
25868
|
const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
|
|
25722
25869
|
const sandbox = yield* localOpener(root.value, held, sandboxes, deadline);
|
|
25723
|
-
yield*
|
|
25870
|
+
yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
|
|
25724
25871
|
const ran = yield* runPart(own, {
|
|
25725
25872
|
params,
|
|
25726
25873
|
host: treeHost(root.value, code.tree),
|
|
@@ -25783,7 +25930,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
|
|
|
25783
25930
|
/** Says a waiting start's tree on stderr, as it stands now. */
|
|
25784
25931
|
const sayTree = Effect.fnUntraced(function* (wait) {
|
|
25785
25932
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25786
|
-
yield*
|
|
25933
|
+
yield* printBlocks([[waitingLines$1(wait, now)]]);
|
|
25787
25934
|
});
|
|
25788
25935
|
/**
|
|
25789
25936
|
* Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
|
|
@@ -25829,7 +25976,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
|
|
|
25829
25976
|
* reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
|
|
25830
25977
|
* at a time.
|
|
25831
25978
|
*/
|
|
25832
|
-
const failedWith$
|
|
25979
|
+
const failedWith$14 = (failed) => sayFailed(failed.nextStep);
|
|
25833
25980
|
const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
|
|
25834
25981
|
const decodeTraced = Schema.decodeUnknownOption(TracedRun);
|
|
25835
25982
|
/** A run named by a flag, or a failure that says how to name one. */
|
|
@@ -25931,7 +26078,7 @@ const run = Command.make("run", {
|
|
|
25931
26078
|
}, asJson, rerunCommand);
|
|
25932
26079
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25933
26080
|
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
25934
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26081
|
+
}).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
26082
|
const trace = Command.make("trace", {
|
|
25936
26083
|
run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
|
|
25937
26084
|
repo: repoFlag,
|
|
@@ -25974,7 +26121,7 @@ const trace = Command.make("trace", {
|
|
|
25974
26121
|
...narrowed,
|
|
25975
26122
|
`--cursor ${next}`
|
|
25976
26123
|
].join(" ")));
|
|
25977
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26124
|
+
}).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
26125
|
const waiting$1 = Command.make("waiting", {
|
|
25979
26126
|
start: startArgument,
|
|
25980
26127
|
repo: repoFlag,
|
|
@@ -26009,7 +26156,7 @@ const waiting$1 = Command.make("waiting", {
|
|
|
26009
26156
|
}, asJson, rerunCommand);
|
|
26010
26157
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
26011
26158
|
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
26012
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26159
|
+
}).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
26160
|
/** What a cancel did, in words. */
|
|
26014
26161
|
const cancelledText = {
|
|
26015
26162
|
start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
|
|
@@ -26035,7 +26182,7 @@ const cancel = Command.make("cancel", {
|
|
|
26035
26182
|
})
|
|
26036
26183
|
});
|
|
26037
26184
|
yield* printAnswer(answer.json, asJson, cancelledText[answer.value.cancelled]);
|
|
26038
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26185
|
+
}).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
26186
|
/** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
|
|
26040
26187
|
const runCommands = [run, Command.make("runs", {
|
|
26041
26188
|
repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
|
|
@@ -26077,14 +26224,14 @@ const runCommands = [run, Command.make("runs", {
|
|
|
26077
26224
|
...narrowed,
|
|
26078
26225
|
`--before ${next}`
|
|
26079
26226
|
].join(" ")));
|
|
26080
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26227
|
+
}).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
26228
|
trace,
|
|
26082
26229
|
waiting$1,
|
|
26083
26230
|
cancel
|
|
26084
26231
|
]))];
|
|
26085
26232
|
//#endregion
|
|
26086
26233
|
//#region src/commands/checks.ts
|
|
26087
|
-
const failedWith$
|
|
26234
|
+
const failedWith$13 = (failed) => sayFailed(failed.nextStep);
|
|
26088
26235
|
const decodeCheck = Schema.decodeUnknownOption(CheckKey);
|
|
26089
26236
|
const decodeToken = Schema.decodeUnknownOption(TokenId);
|
|
26090
26237
|
const decodePerson = Schema.decodeUnknownOption(OwnerName);
|
|
@@ -26120,7 +26267,7 @@ const restore = Command.make("restore", {
|
|
|
26120
26267
|
}
|
|
26121
26268
|
});
|
|
26122
26269
|
yield* printAnswer(answer.json, asJson, restoredWords(answer.value, repo));
|
|
26123
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26270
|
+
}).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
26271
|
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
26272
|
/** `ref0 checks`: a repo's checks. */
|
|
26126
26273
|
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 +26599,7 @@ const projectGateLines = (view, repo) => {
|
|
|
26452
26599
|
};
|
|
26453
26600
|
//#endregion
|
|
26454
26601
|
//#region src/commands/gate.ts
|
|
26455
|
-
const failedWith$
|
|
26602
|
+
const failedWith$12 = (failed) => sayFailed(failed.nextStep);
|
|
26456
26603
|
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
26604
|
/** A pull request's gate. */
|
|
26458
26605
|
const gateCommands = [Command.make("gate", {
|
|
@@ -26478,7 +26625,7 @@ const gateCommands = [Command.make("gate", {
|
|
|
26478
26625
|
}
|
|
26479
26626
|
});
|
|
26480
26627
|
yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
|
|
26481
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26628
|
+
}).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
26629
|
//#endregion
|
|
26483
26630
|
//#region src/clone.ts
|
|
26484
26631
|
/** The remote a clone pushes to: its thread's fork path, named as the thread is. */
|
|
@@ -26836,7 +26983,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
|
|
|
26836
26983
|
* with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
|
|
26837
26984
|
* thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
|
|
26838
26985
|
*/
|
|
26839
|
-
const failedWith$
|
|
26986
|
+
const failedWith$11 = (failed) => sayFailed(failed.nextStep);
|
|
26840
26987
|
/** `ref0 git-credential`, as Git runs it: see `answerGit`. */
|
|
26841
26988
|
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
26989
|
const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
|
|
@@ -26890,7 +27037,7 @@ const gitCommands = [
|
|
|
26890
27037
|
value
|
|
26891
27038
|
])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
|
|
26892
27039
|
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$
|
|
27040
|
+
}).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
27041
|
Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
26895
27042
|
yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
|
|
26896
27043
|
const synced = yield* syncBase(".");
|
|
@@ -26906,7 +27053,7 @@ const gitCommands = [
|
|
|
26906
27053
|
on,
|
|
26907
27054
|
commit
|
|
26908
27055
|
}, asJson, `Merged ${at}. Push it when you’re ready: git push.`);
|
|
26909
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27056
|
+
}).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
27057
|
credential
|
|
26911
27058
|
];
|
|
26912
27059
|
//#endregion
|
|
@@ -27319,7 +27466,7 @@ const usageCommands = [personUsage, personMeters];
|
|
|
27319
27466
|
* invitations. Each is a person's, asked of them by approval, as every terminal command of
|
|
27320
27467
|
* theirs is; agents never change members.
|
|
27321
27468
|
*/
|
|
27322
|
-
const failedWith$
|
|
27469
|
+
const failedWith$10 = (failed) => sayFailed(failed.nextStep);
|
|
27323
27470
|
const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
|
|
27324
27471
|
const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
|
|
27325
27472
|
const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
|
|
@@ -27349,7 +27496,7 @@ const create$1 = Command.make("create", {
|
|
|
27349
27496
|
payload
|
|
27350
27497
|
});
|
|
27351
27498
|
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$
|
|
27499
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
|
|
27353
27500
|
const leave = Command.make("leave", {
|
|
27354
27501
|
organization: organization$2,
|
|
27355
27502
|
json
|
|
@@ -27359,7 +27506,7 @@ const leave = Command.make("leave", {
|
|
|
27359
27506
|
params: { owner: org }
|
|
27360
27507
|
});
|
|
27361
27508
|
yield* printAnswer(answer.json, asJson, `Left ${org}.`);
|
|
27362
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27509
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
|
|
27363
27510
|
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
27511
|
create$1,
|
|
27365
27512
|
leave,
|
|
@@ -27376,7 +27523,7 @@ const orgs = Command.make("orgs", {
|
|
|
27376
27523
|
});
|
|
27377
27524
|
const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
|
|
27378
27525
|
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$
|
|
27526
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The organizations you’re a member of"), Command.withShortDescription("List your organizations"));
|
|
27380
27527
|
const members = Command.make("members", {
|
|
27381
27528
|
organization: organization$2,
|
|
27382
27529
|
cursor: cursor$3,
|
|
@@ -27389,7 +27536,7 @@ const members = Command.make("members", {
|
|
|
27389
27536
|
});
|
|
27390
27537
|
const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
|
|
27391
27538
|
yield* printAnswer(answer.json, asJson, [...lines, ...more(answer.value.next)].join("\n"));
|
|
27392
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27539
|
+
}).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
27540
|
const role = Command.make("role", {
|
|
27394
27541
|
organization: organization$2,
|
|
27395
27542
|
person,
|
|
@@ -27405,7 +27552,7 @@ const role = Command.make("role", {
|
|
|
27405
27552
|
payload: { role: chosen }
|
|
27406
27553
|
});
|
|
27407
27554
|
yield* printAnswer(answer.json, asJson, `${named} is ${answer.value.role} of ${org} now.`);
|
|
27408
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27555
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make a member an admin, or an admin a member"));
|
|
27409
27556
|
const remove$1 = Command.make("remove", {
|
|
27410
27557
|
organization: organization$2,
|
|
27411
27558
|
person,
|
|
@@ -27419,7 +27566,7 @@ const remove$1 = Command.make("remove", {
|
|
|
27419
27566
|
}
|
|
27420
27567
|
});
|
|
27421
27568
|
yield* printAnswer(answer.json, asJson, `Removed ${named} from ${org}.`);
|
|
27422
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27569
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
|
|
27423
27570
|
const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
|
|
27424
27571
|
const listInvitations = Command.make("list", {
|
|
27425
27572
|
organization: organization$2,
|
|
@@ -27433,7 +27580,7 @@ const listInvitations = Command.make("list", {
|
|
|
27433
27580
|
});
|
|
27434
27581
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
|
|
27435
27582
|
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$
|
|
27583
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("An organization’s open invitations"), Command.withShortDescription("List an organization’s open invitations"));
|
|
27437
27584
|
const withdraw = Command.make("withdraw", {
|
|
27438
27585
|
organization: organization$2,
|
|
27439
27586
|
invitation,
|
|
@@ -27447,7 +27594,7 @@ const withdraw = Command.make("withdraw", {
|
|
|
27447
27594
|
}
|
|
27448
27595
|
});
|
|
27449
27596
|
yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
|
|
27450
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27597
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
|
|
27451
27598
|
const invite = Command.make("invite", {
|
|
27452
27599
|
organization: organization$2,
|
|
27453
27600
|
person,
|
|
@@ -27467,7 +27614,7 @@ const invite = Command.make("invite", {
|
|
|
27467
27614
|
payload
|
|
27468
27615
|
});
|
|
27469
27616
|
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$
|
|
27617
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Invite a person to an organization; list or withdraw invitations"), Command.withSubcommands([listInvitations, withdraw]));
|
|
27471
27618
|
const invitations = Command.make("invitations", {
|
|
27472
27619
|
cursor: cursor$3,
|
|
27473
27620
|
json
|
|
@@ -27478,7 +27625,7 @@ const invitations = Command.make("invitations", {
|
|
|
27478
27625
|
});
|
|
27479
27626
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
|
|
27480
27627
|
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$
|
|
27628
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("The invitations you’ve received"), Command.withShortDescription("List invitations you’ve received"));
|
|
27482
27629
|
const accept = Command.make("accept", {
|
|
27483
27630
|
invitation,
|
|
27484
27631
|
json
|
|
@@ -27488,7 +27635,7 @@ const accept = Command.make("accept", {
|
|
|
27488
27635
|
params: { invitation: id }
|
|
27489
27636
|
});
|
|
27490
27637
|
yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
|
|
27491
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27638
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
|
|
27492
27639
|
const decline = Command.make("decline", {
|
|
27493
27640
|
invitation,
|
|
27494
27641
|
json
|
|
@@ -27498,7 +27645,7 @@ const decline = Command.make("decline", {
|
|
|
27498
27645
|
params: { invitation: id }
|
|
27499
27646
|
});
|
|
27500
27647
|
yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
|
|
27501
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27648
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
|
|
27502
27649
|
/** Organizations, their members and invitations. */
|
|
27503
27650
|
const organizationCommands = [
|
|
27504
27651
|
orgCommand,
|
|
@@ -27511,7 +27658,7 @@ const organizationCommands = [
|
|
|
27511
27658
|
];
|
|
27512
27659
|
//#endregion
|
|
27513
27660
|
//#region src/commands/grants.ts
|
|
27514
|
-
const failedWith$
|
|
27661
|
+
const failedWith$9 = (failed) => sayFailed(failed.nextStep);
|
|
27515
27662
|
const isOwnerRef = Schema.is(OwnerRef);
|
|
27516
27663
|
const isProject$1 = Schema.is(WrittenProject);
|
|
27517
27664
|
const isThread = Schema.is(WrittenThread);
|
|
@@ -27633,7 +27780,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
|
|
|
27633
27780
|
onSome: (on) => [on]
|
|
27634
27781
|
})];
|
|
27635
27782
|
yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
|
|
27636
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27783
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$9)));
|
|
27637
27784
|
const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
|
|
27638
27785
|
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
27786
|
/** An organization's grants. */
|
|
@@ -27653,7 +27800,41 @@ const grantCommands = [Command.make("grants", {
|
|
|
27653
27800
|
const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
|
|
27654
27801
|
const next = answer.value.nextAfter;
|
|
27655
27802
|
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$
|
|
27803
|
+
}).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];
|
|
27804
|
+
//#endregion
|
|
27805
|
+
//#region src/commands/identity-token.ts
|
|
27806
|
+
/**
|
|
27807
|
+
* `ref0 identity-token --audience <origin>`: an identity token for the agent's session it runs
|
|
27808
|
+
* in, for one outside service, which verifies it against ref0's published keys. One request, by
|
|
27809
|
+
* the session's own token, which never starts or renews a session. The token alone goes to
|
|
27810
|
+
* stdout, so it pipes into a verifier; anything else goes to stderr, with no token, and fails.
|
|
27811
|
+
*/
|
|
27812
|
+
const decodeAudience = Schema.decodeUnknownOption(IdentityAudience);
|
|
27813
|
+
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.";
|
|
27814
|
+
const audienceOnce = "Name the audience once, as --audience https://verifier.example: the outside service’s HTTPS origin.";
|
|
27815
|
+
const notAnAudience = "Send the outside service’s HTTPS origin as the audience, such as https://verifier.example, with no path.";
|
|
27816
|
+
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.";
|
|
27817
|
+
/**
|
|
27818
|
+
* What went wrong, said once, on stderr. An answer lost or failed partway may still have made a
|
|
27819
|
+
* token, as ref0's log records, but nothing to check or undo: a fresh request is the way on.
|
|
27820
|
+
*/
|
|
27821
|
+
const failedWith$8 = (failed) => sayFailed(failed.status >= 500 || failed.status === 0 && (failed.kind === "Unreachable" || failed.kind === "Unexpected") ? mayHaveIssued : failed.nextStep);
|
|
27822
|
+
/** Identity tokens, for outside services. */
|
|
27823
|
+
const identityTokenCommands = [Command.make("identity-token", {
|
|
27824
|
+
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)),
|
|
27825
|
+
json
|
|
27826
|
+
}, ({ audience: named, json: asJson }) => Effect.gen(function* () {
|
|
27827
|
+
if (named.length !== 1) return yield* sayFailed(audienceOnce);
|
|
27828
|
+
const audience = decodeAudience(named[0]);
|
|
27829
|
+
if (Option.isNone(audience)) return yield* sayFailed(notAnAudience);
|
|
27830
|
+
yield* ownSession(outside);
|
|
27831
|
+
const issued = yield* callEndpoint({
|
|
27832
|
+
endpoint: "me.identityToken",
|
|
27833
|
+
payload: { audience: audience.value }
|
|
27834
|
+
});
|
|
27835
|
+
if (asJson) return yield* printAnswer(issued.json, true, "");
|
|
27836
|
+
yield* Console.log(issued.value.token);
|
|
27837
|
+
}).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
27838
|
//#endregion
|
|
27658
27839
|
//#region src/commands/agents.ts
|
|
27659
27840
|
/**
|
|
@@ -27928,27 +28109,39 @@ const repoCommands = [Command.make("repos", {
|
|
|
27928
28109
|
//#endregion
|
|
27929
28110
|
//#region src/decision-lines.ts
|
|
27930
28111
|
/** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
|
|
27931
|
-
/**
|
|
27932
|
-
|
|
27933
|
-
|
|
27934
|
-
|
|
27935
|
-
|
|
28112
|
+
/**
|
|
28113
|
+
* A thread as a line read in `scope` names it: `#3` when it's in that scope, else in full, as
|
|
28114
|
+
* `pip/default#3`.
|
|
28115
|
+
*/
|
|
28116
|
+
const threadWords = (thread, scope) => thread.startsWith(`${scope}#`) ? thread.slice(scope.length) : thread;
|
|
28117
|
+
/**
|
|
28118
|
+
* What waits, in words: a gate's pull request and version, and the thread it's from, or the thread
|
|
28119
|
+
* an ask is on.
|
|
28120
|
+
*/
|
|
28121
|
+
const subjectWords = ({ subject, names }, scope) => subject.kind === "version" && names.pullRequest !== void 0 ? `${names.pullRequest}, version ${subject.version}, from ${threadWords(names.thread, scope)}` : names.thread;
|
|
28122
|
+
/**
|
|
28123
|
+
* A decision in a line, read in `scope`: what waits, its answers or how it ended, then its ID, for
|
|
28124
|
+
* `ref0 decision` and `ref0 answer`.
|
|
28125
|
+
*/
|
|
28126
|
+
const decisionLines = (decision, scope) => {
|
|
28127
|
+
const on = subjectWords(decision, scope);
|
|
27936
28128
|
const answers = decision.options.map((option) => option.key).join(" or ");
|
|
27937
|
-
return decision.state === "open" ?
|
|
28129
|
+
return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
|
|
27938
28130
|
};
|
|
27939
|
-
const nothing = "Nothing waits on you here.";
|
|
27940
28131
|
/**
|
|
27941
|
-
* What waits
|
|
27942
|
-
* waits on its person, which they answer on their own page.
|
|
28132
|
+
* What waits in `scope`, as `ref0 waiting` prints it: what waits on the caller, then, for an agent,
|
|
28133
|
+
* what waits on its person, which only they answer, on their own page.
|
|
27943
28134
|
*/
|
|
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 ?
|
|
28135
|
+
const waitingLines = (decisions, scope) => {
|
|
28136
|
+
const yours = decisions.flatMap((decision) => decision.waitsOn === "you" ? [decisionLines(decision, scope)] : []);
|
|
28137
|
+
const theirs = decisions.flatMap((decision) => decision.waitsOn === "your person" ? [` ${decisionLines(decision, scope)}`] : []);
|
|
28138
|
+
if (theirs.length === 0) return yours.length === 0 ? `Nothing waits on you in ${scope}.` : yours.join("\n");
|
|
28139
|
+
const them = theirs.length === 1 ? "it" : "them";
|
|
27948
28140
|
return [
|
|
27949
28141
|
...yours,
|
|
27950
|
-
|
|
27951
|
-
...theirs
|
|
28142
|
+
`Waits on your person, who alone can answer ${them}, under Waiting on you on their page:`,
|
|
28143
|
+
...theirs,
|
|
28144
|
+
`ref0 refuses an agent’s answer to ${them}.`
|
|
27952
28145
|
].join("\n");
|
|
27953
28146
|
};
|
|
27954
28147
|
//#endregion
|
|
@@ -27964,7 +28157,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
|
|
|
27964
28157
|
const encodePlan = Schema.encodeEffect(PlanInput);
|
|
27965
28158
|
const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
|
|
27966
28159
|
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
|
-
|
|
28160
|
+
/** A project, `acme/billing`, or an organization, `acme`, for its org threads. */
|
|
28161
|
+
const Scope$1 = Schema.Union([WrittenProject, OwnerName]);
|
|
28162
|
+
const decodeScope = Schema.decodeUnknownOption(Scope$1);
|
|
28163
|
+
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
28164
|
const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
|
|
27969
28165
|
const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
|
|
27970
28166
|
/** Where a version's approval stands: who accepted it, or its check. */
|
|
@@ -28125,25 +28321,37 @@ const comments = Command.make("comments", {
|
|
|
28125
28321
|
].filter((line) => line !== "").join("\n");
|
|
28126
28322
|
yield* printAnswer(answer.json, asJson, words);
|
|
28127
28323
|
}).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"));
|
|
28324
|
+
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([
|
|
28325
|
+
list$4,
|
|
28326
|
+
write,
|
|
28327
|
+
onVersion("show"),
|
|
28328
|
+
onVersion("propose"),
|
|
28329
|
+
comment,
|
|
28330
|
+
comments
|
|
28331
|
+
]));
|
|
28332
|
+
/**
|
|
28333
|
+
* The project of the thread the session runs on, or its organization for an org thread's. Outside
|
|
28334
|
+
* a session, it says how to name one, and fails before anything's sent.
|
|
28335
|
+
*/
|
|
28336
|
+
const sessionScope = Effect.gen(function* () {
|
|
28337
|
+
const thread = yield* (yield* SessionThread).current.pipe(Effect.catchTag("KeychainError", (failed) => sayFailed(failed.nextStep)));
|
|
28338
|
+
const scope = Option.flatMap(thread, (written) => decodeScope(projectOf$2(written)));
|
|
28339
|
+
if (Option.isNone(scope)) return yield* sayFailed("Name the project: ref0 waiting pip/shop. Or, for an organization’s org threads: ref0 waiting acme.");
|
|
28340
|
+
return scope.value;
|
|
28341
|
+
});
|
|
28128
28342
|
/** Plans' and decisions' commands. */
|
|
28129
28343
|
const planCommands = [
|
|
28130
|
-
|
|
28131
|
-
list$4,
|
|
28132
|
-
write,
|
|
28133
|
-
onVersion("show"),
|
|
28134
|
-
onVersion("propose"),
|
|
28135
|
-
comment,
|
|
28136
|
-
comments
|
|
28137
|
-
])),
|
|
28344
|
+
plan,
|
|
28138
28345
|
Command.make("waiting", {
|
|
28139
|
-
project: project$
|
|
28346
|
+
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
28347
|
json
|
|
28141
|
-
}, ({ project:
|
|
28348
|
+
}, ({ project: named, json: asJson }) => Effect.gen(function* () {
|
|
28349
|
+
const address = Option.isSome(named) ? named.value : yield* sessionScope;
|
|
28142
28350
|
const answer = yield* callEndpoint(routed(address, {
|
|
28143
28351
|
endpoint: "decisions.waiting",
|
|
28144
28352
|
params: scopeParams(address)
|
|
28145
28353
|
}));
|
|
28146
|
-
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions));
|
|
28354
|
+
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
|
|
28147
28355
|
}).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
28356
|
Command.make("decision", {
|
|
28149
28357
|
project: project$2,
|
|
@@ -28157,7 +28365,7 @@ const planCommands = [
|
|
|
28157
28365
|
decision: id
|
|
28158
28366
|
}
|
|
28159
28367
|
}));
|
|
28160
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28368
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
28161
28369
|
}).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
28370
|
Command.make("answer", {
|
|
28163
28371
|
project: project$2,
|
|
@@ -28180,7 +28388,7 @@ const planCommands = [
|
|
|
28180
28388
|
})
|
|
28181
28389
|
})
|
|
28182
28390
|
}));
|
|
28183
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28391
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
28184
28392
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
|
|
28185
28393
|
];
|
|
28186
28394
|
//#endregion
|
|
@@ -28220,9 +28428,14 @@ const endedLines = (view) => {
|
|
|
28220
28428
|
const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
|
|
28221
28429
|
return [` ${view.state.padEnd(8)} ${at}${why}`];
|
|
28222
28430
|
};
|
|
28223
|
-
|
|
28224
|
-
|
|
28225
|
-
|
|
28431
|
+
/**
|
|
28432
|
+
* When it opened, and by what: a workflow's run, or an agent's session, whose IDs go on a line of
|
|
28433
|
+
* their own, since ref0 answers no name for either.
|
|
28434
|
+
*/
|
|
28435
|
+
const openedLines = (view) => {
|
|
28436
|
+
const at = minute(view.openedAt);
|
|
28437
|
+
if ("workflow" in view.openedBy) return [` opened ${at} by workflow ${view.openedBy.workflow.name}’s run ${view.openedBy.workflow.run}`];
|
|
28438
|
+
return [` opened ${at} by an agent’s session:`, ` ${view.openedBy.session} of ${view.openedBy.agent.id}`];
|
|
28226
28439
|
};
|
|
28227
28440
|
/** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
|
|
28228
28441
|
const pullRequestLines = (view, repo) => {
|
|
@@ -28233,7 +28446,7 @@ const pullRequestLines = (view, repo) => {
|
|
|
28233
28446
|
`${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
|
|
28234
28447
|
` head ${headWords(head)}`,
|
|
28235
28448
|
` current ${versionWords(current)}, on ${base}`,
|
|
28236
|
-
|
|
28449
|
+
...openedLines(view),
|
|
28237
28450
|
...endedLines(view)
|
|
28238
28451
|
].join("\n");
|
|
28239
28452
|
};
|
|
@@ -29219,7 +29432,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
|
|
|
29219
29432
|
const { thread, blockers } = first.value;
|
|
29220
29433
|
if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
|
|
29221
29434
|
const them = blockers.length === 1 ? "it" : "them";
|
|
29222
|
-
yield*
|
|
29435
|
+
yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
|
|
29223
29436
|
let renewedAt = Number.NEGATIVE_INFINITY;
|
|
29224
29437
|
for (;;) {
|
|
29225
29438
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -30112,7 +30325,7 @@ const enable = Command.make("enable", {
|
|
|
30112
30325
|
const why = found.why === void 0 ? "" : ` (${found.why})`;
|
|
30113
30326
|
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
30327
|
}
|
|
30115
|
-
yield*
|
|
30328
|
+
yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
|
|
30116
30329
|
const answer = yield* callEndpoint({
|
|
30117
30330
|
endpoint: "workflows.enable",
|
|
30118
30331
|
params: repoParts(address),
|
|
@@ -30145,7 +30358,7 @@ const start = Command.make("start", {
|
|
|
30145
30358
|
source: source.value
|
|
30146
30359
|
}
|
|
30147
30360
|
});
|
|
30148
|
-
yield*
|
|
30361
|
+
yield* printBlocks([[attachingLines(written, compiled.value)]]);
|
|
30149
30362
|
const answer = yield* callEndpoint({
|
|
30150
30363
|
endpoint: "threadWorkflows.start",
|
|
30151
30364
|
params,
|
|
@@ -33105,11 +33318,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
33105
33318
|
* started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
|
|
33106
33319
|
* someone else's words, and nothing here reads it.
|
|
33107
33320
|
*/
|
|
33108
|
-
const decodeThread = Schema.decodeUnknownOption(WrittenThread);
|
|
33321
|
+
const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
|
|
33109
33322
|
const decodeEvent = Schema.decodeUnknownOption(EventId);
|
|
33110
33323
|
const optional = (name) => Config.String(name).pipe(Config.option);
|
|
33111
33324
|
const watchedHere = Effect.gen(function* () {
|
|
33112
|
-
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
|
|
33325
|
+
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
|
|
33113
33326
|
const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
|
|
33114
33327
|
return Option.zipWith(thread, event, (on, by) => ({
|
|
33115
33328
|
thread: on,
|
|
@@ -34487,6 +34700,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
|
34487
34700
|
});
|
|
34488
34701
|
return SessionToken.of({ current });
|
|
34489
34702
|
}));
|
|
34703
|
+
const decodeThread = Schema.decodeUnknownOption(WrittenThread);
|
|
34704
|
+
/** `SessionThread` from the keychain, as the start of the harness session this runs in kept it. */
|
|
34705
|
+
const harnessSessionThread = Layer.effect(SessionThread, Effect.gen(function* () {
|
|
34706
|
+
const keychain = yield* Keychain;
|
|
34707
|
+
const harness = yield* HarnessProcess;
|
|
34708
|
+
const current = Effect.gen(function* () {
|
|
34709
|
+
const binding = yield* bindingHere().pipe(Effect.provideService(HarnessProcess, harness), Effect.orElseSucceed(() => Option.none()));
|
|
34710
|
+
if (Option.isNone(binding)) return Option.none();
|
|
34711
|
+
const kept = yield* keychain.get(threadAccount(binding.value));
|
|
34712
|
+
return Option.flatMap(kept, (thread) => decodeThread(Redacted.value(thread)));
|
|
34713
|
+
});
|
|
34714
|
+
return SessionThread.of({ current });
|
|
34715
|
+
}));
|
|
34490
34716
|
//#endregion
|
|
34491
34717
|
//#region src/harness/server-check.ts
|
|
34492
34718
|
/**
|
|
@@ -34848,7 +35074,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
|
|
|
34848
35074
|
...organizationCommands,
|
|
34849
35075
|
...grantCommands,
|
|
34850
35076
|
...agentCommands,
|
|
34851
|
-
...attachmentCommands
|
|
35077
|
+
...attachmentCommands,
|
|
35078
|
+
...identityTokenCommands
|
|
34852
35079
|
]));
|
|
34853
35080
|
const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
|
|
34854
35081
|
const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
|
|
@@ -34863,7 +35090,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
|
|
|
34863
35090
|
Ref0Config.layer,
|
|
34864
35091
|
sessionToken,
|
|
34865
35092
|
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));
|
|
35093
|
+
])), 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
35094
|
//#endregion
|
|
34868
35095
|
//#region src/main.ts
|
|
34869
35096
|
ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
|