@ref0/cli 0.10.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 +1623 -965
- 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
|
/**
|
|
@@ -5486,12 +5486,15 @@ const name$1 = ownerNamePattern.source.replace(/^\^|\$$/g, "");
|
|
|
5486
5486
|
const projectPattern = new RegExp(`^(${name$1})/(${name$1})$`);
|
|
5487
5487
|
const threadPattern = new RegExp(`^(${name$1})(?:/(${name$1}))?#([1-9][0-9]*)$`);
|
|
5488
5488
|
const repoPattern = new RegExp(`^(${name$1})/(${repoNamePattern.source.replace(/^\^|\$$/g, "")})$`);
|
|
5489
|
+
const pullRequestPattern = new RegExp(`${repoPattern.source.replace(/\$$/, "")}#[1-9][0-9]*$`);
|
|
5489
5490
|
/** A project, `owner/project`. */
|
|
5490
5491
|
const WrittenProject = Schema.String.check(Schema.isPattern(projectPattern)).pipe(Schema.brand("WrittenProject"));
|
|
5491
5492
|
/** A thread, `owner/project#number`, or an org thread, `owner#number`. */
|
|
5492
5493
|
const WrittenThread = Schema.String.check(Schema.isPattern(threadPattern)).pipe(Schema.brand("WrittenThread"));
|
|
5493
5494
|
/** A repo, `owner/repo`. */
|
|
5494
5495
|
const WrittenRepo = Schema.String.check(Schema.isPattern(repoPattern)).pipe(Schema.brand("WrittenRepo"));
|
|
5496
|
+
/** A pull request, `owner/repo#number`. */
|
|
5497
|
+
const WrittenPullRequest = Schema.String.check(Schema.isPattern(pullRequestPattern)).pipe(Schema.brand("WrittenPullRequest"));
|
|
5495
5498
|
/** A written repo's parts: its owner, as an endpoint's path takes it, and its name. */
|
|
5496
5499
|
const repoParts = (written) => {
|
|
5497
5500
|
const [, owner = "", repo = ""] = repoPattern.exec(written) ?? [];
|
|
@@ -5702,7 +5705,7 @@ const Me = Schema.Struct({
|
|
|
5702
5705
|
session: SessionId,
|
|
5703
5706
|
depth: SessionDepth
|
|
5704
5707
|
})),
|
|
5705
|
-
scopes: Schema.Array(Scope$
|
|
5708
|
+
scopes: Schema.Array(Scope$2),
|
|
5706
5709
|
expiresAt: Schema.optionalKey(Schema.DateTimeUtcFromMillis)
|
|
5707
5710
|
});
|
|
5708
5711
|
/**
|
|
@@ -6072,7 +6075,7 @@ const Reader = Schema.Struct({
|
|
|
6072
6075
|
agent: Schema.optionalKey(AgentRef),
|
|
6073
6076
|
/** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
|
|
6074
6077
|
attachment: Schema.NullOr(CredentialRef),
|
|
6075
|
-
scopes: Schema.Array(Scope$
|
|
6078
|
+
scopes: Schema.Array(Scope$2)
|
|
6076
6079
|
});
|
|
6077
6080
|
Schema.Struct({
|
|
6078
6081
|
reader: Reader,
|
|
@@ -6586,14 +6589,13 @@ const ConnectOrganization = Schema.Struct({
|
|
|
6586
6589
|
organization: OrgId,
|
|
6587
6590
|
name: OwnerName
|
|
6588
6591
|
});
|
|
6589
|
-
/**
|
|
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
|
+
*/
|
|
6590
6596
|
const GrantReplacement = Schema.Struct({
|
|
6591
6597
|
grant: TokenId,
|
|
6592
|
-
state: Schema.Literals([
|
|
6593
|
-
"replaces",
|
|
6594
|
-
"revoked",
|
|
6595
|
-
"skipped"
|
|
6596
|
-
]),
|
|
6598
|
+
state: Schema.Literals(["replaces", "skipped"]),
|
|
6597
6599
|
attachedAt: Schema.optionalKey(Timestamp)
|
|
6598
6600
|
});
|
|
6599
6601
|
/**
|
|
@@ -6767,383 +6769,6 @@ Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
|
|
|
6767
6769
|
attachGrant: Schema.optionalKey(EventId)
|
|
6768
6770
|
})]);
|
|
6769
6771
|
//#endregion
|
|
6770
|
-
//#region ../contracts/src/approvals.ts
|
|
6771
|
-
/**
|
|
6772
|
-
* Approvals, as their commands travel: what a terminal, a page or an agent asks its person to
|
|
6773
|
-
* say yes to. A command is one endpoint, by its name in the surface table, and its request, as
|
|
6774
|
-
* that endpoint encodes it. ref0 decodes each part by the endpoint's own `Schema` before it
|
|
6775
|
-
* keeps or shows anything, so a command is exactly what the endpoint would take.
|
|
6776
|
-
*/
|
|
6777
|
-
/**
|
|
6778
|
-
* An endpoint by its name in the surface table, `group.endpoint`. Checked against the table
|
|
6779
|
-
* where the command is made canonical, since the table is typed from the API this is part of.
|
|
6780
|
-
*/
|
|
6781
|
-
const CommandEndpoint = Schema.String.check(Schema.isPattern(/^[a-zA-Z]+\.[a-zA-Z]+$/));
|
|
6782
|
-
/** One command, as asked: its endpoint, and its request's path, query and payload, encoded. */
|
|
6783
|
-
const ApprovalCommand = Schema.Struct({
|
|
6784
|
-
endpoint: CommandEndpoint,
|
|
6785
|
-
params: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
6786
|
-
query: Schema.optionalKey(Schema.JsonObject),
|
|
6787
|
-
payload: Schema.optionalKey(Schema.Json)
|
|
6788
|
-
});
|
|
6789
|
-
/** A command ref0 won't ask anyone about: not an endpoint's request, or not one an approval runs. */
|
|
6790
|
-
var CommandRefused = class extends Schema.TaggedError()("CommandRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
|
|
6791
|
-
/** A terminal asking: its command, and the hash of the secret it collects the result with. */
|
|
6792
|
-
const StartApproval = Schema.Struct({
|
|
6793
|
-
command: ApprovalCommand,
|
|
6794
|
-
secretHash: SecretHash
|
|
6795
|
-
});
|
|
6796
|
-
/** A terminal's approval, started: what to show its person, and until when. */
|
|
6797
|
-
const ApprovalStarted = Schema.Struct({
|
|
6798
|
-
approval: ApprovalId,
|
|
6799
|
-
code: ApprovalCode,
|
|
6800
|
-
expiresAt: Timestamp
|
|
6801
|
-
});
|
|
6802
|
-
/**
|
|
6803
|
-
* An answer that waits for the person: the call only the person may make, or that agents ask
|
|
6804
|
-
* for, became an approval, which the person answers on their own page. A `202`, never a failure
|
|
6805
|
-
* of the call: whoever asked collects the result with `GET /v1/approvals/{approval}`.
|
|
6806
|
-
*/
|
|
6807
|
-
var ApprovalNeeded = class extends Schema.TaggedError()("ApprovalNeeded", {
|
|
6808
|
-
approval: ApprovalId,
|
|
6809
|
-
says: Schema.String,
|
|
6810
|
-
expiresAt: Timestamp
|
|
6811
|
-
}, { httpApiStatus: 202 }) {};
|
|
6812
|
-
const terminalAsker = Schema.Struct({ kind: Schema.Literal("terminal") });
|
|
6813
|
-
const pageAsker = Schema.Struct({ kind: Schema.Literal("page") });
|
|
6814
|
-
const agentAsker = Schema.Struct({
|
|
6815
|
-
kind: Schema.Literal("agent"),
|
|
6816
|
-
agent: AgentId,
|
|
6817
|
-
handle: AgentHandle,
|
|
6818
|
-
session: SessionId,
|
|
6819
|
-
/** The attachment its credential was made under, for a person's agent in an organization. */
|
|
6820
|
-
attachment: Schema.optionalKey(CredentialRef)
|
|
6821
|
-
});
|
|
6822
|
-
/**
|
|
6823
|
-
* Who asked, as a receipt keeps it: a terminal, one of ref0's pages, an agent, or a machine
|
|
6824
|
-
* asking to connect, by its ID and its own word for itself, and from where.
|
|
6825
|
-
*/
|
|
6826
|
-
const ApprovalAsker = Schema.Union([
|
|
6827
|
-
terminalAsker,
|
|
6828
|
-
pageAsker,
|
|
6829
|
-
Schema.Struct({
|
|
6830
|
-
kind: Schema.Literal("machine"),
|
|
6831
|
-
machine: MachineId,
|
|
6832
|
-
machineName: MachineName,
|
|
6833
|
-
askedFrom: Schema.optionalKey(Schema.String)
|
|
6834
|
-
}),
|
|
6835
|
-
agentAsker
|
|
6836
|
-
]);
|
|
6837
|
-
/**
|
|
6838
|
-
* Who asked, as the person reads a request before they answer: a terminal, a page or an agent.
|
|
6839
|
-
* A machine's request is read as an `AttachmentRequestView`, never as a command.
|
|
6840
|
-
*/
|
|
6841
|
-
const ApprovalViewAsker = Schema.Union([
|
|
6842
|
-
terminalAsker,
|
|
6843
|
-
pageAsker,
|
|
6844
|
-
agentAsker
|
|
6845
|
-
]);
|
|
6846
|
-
/** A request, as the person reads it before they answer. */
|
|
6847
|
-
const ApprovalView = Schema.Struct({
|
|
6848
|
-
approval: ApprovalId,
|
|
6849
|
-
/** The command as its CLI line. */
|
|
6850
|
-
says: Schema.String,
|
|
6851
|
-
/** Everything the command carries, each named, in order: nothing it would run is left out. */
|
|
6852
|
-
fields: Schema.Array(Schema.Struct({
|
|
6853
|
-
name: Schema.String,
|
|
6854
|
-
value: Schema.String,
|
|
6855
|
-
part: Schema.Literals([
|
|
6856
|
-
"path",
|
|
6857
|
-
"query",
|
|
6858
|
-
"payload"
|
|
6859
|
-
])
|
|
6860
|
-
})),
|
|
6861
|
-
command: ApprovalCommand,
|
|
6862
|
-
asker: ApprovalViewAsker,
|
|
6863
|
-
askedFrom: Schema.optionalKey(Schema.String),
|
|
6864
|
-
askedAt: Timestamp,
|
|
6865
|
-
expiresAt: Timestamp,
|
|
6866
|
-
/**
|
|
6867
|
-
* An agent's ask: the kind of action its answer may set agents' permission for, and whether a
|
|
6868
|
-
* yes may let agents take it without asking. A no with the passkey may always stop them asking.
|
|
6869
|
-
*/
|
|
6870
|
-
agentPermission: Schema.optionalKey(Schema.Struct({
|
|
6871
|
-
action: ScopeAction,
|
|
6872
|
-
mayAllow: Schema.Boolean
|
|
6873
|
-
})),
|
|
6874
|
-
/**
|
|
6875
|
-
* What the command acts on, drawn as ref0 resolves it for the person reading, where its
|
|
6876
|
-
* endpoint names a preview. Without one the page can draw, it offers only no.
|
|
6877
|
-
*/
|
|
6878
|
-
preview: Schema.optionalKey(ApprovalPreview)
|
|
6879
|
-
});
|
|
6880
|
-
/** A statement's hash: the SHA-256 of its words and quoted values as JSON, as lowercase hex. */
|
|
6881
|
-
const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
|
|
6882
|
-
/**
|
|
6883
|
-
* Who asks for a yes to a statement: a workflow's run, `passkey.run`'s, by the workflow and the
|
|
6884
|
-
* run; or a gate's check, `passkey.check`'s, by the check, the pull request's version it gates and
|
|
6885
|
-
* the check run its statement is over.
|
|
6886
|
-
*/
|
|
6887
|
-
const StatementAsker = Schema.Union([Schema.Struct({
|
|
6888
|
-
kind: Schema.Literal("workflow"),
|
|
6889
|
-
workflow: WorkflowName,
|
|
6890
|
-
run: WorkflowRunId
|
|
6891
|
-
}), Schema.Struct({
|
|
6892
|
-
kind: Schema.Literal("gate"),
|
|
6893
|
-
check: CheckKey,
|
|
6894
|
-
pullRequest: PullRequestNumber,
|
|
6895
|
-
version: VersionNumber,
|
|
6896
|
-
run: CheckRunId
|
|
6897
|
-
})]);
|
|
6898
|
-
/**
|
|
6899
|
-
* A passkey request, as the person reads it before they answer: the statement, its words as its
|
|
6900
|
-
* asker wrote them and each value it quotes, and its hash; who asks; where, a workflow's
|
|
6901
|
-
* `owner/repo` or a thread's `owner/project#number`, or a gate's pull request, `owner/repo#number`;
|
|
6902
|
-
* whom it asks, the run's accountable person, or for a gate the person behind its run, a person by
|
|
6903
|
-
* name, or a permission's holders; and until when.
|
|
6904
|
-
*/
|
|
6905
|
-
const StatementView = Schema.Struct({
|
|
6906
|
-
approval: ApprovalId,
|
|
6907
|
-
statement: WorkflowText,
|
|
6908
|
-
hash: StatementHash,
|
|
6909
|
-
asker: StatementAsker,
|
|
6910
|
-
home: Schema.String,
|
|
6911
|
-
from: Schema.Union([Schema.Literals(["accountable", "person"]), Schema.Struct({ permission: RepoPermissionName })]),
|
|
6912
|
-
askedAt: Timestamp,
|
|
6913
|
-
expiresAt: Timestamp
|
|
6914
|
-
});
|
|
6915
|
-
/**
|
|
6916
|
-
* The person's own requests, waiting for them; the passkey requests workflows' runs and gates asked
|
|
6917
|
-
* them; and each machine's request this browser session is the answerer of, so a reload finds it
|
|
6918
|
-
* again without its code.
|
|
6919
|
-
*/
|
|
6920
|
-
const ApprovalsWaiting = Schema.Struct({
|
|
6921
|
-
approvals: Schema.Array(ApprovalView),
|
|
6922
|
-
statements: Schema.Array(StatementView),
|
|
6923
|
-
requests: Schema.Array(AttachmentRequestView)
|
|
6924
|
-
});
|
|
6925
|
-
/** Finding a terminal's request by the code it showed. */
|
|
6926
|
-
const FindApproval = Schema.Union([Schema.Struct({ code: TypedApprovalCode }), Schema.Struct({ link: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/)) })]);
|
|
6927
|
-
/**
|
|
6928
|
-
* Asking for the passkey's options to answer a request: a terminal's request needs its code
|
|
6929
|
-
* again, so its ID alone, which travels where its code doesn't, never answers it. A machine's
|
|
6930
|
-
* request needs its answerer, and the agent it chooses to connect, before a yes has options.
|
|
6931
|
-
*/
|
|
6932
|
-
const ApprovalOptionsRequest = Schema.Struct({
|
|
6933
|
-
code: Schema.optionalKey(TypedApprovalCode),
|
|
6934
|
-
folders: Schema.optionalKey(ConnectFolderChoices)
|
|
6935
|
-
});
|
|
6936
|
-
/**
|
|
6937
|
-
* An answer: yes, with the passkey, or no. For an agent's ask, each answer can set the owner's
|
|
6938
|
-
* permission its own way, so a pairing that can't happen can't be sent: yes, and let agents do
|
|
6939
|
-
* it without asking; no, and stop agents asking, with the passkey too.
|
|
6940
|
-
*/
|
|
6941
|
-
const AnswerApproval = Schema.Union([
|
|
6942
|
-
Schema.Struct({
|
|
6943
|
-
answer: Schema.Literal("yes"),
|
|
6944
|
-
permission: Schema.optionalKey(Schema.Literal("allow")),
|
|
6945
|
-
assertion: PasskeyAsserted,
|
|
6946
|
-
code: Schema.optionalKey(TypedApprovalCode),
|
|
6947
|
-
/** The preview the page drew, where the command's endpoint names one. */
|
|
6948
|
-
preview: Schema.optionalKey(PreviewEcho),
|
|
6949
|
-
/** The statement the page showed, by its hash, for a workflow run's or a gate's passkey request. */
|
|
6950
|
-
statement: Schema.optionalKey(Schema.Struct({ hash: StatementHash }))
|
|
6951
|
-
}),
|
|
6952
|
-
Schema.Struct({
|
|
6953
|
-
answer: Schema.Literal("no"),
|
|
6954
|
-
permission: Schema.Literal("deny"),
|
|
6955
|
-
assertion: PasskeyAsserted,
|
|
6956
|
-
code: Schema.optionalKey(TypedApprovalCode)
|
|
6957
|
-
}),
|
|
6958
|
-
Schema.Struct({
|
|
6959
|
-
answer: Schema.Literal("no"),
|
|
6960
|
-
code: Schema.optionalKey(TypedApprovalCode)
|
|
6961
|
-
})
|
|
6962
|
-
]);
|
|
6963
|
-
/**
|
|
6964
|
-
* What running an approved command came to: its status, and its endpoint's answer or refusal,
|
|
6965
|
-
* as JSON. ref0 reads it by the command's endpoint before anyone sees it, and whoever asked reads
|
|
6966
|
-
* it the same way, with `ranOf`: this schema is part of the API the endpoints' types come from,
|
|
6967
|
-
* so it can't be typed by them itself.
|
|
6968
|
-
*/
|
|
6969
|
-
const Ran = Schema.Struct({
|
|
6970
|
-
status: Schema.Int,
|
|
6971
|
-
result: Schema.Json
|
|
6972
|
-
});
|
|
6973
|
-
/**
|
|
6974
|
-
* A ran command whose answer its endpoint's schemas no longer read, such as one kept from
|
|
6975
|
-
* before they changed: it ran, and what it came to can't be said.
|
|
6976
|
-
*/
|
|
6977
|
-
var ResultUnreadable = class extends Schema.TaggedError()("ResultUnreadable", { nextStep: Schema.String }, { httpApiStatus: 500 }) {};
|
|
6978
|
-
/**
|
|
6979
|
-
* Where a request stands, for whoever asked: still waiting; approved, and running as soon as
|
|
6980
|
-
* ref0 can; refused; expired; or what it ran to, with the endpoint it ran, whose schemas read it.
|
|
6981
|
-
*/
|
|
6982
|
-
const ApprovalState = Schema.Union([
|
|
6983
|
-
Schema.Struct({
|
|
6984
|
-
state: Schema.Literal("waiting"),
|
|
6985
|
-
expiresAt: Timestamp
|
|
6986
|
-
}),
|
|
6987
|
-
Schema.Struct({
|
|
6988
|
-
state: Schema.Literal("approved"),
|
|
6989
|
-
/** A machine's answering browser may forget its saved answer once the terminal kept it. */
|
|
6990
|
-
collected: Schema.optionalKey(Schema.Literal(true))
|
|
6991
|
-
}),
|
|
6992
|
-
Schema.Struct({ state: Schema.Literal("refused") }),
|
|
6993
|
-
Schema.Struct({ state: Schema.Literal("expired") }),
|
|
6994
|
-
Schema.Struct({
|
|
6995
|
-
state: Schema.Literal("ran"),
|
|
6996
|
-
endpoint: CommandEndpoint,
|
|
6997
|
-
...Ran.fields
|
|
6998
|
-
})
|
|
6999
|
-
]);
|
|
7000
|
-
/** Only the answering browser receives a machine's answer code; state reads never carry it. */
|
|
7001
|
-
const ApprovalAnswer = Schema.Union([
|
|
7002
|
-
Schema.Struct({
|
|
7003
|
-
...ApprovalState.members[1].fields,
|
|
7004
|
-
answerCode: ApprovalCode
|
|
7005
|
-
}),
|
|
7006
|
-
Schema.Struct({
|
|
7007
|
-
...ApprovalState.members[4].fields,
|
|
7008
|
-
answerCode: ApprovalCode
|
|
7009
|
-
}),
|
|
7010
|
-
ApprovalState
|
|
7011
|
-
]);
|
|
7012
|
-
/** A request that isn't there, or isn't the caller's to see, answers the same. */
|
|
7013
|
-
var ApprovalNotFound = class extends Schema.TaggedError()("ApprovalNotFound", { nextStep: Schema.String }, { httpApiStatus: 404 }) {};
|
|
7014
|
-
/** Too many asked, or too many codes typed, from here in the last hour. */
|
|
7015
|
-
var SlowDown = class extends Schema.TaggedError()("SlowDown", { nextStep: Schema.String }, { httpApiStatus: 429 }) {};
|
|
7016
|
-
/** A command's hash: the SHA-256 of its canonical JSON, as lowercase hex. */
|
|
7017
|
-
const CommandHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
|
|
7018
|
-
Schema.Struct({
|
|
7019
|
-
approval: ApprovalId,
|
|
7020
|
-
endpoint: CommandEndpoint,
|
|
7021
|
-
/** The command's canonical JSON. */
|
|
7022
|
-
command: Schema.String,
|
|
7023
|
-
commandHash: CommandHash,
|
|
7024
|
-
asker: ApprovalAsker,
|
|
7025
|
-
/** The browser session the person answered in, for their log. */
|
|
7026
|
-
credential: CredentialRef,
|
|
7027
|
-
generation: Generation,
|
|
7028
|
-
/** When the request expires, in epoch milliseconds. */
|
|
7029
|
-
deadline: Schema.Int,
|
|
7030
|
-
challenge: Schema.String,
|
|
7031
|
-
/** A machine answer retries only from its answering session, with this key. */
|
|
7032
|
-
answerKey: Schema.optionalKey(RequestId),
|
|
7033
|
-
/** An agent's ask: the credential it asked with, checked again before the answer counts. */
|
|
7034
|
-
asking: Schema.optionalKey(TokenId),
|
|
7035
|
-
/** The kind of action the person, answering, let agents take without asking from now on. */
|
|
7036
|
-
allow: Schema.optionalKey(ScopeAction)
|
|
7037
|
-
});
|
|
7038
|
-
Schema.Struct({
|
|
7039
|
-
approval: ApprovalId,
|
|
7040
|
-
action: ScopeAction,
|
|
7041
|
-
credential: CredentialRef,
|
|
7042
|
-
generation: Generation
|
|
7043
|
-
});
|
|
7044
|
-
Schema.Union([Schema.Struct({ denied: Schema.Literal(true) }), Schema.Struct({
|
|
7045
|
-
denied: Schema.Literal(false),
|
|
7046
|
-
reason: Schema.Literals(["generation moved", "not set up"])
|
|
7047
|
-
})]);
|
|
7048
|
-
Schema.Union([Schema.Struct({
|
|
7049
|
-
admitted: Schema.Literal(true),
|
|
7050
|
-
admittedAt: Schema.Int,
|
|
7051
|
-
answerCode: Schema.optionalKey(ApprovalCode)
|
|
7052
|
-
}), Schema.Struct({
|
|
7053
|
-
admitted: Schema.Literal(false),
|
|
7054
|
-
reason: Schema.Literals([
|
|
7055
|
-
"expired",
|
|
7056
|
-
"generation moved",
|
|
7057
|
-
"challenge used",
|
|
7058
|
-
"not set up",
|
|
7059
|
-
"asker gone"
|
|
7060
|
-
])
|
|
7061
|
-
})]);
|
|
7062
|
-
Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
|
|
7063
|
-
found: Schema.Literal(true),
|
|
7064
|
-
endpoint: CommandEndpoint,
|
|
7065
|
-
command: Schema.String,
|
|
7066
|
-
commandHash: CommandHash,
|
|
7067
|
-
generation: Generation,
|
|
7068
|
-
generationNow: Generation,
|
|
7069
|
-
/** The `approval.answered` its admission recorded in the person's log. */
|
|
7070
|
-
answeredEvent: EventId,
|
|
7071
|
-
/** Who asked, as that `approval.answered` says. */
|
|
7072
|
-
asker: ApprovalAsker,
|
|
7073
|
-
/** What running it came to, once it ran, as `KeptRan` keeps it. */
|
|
7074
|
-
result: Schema.optionalKey(Schema.String)
|
|
7075
|
-
})]);
|
|
7076
|
-
/**
|
|
7077
|
-
* A `Ran` as it's kept, on its receipt and on its approval: its JSON, as text. The Owner DO
|
|
7078
|
-
* keeps it as it comes and runs no endpoint's schemas, so it's read only by its endpoint, with
|
|
7079
|
-
* `keptRan`, which refuses one they don't take.
|
|
7080
|
-
*/
|
|
7081
|
-
const KeptRan = Schema.fromJsonString(Ran);
|
|
7082
|
-
Schema.Struct({
|
|
7083
|
-
approval: ApprovalId,
|
|
7084
|
-
result: Schema.String
|
|
7085
|
-
});
|
|
7086
|
-
Schema.Struct({ result: Schema.String });
|
|
7087
|
-
Schema.Struct({ approval: ApprovalId });
|
|
7088
|
-
Schema.Struct({ approval: ApprovalId });
|
|
7089
|
-
Schema.Union([Schema.Struct({ expired: Schema.Literal(true) }), Schema.Struct({ expired: Schema.Literal(false) })]);
|
|
7090
|
-
Schema.Struct({
|
|
7091
|
-
person: PersonId,
|
|
7092
|
-
approval: ApprovalId
|
|
7093
|
-
});
|
|
7094
|
-
//#endregion
|
|
7095
|
-
//#region ../contracts/src/browser-sessions.ts
|
|
7096
|
-
/**
|
|
7097
|
-
* A browser session's cookie: Better Auth's, named for ref0, `Secure` and host-only on the
|
|
7098
|
-
* app's one origin. It holds the session's token, signed; the directory and the person's Owner DO hold
|
|
7099
|
-
* only the token's SHA-256.
|
|
7100
|
-
*/
|
|
7101
|
-
const browserSessionCookie = "__Secure-ref0.session_token";
|
|
7102
|
-
Schema.Struct({
|
|
7103
|
-
token: TokenId,
|
|
7104
|
-
secretHash: SecretHash,
|
|
7105
|
-
expiresAt: Schema.DateTimeUtcFromMillis
|
|
7106
|
-
});
|
|
7107
|
-
Schema.Struct({ registered: Schema.Boolean });
|
|
7108
|
-
/**
|
|
7109
|
-
* Where a signed-in person stands: naming, when the name their GitHub login gave was taken or
|
|
7110
|
-
* kept back, with that login as a suggestion; setting up, once their owner is made and before
|
|
7111
|
-
* it's ready with this session registered, which naming again finishes; or ready.
|
|
7112
|
-
*/
|
|
7113
|
-
const Welcome = Schema.Union([Schema.Struct({
|
|
7114
|
-
state: Schema.Literals(["ready", "setting up"]),
|
|
7115
|
-
person: PersonId,
|
|
7116
|
-
owner: OwnerId,
|
|
7117
|
-
name: OwnerName
|
|
7118
|
-
}), Schema.Struct({
|
|
7119
|
-
state: Schema.Literal("naming"),
|
|
7120
|
-
person: PersonId,
|
|
7121
|
-
suggestion: Schema.optionalKey(Schema.String)
|
|
7122
|
-
})]);
|
|
7123
|
-
/**
|
|
7124
|
-
* A browser session renewed: its expiry, in its person's Owner DO and in the directory, and on its cookie.
|
|
7125
|
-
* Renewed at most once a day, so one asked sooner answers its expiry unchanged.
|
|
7126
|
-
*/
|
|
7127
|
-
const SessionRenewed = Schema.Struct({ expiresAt: Schema.DateTimeUtcFromMillis });
|
|
7128
|
-
/**
|
|
7129
|
-
* What starting a GitHub sign-in at `POST /auth/sign-in/social` answers: GitHub's address to go
|
|
7130
|
-
* to, with the sign-in's state in a cookie set alongside. Better Auth's answer, which ref0 serves.
|
|
7131
|
-
*/
|
|
7132
|
-
const NavigationUrl = Schema.String.check(Schema.isPattern(/^https?:\/\/[^\s/]+(?:\/[^\s]*)?$/), Schema.makeFilter((url) => URL.canParse(url) || "an HTTP(S) URL the platform can parse")).pipe(Schema.brand("NavigationUrl"));
|
|
7133
|
-
Schema.Struct({ url: NavigationUrl });
|
|
7134
|
-
/**
|
|
7135
|
-
* Where signing in sends the browser back to, as `POST /auth/sign-in/social` takes it: a path on
|
|
7136
|
-
* ref0's own origin, such as `/welcome`, or a whole URL. Never a socket's.
|
|
7137
|
-
*/
|
|
7138
|
-
const SignInDestination = Schema.String.pipe(Schema.brand("SignInDestination"));
|
|
7139
|
-
Schema.Struct({
|
|
7140
|
-
provider: Schema.Literal("github"),
|
|
7141
|
-
callbackURL: SignInDestination,
|
|
7142
|
-
errorCallbackURL: SignInDestination
|
|
7143
|
-
});
|
|
7144
|
-
/** The name a naming person chose for their owner. */
|
|
7145
|
-
const NameOwner = Schema.Struct({ name: OwnerName });
|
|
7146
|
-
//#endregion
|
|
7147
6772
|
//#region ../contracts/src/plan-events.ts
|
|
7148
6773
|
/**
|
|
7149
6774
|
* A plan's events, in its thread's project's log, written by the Project DO. A version's
|
|
@@ -7282,6 +6907,21 @@ const DecisionView = Schema.Struct({
|
|
|
7282
6907
|
closedWhy: Schema.optionalKey(DecisionClosedWhy)
|
|
7283
6908
|
});
|
|
7284
6909
|
/**
|
|
6910
|
+
* Where a decision is, as people write it: its thread, `pip/default#3`, or an org thread's,
|
|
6911
|
+
* `acme#7`; and a gate's pull request, `pip/notes#1`. Its Project DO writes them as it answers,
|
|
6912
|
+
* from its own threads and repos and the owner's name the Worker gives it, so each reads as it's
|
|
6913
|
+
* called now.
|
|
6914
|
+
*/
|
|
6915
|
+
const DecisionNames = Schema.Struct({
|
|
6916
|
+
thread: WrittenThread,
|
|
6917
|
+
pullRequest: Schema.optionalKey(WrittenPullRequest)
|
|
6918
|
+
});
|
|
6919
|
+
/** A decision as `/v1` answers it: as it stands, and where it is, in words. */
|
|
6920
|
+
const NamedDecision = Schema.Struct({
|
|
6921
|
+
...DecisionView.fields,
|
|
6922
|
+
names: DecisionNames
|
|
6923
|
+
});
|
|
6924
|
+
/**
|
|
7285
6925
|
* A version as it stands: its content, its state, and, once proposed, where its approval is: its
|
|
7286
6926
|
* repos still being checked, or who accepted it.
|
|
7287
6927
|
*/
|
|
@@ -7435,7 +7075,7 @@ const GateAnswerRoute = Schema.TaggedStruct("GateAnswerRoute", {
|
|
|
7435
7075
|
reason: Schema.optionalKey(TextId)
|
|
7436
7076
|
});
|
|
7437
7077
|
Schema.Union([
|
|
7438
|
-
|
|
7078
|
+
NamedDecision,
|
|
7439
7079
|
GateAnswerRoute,
|
|
7440
7080
|
DecisionNotFound,
|
|
7441
7081
|
AnswerRefused,
|
|
@@ -7467,14 +7107,29 @@ Schema.Union([
|
|
|
7467
7107
|
]) }),
|
|
7468
7108
|
DeliveryOutsideWindow
|
|
7469
7109
|
]);
|
|
7470
|
-
|
|
7110
|
+
/** Someone decisions wait on: a person, or an agent, and the roles the Worker found they hold. */
|
|
7111
|
+
const Waiter = Schema.Struct({
|
|
7471
7112
|
decider: DeciderRef,
|
|
7472
|
-
roles: Schema.
|
|
7113
|
+
roles: Schema.Array(Schema.Struct({
|
|
7473
7114
|
role: Schema.Literal("admin"),
|
|
7474
7115
|
owner: OwnerId
|
|
7475
|
-
}))
|
|
7116
|
+
}))
|
|
7476
7117
|
});
|
|
7477
|
-
|
|
7118
|
+
Schema.Struct({
|
|
7119
|
+
ownerName: OwnerName,
|
|
7120
|
+
you: Waiter,
|
|
7121
|
+
person: Schema.optionalKey(Waiter),
|
|
7122
|
+
decisions: Schema.optionalKey(Schema.Array(DecisionId).check(Schema.isMaxLength(100)))
|
|
7123
|
+
});
|
|
7124
|
+
/**
|
|
7125
|
+
* An open decision that waits on the caller: on them, or, for an agent, on its person, who
|
|
7126
|
+
* answers it. A gate never takes an agent's answer, so an agent's gate always waits on its person.
|
|
7127
|
+
*/
|
|
7128
|
+
const WaitingDecision = Schema.Struct({
|
|
7129
|
+
...NamedDecision.fields,
|
|
7130
|
+
waitsOn: Schema.Literals(["you", "your person"])
|
|
7131
|
+
});
|
|
7132
|
+
const WaitingPage = Schema.Struct({ decisions: Schema.Array(WaitingDecision) });
|
|
7478
7133
|
Schema.Struct({
|
|
7479
7134
|
ownerName: OwnerName,
|
|
7480
7135
|
number: ThreadNumber
|
|
@@ -7492,7 +7147,7 @@ Schema.Struct({
|
|
|
7492
7147
|
decision: DecisionId,
|
|
7493
7148
|
wait: Schema.optionalKey(DecisionWait)
|
|
7494
7149
|
});
|
|
7495
|
-
Schema.Union([
|
|
7150
|
+
Schema.Union([NamedDecision, DecisionNotFound]);
|
|
7496
7151
|
/**
|
|
7497
7152
|
* `POST …/plans`: a version, as a draft unless it's proposed at once, and why it was written.
|
|
7498
7153
|
* A first version needs no reason, and a later one may leave it out.
|
|
@@ -7541,6 +7196,385 @@ const ChildStageView = Schema.Struct({
|
|
|
7541
7196
|
});
|
|
7542
7197
|
Schema.Union([ChildStageView, ThreadNotFound]);
|
|
7543
7198
|
//#endregion
|
|
7199
|
+
//#region ../contracts/src/approvals.ts
|
|
7200
|
+
/**
|
|
7201
|
+
* Approvals, as their commands travel: what a terminal, a page or an agent asks its person to
|
|
7202
|
+
* say yes to. A command is one endpoint, by its name in the surface table, and its request, as
|
|
7203
|
+
* that endpoint encodes it. ref0 decodes each part by the endpoint's own `Schema` before it
|
|
7204
|
+
* keeps or shows anything, so a command is exactly what the endpoint would take.
|
|
7205
|
+
*/
|
|
7206
|
+
/**
|
|
7207
|
+
* An endpoint by its name in the surface table, `group.endpoint`. Checked against the table
|
|
7208
|
+
* where the command is made canonical, since the table is typed from the API this is part of.
|
|
7209
|
+
*/
|
|
7210
|
+
const CommandEndpoint = Schema.String.check(Schema.isPattern(/^[a-zA-Z]+\.[a-zA-Z]+$/));
|
|
7211
|
+
/** One command, as asked: its endpoint, and its request's path, query and payload, encoded. */
|
|
7212
|
+
const ApprovalCommand = Schema.Struct({
|
|
7213
|
+
endpoint: CommandEndpoint,
|
|
7214
|
+
params: Schema.optionalKey(Schema.Record(Schema.String, Schema.String)),
|
|
7215
|
+
query: Schema.optionalKey(Schema.JsonObject),
|
|
7216
|
+
payload: Schema.optionalKey(Schema.Json)
|
|
7217
|
+
});
|
|
7218
|
+
/** A command ref0 won't ask anyone about: not an endpoint's request, or not one an approval runs. */
|
|
7219
|
+
var CommandRefused = class extends Schema.TaggedError()("CommandRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
|
|
7220
|
+
/** A terminal asking: its command, and the hash of the secret it collects the result with. */
|
|
7221
|
+
const StartApproval = Schema.Struct({
|
|
7222
|
+
command: ApprovalCommand,
|
|
7223
|
+
secretHash: SecretHash
|
|
7224
|
+
});
|
|
7225
|
+
/** A terminal's approval, started: what to show its person, and until when. */
|
|
7226
|
+
const ApprovalStarted = Schema.Struct({
|
|
7227
|
+
approval: ApprovalId,
|
|
7228
|
+
code: ApprovalCode,
|
|
7229
|
+
expiresAt: Timestamp
|
|
7230
|
+
});
|
|
7231
|
+
/**
|
|
7232
|
+
* An answer that waits for the person: the call only the person may make, or that agents ask
|
|
7233
|
+
* for, became an approval, which the person answers on their own page. A `202`, never a failure
|
|
7234
|
+
* of the call: whoever asked collects the result with `GET /v1/approvals/{approval}`.
|
|
7235
|
+
*/
|
|
7236
|
+
var ApprovalNeeded = class extends Schema.TaggedError()("ApprovalNeeded", {
|
|
7237
|
+
approval: ApprovalId,
|
|
7238
|
+
says: Schema.String,
|
|
7239
|
+
expiresAt: Timestamp
|
|
7240
|
+
}, { httpApiStatus: 202 }) {};
|
|
7241
|
+
const terminalAsker = Schema.Struct({ kind: Schema.Literal("terminal") });
|
|
7242
|
+
const pageAsker = Schema.Struct({ kind: Schema.Literal("page") });
|
|
7243
|
+
const agentAsker = Schema.Struct({
|
|
7244
|
+
kind: Schema.Literal("agent"),
|
|
7245
|
+
agent: AgentId,
|
|
7246
|
+
handle: AgentHandle,
|
|
7247
|
+
session: SessionId,
|
|
7248
|
+
/** The attachment its credential was made under, for a person's agent in an organization. */
|
|
7249
|
+
attachment: Schema.optionalKey(CredentialRef)
|
|
7250
|
+
});
|
|
7251
|
+
/**
|
|
7252
|
+
* Who asked, as a receipt keeps it: a terminal, one of ref0's pages, an agent, or a machine
|
|
7253
|
+
* asking to connect, by its ID and its own word for itself, and from where.
|
|
7254
|
+
*/
|
|
7255
|
+
const ApprovalAsker = Schema.Union([
|
|
7256
|
+
terminalAsker,
|
|
7257
|
+
pageAsker,
|
|
7258
|
+
Schema.Struct({
|
|
7259
|
+
kind: Schema.Literal("machine"),
|
|
7260
|
+
machine: MachineId,
|
|
7261
|
+
machineName: MachineName,
|
|
7262
|
+
askedFrom: Schema.optionalKey(Schema.String)
|
|
7263
|
+
}),
|
|
7264
|
+
agentAsker
|
|
7265
|
+
]);
|
|
7266
|
+
/**
|
|
7267
|
+
* Who asked, as the person reads a request before they answer: a terminal, a page or an agent.
|
|
7268
|
+
* A machine's request is read as an `AttachmentRequestView`, never as a command.
|
|
7269
|
+
*/
|
|
7270
|
+
const ApprovalViewAsker = Schema.Union([
|
|
7271
|
+
terminalAsker,
|
|
7272
|
+
pageAsker,
|
|
7273
|
+
agentAsker
|
|
7274
|
+
]);
|
|
7275
|
+
/** A request, as the person reads it before they answer. */
|
|
7276
|
+
const ApprovalView = Schema.Struct({
|
|
7277
|
+
approval: ApprovalId,
|
|
7278
|
+
/** The command as its CLI line. */
|
|
7279
|
+
says: Schema.String,
|
|
7280
|
+
/** Everything the command carries, each named, in order: nothing it would run is left out. */
|
|
7281
|
+
fields: Schema.Array(Schema.Struct({
|
|
7282
|
+
name: Schema.String,
|
|
7283
|
+
value: Schema.String,
|
|
7284
|
+
part: Schema.Literals([
|
|
7285
|
+
"path",
|
|
7286
|
+
"query",
|
|
7287
|
+
"payload"
|
|
7288
|
+
])
|
|
7289
|
+
})),
|
|
7290
|
+
command: ApprovalCommand,
|
|
7291
|
+
asker: ApprovalViewAsker,
|
|
7292
|
+
askedFrom: Schema.optionalKey(Schema.String),
|
|
7293
|
+
askedAt: Timestamp,
|
|
7294
|
+
expiresAt: Timestamp,
|
|
7295
|
+
/**
|
|
7296
|
+
* An agent's ask: the kind of action its answer may set agents' permission for, and whether a
|
|
7297
|
+
* yes may let agents take it without asking. A no with the passkey may always stop them asking.
|
|
7298
|
+
*/
|
|
7299
|
+
agentPermission: Schema.optionalKey(Schema.Struct({
|
|
7300
|
+
action: ScopeAction,
|
|
7301
|
+
mayAllow: Schema.Boolean
|
|
7302
|
+
})),
|
|
7303
|
+
/**
|
|
7304
|
+
* What the command acts on, drawn as ref0 resolves it for the person reading, where its
|
|
7305
|
+
* endpoint names a preview. Without one the page can draw, it offers only no.
|
|
7306
|
+
*/
|
|
7307
|
+
preview: Schema.optionalKey(ApprovalPreview)
|
|
7308
|
+
});
|
|
7309
|
+
/** A statement's hash: the SHA-256 of its words and quoted values as JSON, as lowercase hex. */
|
|
7310
|
+
const StatementHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
|
|
7311
|
+
/**
|
|
7312
|
+
* Who asks for a yes to a statement: a workflow's run, `passkey.run`'s, by the workflow and the
|
|
7313
|
+
* run; or a gate's check, `passkey.check`'s, by the check, the pull request's version it gates and
|
|
7314
|
+
* the check run its statement is over.
|
|
7315
|
+
*/
|
|
7316
|
+
const StatementAsker = Schema.Union([Schema.Struct({
|
|
7317
|
+
kind: Schema.Literal("workflow"),
|
|
7318
|
+
workflow: WorkflowName,
|
|
7319
|
+
run: WorkflowRunId
|
|
7320
|
+
}), Schema.Struct({
|
|
7321
|
+
kind: Schema.Literal("gate"),
|
|
7322
|
+
check: CheckKey,
|
|
7323
|
+
pullRequest: PullRequestNumber,
|
|
7324
|
+
version: VersionNumber,
|
|
7325
|
+
run: CheckRunId
|
|
7326
|
+
})]);
|
|
7327
|
+
/**
|
|
7328
|
+
* A passkey request, as the person reads it before they answer: the statement, its words as its
|
|
7329
|
+
* asker wrote them and each value it quotes, and its hash; who asks; where, a workflow's
|
|
7330
|
+
* `owner/repo` or a thread's `owner/project#number`, or a gate's pull request, `owner/repo#number`;
|
|
7331
|
+
* whom it asks, the run's accountable person, or for a gate the person behind its run, a person by
|
|
7332
|
+
* name, or a permission's holders; and until when.
|
|
7333
|
+
*/
|
|
7334
|
+
const StatementView = Schema.Struct({
|
|
7335
|
+
approval: ApprovalId,
|
|
7336
|
+
statement: WorkflowText,
|
|
7337
|
+
hash: StatementHash,
|
|
7338
|
+
asker: StatementAsker,
|
|
7339
|
+
home: Schema.String,
|
|
7340
|
+
from: Schema.Union([Schema.Literals(["accountable", "person"]), Schema.Struct({ permission: RepoPermissionName })]),
|
|
7341
|
+
askedAt: Timestamp,
|
|
7342
|
+
expiresAt: Timestamp
|
|
7343
|
+
});
|
|
7344
|
+
/**
|
|
7345
|
+
* The person's own requests, waiting for them; the passkey requests workflows' runs and gates asked
|
|
7346
|
+
* them; each machine's request this browser session is the answerer of, so a reload finds it
|
|
7347
|
+
* again without its code; and the gates' decisions that wait on their yes or no, in every project
|
|
7348
|
+
* they see, oldest first.
|
|
7349
|
+
*/
|
|
7350
|
+
const ApprovalsWaiting = Schema.Struct({
|
|
7351
|
+
approvals: Schema.Array(ApprovalView),
|
|
7352
|
+
statements: Schema.Array(StatementView),
|
|
7353
|
+
requests: Schema.Array(AttachmentRequestView),
|
|
7354
|
+
decisions: Schema.Array(NamedDecision)
|
|
7355
|
+
});
|
|
7356
|
+
/** Finding a terminal's request by the code it showed. */
|
|
7357
|
+
const FindApproval = Schema.Union([Schema.Struct({ code: TypedApprovalCode }), Schema.Struct({ link: Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{22}$/)) })]);
|
|
7358
|
+
/**
|
|
7359
|
+
* Asking for the passkey's options to answer a request: a terminal's request needs its code
|
|
7360
|
+
* again, so its ID alone, which travels where its code doesn't, never answers it. A machine's
|
|
7361
|
+
* request needs its answerer, and the agent it chooses to connect, before a yes has options.
|
|
7362
|
+
*/
|
|
7363
|
+
const ApprovalOptionsRequest = Schema.Struct({
|
|
7364
|
+
code: Schema.optionalKey(TypedApprovalCode),
|
|
7365
|
+
folders: Schema.optionalKey(ConnectFolderChoices)
|
|
7366
|
+
});
|
|
7367
|
+
/**
|
|
7368
|
+
* An answer: yes, with the passkey, or no. For an agent's ask, each answer can set the owner's
|
|
7369
|
+
* permission its own way, so a pairing that can't happen can't be sent: yes, and let agents do
|
|
7370
|
+
* it without asking; no, and stop agents asking, with the passkey too.
|
|
7371
|
+
*/
|
|
7372
|
+
const AnswerApproval = Schema.Union([
|
|
7373
|
+
Schema.Struct({
|
|
7374
|
+
answer: Schema.Literal("yes"),
|
|
7375
|
+
permission: Schema.optionalKey(Schema.Literal("allow")),
|
|
7376
|
+
assertion: PasskeyAsserted,
|
|
7377
|
+
code: Schema.optionalKey(TypedApprovalCode),
|
|
7378
|
+
/** The preview the page drew, where the command's endpoint names one. */
|
|
7379
|
+
preview: Schema.optionalKey(PreviewEcho),
|
|
7380
|
+
/** The statement the page showed, by its hash, for a workflow run's or a gate's passkey request. */
|
|
7381
|
+
statement: Schema.optionalKey(Schema.Struct({ hash: StatementHash }))
|
|
7382
|
+
}),
|
|
7383
|
+
Schema.Struct({
|
|
7384
|
+
answer: Schema.Literal("no"),
|
|
7385
|
+
permission: Schema.Literal("deny"),
|
|
7386
|
+
assertion: PasskeyAsserted,
|
|
7387
|
+
code: Schema.optionalKey(TypedApprovalCode)
|
|
7388
|
+
}),
|
|
7389
|
+
Schema.Struct({
|
|
7390
|
+
answer: Schema.Literal("no"),
|
|
7391
|
+
code: Schema.optionalKey(TypedApprovalCode)
|
|
7392
|
+
})
|
|
7393
|
+
]);
|
|
7394
|
+
/**
|
|
7395
|
+
* What running an approved command came to: its status, and its endpoint's answer or refusal,
|
|
7396
|
+
* as JSON. ref0 reads it by the command's endpoint before anyone sees it, and whoever asked reads
|
|
7397
|
+
* it the same way, with `ranOf`: this schema is part of the API the endpoints' types come from,
|
|
7398
|
+
* so it can't be typed by them itself.
|
|
7399
|
+
*/
|
|
7400
|
+
const Ran = Schema.Struct({
|
|
7401
|
+
status: Schema.Int,
|
|
7402
|
+
result: Schema.Json
|
|
7403
|
+
});
|
|
7404
|
+
/**
|
|
7405
|
+
* A ran command whose answer its endpoint's schemas no longer read, such as one kept from
|
|
7406
|
+
* before they changed: it ran, and what it came to can't be said.
|
|
7407
|
+
*/
|
|
7408
|
+
var ResultUnreadable = class extends Schema.TaggedError()("ResultUnreadable", { nextStep: Schema.String }, { httpApiStatus: 500 }) {};
|
|
7409
|
+
/**
|
|
7410
|
+
* Where a request stands, for whoever asked: still waiting; approved, and running as soon as
|
|
7411
|
+
* ref0 can; refused; expired; or what it ran to, with the endpoint it ran, whose schemas read it.
|
|
7412
|
+
*/
|
|
7413
|
+
const ApprovalState = Schema.Union([
|
|
7414
|
+
Schema.Struct({
|
|
7415
|
+
state: Schema.Literal("waiting"),
|
|
7416
|
+
expiresAt: Timestamp
|
|
7417
|
+
}),
|
|
7418
|
+
Schema.Struct({
|
|
7419
|
+
state: Schema.Literal("approved"),
|
|
7420
|
+
/** A machine's answering browser may forget its saved answer once the terminal kept it. */
|
|
7421
|
+
collected: Schema.optionalKey(Schema.Literal(true))
|
|
7422
|
+
}),
|
|
7423
|
+
Schema.Struct({ state: Schema.Literal("refused") }),
|
|
7424
|
+
Schema.Struct({ state: Schema.Literal("expired") }),
|
|
7425
|
+
Schema.Struct({
|
|
7426
|
+
state: Schema.Literal("ran"),
|
|
7427
|
+
endpoint: CommandEndpoint,
|
|
7428
|
+
...Ran.fields
|
|
7429
|
+
})
|
|
7430
|
+
]);
|
|
7431
|
+
/** Only the answering browser receives a machine's answer code; state reads never carry it. */
|
|
7432
|
+
const ApprovalAnswer = Schema.Union([
|
|
7433
|
+
Schema.Struct({
|
|
7434
|
+
...ApprovalState.members[1].fields,
|
|
7435
|
+
answerCode: ApprovalCode
|
|
7436
|
+
}),
|
|
7437
|
+
Schema.Struct({
|
|
7438
|
+
...ApprovalState.members[4].fields,
|
|
7439
|
+
answerCode: ApprovalCode
|
|
7440
|
+
}),
|
|
7441
|
+
ApprovalState
|
|
7442
|
+
]);
|
|
7443
|
+
/** A request that isn't there, or isn't the caller's to see, answers the same. */
|
|
7444
|
+
var ApprovalNotFound = class extends Schema.TaggedError()("ApprovalNotFound", { nextStep: Schema.String }, { httpApiStatus: 404 }) {};
|
|
7445
|
+
/** Too many asked, or too many codes typed, from here in the last hour. */
|
|
7446
|
+
var SlowDown = class extends Schema.TaggedError()("SlowDown", { nextStep: Schema.String }, { httpApiStatus: 429 }) {};
|
|
7447
|
+
/** A command's hash: the SHA-256 of its canonical JSON, as lowercase hex. */
|
|
7448
|
+
const CommandHash = Schema.String.check(Schema.isPattern(/^[0-9a-f]{64}$/));
|
|
7449
|
+
Schema.Struct({
|
|
7450
|
+
approval: ApprovalId,
|
|
7451
|
+
endpoint: CommandEndpoint,
|
|
7452
|
+
/** The command's canonical JSON. */
|
|
7453
|
+
command: Schema.String,
|
|
7454
|
+
commandHash: CommandHash,
|
|
7455
|
+
asker: ApprovalAsker,
|
|
7456
|
+
/** The browser session the person answered in, for their log. */
|
|
7457
|
+
credential: CredentialRef,
|
|
7458
|
+
generation: Generation,
|
|
7459
|
+
/** When the request expires, in epoch milliseconds. */
|
|
7460
|
+
deadline: Schema.Int,
|
|
7461
|
+
challenge: Schema.String,
|
|
7462
|
+
/** A machine answer retries only from its answering session, with this key. */
|
|
7463
|
+
answerKey: Schema.optionalKey(RequestId),
|
|
7464
|
+
/** An agent's ask: the credential it asked with, checked again before the answer counts. */
|
|
7465
|
+
asking: Schema.optionalKey(TokenId),
|
|
7466
|
+
/** The kind of action the person, answering, let agents take without asking from now on. */
|
|
7467
|
+
allow: Schema.optionalKey(ScopeAction)
|
|
7468
|
+
});
|
|
7469
|
+
Schema.Struct({
|
|
7470
|
+
approval: ApprovalId,
|
|
7471
|
+
action: ScopeAction,
|
|
7472
|
+
credential: CredentialRef,
|
|
7473
|
+
generation: Generation
|
|
7474
|
+
});
|
|
7475
|
+
Schema.Union([Schema.Struct({ denied: Schema.Literal(true) }), Schema.Struct({
|
|
7476
|
+
denied: Schema.Literal(false),
|
|
7477
|
+
reason: Schema.Literals(["generation moved", "not set up"])
|
|
7478
|
+
})]);
|
|
7479
|
+
Schema.Union([Schema.Struct({
|
|
7480
|
+
admitted: Schema.Literal(true),
|
|
7481
|
+
admittedAt: Schema.Int,
|
|
7482
|
+
answerCode: Schema.optionalKey(ApprovalCode)
|
|
7483
|
+
}), Schema.Struct({
|
|
7484
|
+
admitted: Schema.Literal(false),
|
|
7485
|
+
reason: Schema.Literals([
|
|
7486
|
+
"expired",
|
|
7487
|
+
"generation moved",
|
|
7488
|
+
"challenge used",
|
|
7489
|
+
"not set up",
|
|
7490
|
+
"asker gone"
|
|
7491
|
+
])
|
|
7492
|
+
})]);
|
|
7493
|
+
Schema.Union([Schema.Struct({ found: Schema.Literal(false) }), Schema.Struct({
|
|
7494
|
+
found: Schema.Literal(true),
|
|
7495
|
+
endpoint: CommandEndpoint,
|
|
7496
|
+
command: Schema.String,
|
|
7497
|
+
commandHash: CommandHash,
|
|
7498
|
+
generation: Generation,
|
|
7499
|
+
generationNow: Generation,
|
|
7500
|
+
/** The `approval.answered` its admission recorded in the person's log. */
|
|
7501
|
+
answeredEvent: EventId,
|
|
7502
|
+
/** Who asked, as that `approval.answered` says. */
|
|
7503
|
+
asker: ApprovalAsker,
|
|
7504
|
+
/** What running it came to, once it ran, as `KeptRan` keeps it. */
|
|
7505
|
+
result: Schema.optionalKey(Schema.String)
|
|
7506
|
+
})]);
|
|
7507
|
+
/**
|
|
7508
|
+
* A `Ran` as it's kept, on its receipt and on its approval: its JSON, as text. The Owner DO
|
|
7509
|
+
* keeps it as it comes and runs no endpoint's schemas, so it's read only by its endpoint, with
|
|
7510
|
+
* `keptRan`, which refuses one they don't take.
|
|
7511
|
+
*/
|
|
7512
|
+
const KeptRan = Schema.fromJsonString(Ran);
|
|
7513
|
+
Schema.Struct({
|
|
7514
|
+
approval: ApprovalId,
|
|
7515
|
+
result: Schema.String
|
|
7516
|
+
});
|
|
7517
|
+
Schema.Struct({ result: Schema.String });
|
|
7518
|
+
Schema.Struct({ approval: ApprovalId });
|
|
7519
|
+
Schema.Struct({ approval: ApprovalId });
|
|
7520
|
+
Schema.Union([Schema.Struct({ expired: Schema.Literal(true) }), Schema.Struct({ expired: Schema.Literal(false) })]);
|
|
7521
|
+
Schema.Struct({
|
|
7522
|
+
person: PersonId,
|
|
7523
|
+
approval: ApprovalId
|
|
7524
|
+
});
|
|
7525
|
+
//#endregion
|
|
7526
|
+
//#region ../contracts/src/browser-sessions.ts
|
|
7527
|
+
/**
|
|
7528
|
+
* A browser session's cookie: Better Auth's, named for ref0, `Secure` and host-only on the
|
|
7529
|
+
* app's one origin. It holds the session's token, signed; the directory and the person's Owner DO hold
|
|
7530
|
+
* only the token's SHA-256.
|
|
7531
|
+
*/
|
|
7532
|
+
const browserSessionCookie = "__Secure-ref0.session_token";
|
|
7533
|
+
Schema.Struct({
|
|
7534
|
+
token: TokenId,
|
|
7535
|
+
secretHash: SecretHash,
|
|
7536
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
7537
|
+
});
|
|
7538
|
+
Schema.Struct({ registered: Schema.Boolean });
|
|
7539
|
+
/**
|
|
7540
|
+
* Where a signed-in person stands: naming, when the name their GitHub login gave was taken or
|
|
7541
|
+
* kept back, with that login as a suggestion; setting up, once their owner is made and before
|
|
7542
|
+
* it's ready with this session registered, which naming again finishes; or ready.
|
|
7543
|
+
*/
|
|
7544
|
+
const Welcome = Schema.Union([Schema.Struct({
|
|
7545
|
+
state: Schema.Literals(["ready", "setting up"]),
|
|
7546
|
+
person: PersonId,
|
|
7547
|
+
owner: OwnerId,
|
|
7548
|
+
name: OwnerName
|
|
7549
|
+
}), Schema.Struct({
|
|
7550
|
+
state: Schema.Literal("naming"),
|
|
7551
|
+
person: PersonId,
|
|
7552
|
+
suggestion: Schema.optionalKey(Schema.String)
|
|
7553
|
+
})]);
|
|
7554
|
+
/**
|
|
7555
|
+
* A browser session renewed: its expiry, in its person's Owner DO and in the directory, and on its cookie.
|
|
7556
|
+
* Renewed at most once a day, so one asked sooner answers its expiry unchanged.
|
|
7557
|
+
*/
|
|
7558
|
+
const SessionRenewed = Schema.Struct({ expiresAt: Schema.DateTimeUtcFromMillis });
|
|
7559
|
+
/**
|
|
7560
|
+
* What starting a GitHub sign-in at `POST /auth/sign-in/social` answers: GitHub's address to go
|
|
7561
|
+
* to, with the sign-in's state in a cookie set alongside. Better Auth's answer, which ref0 serves.
|
|
7562
|
+
*/
|
|
7563
|
+
const NavigationUrl = Schema.String.check(Schema.isPattern(/^https?:\/\/[^\s/]+(?:\/[^\s]*)?$/), Schema.makeFilter((url) => URL.canParse(url) || "an HTTP(S) URL the platform can parse")).pipe(Schema.brand("NavigationUrl"));
|
|
7564
|
+
Schema.Struct({ url: NavigationUrl });
|
|
7565
|
+
/**
|
|
7566
|
+
* Where signing in sends the browser back to, as `POST /auth/sign-in/social` takes it: a path on
|
|
7567
|
+
* ref0's own origin, such as `/welcome`, or a whole URL. Never a socket's.
|
|
7568
|
+
*/
|
|
7569
|
+
const SignInDestination = Schema.String.pipe(Schema.brand("SignInDestination"));
|
|
7570
|
+
Schema.Struct({
|
|
7571
|
+
provider: Schema.Literal("github"),
|
|
7572
|
+
callbackURL: SignInDestination,
|
|
7573
|
+
errorCallbackURL: SignInDestination
|
|
7574
|
+
});
|
|
7575
|
+
/** The name a naming person chose for their owner. */
|
|
7576
|
+
const NameOwner = Schema.Struct({ name: OwnerName });
|
|
7577
|
+
//#endregion
|
|
7544
7578
|
//#region ../contracts/src/ask-calls.ts
|
|
7545
7579
|
/**
|
|
7546
7580
|
* Asks: a question that waits on an answer, from an agent's session or a person, on a thread,
|
|
@@ -11659,6 +11693,151 @@ Schema.Struct({
|
|
|
11659
11693
|
name: RepoName
|
|
11660
11694
|
}))
|
|
11661
11695
|
});
|
|
11696
|
+
/** Where an agent's session asks for an identity token. */
|
|
11697
|
+
const identityTokenPath = "/v1/me/identity-token";
|
|
11698
|
+
/** One DNS label, or one part of an IPv4 address: letters, digits and inner hyphens. */
|
|
11699
|
+
const label = "[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?";
|
|
11700
|
+
/** `https://`, a host name, then maybe `:` and a port with no leading zero. */
|
|
11701
|
+
const audiencePattern = new RegExp(`^https://(${label}(?:\\.${label})*)(?::([1-9][0-9]{0,4}))?$`);
|
|
11702
|
+
/**
|
|
11703
|
+
* Whether the platform's URL parser reads the audience as written: an `https` URL whose host is
|
|
11704
|
+
* the audience's own, lowercased, and whose port is its own, or none for 443. That refuses a host
|
|
11705
|
+
* the parser rewrites, such as `https://1.2.3` read as `1.2.0.3`, and a port past 65535.
|
|
11706
|
+
*/
|
|
11707
|
+
const parsesAsWritten = (audience) => {
|
|
11708
|
+
const parts = audiencePattern.exec(audience);
|
|
11709
|
+
if (parts === null || !URL.canParse(audience)) return false;
|
|
11710
|
+
const url = new URL(audience);
|
|
11711
|
+
const port = parts[2] ?? "";
|
|
11712
|
+
return url.protocol === "https:" && url.hostname === (parts[1] ?? "").toLowerCase() && url.port === (port === "443" ? "" : port);
|
|
11713
|
+
};
|
|
11714
|
+
/**
|
|
11715
|
+
* Who an identity token is for: an outside service's HTTPS origin, in ASCII, at most 255 bytes,
|
|
11716
|
+
* with no credentials, path, query, fragment, wildcard or space. It's kept exactly as asked, its
|
|
11717
|
+
* case and any port too, so `https://verifier.example:443` is another audience than
|
|
11718
|
+
* `https://verifier.example`. ref0 never fetches or resolves it.
|
|
11719
|
+
*/
|
|
11720
|
+
const IdentityAudience = Schema.String.check(Schema.isMaxLength(255), Schema.isPattern(audiencePattern), Schema.makeFilter((audience) => parsesAsWritten(audience) || "an origin the platform reads as written")).pipe(Schema.brand("IdentityAudience"));
|
|
11721
|
+
/** `POST /v1/me/identity-token`'s body: the one audience, and nothing else. */
|
|
11722
|
+
const IdentityTokenRequest = Schema.Struct({ audience: IdentityAudience });
|
|
11723
|
+
/** A compact JWT: its header, payload and signature, each in base64url, at most 4 KiB. */
|
|
11724
|
+
const IdentityToken = Schema.String.check(Schema.isMaxLength(4096), Schema.isPattern(/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/)).pipe(Schema.brand("IdentityToken"));
|
|
11725
|
+
/** What asking for an identity token answers: the token, and when it expires, its `exp`. */
|
|
11726
|
+
const IssuedIdentityToken = Schema.Struct({
|
|
11727
|
+
token: IdentityToken,
|
|
11728
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
11729
|
+
});
|
|
11730
|
+
/**
|
|
11731
|
+
* An identity token's `jti`: a fresh UUIDv4 from the platform's cryptographic source for each
|
|
11732
|
+
* token, naming its issuance in ref0's log without naming any row or secret.
|
|
11733
|
+
*/
|
|
11734
|
+
const IdentityTokenId = Schema.String.check(Schema.isPattern(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/)).pipe(Schema.brand("IdentityTokenId"));
|
|
11735
|
+
/**
|
|
11736
|
+
* A person, as one audience knows them: `pperson_`, then the first 128 bits of an HMAC of the
|
|
11737
|
+
* person's ref0 ID for the token's issuer and audience, as a TypeID suffix. Stable at one
|
|
11738
|
+
* audience, different at every other, and carrying no time.
|
|
11739
|
+
*/
|
|
11740
|
+
const PairwisePersonId = TypeId("pperson").pipe(Schema.brand("PairwisePersonId"));
|
|
11741
|
+
/** A session, as one audience knows it, made as a person's is. */
|
|
11742
|
+
const PairwiseSessionId = TypeId("psession").pipe(Schema.brand("PairwiseSessionId"));
|
|
11743
|
+
/** The accountable person, last in a token's chain: only their pairwise ID. */
|
|
11744
|
+
const PersonActor = Schema.Struct({ sub: PairwisePersonId });
|
|
11745
|
+
/** A session in a token's chain, then whoever it acts for: its parent session, or the person. */
|
|
11746
|
+
const sessionActing = (act) => Schema.Struct({
|
|
11747
|
+
sub: PairwiseSessionId,
|
|
11748
|
+
act
|
|
11749
|
+
});
|
|
11750
|
+
const oneSession = sessionActing(PersonActor);
|
|
11751
|
+
const twoSessions = sessionActing(oneSession);
|
|
11752
|
+
const threeSessions = sessionActing(twoSessions);
|
|
11753
|
+
/**
|
|
11754
|
+
* A token's `act`, as RFC 8693 nests it: the current session, then up to three parent sessions,
|
|
11755
|
+
* then the accountable person, each by its pairwise ID. It's history, not authority: a verifier
|
|
11756
|
+
* allows the agent in `sub`, never an actor.
|
|
11757
|
+
*/
|
|
11758
|
+
const IdentityActor = Schema.Union([
|
|
11759
|
+
oneSession,
|
|
11760
|
+
twoSessions,
|
|
11761
|
+
threeSessions,
|
|
11762
|
+
sessionActing(threeSessions)
|
|
11763
|
+
]);
|
|
11764
|
+
/** A time in a token, in whole UTC epoch seconds. */
|
|
11765
|
+
const EpochSeconds = Schema.Int.check(Schema.isGreaterThan(0));
|
|
11766
|
+
Schema.Struct({
|
|
11767
|
+
iss: ApiOrigin,
|
|
11768
|
+
sub: AgentId,
|
|
11769
|
+
aud: IdentityAudience,
|
|
11770
|
+
iat: EpochSeconds,
|
|
11771
|
+
exp: EpochSeconds,
|
|
11772
|
+
jti: IdentityTokenId,
|
|
11773
|
+
act: IdentityActor
|
|
11774
|
+
}).check(Schema.makeFilter(({ iat, exp }) => exp > iat && exp - iat <= 600 || `an expiry after its issue, at most 600 seconds later`));
|
|
11775
|
+
/**
|
|
11776
|
+
* A signing key's ID, its `kid`: the key's RFC 7638 thumbprint, SHA-256 in base64url, so one ID
|
|
11777
|
+
* never names two keys.
|
|
11778
|
+
*/
|
|
11779
|
+
const SigningKeyId = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{43}$/)).pipe(Schema.brand("SigningKeyId"));
|
|
11780
|
+
Schema.Struct({
|
|
11781
|
+
alg: Schema.Literal("ES256"),
|
|
11782
|
+
kid: SigningKeyId,
|
|
11783
|
+
typ: Schema.Literal("JWT")
|
|
11784
|
+
});
|
|
11785
|
+
/** A P-256 coordinate: 32 bytes in base64url, unpadded, in its one canonical spelling. */
|
|
11786
|
+
const Coordinate = Schema.String.check(Schema.isPattern(/^[A-Za-z0-9_-]{42}[AEIMQUYcgkosw048]$/));
|
|
11787
|
+
/** One public key in the JWKS: exactly these fields, and never a private one. */
|
|
11788
|
+
const PublicSigningKey = Schema.Struct({
|
|
11789
|
+
kty: Schema.Literal("EC"),
|
|
11790
|
+
crv: Schema.Literal("P-256"),
|
|
11791
|
+
x: Coordinate,
|
|
11792
|
+
y: Coordinate,
|
|
11793
|
+
use: Schema.Literal("sig"),
|
|
11794
|
+
alg: Schema.Literal("ES256"),
|
|
11795
|
+
kid: SigningKeyId
|
|
11796
|
+
});
|
|
11797
|
+
Schema.Struct({ keys: Schema.Array(PublicSigningKey).check(Schema.isMaxLength(3)) });
|
|
11798
|
+
Schema.Struct({
|
|
11799
|
+
issuer: ApiOrigin,
|
|
11800
|
+
jwks_uri: Schema.String,
|
|
11801
|
+
subject_types_supported: Schema.Tuple([Schema.Literal("public")]),
|
|
11802
|
+
id_token_signing_alg_values_supported: Schema.Tuple([Schema.Literal("ES256")]),
|
|
11803
|
+
claims_supported: Schema.Tuple([
|
|
11804
|
+
Schema.Literal("iss"),
|
|
11805
|
+
Schema.Literal("sub"),
|
|
11806
|
+
Schema.Literal("aud"),
|
|
11807
|
+
Schema.Literal("exp"),
|
|
11808
|
+
Schema.Literal("iat"),
|
|
11809
|
+
Schema.Literal("jti"),
|
|
11810
|
+
Schema.Literal("act")
|
|
11811
|
+
])
|
|
11812
|
+
});
|
|
11813
|
+
/**
|
|
11814
|
+
* The audience isn't one ref0 signs for: missing, given twice, or not an HTTPS origin as
|
|
11815
|
+
* `IdentityAudience` says. The refused text is never echoed.
|
|
11816
|
+
*/
|
|
11817
|
+
var IdentityAudienceRefused = class extends Schema.TaggedError()("IdentityAudienceRefused", { nextStep: Schema.String }, { httpApiStatus: 400 }) {};
|
|
11818
|
+
/**
|
|
11819
|
+
* Less than a whole second is left before the session, a parent or the credential expires, or
|
|
11820
|
+
* the token would expire before it's recorded: the next prompt renews or replaces the session.
|
|
11821
|
+
*/
|
|
11822
|
+
var IdentityTokenNotReady = class extends Schema.TaggedError()("IdentityTokenNotReady", { nextStep: Schema.String }, { httpApiStatus: 409 }) {};
|
|
11823
|
+
/**
|
|
11824
|
+
* ref0 signed an identity token for one of a project's sessions: in that project's log, on the
|
|
11825
|
+
* session's thread, with ref0's own IDs for the session, its agent and its accountable person,
|
|
11826
|
+
* and the token's `jti`, never the token. It proves ref0 made the token, not that any service
|
|
11827
|
+
* received or used it. A failed append discards the token, so every token has its event.
|
|
11828
|
+
*/
|
|
11829
|
+
const IdentityTokenIssued = eventKind("identity_token.issued", Schema.Struct({
|
|
11830
|
+
session: SessionId,
|
|
11831
|
+
agent: AgentRef,
|
|
11832
|
+
/** The person whose attachment authorized the session's chain, last in the token's `act`. */
|
|
11833
|
+
accountable: PersonId,
|
|
11834
|
+
jti: IdentityTokenId,
|
|
11835
|
+
audience: IdentityAudience,
|
|
11836
|
+
issuer: ApiOrigin,
|
|
11837
|
+
keyId: SigningKeyId,
|
|
11838
|
+
issuedAt: Schema.DateTimeUtcFromMillis,
|
|
11839
|
+
expiresAt: Schema.DateTimeUtcFromMillis
|
|
11840
|
+
}));
|
|
11662
11841
|
Schema.Literals([
|
|
11663
11842
|
"info/refs",
|
|
11664
11843
|
"git-upload-pack",
|
|
@@ -11911,6 +12090,71 @@ const GateAddition = Schema.Struct({
|
|
|
11911
12090
|
from: NeedFrom
|
|
11912
12091
|
});
|
|
11913
12092
|
/**
|
|
12093
|
+
* A yes that met a need: its decider's, given by the person `by` when one gave it, such as an
|
|
12094
|
+
* admin answering as the owner's admins; or, `overridden`, a check ref0 said no to, met by the
|
|
12095
|
+
* yes of each of its overrides.
|
|
12096
|
+
*/
|
|
12097
|
+
const NeedYes = Schema.Struct({
|
|
12098
|
+
decider: DeciderRef,
|
|
12099
|
+
by: Schema.optionalKey(PersonId),
|
|
12100
|
+
overridden: Schema.optionalKey(Schema.Literal(true))
|
|
12101
|
+
});
|
|
12102
|
+
/**
|
|
12103
|
+
* An override left of a check ref0 said no to: of a part's failed run, or of the check not running,
|
|
12104
|
+
* and who answers it.
|
|
12105
|
+
*/
|
|
12106
|
+
const OverrideLeft = Schema.Struct({
|
|
12107
|
+
of: Schema.Literals(["failed run", "not run"]),
|
|
12108
|
+
by: OverrideAnswerer
|
|
12109
|
+
});
|
|
12110
|
+
/**
|
|
12111
|
+
* A decider a need waits on. For a check ref0 said no to, `overrides` lists each of its overrides
|
|
12112
|
+
* left, every one of whose yeses meets it in a pass's place, none when it has none: one yes to a
|
|
12113
|
+
* gate answers every decider its person may answer as, each override among them.
|
|
12114
|
+
*/
|
|
12115
|
+
const NeedWait = Schema.Struct({
|
|
12116
|
+
decider: DeciderRef,
|
|
12117
|
+
overrides: Schema.optionalKey(Schema.Array(OverrideLeft).check(Schema.isMaxLength(maxGateDeciders)))
|
|
12118
|
+
});
|
|
12119
|
+
/**
|
|
12120
|
+
* How one of a version's needs stands under its gate's answers now, as the gate is decided from
|
|
12121
|
+
* them: `met` by each yes in `by`; `met` with nothing to wait for, `as` `no checks`, for `checks`
|
|
12122
|
+
* when none of the repo's checks runs on the change, or `on the tip`, for a check only the merge
|
|
12123
|
+
* queue's tip runs, which judges it before `main` moves; `waiting` on each decider in `on`, when
|
|
12124
|
+
* `all`, or else on a yes from any one of them; `waiting` for ref0 to read which of `main`'s
|
|
12125
|
+
* checks run on the change; or `refused` by a person's no to one of its deciders, or to an
|
|
12126
|
+
* override of one, which decides the whole gate no.
|
|
12127
|
+
*/
|
|
12128
|
+
const NeedStanding = Schema.Union([
|
|
12129
|
+
Schema.Struct({
|
|
12130
|
+
state: Schema.Literal("met"),
|
|
12131
|
+
by: Schema.Array(NeedYes).check(Schema.isMaxLength(maxGateDeciders))
|
|
12132
|
+
}),
|
|
12133
|
+
Schema.Struct({
|
|
12134
|
+
state: Schema.Literal("met"),
|
|
12135
|
+
as: Schema.Literals(["no checks", "on the tip"])
|
|
12136
|
+
}),
|
|
12137
|
+
Schema.Struct({
|
|
12138
|
+
state: Schema.Literal("waiting"),
|
|
12139
|
+
on: Schema.Array(NeedWait).check(Schema.isMaxLength(maxGateDeciders)),
|
|
12140
|
+
all: Schema.Boolean
|
|
12141
|
+
}),
|
|
12142
|
+
Schema.Struct({
|
|
12143
|
+
state: Schema.Literal("waiting"),
|
|
12144
|
+
for: Schema.Literal("checks read")
|
|
12145
|
+
}),
|
|
12146
|
+
Schema.Struct({
|
|
12147
|
+
state: Schema.Literal("refused"),
|
|
12148
|
+
decider: DeciderRef,
|
|
12149
|
+
by: PersonId
|
|
12150
|
+
})
|
|
12151
|
+
]);
|
|
12152
|
+
/** A version's need, with every place it came from, and how it stands once its gate is open. */
|
|
12153
|
+
const GateNeedView = Schema.Struct({
|
|
12154
|
+
...GateNeed.fields,
|
|
12155
|
+
standing: Schema.optionalKey(NeedStanding)
|
|
12156
|
+
});
|
|
12157
|
+
/**
|
|
11914
12158
|
* The version's gate decided again in place under `main`'s `.ref0/` now (Q7c): on which `main`
|
|
11915
12159
|
* and under its tree, `pending` until `main`'s gate answers, then `held` or `grew`; the needs
|
|
11916
12160
|
* it answered, joined to ref0's floor, absent while pending; what `main`'s gate read of the
|
|
@@ -12012,8 +12256,21 @@ const GateView = Schema.Struct({
|
|
|
12012
12256
|
state: GateState,
|
|
12013
12257
|
/** Why the gate failed; null otherwise. */
|
|
12014
12258
|
why: Schema.NullOr(Schema.String),
|
|
12015
|
-
/**
|
|
12016
|
-
|
|
12259
|
+
/**
|
|
12260
|
+
* None while pending; once answered, the floor's too, sorted as they're spelled, each with how
|
|
12261
|
+
* it stands once the version's gate is open.
|
|
12262
|
+
*/
|
|
12263
|
+
needs: Schema.Array(GateNeedView),
|
|
12264
|
+
/** The repo's owner, whose admins answer its admins' needs: at home, its person alone. */
|
|
12265
|
+
owner: OwnerId,
|
|
12266
|
+
/**
|
|
12267
|
+
* The names of the owner and of the people its needs' standings name, where the directory has
|
|
12268
|
+
* them.
|
|
12269
|
+
*/
|
|
12270
|
+
names: Schema.optionalKey(Schema.Array(Schema.Struct({
|
|
12271
|
+
owner: OwnerId,
|
|
12272
|
+
name: OwnerName
|
|
12273
|
+
})).check(Schema.isMaxLength(maxGateDeciders + 1))),
|
|
12017
12274
|
/** What the gate read of the change's trees, each read once, in order; none while pending. */
|
|
12018
12275
|
reads: GateReads,
|
|
12019
12276
|
/** The project gate revision the version was decided under; none for 0. */
|
|
@@ -12984,7 +13241,7 @@ Schema.Struct({
|
|
|
12984
13241
|
actor: ActorRef,
|
|
12985
13242
|
/** The attachment an agent's credential was made under, which an organization asks after; null for a person. */
|
|
12986
13243
|
attachment: Schema.NullOr(CredentialRef),
|
|
12987
|
-
scopes: Schema.Array(Scope$
|
|
13244
|
+
scopes: Schema.Array(Scope$2),
|
|
12988
13245
|
ownerName: OwnerName,
|
|
12989
13246
|
number: ThreadNumber
|
|
12990
13247
|
});
|
|
@@ -14133,6 +14390,7 @@ const KnownEvent = Schema.Union([
|
|
|
14133
14390
|
ThreadListingChanged,
|
|
14134
14391
|
SessionStarted,
|
|
14135
14392
|
SessionEnded,
|
|
14393
|
+
IdentityTokenIssued,
|
|
14136
14394
|
MemberRoleChanged,
|
|
14137
14395
|
MemberRemoved,
|
|
14138
14396
|
InvitationSent,
|
|
@@ -14994,6 +15252,11 @@ var ApprovalSecret = class extends HttpApiMiddleware.Service()("ref0/contracts/A
|
|
|
14994
15252
|
* what to change. Any other part that fails its contract is answered with Effect's empty 400.
|
|
14995
15253
|
*/
|
|
14996
15254
|
var MachineNameRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/MachineNameRefusal", { error: CommandRefused }) {};
|
|
15255
|
+
/**
|
|
15256
|
+
* An identity token's request whose audience its contract refuses, missing or not an HTTPS
|
|
15257
|
+
* origin, answered as `IdentityAudienceRefused`, which never echoes what was sent.
|
|
15258
|
+
*/
|
|
15259
|
+
var IdentityAudienceRefusal = class extends HttpApiMiddleware.Service()("ref0/contracts/IdentityAudienceRefusal", { error: IdentityAudienceRefused }) {};
|
|
14997
15260
|
const idempotency = { "idempotency-key": RequestId };
|
|
14998
15261
|
const inProject$1 = {
|
|
14999
15262
|
owner: OwnerRef,
|
|
@@ -15216,7 +15479,18 @@ var MeGroup = class extends HttpApiGroup.make("me").add(HttpApiEndpoint.get("get
|
|
|
15216
15479
|
UsageRangeRefused,
|
|
15217
15480
|
AuthorityUnavailable
|
|
15218
15481
|
]
|
|
15219
|
-
})
|
|
15482
|
+
}), HttpApiEndpoint.post("identityToken", identityTokenPath, {
|
|
15483
|
+
payload: IdentityTokenRequest,
|
|
15484
|
+
success: IssuedIdentityToken,
|
|
15485
|
+
error: [
|
|
15486
|
+
IdentityAudienceRefused,
|
|
15487
|
+
IdentityTokenNotReady,
|
|
15488
|
+
NotPermitted,
|
|
15489
|
+
SessionHasEnded,
|
|
15490
|
+
SlowDown,
|
|
15491
|
+
AuthorityUnavailable
|
|
15492
|
+
]
|
|
15493
|
+
}).middleware(IdentityAudienceRefusal)).middleware(Authorization) {};
|
|
15220
15494
|
/**
|
|
15221
15495
|
* A thread's endpoints, as one of a project's threads and an org thread take them alike, by
|
|
15222
15496
|
* where they are: `at` is a project's path parameters, or the owner's alone for its own scope.
|
|
@@ -16152,15 +16426,18 @@ const decisionEndpoints = (at) => {
|
|
|
16152
16426
|
HttpApiEndpoint.get("get", "/:decision", {
|
|
16153
16427
|
params: onDecision,
|
|
16154
16428
|
query: { wait: Schema.optionalKey(DecisionWait) },
|
|
16155
|
-
success:
|
|
16429
|
+
success: NamedDecision,
|
|
16156
16430
|
error: [...projectErrors, DecisionNotFound]
|
|
16157
16431
|
}),
|
|
16158
16432
|
HttpApiEndpoint.post("answer", "/:decision/answers", {
|
|
16159
16433
|
params: onDecision,
|
|
16160
16434
|
headers: idempotency,
|
|
16161
16435
|
payload: AnswerRequest,
|
|
16162
|
-
success:
|
|
16436
|
+
success: NamedDecision,
|
|
16163
16437
|
error: [
|
|
16438
|
+
ApprovalNeeded,
|
|
16439
|
+
CommandRefused,
|
|
16440
|
+
SlowDown,
|
|
16164
16441
|
...projectErrors,
|
|
16165
16442
|
DecisionNotFound,
|
|
16166
16443
|
AnswerRefused,
|
|
@@ -17007,6 +17284,15 @@ const ownSurfaces = {
|
|
|
17007
17284
|
cli: "meters",
|
|
17008
17285
|
mcp: null
|
|
17009
17286
|
},
|
|
17287
|
+
"me.identityToken": {
|
|
17288
|
+
action: "session:change",
|
|
17289
|
+
actors: "agent",
|
|
17290
|
+
target: "session",
|
|
17291
|
+
stepUp: "never",
|
|
17292
|
+
asks: "never",
|
|
17293
|
+
cli: "identity-token",
|
|
17294
|
+
mcp: null
|
|
17295
|
+
},
|
|
17010
17296
|
"threads.list": {
|
|
17011
17297
|
action: "project:read",
|
|
17012
17298
|
actors: "both",
|
|
@@ -18822,271 +19108,6 @@ const freshTokenSecret = Effect.suspend(() => hashed(encodeSecret(crypto.getRand
|
|
|
18822
19108
|
/** A link carries 128 random bits in its fragment; ref0 receives only their hash at start. */
|
|
18823
19109
|
const freshLinkSecret = Effect.suspend(() => hashed(Buffer.from(crypto.getRandomValues(/* @__PURE__ */ new Uint8Array(16))).toString("base64url")));
|
|
18824
19110
|
//#endregion
|
|
18825
|
-
//#region src/session.ts
|
|
18826
|
-
/**
|
|
18827
|
-
* An agent's command in a harness session that has no ref0 session, though an agent is connected
|
|
18828
|
-
* for its config folder: why, as far as ref0 can tell, and what to do. It never runs as its
|
|
18829
|
-
* person instead.
|
|
18830
|
-
*/
|
|
18831
|
-
var NoSession = class extends Schema.TaggedError()("NoSession", { nextStep: Schema.String }) {};
|
|
18832
|
-
/**
|
|
18833
|
-
* The token of the session this command runs in, if it runs in one: an agent's, which its
|
|
18834
|
-
* harness bound when the session started. Where it's found is the harness's to say. Outside a
|
|
18835
|
-
* session, there's none, and every command is the person's, asked by approval.
|
|
18836
|
-
*/
|
|
18837
|
-
var SessionToken = class SessionToken extends Context.Service()("ref0/cli/SessionToken") {
|
|
18838
|
-
/** No session: until a harness binds one, the person asks, by approval. */
|
|
18839
|
-
static none = Layer.succeed(SessionToken, SessionToken.of({ current: Effect.succeedNone }));
|
|
18840
|
-
};
|
|
18841
|
-
//#endregion
|
|
18842
|
-
//#region src/call.ts
|
|
18843
|
-
/**
|
|
18844
|
-
* Every command's one way to ref0: an endpoint's request, sent with the session's token when
|
|
18845
|
-
* the command runs in a session, or else asked of the person, by approval, from this terminal.
|
|
18846
|
-
* Either way the answer is the endpoint's own, as JSON.
|
|
18847
|
-
*/
|
|
18848
|
-
/** What ref0 said no to, or what went wrong on the way: its status, its kind, and what to do. */
|
|
18849
|
-
var CallFailed = class extends Schema.TaggedError()("CallFailed", {
|
|
18850
|
-
status: Schema.Int,
|
|
18851
|
-
kind: Schema.String,
|
|
18852
|
-
nextStep: Schema.String
|
|
18853
|
-
}) {};
|
|
18854
|
-
const decodeJson$1 = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
|
|
18855
|
-
const decodeSecretHash$2 = Schema.decodeUnknownEffect(SecretHash);
|
|
18856
|
-
const decodeNeeded = Schema.decodeUnknownOption(Schema.toCodecJson(ApprovalNeeded));
|
|
18857
|
-
const decodeRefusal = Schema.decodeUnknownOption(AnyError);
|
|
18858
|
-
/** How often a terminal asks whether its person has answered. */
|
|
18859
|
-
const pollMillis$1 = 2e3;
|
|
18860
|
-
/** How long one request may take before it's asked again, or the call gives up. */
|
|
18861
|
-
const requestTimeout$1 = Duration.seconds(15);
|
|
18862
|
-
/**
|
|
18863
|
-
* How long a session's request may take for the commands that read and compile before they
|
|
18864
|
-
* answer: starting check runs reads `.ref0/` from `main`, and from a thread's head when `main`
|
|
18865
|
-
* lacks the check, one paced read at a time, up to 256 files each.
|
|
18866
|
-
*/
|
|
18867
|
-
const timeoutOf = (endpoint) => endpoint === "runs.start" ? Duration.minutes(2) : requestTimeout$1;
|
|
18868
|
-
/** How long past the time to answer a terminal still asks, if ref0 hasn't said expired. */
|
|
18869
|
-
const giveUpAfterMillis$1 = 3e5;
|
|
18870
|
-
/** How long a terminal goes on asking while ref0 doesn't answer at all, before it says so. */
|
|
18871
|
-
const outageMillis = 3e5;
|
|
18872
|
-
/** What a change that failed partway says: ref0 may have made it all the same. */
|
|
18873
|
-
const mayHaveRun = "ref0 failed partway, so it may have run. Check before you run it again.";
|
|
18874
|
-
/**
|
|
18875
|
-
* An answer that isn't a success, as the command's failure. A change that ref0 failed on, a
|
|
18876
|
-
* 5xx, may have been made before it failed, so it isn't said to try again.
|
|
18877
|
-
*/
|
|
18878
|
-
const failedWith$15 = (status, body, change) => Option.match(decodeRefusal(body), {
|
|
18879
|
-
onNone: () => new CallFailed({
|
|
18880
|
-
status,
|
|
18881
|
-
kind: "Unexpected",
|
|
18882
|
-
nextStep: change && status >= 500 ? mayHaveRun : "ref0 answered oddly. Try again."
|
|
18883
|
-
}),
|
|
18884
|
-
onSome: (refusal) => new CallFailed({
|
|
18885
|
-
status,
|
|
18886
|
-
kind: refusal._tag,
|
|
18887
|
-
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
18888
|
-
})
|
|
18889
|
-
});
|
|
18890
|
-
/**
|
|
18891
|
-
* A ran command's refusal, read as its endpoint's own error, or ref0's in its place, as the
|
|
18892
|
-
* command's failure: a change that failed at a 5xx may have been made all the same.
|
|
18893
|
-
*/
|
|
18894
|
-
const refusedBy = (status, refusal, change) => new CallFailed({
|
|
18895
|
-
status,
|
|
18896
|
-
kind: refusal._tag,
|
|
18897
|
-
nextStep: change && status >= 500 ? mayHaveRun : refusal.nextStep
|
|
18898
|
-
});
|
|
18899
|
-
/**
|
|
18900
|
-
* An answer none of its endpoint's schemas reads: another endpoint's, or one a bug in ref0 or
|
|
18901
|
-
* the CLI made. A change it answered may have run.
|
|
18902
|
-
*/
|
|
18903
|
-
const unreadable = (endpoint, change) => new CallFailed({
|
|
18904
|
-
status: 0,
|
|
18905
|
-
kind: "Unexpected",
|
|
18906
|
-
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.`
|
|
18907
|
-
});
|
|
18908
|
-
const unreachable = new CallFailed({
|
|
18909
|
-
status: 0,
|
|
18910
|
-
kind: "Unreachable",
|
|
18911
|
-
nextStep: "Couldn’t reach ref0. Try again."
|
|
18912
|
-
});
|
|
18913
|
-
/**
|
|
18914
|
-
* An ask ref0 doesn't know: a result kept past its day, or an ask never kept. Either way a
|
|
18915
|
-
* change may have run, and asking again could make it twice.
|
|
18916
|
-
*/
|
|
18917
|
-
const gone = (change, nextStep) => new CallFailed({
|
|
18918
|
-
status: 404,
|
|
18919
|
-
kind: "ApprovalNotFound",
|
|
18920
|
-
nextStep: change ? "ref0 no longer knows how this ended, so it may have run. Check before you run it again." : nextStep
|
|
18921
|
-
});
|
|
18922
|
-
/** A change sent with no answer: ref0 may have made it, so running it again may make it twice. */
|
|
18923
|
-
const unanswered$1 = new CallFailed({
|
|
18924
|
-
status: 0,
|
|
18925
|
-
kind: "Unreachable",
|
|
18926
|
-
nextStep: "ref0 didn’t answer, so it may have run. Check before you run it again."
|
|
18927
|
-
});
|
|
18928
|
-
/**
|
|
18929
|
-
* Waits for the person's answer to an ask, a terminal's or an agent's, by collecting where it
|
|
18930
|
-
* stands until it ran, was refused or expired: ref0's state, or none when it didn't answer.
|
|
18931
|
-
* What it ran to is read as the endpoint it asked for: its answer, as JSON, or its refusal.
|
|
18932
|
-
*/
|
|
18933
|
-
const waitForAnswer = Effect.fnUntraced(function* (collect, expiresAt, change, endpoint) {
|
|
18934
|
-
const giveUpAt = DateTime.toEpochMillis(expiresAt) + giveUpAfterMillis$1;
|
|
18935
|
-
let approved = false;
|
|
18936
|
-
let heardAt = yield* Clock.currentTimeMillis;
|
|
18937
|
-
while (true) {
|
|
18938
|
-
const answered = yield* collect;
|
|
18939
|
-
const now = yield* Clock.currentTimeMillis;
|
|
18940
|
-
if (Option.isNone(answered)) {
|
|
18941
|
-
if (now - heardAt > outageMillis) return yield* new CallFailed({
|
|
18942
|
-
status: 0,
|
|
18943
|
-
kind: "Unreachable",
|
|
18944
|
-
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."
|
|
18945
|
-
});
|
|
18946
|
-
yield* Effect.sleep(pollMillis$1);
|
|
18947
|
-
continue;
|
|
18948
|
-
}
|
|
18949
|
-
const state = answered.value;
|
|
18950
|
-
heardAt = now;
|
|
18951
|
-
if (state.state === "ran") return yield* Option.match(ranOf(endpoint, state), {
|
|
18952
|
-
onNone: () => Effect.fail(unreadable(endpoint, change)),
|
|
18953
|
-
onSome: Result.match({
|
|
18954
|
-
onSuccess: () => Effect.succeed(state.result),
|
|
18955
|
-
onFailure: (refusal) => Effect.fail(refusedBy(state.status, refusal, change))
|
|
18956
|
-
})
|
|
18957
|
-
});
|
|
18958
|
-
if (state.state === "refused") return yield* new CallFailed({
|
|
18959
|
-
status: 403,
|
|
18960
|
-
kind: "Refused",
|
|
18961
|
-
nextStep: "It was refused."
|
|
18962
|
-
});
|
|
18963
|
-
if (state.state === "approved") {
|
|
18964
|
-
if (!approved) yield* Console.error("Approved. Waiting for it to run.");
|
|
18965
|
-
approved = true;
|
|
18966
|
-
yield* Effect.sleep(pollMillis$1);
|
|
18967
|
-
continue;
|
|
18968
|
-
}
|
|
18969
|
-
if (state.state === "expired" || !approved && now > giveUpAt) return yield* new CallFailed({
|
|
18970
|
-
status: 408,
|
|
18971
|
-
kind: "Expired",
|
|
18972
|
-
nextStep: "Nobody approved it in time. Run it again."
|
|
18973
|
-
});
|
|
18974
|
-
yield* Effect.sleep(pollMillis$1);
|
|
18975
|
-
}
|
|
18976
|
-
});
|
|
18977
|
-
var Ref0 = class Ref0 extends Context.Service()("ref0/cli/Ref0") {
|
|
18978
|
-
static layer = Layer.effect(Ref0, Effect.gen(function* () {
|
|
18979
|
-
const config = yield* Ref0Config;
|
|
18980
|
-
const session = yield* SessionToken;
|
|
18981
|
-
const http = yield* HttpClient.HttpClient;
|
|
18982
|
-
const asSession = Effect.fnUntraced(function* (token, request, endpoint) {
|
|
18983
|
-
const timeout = timeoutOf(request.command.endpoint);
|
|
18984
|
-
const base = HttpClientRequest.make(request.method)(`${config.api}${request.url}`).pipe(HttpClientRequest.bearerToken(token), HttpClientRequest.setHeader("idempotency-key", yield* newRequestId));
|
|
18985
|
-
const sent = request.body === void 0 ? base : HttpClientRequest.bodyText(base, request.body, "application/json");
|
|
18986
|
-
const change = request.method !== "GET";
|
|
18987
|
-
const lost = change ? unanswered$1 : unreachable;
|
|
18988
|
-
const { response, text } = yield* http.execute(sent).pipe(Effect.flatMap((response) => Effect.map(response.text, (text) => ({
|
|
18989
|
-
response,
|
|
18990
|
-
text
|
|
18991
|
-
}))), Effect.timeoutOrElse({
|
|
18992
|
-
duration: timeout,
|
|
18993
|
-
orElse: () => Effect.fail(lost)
|
|
18994
|
-
}), Effect.mapError(() => lost));
|
|
18995
|
-
const body = text.length === 0 ? null : yield* Effect.orElseSucceed(decodeJson$1(text), () => null);
|
|
18996
|
-
if (response.status === 202) {
|
|
18997
|
-
const needed = decodeNeeded(body);
|
|
18998
|
-
if (Option.isNone(needed)) return yield* failedWith$15(202, body, change);
|
|
18999
|
-
yield* Console.error(`Waiting for your person to approve it under Waiting on you, at ${config.api}${webPages.home}: ${needed.value.says}.`);
|
|
19000
|
-
return yield* waitForAnswer(yield* collectAsSession(token, needed.value.approval, change), needed.value.expiresAt, change, endpoint);
|
|
19001
|
-
}
|
|
19002
|
-
return response.status < 300 ? body : yield* failedWith$15(response.status, body, change);
|
|
19003
|
-
});
|
|
19004
|
-
const collectAsSession = Effect.fnUntraced(function* (token, approval, change) {
|
|
19005
|
-
const client = yield* ref0Client(config.api).pipe(Effect.provide(bearerCredential(token)), Effect.provideService(HttpClient.HttpClient, http));
|
|
19006
|
-
const refused = (status) => (error) => Effect.fail(new CallFailed({
|
|
19007
|
-
status,
|
|
19008
|
-
kind: error._tag,
|
|
19009
|
-
nextStep: error.nextStep
|
|
19010
|
-
}));
|
|
19011
|
-
return client.approvals.get({ params: { approval } }).pipe(Effect.map(Option.some), Effect.timeoutOrElse({
|
|
19012
|
-
duration: requestTimeout$1,
|
|
19013
|
-
orElse: () => Effect.succeedNone
|
|
19014
|
-
}), Effect.catchTags({
|
|
19015
|
-
ApprovalNotFound: (missing) => Effect.fail(gone(change, missing.nextStep)),
|
|
19016
|
-
Unauthenticated: refused(401),
|
|
19017
|
-
SessionHasEnded: refused(410)
|
|
19018
|
-
}), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19019
|
-
});
|
|
19020
|
-
const asPerson = Effect.fnUntraced(function* (command, change, endpoint) {
|
|
19021
|
-
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
19022
|
-
const { secret, hash } = yield* freshSecret;
|
|
19023
|
-
const secretHash = yield* Effect.orDie(decodeSecretHash$2(hash));
|
|
19024
|
-
const started = yield* client.terminalApprovals.start({
|
|
19025
|
-
headers: { "idempotency-key": yield* newRequestId },
|
|
19026
|
-
payload: {
|
|
19027
|
-
command,
|
|
19028
|
-
secretHash
|
|
19029
|
-
}
|
|
19030
|
-
}).pipe(Effect.timeoutOrElse({
|
|
19031
|
-
duration: requestTimeout$1,
|
|
19032
|
-
orElse: () => Effect.fail(unreachable)
|
|
19033
|
-
}), Effect.catchTags({
|
|
19034
|
-
CommandRefused: (refused) => Effect.fail(new CallFailed({
|
|
19035
|
-
status: 400,
|
|
19036
|
-
kind: refused._tag,
|
|
19037
|
-
nextStep: refused.nextStep
|
|
19038
|
-
})),
|
|
19039
|
-
SlowDown: (slow) => Effect.fail(new CallFailed({
|
|
19040
|
-
status: 429,
|
|
19041
|
-
kind: slow._tag,
|
|
19042
|
-
nextStep: slow.nextStep
|
|
19043
|
-
}))
|
|
19044
|
-
}), Effect.mapError((error) => error instanceof CallFailed ? error : unreachable));
|
|
19045
|
-
yield* Console.error(`Approve this under Waiting on you, at ${config.api}${webPages.home}, with the code ${started.code}`);
|
|
19046
|
-
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, secret)));
|
|
19047
|
-
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({
|
|
19048
|
-
duration: requestTimeout$1,
|
|
19049
|
-
orElse: () => Effect.succeedNone
|
|
19050
|
-
}), Effect.catchTag("ApprovalNotFound", (missing) => Effect.fail(gone(change, missing.nextStep))), Effect.catch((error) => error instanceof CallFailed ? Effect.fail(error) : Effect.succeedNone));
|
|
19051
|
-
return yield* waitForAnswer(collect, started.expiresAt, change, endpoint);
|
|
19052
|
-
});
|
|
19053
|
-
const call = Effect.fn("Ref0.call")(function* (command) {
|
|
19054
|
-
const token = yield* session.current.pipe(Effect.mapError((error) => new CallFailed({
|
|
19055
|
-
status: 0,
|
|
19056
|
-
kind: error._tag,
|
|
19057
|
-
nextStep: error.nextStep
|
|
19058
|
-
})));
|
|
19059
|
-
const request = yield* (Option.isSome(token) ? commandRequest(command) : approvalRequest(command)).pipe(Effect.mapError((refused) => new CallFailed({
|
|
19060
|
-
status: 400,
|
|
19061
|
-
kind: refused._tag,
|
|
19062
|
-
nextStep: refused.nextStep
|
|
19063
|
-
})));
|
|
19064
|
-
const change = request.method !== "GET";
|
|
19065
|
-
const answer = yield* Option.match(token, {
|
|
19066
|
-
onNone: () => asPerson(request.command, change, command.endpoint),
|
|
19067
|
-
onSome: (bearer) => asSession(bearer, request, command.endpoint)
|
|
19068
|
-
});
|
|
19069
|
-
if (!isSuccessOf(request.command.endpoint, answer)) return yield* unreadable(request.command.endpoint, change);
|
|
19070
|
-
return answer;
|
|
19071
|
-
});
|
|
19072
|
-
return Ref0.of({ call });
|
|
19073
|
-
}));
|
|
19074
|
-
};
|
|
19075
|
-
/**
|
|
19076
|
-
* One endpoint's call, typed from the API: the compiler checks its command, and its answer is
|
|
19077
|
-
* read by the endpoint's own success schemas, so what a command shows of it is checked too.
|
|
19078
|
-
*/
|
|
19079
|
-
const callEndpoint = (command) => Effect.gen(function* () {
|
|
19080
|
-
const json = yield* (yield* Ref0).call(command);
|
|
19081
|
-
return {
|
|
19082
|
-
json,
|
|
19083
|
-
value: yield* Option.match(successOf(command.endpoint, json), {
|
|
19084
|
-
onNone: () => Effect.fail(unreadable(command.endpoint, false)),
|
|
19085
|
-
onSome: Effect.succeed
|
|
19086
|
-
})
|
|
19087
|
-
};
|
|
19088
|
-
});
|
|
19089
|
-
//#endregion
|
|
19090
19111
|
//#region src/output.ts
|
|
19091
19112
|
/**
|
|
19092
19113
|
* What a command prints: words for a person, or, with `--json`, the endpoint's answer as JSON,
|
|
@@ -19112,12 +19133,88 @@ const renderLine = (line, terminal) => {
|
|
|
19112
19133
|
if (line.status === "unknown") return `? unknown ${printable(line.text)}`;
|
|
19113
19134
|
return `${line.status === "yes" ? styled("✓", 32, terminal) : styled("✗", 31, terminal)} ${line.status} ${printable(line.text)}`;
|
|
19114
19135
|
};
|
|
19115
|
-
|
|
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) => {
|
|
19116
19205
|
const terminal = process[stream].isTTY === true && process.env["NO_COLOR"] === void 0;
|
|
19117
|
-
|
|
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");
|
|
19118
19212
|
};
|
|
19119
|
-
/**
|
|
19120
|
-
|
|
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)));
|
|
19121
19218
|
/**
|
|
19122
19219
|
* A yes-or-no question as the terminal shows it, bold, then `(y/N)`: no unless the person says
|
|
19123
19220
|
* yes. The cursor stays after it, for their answer.
|
|
@@ -19187,6 +19284,279 @@ var Said = class extends Schema.TaggedError()("Said", {}) {
|
|
|
19187
19284
|
};
|
|
19188
19285
|
/** Says what went wrong, once, on stderr, and fails the command. */
|
|
19189
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
|
+
});
|
|
19190
19560
|
/** The account's commands. */
|
|
19191
19561
|
const accountCommands = [Command.make("whoami", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
19192
19562
|
const answer = yield* callEndpoint({ endpoint: "me.get" });
|
|
@@ -20130,7 +20500,7 @@ const waitsFor = (each) => {
|
|
|
20130
20500
|
* A thread a wait names, as a line says it: its number, written with its project, `acme/billing#3`,
|
|
20131
20501
|
* when it's in another of `owner`'s, then its title and stage.
|
|
20132
20502
|
*/
|
|
20133
|
-
const threadWords = (thread, owner = "") => {
|
|
20503
|
+
const threadWords$1 = (thread, owner = "") => {
|
|
20134
20504
|
return `${thread.project === void 0 ? `#${thread.number}` : `${owner}/${thread.project}#${thread.number}`}${thread.title === void 0 ? "" : ` ${thread.title}`}, ${thread.stage}`;
|
|
20135
20505
|
};
|
|
20136
20506
|
/** What holds a stack back below the thread, as a line says it, to follow "and". */
|
|
@@ -20148,13 +20518,13 @@ const stackHeld = (stack) => {
|
|
|
20148
20518
|
*/
|
|
20149
20519
|
const planLine = (wait, number, owner) => {
|
|
20150
20520
|
if (wait.kind === "stack") {
|
|
20151
|
-
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)}`;
|
|
20152
20522
|
return wait.on === void 0 ? line : `${wait.on.thread} ${line}`;
|
|
20153
20523
|
}
|
|
20154
20524
|
if (wait.child === void 0) return `required child ${wait.entry}, not started yet: #${number} is settled once it is`;
|
|
20155
20525
|
if (wait.child === null) return `required child, a thread you can’t see: #${number} is settled once it is`;
|
|
20156
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`;
|
|
20157
|
-
return `${wait.child.thread} required child ${threadWords(wait.child, owner)}: ${holds}`;
|
|
20527
|
+
return `${wait.child.thread} required child ${threadWords$1(wait.child, owner)}: ${holds}`;
|
|
20158
20528
|
};
|
|
20159
20529
|
/** What blocks the thread, in words: so many of its own, and its stack, while it holds it. */
|
|
20160
20530
|
const blockedBy$1 = (list) => {
|
|
@@ -20180,10 +20550,10 @@ const blockerLines = (list, address) => {
|
|
|
20180
20550
|
].join("\n");
|
|
20181
20551
|
};
|
|
20182
20552
|
/** How many blockers stand on the thread now, in words. */
|
|
20183
|
-
const standingWords = (changed) => changed.standing === 0 ? "None stands now, so ref0 merges it once it’s ready." : `${changed.standing} ${changed.standing === 1 ? "stands" : "stand"}, so ref0 won’t merge it yet.`;
|
|
20553
|
+
const standingWords$1 = (changed) => changed.standing === 0 ? "None stands now, so ref0 merges it once it’s ready." : `${changed.standing} ${changed.standing === 1 ? "stands" : "stand"}, so ref0 won’t merge it yet.`;
|
|
20184
20554
|
/** A blocker added, in words: which, its ask, how many stand, and a publish it didn't stop. */
|
|
20185
20555
|
const addedWords = (changed, address) => [
|
|
20186
|
-
`Blocked ${address} by ${changed.blocker}. ${standingWords(changed)}`,
|
|
20556
|
+
`Blocked ${address} by ${changed.blocker}. ${standingWords$1(changed)}`,
|
|
20187
20557
|
...changed.decision === void 0 ? [] : [`It waits on a person’s answer to the ask ${changed.decision}.`],
|
|
20188
20558
|
...changed.admitted === void 0 ? [] : [`A merge carrying it was already under way in ${changed.admitted.join(", ")}; the blocker doesn’t stop that one.`]
|
|
20189
20559
|
].join("\n");
|
|
@@ -20330,7 +20700,7 @@ const clearing = (name, description) => Command.make(name, {
|
|
|
20330
20700
|
payload
|
|
20331
20701
|
}));
|
|
20332
20702
|
const done = name === "remove" ? "Removed" : "Marked met";
|
|
20333
|
-
yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords(answer.value)}`);
|
|
20703
|
+
yield* printAnswer(answer.json, asJson, `${done} ${answer.value.blocker} on ${address}. ${standingWords$1(answer.value)}`);
|
|
20334
20704
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription(description));
|
|
20335
20705
|
/** `ref0 blocked` and its subcommands. */
|
|
20336
20706
|
const blockedCommands = [Command.make("blocked").pipe(Command.withDescription("What a thread is blocked by, which stops ref0 merging it: list, add, hold, remove, and mark met"), Command.withShortDescription("List and manage a thread’s blockers"), Command.withSubcommands([
|
|
@@ -20359,7 +20729,7 @@ const browserOpener = Effect.gen(function* () {
|
|
|
20359
20729
|
});
|
|
20360
20730
|
/** Starts the browser independently; a missing or failing desktop never stops the paste. */
|
|
20361
20731
|
const openBrowser = (program, link) => Effect.sync(() => {
|
|
20362
|
-
const failed = () => Effect.runFork(
|
|
20732
|
+
const failed = () => Effect.runFork(printBlocks([["Couldn’t open the browser. Open the printed link yourself."]]));
|
|
20363
20733
|
try {
|
|
20364
20734
|
const child = spawn(program, [link], {
|
|
20365
20735
|
detached: true,
|
|
@@ -21030,7 +21400,7 @@ const grantForAgent = Effect.fnUntraced(function* (owner, agent) {
|
|
|
21030
21400
|
* an update installs anew. Node, run from source, loads JSON only with the `type` attribute,
|
|
21031
21401
|
* and only as a default export.
|
|
21032
21402
|
*/
|
|
21033
|
-
const cliVersion = "0.
|
|
21403
|
+
const cliVersion = "0.12.0";
|
|
21034
21404
|
//#endregion
|
|
21035
21405
|
//#region src/harness/hook-protocol.ts
|
|
21036
21406
|
/**
|
|
@@ -21348,7 +21718,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21348
21718
|
const link = yield* freshLinkSecret;
|
|
21349
21719
|
const pasted = yield* Ref.make(void 0);
|
|
21350
21720
|
const opener = yield* browserOpener;
|
|
21351
|
-
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)));
|
|
21352
21722
|
const machineId = yield* machine.id.pipe(Effect.catchTag("MachineError", said));
|
|
21353
21723
|
const client = yield* ref0Client(config.api).pipe(Effect.provide(anonymousCredential), Effect.provideService(HttpClient.HttpClient, http));
|
|
21354
21724
|
const unreachable = () => sayFailed(`Couldn’t reach ref0 at ${config.api}. Try again.`);
|
|
@@ -21359,7 +21729,8 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21359
21729
|
...folder,
|
|
21360
21730
|
shown: shownConfigFolder(home, folder.configFolder),
|
|
21361
21731
|
grant: yield* freshTokenSecret,
|
|
21362
|
-
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()
|
|
21363
21734
|
};
|
|
21364
21735
|
}));
|
|
21365
21736
|
const folders = yield* Effect.forEach(asking, Effect.fnUntraced(function* (folder) {
|
|
@@ -21408,9 +21779,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21408
21779
|
"Approve it only if you ran this ref0 connect yourself, just now: a yes gives this machine a lasting credential for each agent it connects.",
|
|
21409
21780
|
"Waiting for the answer. Ctrl-C stops it."
|
|
21410
21781
|
]]);
|
|
21411
|
-
|
|
21412
|
-
const show = (text) => Effect.ignore(terminal.display(text));
|
|
21413
|
-
const endWait = yield* Effect.cached(Effect.suspend(() => typed === "" ? Effect.void : show("\n")).pipe(Effect.andThen(Console.error(""))));
|
|
21782
|
+
const endWait = yield* Effect.cached(Console.error(""));
|
|
21414
21783
|
const saidAfterWait = (refusal) => Effect.andThen(endWait, said(refusal));
|
|
21415
21784
|
if (opener !== void 0) yield* openBrowser(opener, `${config.api}/connect#${link.secret}`);
|
|
21416
21785
|
const withSecret = HttpApiMiddleware.layerClient(ApprovalSecret, ({ request, next }) => next(HttpClientRequest.bearerToken(request, collecting.secret)));
|
|
@@ -21441,17 +21810,10 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21441
21810
|
yield* Effect.sleep(pollMillis);
|
|
21442
21811
|
}
|
|
21443
21812
|
});
|
|
21444
|
-
const
|
|
21813
|
+
const keepTrying = Effect.fnUntraced(function* (configFolder, grant, keepBy, late) {
|
|
21445
21814
|
const left = keepBy - (yield* Clock.currentTimeMillis);
|
|
21446
|
-
if (left <= 0) return Option.some(
|
|
21447
|
-
const
|
|
21448
|
-
const kept = yield* hostGrant.keep(folder.configFolder, {
|
|
21449
|
-
harness: folder.harness,
|
|
21450
|
-
owner: connection.owner,
|
|
21451
|
-
agent: connection.handle,
|
|
21452
|
-
agentId: connection.agent.id,
|
|
21453
|
-
token: Redacted.make(token)
|
|
21454
|
-
}).pipe(Effect.timeoutOrElse({
|
|
21815
|
+
if (left <= 0) return Option.some(late);
|
|
21816
|
+
const kept = yield* hostGrant.keep(configFolder, grant).pipe(Effect.timeoutOrElse({
|
|
21455
21817
|
duration: Duration.min(keepTimeout, Duration.millis(left)),
|
|
21456
21818
|
orElse: () => Effect.fail(new KeychainError({ nextStep: "The keychain didn’t answer within a minute. Check that it’s unlocked." }))
|
|
21457
21819
|
}), Effect.retry({
|
|
@@ -21460,10 +21822,18 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21460
21822
|
}), Effect.result);
|
|
21461
21823
|
return Result.isSuccess(kept) ? Option.none() : Option.some(kept.failure.nextStep);
|
|
21462
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.");
|
|
21463
21832
|
const finish = Effect.fnUntraced(function* (state) {
|
|
21464
21833
|
if (state.state !== "connected") return yield* sayFailed(endedWords$1(state));
|
|
21465
21834
|
const keepBy = (yield* Clock.currentTimeMillis) + Duration.toMillis(keepBudget);
|
|
21466
21835
|
const kept = [];
|
|
21836
|
+
const keptFrom = [];
|
|
21467
21837
|
const grants = [];
|
|
21468
21838
|
const notKept = [];
|
|
21469
21839
|
for (const folder of asking) {
|
|
@@ -21493,9 +21863,9 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21493
21863
|
handle: connection.handle,
|
|
21494
21864
|
organizations: connection.organizations
|
|
21495
21865
|
});
|
|
21866
|
+
keptFrom.push(folder);
|
|
21496
21867
|
grants.push(connection.credential.id);
|
|
21497
21868
|
}
|
|
21498
|
-
yield* oldGrants.retire(kept);
|
|
21499
21869
|
const [first, ...rest] = grants;
|
|
21500
21870
|
if (first === void 0) return yield* sayFailed([[
|
|
21501
21871
|
"It connected, but this machine couldn’t keep what it needs to act:",
|
|
@@ -21528,9 +21898,27 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21528
21898
|
times: 2,
|
|
21529
21899
|
schedule: Schedule.spaced("1 second")
|
|
21530
21900
|
}), Effect.catchTag("Unanswered", unconfirmed))).confirmed) {
|
|
21531
|
-
|
|
21532
|
-
|
|
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
|
+
]]);
|
|
21533
21920
|
}
|
|
21921
|
+
yield* oldGrants.retire(kept);
|
|
21534
21922
|
return {
|
|
21535
21923
|
owner: state.owner,
|
|
21536
21924
|
kept,
|
|
@@ -21542,7 +21930,7 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21542
21930
|
};
|
|
21543
21931
|
});
|
|
21544
21932
|
const stopped = Effect.gen(function* () {
|
|
21545
|
-
yield*
|
|
21933
|
+
yield* printBlocks([["Stopping: aborting the request."]]);
|
|
21546
21934
|
const abortId = yield* newRequestId;
|
|
21547
21935
|
const state = yield* collector.machineAttachments.abort({
|
|
21548
21936
|
params,
|
|
@@ -21561,46 +21949,41 @@ const requestAttachment = Effect.fnUntraced(function* (asked) {
|
|
|
21561
21949
|
times: 2
|
|
21562
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.")));
|
|
21563
21951
|
if (!isEnded(state)) {
|
|
21564
|
-
if ((yield* Ref.get(pasted)) === void 0) return yield* sayFailed("It was already answered, but no code was pasted
|
|
21565
|
-
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."]]);
|
|
21566
21954
|
}
|
|
21567
21955
|
const connected = yield* finish(isEnded(state) ? state : yield* answered);
|
|
21568
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) }))]]);
|
|
21569
21957
|
});
|
|
21570
|
-
const
|
|
21958
|
+
const readAnswer = Effect.gen(function* () {
|
|
21959
|
+
if (listener === void 0) yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21571
21960
|
const keys = yield* terminal.readInput;
|
|
21961
|
+
let typed = "";
|
|
21572
21962
|
while (true) {
|
|
21573
21963
|
const pressed = yield* Effect.option(Queue.take(keys));
|
|
21574
21964
|
if (Option.isNone(pressed) || quits(pressed.value.key)) return yield* Effect.interrupt;
|
|
21965
|
+
if ((yield* Ref.get(pasted)) !== void 0) continue;
|
|
21575
21966
|
const { input, key } = pressed.value;
|
|
21576
21967
|
if (key.name === "backspace") {
|
|
21577
|
-
if (typed === "") continue;
|
|
21578
21968
|
typed = takeBack(typed);
|
|
21579
|
-
yield* show("\b \b");
|
|
21580
21969
|
continue;
|
|
21581
21970
|
}
|
|
21582
21971
|
if (key.name !== "return" && key.name !== "enter") {
|
|
21583
21972
|
const text = Option.getOrElse(input, () => "");
|
|
21584
|
-
if (
|
|
21585
|
-
typed += text;
|
|
21586
|
-
yield* show(text);
|
|
21973
|
+
if (typesText(text)) typed += text;
|
|
21587
21974
|
continue;
|
|
21588
21975
|
}
|
|
21589
21976
|
const line = typed.trim();
|
|
21590
|
-
if (typed !== "") yield* show("\n");
|
|
21591
21977
|
typed = "";
|
|
21592
21978
|
if (line === "") continue;
|
|
21593
|
-
if (isAnswerCode(line))
|
|
21594
|
-
|
|
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;
|
|
21595
21985
|
}
|
|
21596
21986
|
}).pipe(Effect.scoped);
|
|
21597
|
-
const readAnswer = Effect.gen(function* () {
|
|
21598
|
-
yield* printBlocks([[`Paste the code from your own page on ${config.api}.`]]);
|
|
21599
|
-
const code = yield* typedCode;
|
|
21600
|
-
yield* Ref.update(pasted, (first) => first ?? code);
|
|
21601
|
-
if (listener !== void 0) yield* listener.stop;
|
|
21602
|
-
return yield* Effect.never;
|
|
21603
|
-
});
|
|
21604
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)));
|
|
21605
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)));
|
|
21606
21989
|
}, Effect.scoped);
|
|
@@ -22580,6 +22963,111 @@ function parse(toml, options = {}) {
|
|
|
22580
22963
|
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
22581
22964
|
*/
|
|
22582
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
|
|
22583
23071
|
//#region src/harness/codex-checkouts.ts
|
|
22584
23072
|
const codexGitFolder = (configFolder, pluginId) => {
|
|
22585
23073
|
const at = pluginId.indexOf("@");
|
|
@@ -22696,111 +23184,6 @@ const codexSignOff = [
|
|
|
22696
23184
|
`perl -e ${shellWord(codexCheckoutWriter)} "$ref0_codex/checkouts" "$ref0_common" "$ref0_codex_hooks" 1 || exit 1`
|
|
22697
23185
|
];
|
|
22698
23186
|
//#endregion
|
|
22699
|
-
//#region src/harness/empty-folder.ts
|
|
22700
|
-
/**
|
|
22701
|
-
* The folder one of a harness's own commands runs in: made empty for it in the system's temporary
|
|
22702
|
-
* folder, and removed once the command's scope ends. ref0 never runs one in the folder the person
|
|
22703
|
-
* runs ref0 in, and names the config folder only by `CLAUDE_CONFIG_DIR` or `CODEX_HOME`.
|
|
22704
|
-
*
|
|
22705
|
-
* Measured 24 Oct 2026. Claude Code 2.1.310's `plugin marketplace remove` also clears the name
|
|
22706
|
-
* from `.claude/settings.json` and `.claude/settings.local.json` in the folder it runs in, and
|
|
22707
|
-
* `plugin list` reads them. In the home folder, the first is `~/.claude`'s own settings: removing
|
|
22708
|
-
* ref0's marketplace from `~/.claude-work` there turned ref0's plugin off in `~/.claude`. Codex
|
|
22709
|
-
* 0.164.0 reads a trusted project's `.codex/` in each folder from its own up to the Git root: from
|
|
22710
|
-
* a folder under a home kept in Git, such as `~/.ref0`, it reads `~/.codex` as that project's too.
|
|
22711
|
-
*/
|
|
22712
|
-
const emptyFolder = (fs) => fs.makeTempDirectoryScoped({ prefix: "ref0-harness-" });
|
|
22713
|
-
//#endregion
|
|
22714
|
-
//#region src/harness/codex-app-server.ts
|
|
22715
|
-
/**
|
|
22716
|
-
* Codex's app server, spoken as its TUI speaks it: `codex app-server` started for one exchange,
|
|
22717
|
-
* from an empty folder of its own, the requests written a line each, the answers read until each
|
|
22718
|
-
* request has one, then stopped. Each answer is matched to its request by ID, and read by that
|
|
22719
|
-
* request's method. Anything else it writes, such as notifications, or an answer to an ID ref0
|
|
22720
|
-
* didn't send, is passed over.
|
|
22721
|
-
*/
|
|
22722
|
-
/** Codex's app server didn't answer a request within 15 seconds, or didn't start. */
|
|
22723
|
-
var CodexUnanswered = class extends Schema.TaggedError()("CodexUnanswered", { method: Schema.String }) {};
|
|
22724
|
-
/** Codex's app server answered a request with an error. */
|
|
22725
|
-
var CodexRefused = class extends Schema.TaggedError()("CodexRefused", {
|
|
22726
|
-
method: Schema.String,
|
|
22727
|
-
error: CodexError
|
|
22728
|
-
}) {};
|
|
22729
|
-
/** Codex's app server answered a request with a result its method's schema doesn't read. */
|
|
22730
|
-
var CodexAnswerMismatch = class extends Schema.TaggedError()("CodexAnswerMismatch", {
|
|
22731
|
-
method: Schema.String,
|
|
22732
|
-
issue: Schema.String
|
|
22733
|
-
}) {};
|
|
22734
|
-
const encodeRequest = Schema.encodeUnknownEffect(Schema.fromJsonString(CodexRequest));
|
|
22735
|
-
const decodeAnswer$2 = Schema.decodeUnknownOption(Schema.fromJsonString(CodexAnswer));
|
|
22736
|
-
/** The IDs of the exchange's two requests: the opening's, and the one asked. */
|
|
22737
|
-
const initializeId = "ref0-initialize";
|
|
22738
|
-
const askedId = "ref0-asked";
|
|
22739
|
-
/** The exchange's opening: who's asking, then that it's ready. */
|
|
22740
|
-
const opening = [{
|
|
22741
|
-
jsonrpc: "2.0",
|
|
22742
|
-
id: initializeId,
|
|
22743
|
-
method: codexMethods.initialize.method,
|
|
22744
|
-
params: { clientInfo: {
|
|
22745
|
-
name: "ref0",
|
|
22746
|
-
title: null,
|
|
22747
|
-
version: cliVersion
|
|
22748
|
-
} }
|
|
22749
|
-
}, {
|
|
22750
|
-
jsonrpc: "2.0",
|
|
22751
|
-
method: codexMethods.initialized.method
|
|
22752
|
-
}];
|
|
22753
|
-
/**
|
|
22754
|
-
* Codex's answers to `lines`, by ID, or none if it didn't answer each of `ids`. Each ID's first
|
|
22755
|
-
* answer is its own: a repeat, or an answer to an ID ref0 didn't send, is passed over.
|
|
22756
|
-
*/
|
|
22757
|
-
const exchange = (codex, lines, ids, env) => Effect.scoped(Effect.gen(function* () {
|
|
22758
|
-
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
22759
|
-
const fs = yield* FileSystem.FileSystem;
|
|
22760
|
-
const answered = yield* (yield* spawner.spawn(ChildProcess.make(codex, ["app-server"], {
|
|
22761
|
-
cwd: yield* emptyFolder(fs),
|
|
22762
|
-
env,
|
|
22763
|
-
extendEnv: true,
|
|
22764
|
-
forceKillAfter: "2 seconds",
|
|
22765
|
-
stdin: {
|
|
22766
|
-
stream: Stream.fromIterable(lines.map((line) => `${line}\n`)).pipe(Stream.encodeText),
|
|
22767
|
-
endOnDone: false
|
|
22768
|
-
}
|
|
22769
|
-
}))).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);
|
|
22770
|
-
return Option.filter(answered, (answers) => answers.size === ids.length);
|
|
22771
|
-
})).pipe(Effect.timeoutOption("15 seconds"), Effect.map(Option.flatten), Effect.catchCause(() => Effect.succeed(Option.none())));
|
|
22772
|
-
/** `answer`'s result, read by `method`'s own schema, or why it can't be. */
|
|
22773
|
-
const resultOf = (method, answer) => {
|
|
22774
|
-
if (answer === void 0) return Effect.fail(new CodexUnanswered({ method: method.method }));
|
|
22775
|
-
if ("error" in answer) return Effect.fail(new CodexRefused({
|
|
22776
|
-
method: method.method,
|
|
22777
|
-
error: answer.error
|
|
22778
|
-
}));
|
|
22779
|
-
const { result } = method;
|
|
22780
|
-
return Schema.decodeUnknownEffect(result)(answer.result).pipe(Effect.mapError((error) => new CodexAnswerMismatch({
|
|
22781
|
-
method: method.method,
|
|
22782
|
-
issue: error.message
|
|
22783
|
-
})));
|
|
22784
|
-
};
|
|
22785
|
-
/**
|
|
22786
|
-
* One exchange with Codex's app server under the person's Codex home: the opening, then `method`
|
|
22787
|
-
* asked with `params`, which TypeScript holds to the method's own. Its answer is the one with
|
|
22788
|
-
* its request's ID, and its result is read by the method's result schema.
|
|
22789
|
-
*/
|
|
22790
|
-
const askCodex = (codex, method, params, env = {}) => Effect.gen(function* () {
|
|
22791
|
-
const asked = {
|
|
22792
|
-
jsonrpc: "2.0",
|
|
22793
|
-
id: askedId,
|
|
22794
|
-
method: method.method,
|
|
22795
|
-
params
|
|
22796
|
-
};
|
|
22797
|
-
const lines = yield* Effect.forEach([...opening, asked], (request) => encodeRequest(request).pipe(Effect.orDie));
|
|
22798
|
-
const answers = yield* exchange(codex, lines, [initializeId, askedId], env);
|
|
22799
|
-
if (Option.isNone(answers)) return yield* new CodexUnanswered({ method: method.method });
|
|
22800
|
-
yield* resultOf(codexMethods.initialize, answers.value.get(initializeId));
|
|
22801
|
-
return yield* resultOf(method, answers.value.get(askedId));
|
|
22802
|
-
});
|
|
22803
|
-
//#endregion
|
|
22804
23187
|
//#region src/harness/commit-hooks.ts
|
|
22805
23188
|
/**
|
|
22806
23189
|
* ref0's part of Git's `prepare-commit-msg` and `commit-msg` hooks, which their forwarders in
|
|
@@ -23110,21 +23493,75 @@ const codexAgentConfig = Effect.fnUntraced(function* (agent, root, home) {
|
|
|
23110
23493
|
parameters: shellWord(`include.path=${file}`)
|
|
23111
23494
|
};
|
|
23112
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)}`;
|
|
23113
23539
|
/**
|
|
23114
23540
|
* Codex has no environment file: its install selects a file; session start fills its checkout,
|
|
23115
|
-
* 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.
|
|
23116
23544
|
*/
|
|
23117
23545
|
const setCodexCommitEnvironment = Effect.fnUntraced(function* (cwd, checkout) {
|
|
23118
23546
|
const folders = yield* ConfigFolders;
|
|
23119
23547
|
const fs = yield* FileSystem.FileSystem;
|
|
23120
23548
|
const path = yield* Path.Path;
|
|
23121
23549
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
23122
|
-
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));
|
|
23123
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));
|
|
23124
23562
|
const pluginRoot = yield* Config.String("CLAUDE_PLUGIN_ROOT").pipe(Config.withDefault(""));
|
|
23125
23563
|
if (pluginRoot === "") return yield* new CodexCommitFailed({ detail: "Codex gave the hook no plugin root." });
|
|
23126
23564
|
const gitFolder = path.join(path.dirname(pluginRoot), "@git");
|
|
23127
|
-
const home = yield* Config.String("HOME");
|
|
23128
23565
|
const author = yield* codexAgentConfig(agent.value, gitFolder, home);
|
|
23129
23566
|
const common = (yield* spawner.string(ChildProcess.make("git", [
|
|
23130
23567
|
"-C",
|
|
@@ -23480,35 +23917,27 @@ const marketplaceFor = Effect.fnUntraced(function* (harness, options) {
|
|
|
23480
23917
|
//#region src/harness/codex-install-commits.ts
|
|
23481
23918
|
const decodeProfile = Schema.decodeUnknownEffect(CodexConfigRead);
|
|
23482
23919
|
const sameJson = Schema.toEquivalence(Schema.Json);
|
|
23483
|
-
/**
|
|
23484
|
-
|
|
23485
|
-
|
|
23486
|
-
|
|
23487
|
-
|
|
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
|
+
})}`;
|
|
23488
23930
|
};
|
|
23489
|
-
/**
|
|
23490
|
-
|
|
23491
|
-
|
|
23492
|
-
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
const
|
|
23496
|
-
|
|
23497
|
-
|
|
23498
|
-
|
|
23499
|
-
const name = yield* originPluginName((yield* Ref0Config).api);
|
|
23500
|
-
if (file === `${codexGitFolder(options.configFolder, `${name}@${name}`)}/agents/${agent.agentId}.gitconfig`) return;
|
|
23501
|
-
const oldRoot = file.slice(0, file.lastIndexOf("/agents/"));
|
|
23502
|
-
const fallback = yield* codexAgentConfig(agent, oldRoot, options.home ?? (yield* Config.String("HOME").pipe(Config.withDefault("."))));
|
|
23503
|
-
yield* writeCodexCheckout({
|
|
23504
|
-
gitFolder: oldRoot,
|
|
23505
|
-
common: "",
|
|
23506
|
-
folder: "",
|
|
23507
|
-
signsOff: false,
|
|
23508
|
-
clearAuthor: file,
|
|
23509
|
-
author: fallback
|
|
23510
|
-
});
|
|
23511
|
-
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)]]);
|
|
23512
23941
|
});
|
|
23513
23942
|
/** Install the connected agent's include and shell policy. */
|
|
23514
23943
|
const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
@@ -23517,6 +23946,7 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23517
23946
|
CODEX_HOME: options.configFolder
|
|
23518
23947
|
};
|
|
23519
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." });
|
|
23520
23950
|
return yield* Effect.gen(function* () {
|
|
23521
23951
|
const fs = yield* FileSystem.FileSystem;
|
|
23522
23952
|
const spawner = yield* ChildProcessSpawner.ChildProcessSpawner;
|
|
@@ -23544,8 +23974,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23544
23974
|
].find((key) => Object.hasOwn(set, key) && !(own && (key === "GIT_CONFIG_PARAMETERS" || key === "REF0_HARNESS")));
|
|
23545
23975
|
const allowlist = policy?.include_only != null || Object.values(policy?.filters ?? {}).includes("include");
|
|
23546
23976
|
if (collision !== void 0 || allowlist) {
|
|
23547
|
-
yield*
|
|
23548
|
-
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;
|
|
23549
23979
|
return;
|
|
23550
23980
|
}
|
|
23551
23981
|
const profiles = (yield* fs.readDirectory(options.configFolder)).filter((file) => file.endsWith(".config.toml"));
|
|
@@ -23570,8 +24000,8 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23570
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 })));
|
|
23571
24001
|
const allowlist = profilePolicy?.include_only != null || Object.values(filters ?? {}).includes("include");
|
|
23572
24002
|
if (key === void 0 && !losesAuthors && !allowlist) continue;
|
|
23573
|
-
yield*
|
|
23574
|
-
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;
|
|
23575
24005
|
return;
|
|
23576
24006
|
}
|
|
23577
24007
|
if (!own && (options.approveCommits === void 0 || !(yield* options.approveCommits()))) return;
|
|
@@ -23659,9 +24089,9 @@ const installCodexCommits = Effect.fnUntraced(function* (agent, options) {
|
|
|
23659
24089
|
}, env)).status !== "ok") return yield* failed("set Codex’s commit environment because another config layer overrides it");
|
|
23660
24090
|
}).pipe(Effect.onError(() => Effect.gen(function* () {
|
|
23661
24091
|
if (!attemptedConfig || (yield* recover)) yield* restore(author.file, previousAuthor);
|
|
23662
|
-
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."]]);
|
|
23663
24093
|
}).pipe(Effect.orDie)));
|
|
23664
|
-
}).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")));
|
|
23665
24095
|
}, Effect.scoped);
|
|
23666
24096
|
//#endregion
|
|
23667
24097
|
//#region src/harness/install-claude-code.ts
|
|
@@ -23825,7 +24255,7 @@ const installCodex = Effect.fnUntraced(function* (release, agent, options) {
|
|
|
23825
24255
|
trusted,
|
|
23826
24256
|
profile
|
|
23827
24257
|
};
|
|
23828
|
-
}).pipe(Effect.onError(() =>
|
|
24258
|
+
}).pipe(Effect.onError((cause) => clearUnfinishedCodexInstall(agent, Cause.findErrorOption(cause), options).pipe(Effect.orDie)));
|
|
23829
24259
|
});
|
|
23830
24260
|
//#endregion
|
|
23831
24261
|
//#region src/harness/install.ts
|
|
@@ -23931,7 +24361,7 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
23931
24361
|
for (const folder of into) answers.set(folder, Result.fail(failed));
|
|
23932
24362
|
};
|
|
23933
24363
|
const started = /* @__PURE__ */ new Set();
|
|
23934
|
-
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, {
|
|
23935
24365
|
configFolder: folder.configFolder,
|
|
23936
24366
|
home
|
|
23937
24367
|
}).pipe(Effect.orDie)), Effect.catchTag("InstallFailed", () => Effect.void)), { discard: true });
|
|
@@ -23946,8 +24376,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
23946
24376
|
if (into.length === 0) continue;
|
|
23947
24377
|
const command = yield* onPath(harness === "codex" ? "codex" : "claude");
|
|
23948
24378
|
if (Option.isNone(command)) {
|
|
23949
|
-
|
|
23950
|
-
|
|
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));
|
|
23951
24382
|
continue;
|
|
23952
24383
|
}
|
|
23953
24384
|
yield* Effect.scoped(Effect.gen(function* () {
|
|
@@ -23968,9 +24399,9 @@ const installInto = Effect.fnUntraced(function* (folders, options) {
|
|
|
23968
24399
|
})).pipe(Effect.result);
|
|
23969
24400
|
answers.set(folder, installed);
|
|
23970
24401
|
}
|
|
23971
|
-
}).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)))))));
|
|
23972
24403
|
}
|
|
23973
|
-
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders)));
|
|
24404
|
+
}).pipe(Effect.onInterrupt(() => clearUnstarted(folders, Option.none())));
|
|
23974
24405
|
return folders.map((folder) => ({
|
|
23975
24406
|
...folder,
|
|
23976
24407
|
installed: answers.get(folder) ?? Result.fail(new InstallFailed({ says: "ref0 didn’t install into this folder." }))
|
|
@@ -24062,7 +24493,12 @@ const mostFolders = 8;
|
|
|
24062
24493
|
const isConfigFolderPath = Schema.is(ConfigFolderPath);
|
|
24063
24494
|
/** The line that asks again. */
|
|
24064
24495
|
const again = "ref0 connect";
|
|
24065
|
-
/**
|
|
24496
|
+
/**
|
|
24497
|
+
* Every folder found, each ticked but one ref0 can't be shown; Enter keeps the ticked ones. The
|
|
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.
|
|
24501
|
+
*/
|
|
24066
24502
|
const choose = Effect.fnUntraced(function* (found, home) {
|
|
24067
24503
|
const choices = found.map((folder) => {
|
|
24068
24504
|
const shown = shownConfigFolder(home, folder.configFolder);
|
|
@@ -24079,8 +24515,9 @@ const choose = Effect.fnUntraced(function* (found, home) {
|
|
|
24079
24515
|
};
|
|
24080
24516
|
});
|
|
24081
24517
|
const ticked = choices.filter((choice) => choice.selected).length;
|
|
24518
|
+
const keys = "Space ticks or unticks one; Enter connects those ticked.";
|
|
24519
|
+
yield* printBlocks([[{ label: "Connect which config folders?" }, ticked > mostFolders ? `One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. ${keys}` : keys]]);
|
|
24082
24520
|
return yield* Prompt.run(Prompt.MultiSelect({
|
|
24083
|
-
message: ticked > mostFolders ? `Connect which config folders? One request connects at most ${mostFolders}, so untick ${ticked - mostFolders} or more. Space ticks or unticks one; Enter connects those ticked.` : "Connect which config folders? Space ticks or unticks one; Enter connects those ticked.",
|
|
24084
24521
|
choices,
|
|
24085
24522
|
max: mostFolders
|
|
24086
24523
|
}));
|
|
@@ -24197,6 +24634,7 @@ const connectCommands = [Command.make("connect", { json }, ({ json: asJson }) =>
|
|
|
24197
24634
|
if (found.length === 0) return yield* sayFailed(`ref0 found no Claude Code or Codex config folder in your home. Start Claude Code or Codex once, then run ${again} again.`);
|
|
24198
24635
|
const chosen = yield* choose(found, home).pipe(Effect.catchTag("QuitError", () => sayFailed("Stopped, and nothing was asked.")));
|
|
24199
24636
|
if (chosen.length === 0) return yield* sayFailed(`No config folder was ticked, so nothing was asked. To connect one, run ${again} again.`);
|
|
24637
|
+
yield* Console.error("");
|
|
24200
24638
|
const connected = yield* requestAttachment(chosen);
|
|
24201
24639
|
const installed = yield* installInto(connected.kept, { asJson });
|
|
24202
24640
|
const others = yield* otherOwners(connected);
|
|
@@ -24271,7 +24709,7 @@ const signalLines = (answer) => {
|
|
|
24271
24709
|
* credential, a Sentry token, is pasted on that page too, never in a terminal. Anyone who may start a
|
|
24272
24710
|
* thread in the project sends a signal to one of its connections by name.
|
|
24273
24711
|
*/
|
|
24274
|
-
const failedWith$
|
|
24712
|
+
const failedWith$15 = (failed) => sayFailed(failed.nextStep);
|
|
24275
24713
|
const project$5 = Argument.String("project").pipe(Argument.withDescription("The project, as owner/project"), Argument.withSchema(WrittenProject));
|
|
24276
24714
|
const name = Argument.String("name").pipe(Argument.withDescription("The connection’s name in the project, such as deploys"), Argument.withSchema(ConnectionName));
|
|
24277
24715
|
const list$5 = Command.make("list", {
|
|
@@ -24283,7 +24721,7 @@ const list$5 = Command.make("list", {
|
|
|
24283
24721
|
params: projectParts(address)
|
|
24284
24722
|
});
|
|
24285
24723
|
yield* printAnswer(answer.json, asJson, connectionLines(answer.value));
|
|
24286
|
-
}).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"));
|
|
24287
24725
|
/** A change made on the project's page: its link, and what to do there. */
|
|
24288
24726
|
const onThePage = (address, change) => Effect.gen(function* () {
|
|
24289
24727
|
const config = yield* Ref0Config;
|
|
@@ -24359,14 +24797,14 @@ const send = Command.make("send", {
|
|
|
24359
24797
|
})
|
|
24360
24798
|
});
|
|
24361
24799
|
yield* printAnswer(answer.json, asJson, signalLines(answer.value));
|
|
24362
|
-
}).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"));
|
|
24363
24801
|
/** A project's connections, and sending a signal by one. */
|
|
24364
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]))];
|
|
24365
24803
|
//#endregion
|
|
24366
24804
|
//#region src/harness/checkout.ts
|
|
24367
24805
|
const decodeRepoName$2 = Schema.decodeUnknownOption(RepoName);
|
|
24368
24806
|
const decodePath$2 = Schema.decodeUnknownOption(RepoPath);
|
|
24369
|
-
const decodeThread$
|
|
24807
|
+
const decodeThread$2 = Schema.decodeUnknownOption(WrittenThread);
|
|
24370
24808
|
const decodeWrittenRepo$1 = Schema.decodeUnknownOption(WrittenRepo);
|
|
24371
24809
|
var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
24372
24810
|
static layer = Layer.effect(Checkout, Effect.gen(function* () {
|
|
@@ -24405,7 +24843,7 @@ var Checkout = class Checkout extends Context.Service()("ref0/cli/Checkout") {
|
|
|
24405
24843
|
"config",
|
|
24406
24844
|
"--get",
|
|
24407
24845
|
"ref0.thread"
|
|
24408
|
-
]), (written) => decodeThread$
|
|
24846
|
+
]), (written) => decodeThread$2(written.trim()));
|
|
24409
24847
|
const repoAt = (folder) => Effect.map(git(folder, [
|
|
24410
24848
|
"config",
|
|
24411
24849
|
"--get",
|
|
@@ -24582,7 +25020,7 @@ const paramValues = (params) => {
|
|
|
24582
25020
|
* its params, and when it ends; under it, each workflow's run, by its title and its file, and
|
|
24583
25021
|
* how it stands; and under each, its newest asks and passkeys.
|
|
24584
25022
|
*/
|
|
24585
|
-
const waitingLines = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
|
|
25023
|
+
const waitingLines$1 = (wait, now) => [`${wait.check}${paramValues(wait.params)} · waiting, until ${minute(wait.endsAt)} · ${wait.start}`, ...wait.runs.flatMap((run, index) => {
|
|
24586
25024
|
const last = index === wait.runs.length - 1;
|
|
24587
25025
|
const under = last ? " " : "│ ";
|
|
24588
25026
|
return [`${last ? "└─" : "├─"} ${run.title ?? run.workflow} · ${waitedText[run.state]} · ${run.workflow}.ts`, ...run.leaves.map((leaf, at) => `${under}${at === run.leaves.length - 1 ? "└─" : "├─"} ${leafText(leaf, now)}`)];
|
|
@@ -25408,7 +25846,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25408
25846
|
})).value;
|
|
25409
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}.`);
|
|
25410
25848
|
if (code.runs.length === 0) {
|
|
25411
|
-
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.`]]);
|
|
25412
25850
|
return;
|
|
25413
25851
|
}
|
|
25414
25852
|
yield* holdsVersion(root.value, code);
|
|
@@ -25417,7 +25855,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25417
25855
|
thread: `${thread.value}’s own .ref0/`,
|
|
25418
25856
|
project: "the project’s checks"
|
|
25419
25857
|
}[code.code.origin];
|
|
25420
|
-
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.`]]);
|
|
25421
25859
|
const own = yield* loadCheck(code.compiled).pipe(Effect.catchTag("LocalRunFailed", (failed) => sayFailed(failed.why)));
|
|
25422
25860
|
const sandboxes = code.compiled.entry.sandboxes ?? [];
|
|
25423
25861
|
const held = /* @__PURE__ */ new Set();
|
|
@@ -25429,7 +25867,7 @@ const runLocally = Effect.fnUntraced(function* (folder, check, named, asJson) {
|
|
|
25429
25867
|
const run = yield* newRunId;
|
|
25430
25868
|
const deadline = (yield* Clock.currentTimeMillis) + code.timeoutSeconds * 1e3;
|
|
25431
25869
|
const sandbox = yield* localOpener(root.value, held, sandboxes, deadline);
|
|
25432
|
-
yield*
|
|
25870
|
+
yield* printBlocks([[`Running ${code.check} locally, part ${index + 1} of ${code.runs.length}, as ${run}.`]]);
|
|
25433
25871
|
const ran = yield* runPart(own, {
|
|
25434
25872
|
params,
|
|
25435
25873
|
host: treeHost(root.value, code.tree),
|
|
@@ -25492,7 +25930,7 @@ const nextStepsFor = (place, wait) => `Wait on it with ref0 runs waiting ${wait.
|
|
|
25492
25930
|
/** Says a waiting start's tree on stderr, as it stands now. */
|
|
25493
25931
|
const sayTree = Effect.fnUntraced(function* (wait) {
|
|
25494
25932
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25495
|
-
yield*
|
|
25933
|
+
yield* printBlocks([[waitingLines$1(wait, now)]]);
|
|
25496
25934
|
});
|
|
25497
25935
|
/**
|
|
25498
25936
|
* Waits on a start that waits until it goes on or ends, or 9 minutes pass: what it got, printed
|
|
@@ -25538,7 +25976,7 @@ const waitOnStart = Effect.fnUntraced(function* (place, first, asJson, rerunComm
|
|
|
25538
25976
|
* reports it. `ref0 runs trace` reads one run's trace, a check run's or a workflow run's, a page
|
|
25539
25977
|
* at a time.
|
|
25540
25978
|
*/
|
|
25541
|
-
const failedWith$
|
|
25979
|
+
const failedWith$14 = (failed) => sayFailed(failed.nextStep);
|
|
25542
25980
|
const decodeRunId = Schema.decodeUnknownOption(CheckRunId);
|
|
25543
25981
|
const decodeTraced = Schema.decodeUnknownOption(TracedRun);
|
|
25544
25982
|
/** A run named by a flag, or a failure that says how to name one. */
|
|
@@ -25639,8 +26077,8 @@ const run = Command.make("run", {
|
|
|
25639
26077
|
value
|
|
25640
26078
|
}, asJson, rerunCommand);
|
|
25641
26079
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25642
|
-
yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
25643
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26080
|
+
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
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"));
|
|
25644
26082
|
const trace = Command.make("trace", {
|
|
25645
26083
|
run: Argument.String("run").pipe(Argument.withDescription("The run, as checkrun_… or workflowrun_…")),
|
|
25646
26084
|
repo: repoFlag,
|
|
@@ -25683,7 +26121,7 @@ const trace = Command.make("trace", {
|
|
|
25683
26121
|
...narrowed,
|
|
25684
26122
|
`--cursor ${next}`
|
|
25685
26123
|
].join(" ")));
|
|
25686
|
-
}).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"));
|
|
25687
26125
|
const waiting$1 = Command.make("waiting", {
|
|
25688
26126
|
start: startArgument,
|
|
25689
26127
|
repo: repoFlag,
|
|
@@ -25717,8 +26155,8 @@ const waiting$1 = Command.make("waiting", {
|
|
|
25717
26155
|
value
|
|
25718
26156
|
}, asJson, rerunCommand);
|
|
25719
26157
|
const now = DateTime.makeUnsafe(yield* Clock.currentTimeMillis);
|
|
25720
|
-
yield* printAnswer(answer.json, asJson, `${waitingLines(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
25721
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
26158
|
+
yield* printAnswer(answer.json, asJson, `${waitingLines$1(value.waiting, now)}\n${nextStepsFor(place, value.waiting)}`);
|
|
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"));
|
|
25722
26160
|
/** What a cancel did, in words. */
|
|
25723
26161
|
const cancelledText = {
|
|
25724
26162
|
start: "Cancelled: nothing started, and the workflow runs it waited on ended with it.",
|
|
@@ -25744,7 +26182,7 @@ const cancel = Command.make("cancel", {
|
|
|
25744
26182
|
})
|
|
25745
26183
|
});
|
|
25746
26184
|
yield* printAnswer(answer.json, asJson, cancelledText[answer.value.cancelled]);
|
|
25747
|
-
}).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"));
|
|
25748
26186
|
/** Starting a check on `main` or a thread's head, listing one scope's runs, and reading a trace. */
|
|
25749
26187
|
const runCommands = [run, Command.make("runs", {
|
|
25750
26188
|
repo: Argument.String("repo").pipe(Argument.withDescription("The repo, as owner/name"), Argument.withSchema(WrittenRepo)),
|
|
@@ -25786,14 +26224,14 @@ const runCommands = [run, Command.make("runs", {
|
|
|
25786
26224
|
...narrowed,
|
|
25787
26225
|
`--before ${next}`
|
|
25788
26226
|
].join(" ")));
|
|
25789
|
-
}).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([
|
|
25790
26228
|
trace,
|
|
25791
26229
|
waiting$1,
|
|
25792
26230
|
cancel
|
|
25793
26231
|
]))];
|
|
25794
26232
|
//#endregion
|
|
25795
26233
|
//#region src/commands/checks.ts
|
|
25796
|
-
const failedWith$
|
|
26234
|
+
const failedWith$13 = (failed) => sayFailed(failed.nextStep);
|
|
25797
26235
|
const decodeCheck = Schema.decodeUnknownOption(CheckKey);
|
|
25798
26236
|
const decodeToken = Schema.decodeUnknownOption(TokenId);
|
|
25799
26237
|
const decodePerson = Schema.decodeUnknownOption(OwnerName);
|
|
@@ -25829,26 +26267,28 @@ const restore = Command.make("restore", {
|
|
|
25829
26267
|
}
|
|
25830
26268
|
});
|
|
25831
26269
|
yield* printAnswer(answer.json, asJson, restoredWords(answer.value, repo));
|
|
25832
|
-
}).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"));
|
|
25833
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]));
|
|
25834
26272
|
/** `ref0 checks`: a repo's checks. */
|
|
25835
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]))];
|
|
25836
26274
|
//#endregion
|
|
25837
26275
|
//#region src/gate-lines.ts
|
|
25838
26276
|
/**
|
|
25839
|
-
* A pull request's gate as `ref0 gate` says it: its current version, whether its
|
|
25840
|
-
*
|
|
25841
|
-
*
|
|
25842
|
-
*
|
|
25843
|
-
*
|
|
25844
|
-
*
|
|
25845
|
-
*
|
|
25846
|
-
*
|
|
25847
|
-
*
|
|
25848
|
-
*
|
|
25849
|
-
*
|
|
25850
|
-
*
|
|
25851
|
-
*
|
|
26277
|
+
* A pull request's gate as `ref0 gate` says it: its current version, whether its gate returned its
|
|
26278
|
+
* needs, and once it's open, how many still wait and whose one yes would meet them all; the project
|
|
26279
|
+
* gate's revision and whether it failed; and each need on its own line with where it came from,
|
|
26280
|
+
* ref0's floor or the line of `.ref0/gate.ts`, or of the project gate's, that returned it, with
|
|
26281
|
+
* that line's code, and under it whether it's met, by whose yes, or on whom or what it waits; in an
|
|
26282
|
+
* organization, what one yes gives its giver. A need for one of the project's checks says the
|
|
26283
|
+
* project gate revision whose checks run for it, the version's own. Then what each gate read of the
|
|
26284
|
+
* change, each read's tree, path and the object it found, short. Then its gate decided again in
|
|
26285
|
+
* place under `main`'s `.ref0/`, with `main`'s commit: the needs it held, or those that grew, and
|
|
26286
|
+
* why. Then the additions standing on the pull request now, each with the ID `ref0 pr needs remove`
|
|
26287
|
+
* takes: the version's own say who added its needs when it was made, and one may have gone since.
|
|
26288
|
+
* Then the local runs of its parts' identities, each with its signer; each part that takes local or
|
|
26289
|
+
* needs a yes, with its wrappers, the run and the yeses that meet it or what it waits for; and
|
|
26290
|
+
* last, each signer revoked from answering one of their checks, an attachment or a person by name,
|
|
26291
|
+
* with how an admin restores them.
|
|
25852
26292
|
*/
|
|
25853
26293
|
const fromWords = (from) => {
|
|
25854
26294
|
switch (from.kind) {
|
|
@@ -25879,11 +26319,116 @@ const columns = (lines) => {
|
|
|
25879
26319
|
* version was decided under, whose checks run for it, never `main`'s.
|
|
25880
26320
|
*/
|
|
25881
26321
|
const noteOf = (need, view) => need.kind === "check" && need.source === "project" && view.projectRevision !== void 0 ? ` at gate revision ${view.projectRevision}` : "";
|
|
25882
|
-
const
|
|
26322
|
+
const isPerson = Schema.is(PersonId);
|
|
26323
|
+
/** An owner or a person, by name where it's known. */
|
|
26324
|
+
const nameOf = (id, named) => named.names.get(id) ?? id;
|
|
26325
|
+
/** The admins of an owner, in words: at home, its person alone, by name. */
|
|
26326
|
+
const adminsOf = (owner, named) => isPerson(owner) ? nameOf(owner, named) : `an admin of ${nameOf(owner, named)}`;
|
|
26327
|
+
/** Who answers as a decider, in words: a person by their name, a check by its key. */
|
|
26328
|
+
const whoWords = (decider, named) => {
|
|
26329
|
+
switch (decider.kind) {
|
|
26330
|
+
case "person": return nameOf(decider.id, named);
|
|
26331
|
+
case "role": return adminsOf(decider.owner, named);
|
|
26332
|
+
case "permission": return adminsOf(named.owner, named);
|
|
26333
|
+
case "check": return checkKey(decider);
|
|
26334
|
+
case "agent": return decider.id;
|
|
26335
|
+
case "override": return whoWords(decider.by, named);
|
|
26336
|
+
}
|
|
26337
|
+
};
|
|
26338
|
+
/** Alternatives in words, each once: `pip or an admin of acme`. */
|
|
26339
|
+
const anyOf = (deciders, named) => [...new Set(deciders.map((each) => whoWords(each, named)))].join(" or ");
|
|
26340
|
+
/** A yes that met a need, in words: whose, or the check that passed or was overridden. */
|
|
26341
|
+
const needYesWords = (yes, named) => {
|
|
26342
|
+
if (yes.decider.kind === "check") return `${checkKey(yes.decider)} ${yes.overridden === true ? "overridden" : "passed"}`;
|
|
26343
|
+
if (yes.by === void 0) return `${whoWords(yes.decider, named)}’s yes`;
|
|
26344
|
+
const by = nameOf(yes.by, named);
|
|
26345
|
+
return yes.decider.kind === "person" ? `${by}’s yes` : `${by}’s yes, as an admin`;
|
|
26346
|
+
};
|
|
26347
|
+
/** An override left of a check, in words: whose yes overrides which of its noes. */
|
|
26348
|
+
const overrideWords = (check, left, named) => `a yes from ${whoWords(left.by, named)} to override ${left.of === "failed run" ? `${check}’s failed run` : `${check} not running`}`;
|
|
26349
|
+
/**
|
|
26350
|
+
* One decider a need waits on, in words: a check to pass, or its parts when the need wraps it,
|
|
26351
|
+
* or, once ref0 said no to it, every override left.
|
|
26352
|
+
*/
|
|
26353
|
+
const waitWords = (wait, need, named) => {
|
|
26354
|
+
if (wait.decider.kind !== "check") return whoWords(wait.decider, named);
|
|
26355
|
+
const check = checkKey(wait.decider);
|
|
26356
|
+
if (wait.overrides === void 0) return need.kind === "check" && (need.local === true || need.passkey !== void 0) ? `${check}’s parts, as below` : `${check} to pass`;
|
|
26357
|
+
if (wait.overrides.length === 0) return `${check}, which ref0 said no to`;
|
|
26358
|
+
return [...new Set(wait.overrides.map((left) => overrideWords(check, left, named)))].join(", and ");
|
|
26359
|
+
};
|
|
26360
|
+
/** The decider keys whose answers meet a need, for telling two needs met by the same yes. */
|
|
26361
|
+
const yesKey = (standing) => {
|
|
26362
|
+
if (standing.state === "met" && "by" in standing && standing.by.every(({ decider }) => decider.kind !== "check")) return standing.by.map(({ decider }) => deciderKey(decider)).join(" ");
|
|
26363
|
+
if (standing.state === "waiting" && "on" in standing && !standing.all) return standing.on.map(({ decider }) => deciderKey(decider)).join(" ");
|
|
26364
|
+
};
|
|
26365
|
+
/** How a need stands, in words. */
|
|
26366
|
+
const standingWords = (standing, need, named) => {
|
|
26367
|
+
switch (standing.state) {
|
|
26368
|
+
case "refused": return `refused: ${nameOf(standing.by, named)} said no, which decides the gate no`;
|
|
26369
|
+
case "met": {
|
|
26370
|
+
if ("as" in standing) return standing.as === "no checks" ? "met: no check runs on this change" : "met: the merge queue’s tip runs it, and main moves only once it passes there";
|
|
26371
|
+
const people = /* @__PURE__ */ new Map();
|
|
26372
|
+
for (const yes of standing.by) {
|
|
26373
|
+
const who = yes.by ?? deciderKey(yes.decider);
|
|
26374
|
+
if (!people.has(who)) people.set(who, yes);
|
|
26375
|
+
}
|
|
26376
|
+
const yeses = [...new Set([...people.values()].map((yes) => needYesWords(yes, named)))];
|
|
26377
|
+
return standing.by.some(({ decider }) => decider.kind === "check") ? `met: ${yeses.join(", ")}` : `met by ${yeses.join(" and ")}`;
|
|
26378
|
+
}
|
|
26379
|
+
case "waiting": {
|
|
26380
|
+
if ("for" in standing) return "waiting for ref0 to read which of main’s checks run on this change";
|
|
26381
|
+
if (standing.all) return `waiting for ${standing.on.map((wait) => waitWords(wait, need, named)).join(", and ")}`;
|
|
26382
|
+
const asked = `a yes from ${anyOf(standing.on.map(({ decider }) => decider), named)}`;
|
|
26383
|
+
return need.kind === "check" || need.kind === "checks" ? `waiting on ${asked}: no run can meet it before main moves` : `waiting on ${asked}`;
|
|
26384
|
+
}
|
|
26385
|
+
}
|
|
26386
|
+
};
|
|
26387
|
+
/**
|
|
26388
|
+
* Who one yes would meet a waiting need with, in words, each alone: anyone it asks a yes of, or
|
|
26389
|
+
* the one answerer of every override it waits on; none for a check to pass.
|
|
26390
|
+
*/
|
|
26391
|
+
const meetersOf = (standing, named) => {
|
|
26392
|
+
if (standing.state !== "waiting" || !("on" in standing)) return [];
|
|
26393
|
+
if (!standing.all) return standing.on.map(({ decider }) => whoWords(decider, named));
|
|
26394
|
+
const answerers = new Set(standing.on.flatMap(({ overrides }) => overrides === void 0 || overrides.length === 0 ? [""] : overrides.map((left) => whoWords(left.by, named))));
|
|
26395
|
+
return answerers.size === 1 && !answerers.has("") ? [...answerers] : [];
|
|
26396
|
+
};
|
|
26397
|
+
/** An open gate's standings, or none until every need has one. */
|
|
26398
|
+
const standingsOf = (needs) => {
|
|
26399
|
+
const standings = needs.flatMap(({ standing }) => standing === void 0 ? [] : [standing]);
|
|
26400
|
+
return standings.length === 0 || standings.length < needs.length ? void 0 : standings;
|
|
26401
|
+
};
|
|
26402
|
+
/**
|
|
26403
|
+
* Where an open gate's needs stand, after its state: each met; refused, by whom; or how many
|
|
26404
|
+
* still wait, and whose one yes would meet them all, since one yes answers every decider its
|
|
26405
|
+
* person may answer as.
|
|
26406
|
+
*/
|
|
26407
|
+
const standsWords = (needs, named) => {
|
|
26408
|
+
const standings = standingsOf(needs);
|
|
26409
|
+
if (standings === void 0) return "";
|
|
26410
|
+
const refused = standings.find((each) => each.state === "refused");
|
|
26411
|
+
if (refused !== void 0 && refused.state === "refused") return `; ${nameOf(refused.by, named)} said no, which decides it no`;
|
|
26412
|
+
const waiting = standings.filter((each) => each.state === "waiting");
|
|
26413
|
+
if (waiting.length === 0) return "; each need is met";
|
|
26414
|
+
const [first, ...rest] = waiting.map((each) => meetersOf(each, named));
|
|
26415
|
+
const common = (first ?? []).filter((who) => rest.every((each) => each.includes(who)));
|
|
26416
|
+
const still = waiting.length === 1 ? "1 still waits" : `${waiting.length} still wait`;
|
|
26417
|
+
if (common.length === 0) return `; ${still}`;
|
|
26418
|
+
return `; ${still}, and one yes from ${[...new Set(common)].join(" or ")} meets ${waiting.length === 1 ? "it" : "them all"}`;
|
|
26419
|
+
};
|
|
26420
|
+
/**
|
|
26421
|
+
* In an organization, what one yes gives, under the needs, while one waits on a yes: every
|
|
26422
|
+
* approval and override its giver may give, so an admin's meets every need an admin may meet.
|
|
26423
|
+
*/
|
|
26424
|
+
const oneYesLines = (view, named) => {
|
|
26425
|
+
return (standingsOf(view.needs) ?? []).some((each) => each.state === "waiting" && "on" in each && each.on.some(({ decider, overrides }) => decider.kind !== "check" || overrides !== void 0)) && !isPerson(view.owner) ? [` one yes to this gate gives every approval and override above its giver may give, as themselves and as ${adminsOf(view.owner, named)}`] : [];
|
|
26426
|
+
};
|
|
26427
|
+
const stateWords = (view, named) => {
|
|
25883
26428
|
switch (view.state) {
|
|
25884
|
-
case "pending": return "its gate hasn’t
|
|
25885
|
-
case "answered": return
|
|
25886
|
-
case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}`;
|
|
26429
|
+
case "pending": return "its gate hasn’t returned its needs yet";
|
|
26430
|
+
case "answered": return `its gate returned its needs${standsWords(view.needs, named)}`;
|
|
26431
|
+
case "failed": return `its gate failed: ${view.why ?? "ref0 couldn’t run it"}${standsWords(view.needs, named)}`;
|
|
25887
26432
|
}
|
|
25888
26433
|
};
|
|
25889
26434
|
/** The project gate's line, under the head: its revision, and why it failed. */
|
|
@@ -25891,7 +26436,7 @@ const projectWords$1 = (view) => {
|
|
|
25891
26436
|
const project = view.projectGate;
|
|
25892
26437
|
if (project === void 0) return [];
|
|
25893
26438
|
const at = ` project gate, revision ${project.revision}`;
|
|
25894
|
-
return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state}`];
|
|
26439
|
+
return [project.state === "failed" ? `${at}: failed: ${project.why ?? "ref0 couldn’t run it"}, so an admin answers it` : `${at}: ${project.state === "pending" ? "hasn’t returned its needs yet" : "returned its needs"}`];
|
|
25895
26440
|
};
|
|
25896
26441
|
/**
|
|
25897
26442
|
* What a gate read of the change, under `head`: each read's tree, its path and the object it
|
|
@@ -25906,6 +26451,23 @@ const needLines = (needs, view) => columns(needs.map(({ need, from }) => ({
|
|
|
25906
26451
|
need: spellNeed(need),
|
|
25907
26452
|
from: `${from.map(fromWords).join("; ")}${noteOf(need, view)}`
|
|
25908
26453
|
})));
|
|
26454
|
+
/**
|
|
26455
|
+
* The version's needs, each with how it stands on the line under it, once its gate is open: a
|
|
26456
|
+
* need met by, or waiting on, the same yes as one above says so.
|
|
26457
|
+
*/
|
|
26458
|
+
const standingLines = (view, named) => {
|
|
26459
|
+
const lines = needLines(view.needs, view);
|
|
26460
|
+
const seen = /* @__PURE__ */ new Map();
|
|
26461
|
+
return view.needs.flatMap(({ need, standing }, i) => {
|
|
26462
|
+
const line = lines[i] ?? "";
|
|
26463
|
+
if (standing === void 0) return [line];
|
|
26464
|
+
const key = yesKey(standing);
|
|
26465
|
+
const same = key === void 0 ? void 0 : seen.get(key);
|
|
26466
|
+
if (key !== void 0 && same === void 0) seen.set(key, need);
|
|
26467
|
+
const also = same === void 0 ? "" : `, the same yes as ${spellNeed(same)}`;
|
|
26468
|
+
return [line, ` ${standingWords(standing, need, named)}${also}`];
|
|
26469
|
+
});
|
|
26470
|
+
};
|
|
25909
26471
|
const grewWords = (grew) => {
|
|
25910
26472
|
const why = grew.why ?? "ref0 couldn’t say why";
|
|
25911
26473
|
switch (grew.kind) {
|
|
@@ -25917,7 +26479,7 @@ const grewWords = (grew) => {
|
|
|
25917
26479
|
};
|
|
25918
26480
|
const inPlaceLines = (inPlace, view) => {
|
|
25919
26481
|
const head = `under main’s .ref0/, at ${inPlace.main.slice(0, 7)}:`;
|
|
25920
|
-
if (inPlace.state === "pending") return [`${head} its gate there hasn’t
|
|
26482
|
+
if (inPlace.state === "pending") return [`${head} its gate there hasn’t returned its needs yet`];
|
|
25921
26483
|
const reads = readLines("its gate there read:", inPlace.reads ?? []);
|
|
25922
26484
|
if (inPlace.state === "held" || inPlace.grew === void 0) return [
|
|
25923
26485
|
`${head} ${inPlace.state}`,
|
|
@@ -25991,7 +26553,11 @@ const wrappedPartLines = (parts) => parts.length === 0 ? [] : ["parts that take
|
|
|
25991
26553
|
const revokedSignerLines = (one) => one.kind === "attachment" ? [` ${one.check} attachment ${one.attachment.id} of ${one.attachment.person}, since ${DateTime.formatIso(one.at)}: ref0 failed local run ${one.signed}'s identity twice, the second in run ${one.failed}`, ` an admin restores it: ref0 checks signers restore ${one.check} --attachment ${one.attachment.id}`] : [` ${one.check} ${one.person}, since ${DateTime.formatIso(one.at)}: ref0 failed the identity of run ${one.signed}, which they said yes over, twice, the second in run ${one.failed}`, ` an admin restores them: ref0 checks signers restore ${one.check} --person ${one.person}`];
|
|
25992
26554
|
const revokedLines = (revoked) => revoked.length === 0 ? [] : ["signers revoked:", ...revoked.flatMap(revokedSignerLines)];
|
|
25993
26555
|
const gateLines = (view, repo) => {
|
|
25994
|
-
const
|
|
26556
|
+
const named = {
|
|
26557
|
+
names: new Map((view.names ?? []).map(({ owner, name }) => [owner, name])),
|
|
26558
|
+
owner: view.owner
|
|
26559
|
+
};
|
|
26560
|
+
const head = `${repo}#${view.pullRequest}, version ${view.version}: ${stateWords(view, named)}`;
|
|
25995
26561
|
const inPlace = view.inPlace === void 0 ? [] : inPlaceLines(view.inPlace, view);
|
|
25996
26562
|
const added = view.added.map(({ need, from }) => ({
|
|
25997
26563
|
need: spellNeed(need),
|
|
@@ -26003,7 +26569,8 @@ const gateLines = (view, repo) => {
|
|
|
26003
26569
|
return [
|
|
26004
26570
|
head,
|
|
26005
26571
|
...projectWords$1(view),
|
|
26006
|
-
...
|
|
26572
|
+
...standingLines(view, named),
|
|
26573
|
+
...oneYesLines(view, named),
|
|
26007
26574
|
...readLines("its gate read:", view.reads),
|
|
26008
26575
|
...readLines("its project gate read:", view.projectGate?.reads ?? []),
|
|
26009
26576
|
...inPlace,
|
|
@@ -26032,7 +26599,7 @@ const projectGateLines = (view, repo) => {
|
|
|
26032
26599
|
};
|
|
26033
26600
|
//#endregion
|
|
26034
26601
|
//#region src/commands/gate.ts
|
|
26035
|
-
const failedWith$
|
|
26602
|
+
const failedWith$12 = (failed) => sayFailed(failed.nextStep);
|
|
26036
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));
|
|
26037
26604
|
/** A pull request's gate. */
|
|
26038
26605
|
const gateCommands = [Command.make("gate", {
|
|
@@ -26058,7 +26625,7 @@ const gateCommands = [Command.make("gate", {
|
|
|
26058
26625
|
}
|
|
26059
26626
|
});
|
|
26060
26627
|
yield* printAnswer(answer.json, asJson, gateLines(answer.value, repo));
|
|
26061
|
-
}).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"))];
|
|
26062
26629
|
//#endregion
|
|
26063
26630
|
//#region src/clone.ts
|
|
26064
26631
|
/** The remote a clone pushes to: its thread's fork path, named as the thread is. */
|
|
@@ -26416,7 +26983,7 @@ const syncBase = Effect.fnUntraced(function* (folder) {
|
|
|
26416
26983
|
* with the session's fork to push to; `ref0 sync`, the session's base merged in, `main` or the
|
|
26417
26984
|
* thread it's stacked on; and `ref0 git-credential`, the helper Git asks there.
|
|
26418
26985
|
*/
|
|
26419
|
-
const failedWith$
|
|
26986
|
+
const failedWith$11 = (failed) => sayFailed(failed.nextStep);
|
|
26420
26987
|
/** `ref0 git-credential`, as Git runs it: see `answerGit`. */
|
|
26421
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* () {
|
|
26422
26989
|
const input = yield* (yield* Stdio.Stdio).stdin.pipe(Stream.decodeText(), Stream.mkString);
|
|
@@ -26470,7 +27037,7 @@ const gitCommands = [
|
|
|
26470
27037
|
value
|
|
26471
27038
|
])) !== 0) return yield* sayFailed(`Setting ${key} in ${into} failed.`);
|
|
26472
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.`);
|
|
26473
|
-
}).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")),
|
|
26474
27041
|
Command.make("sync", { json }, ({ json: asJson }) => Effect.gen(function* () {
|
|
26475
27042
|
yield* ownSession("ref0 sync runs in an agent’s session, in a clone ref0 clone made.");
|
|
26476
27043
|
const synced = yield* syncBase(".");
|
|
@@ -26486,7 +27053,7 @@ const gitCommands = [
|
|
|
26486
27053
|
on,
|
|
26487
27054
|
commit
|
|
26488
27055
|
}, asJson, `Merged ${at}. Push it when you’re ready: git push.`);
|
|
26489
|
-
}).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")),
|
|
26490
27057
|
credential
|
|
26491
27058
|
];
|
|
26492
27059
|
//#endregion
|
|
@@ -26899,7 +27466,7 @@ const usageCommands = [personUsage, personMeters];
|
|
|
26899
27466
|
* invitations. Each is a person's, asked of them by approval, as every terminal command of
|
|
26900
27467
|
* theirs is; agents never change members.
|
|
26901
27468
|
*/
|
|
26902
|
-
const failedWith$
|
|
27469
|
+
const failedWith$10 = (failed) => sayFailed(failed.nextStep);
|
|
26903
27470
|
const organization$2 = Argument.String("org").pipe(Argument.withDescription("The organization, by name"), Argument.withSchema(OwnerRef));
|
|
26904
27471
|
const person = Argument.String("person").pipe(Argument.withDescription("The person, by name"), Argument.withSchema(OwnerRef));
|
|
26905
27472
|
const invitation = Argument.String("invitation").pipe(Argument.withDescription("The invitation’s ID, such as invitation_01k6…"));
|
|
@@ -26929,7 +27496,7 @@ const create$1 = Command.make("create", {
|
|
|
26929
27496
|
payload
|
|
26930
27497
|
});
|
|
26931
27498
|
yield* printAnswer(answer.json, asJson, answer.value.state === "ready" ? `Made ${answer.value.name}.` : `Made ${answer.value.name}; it’s being set up.`);
|
|
26932
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27499
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Make an organization, with you as its admin"));
|
|
26933
27500
|
const leave = Command.make("leave", {
|
|
26934
27501
|
organization: organization$2,
|
|
26935
27502
|
json
|
|
@@ -26939,7 +27506,7 @@ const leave = Command.make("leave", {
|
|
|
26939
27506
|
params: { owner: org }
|
|
26940
27507
|
});
|
|
26941
27508
|
yield* printAnswer(answer.json, asJson, `Left ${org}.`);
|
|
26942
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27509
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Leave an organization"));
|
|
26943
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([
|
|
26944
27511
|
create$1,
|
|
26945
27512
|
leave,
|
|
@@ -26956,7 +27523,7 @@ const orgs = Command.make("orgs", {
|
|
|
26956
27523
|
});
|
|
26957
27524
|
const lines = answer.value.organizations.map((each) => `${each.name} ${each.role}`);
|
|
26958
27525
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You’re in no organization yet." : [...lines, ...more(answer.value.next)].join("\n"));
|
|
26959
|
-
}).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"));
|
|
26960
27527
|
const members = Command.make("members", {
|
|
26961
27528
|
organization: organization$2,
|
|
26962
27529
|
cursor: cursor$3,
|
|
@@ -26969,7 +27536,7 @@ const members = Command.make("members", {
|
|
|
26969
27536
|
});
|
|
26970
27537
|
const lines = answer.value.members.map((each) => `${each.name} ${each.role}${each.member.kind === "agent" ? " (its agent)" : ""}`);
|
|
26971
27538
|
yield* printAnswer(answer.json, asJson, [...lines, ...more(answer.value.next)].join("\n"));
|
|
26972
|
-
}).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"));
|
|
26973
27540
|
const role = Command.make("role", {
|
|
26974
27541
|
organization: organization$2,
|
|
26975
27542
|
person,
|
|
@@ -26985,7 +27552,7 @@ const role = Command.make("role", {
|
|
|
26985
27552
|
payload: { role: chosen }
|
|
26986
27553
|
});
|
|
26987
27554
|
yield* printAnswer(answer.json, asJson, `${named} is ${answer.value.role} of ${org} now.`);
|
|
26988
|
-
}).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"));
|
|
26989
27556
|
const remove$1 = Command.make("remove", {
|
|
26990
27557
|
organization: organization$2,
|
|
26991
27558
|
person,
|
|
@@ -26999,7 +27566,7 @@ const remove$1 = Command.make("remove", {
|
|
|
26999
27566
|
}
|
|
27000
27567
|
});
|
|
27001
27568
|
yield* printAnswer(answer.json, asJson, `Removed ${named} from ${org}.`);
|
|
27002
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27569
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Remove a member, with the grants they held there"));
|
|
27003
27570
|
const memberCommand = Command.make("member").pipe(Command.withDescription("Change a member’s role, or remove them"), Command.withSubcommands([role, remove$1]));
|
|
27004
27571
|
const listInvitations = Command.make("list", {
|
|
27005
27572
|
organization: organization$2,
|
|
@@ -27013,7 +27580,7 @@ const listInvitations = Command.make("list", {
|
|
|
27013
27580
|
});
|
|
27014
27581
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.invitee.name} ${each.role} from ${each.sentBy.name}`);
|
|
27015
27582
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? `${org} has no open invitations.` : [...lines, ...more(answer.value.next)].join("\n"));
|
|
27016
|
-
}).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"));
|
|
27017
27584
|
const withdraw = Command.make("withdraw", {
|
|
27018
27585
|
organization: organization$2,
|
|
27019
27586
|
invitation,
|
|
@@ -27027,7 +27594,7 @@ const withdraw = Command.make("withdraw", {
|
|
|
27027
27594
|
}
|
|
27028
27595
|
});
|
|
27029
27596
|
yield* printAnswer(answer.json, asJson, `Withdrew ${answer.value.invitation}.`);
|
|
27030
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27597
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Take an open invitation back"));
|
|
27031
27598
|
const invite = Command.make("invite", {
|
|
27032
27599
|
organization: organization$2,
|
|
27033
27600
|
person,
|
|
@@ -27047,7 +27614,7 @@ const invite = Command.make("invite", {
|
|
|
27047
27614
|
payload
|
|
27048
27615
|
});
|
|
27049
27616
|
yield* printAnswer(answer.json, asJson, `Invited ${answer.value.invitee.name} to ${org} as ${answer.value.role}: ${answer.value.invitation}.`);
|
|
27050
|
-
}).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]));
|
|
27051
27618
|
const invitations = Command.make("invitations", {
|
|
27052
27619
|
cursor: cursor$3,
|
|
27053
27620
|
json
|
|
@@ -27058,7 +27625,7 @@ const invitations = Command.make("invitations", {
|
|
|
27058
27625
|
});
|
|
27059
27626
|
const lines = answer.value.invitations.map((each) => `${each.invitation} ${each.organization.name} ${each.role}`);
|
|
27060
27627
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? "You have no open invitations." : [...lines, ...more(answer.value.next)].join("\n"));
|
|
27061
|
-
}).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"));
|
|
27062
27629
|
const accept = Command.make("accept", {
|
|
27063
27630
|
invitation,
|
|
27064
27631
|
json
|
|
@@ -27068,7 +27635,7 @@ const accept = Command.make("accept", {
|
|
|
27068
27635
|
params: { invitation: id }
|
|
27069
27636
|
});
|
|
27070
27637
|
yield* printAnswer(answer.json, asJson, `Joined as ${answer.value.role}.`);
|
|
27071
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27638
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Accept an invitation"));
|
|
27072
27639
|
const decline = Command.make("decline", {
|
|
27073
27640
|
invitation,
|
|
27074
27641
|
json
|
|
@@ -27078,7 +27645,7 @@ const decline = Command.make("decline", {
|
|
|
27078
27645
|
params: { invitation: id }
|
|
27079
27646
|
});
|
|
27080
27647
|
yield* printAnswer(answer.json, asJson, `Declined ${answer.value.invitation}.`);
|
|
27081
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27648
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$10))).pipe(Command.withDescription("Decline an invitation"));
|
|
27082
27649
|
/** Organizations, their members and invitations. */
|
|
27083
27650
|
const organizationCommands = [
|
|
27084
27651
|
orgCommand,
|
|
@@ -27091,7 +27658,7 @@ const organizationCommands = [
|
|
|
27091
27658
|
];
|
|
27092
27659
|
//#endregion
|
|
27093
27660
|
//#region src/commands/grants.ts
|
|
27094
|
-
const failedWith$
|
|
27661
|
+
const failedWith$9 = (failed) => sayFailed(failed.nextStep);
|
|
27095
27662
|
const isOwnerRef = Schema.is(OwnerRef);
|
|
27096
27663
|
const isProject$1 = Schema.is(WrittenProject);
|
|
27097
27664
|
const isThread = Schema.is(WrittenThread);
|
|
@@ -27213,7 +27780,7 @@ const changeCommand = (name, endpoint) => Command.make(name, {
|
|
|
27213
27780
|
onSome: (on) => [on]
|
|
27214
27781
|
})];
|
|
27215
27782
|
yield* printAnswer(answer.json, asJson, endpoint === "grants.set" ? `Granted ${role}: ${said.join(" ")}.` : `Took back ${role}: ${said.join(" ")}.`);
|
|
27216
|
-
}).pipe(Effect.catchTag("CallFailed", failedWith$
|
|
27783
|
+
}).pipe(Effect.catchTag("CallFailed", failedWith$9)));
|
|
27217
27784
|
const revoke = changeCommand("revoke", "grants.revoke").pipe(Command.withDescription("Take a grant back, written as it was given"));
|
|
27218
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]));
|
|
27219
27786
|
/** An organization's grants. */
|
|
@@ -27233,7 +27800,41 @@ const grantCommands = [Command.make("grants", {
|
|
|
27233
27800
|
const lines = answer.value.grants.map((each) => `${writtenSubject(org, each.subject)} ${writtenPrincipal(each.principal)} ${each.role}`);
|
|
27234
27801
|
const next = answer.value.nextAfter;
|
|
27235
27802
|
yield* printAnswer(answer.json, asJson, lines.length === 0 ? `You see no grants in ${org}.` : [...lines, ...next === void 0 ? [] : [`More: --cursor ${next}`]].join("\n"));
|
|
27236
|
-
}).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"))];
|
|
27237
27838
|
//#endregion
|
|
27238
27839
|
//#region src/commands/agents.ts
|
|
27239
27840
|
/**
|
|
@@ -27508,18 +28109,40 @@ const repoCommands = [Command.make("repos", {
|
|
|
27508
28109
|
//#endregion
|
|
27509
28110
|
//#region src/decision-lines.ts
|
|
27510
28111
|
/** A decision in words, as `ref0 waiting`, `ref0 decision` and `ref0 answer` print it. */
|
|
27511
|
-
/**
|
|
27512
|
-
|
|
27513
|
-
|
|
27514
|
-
|
|
27515
|
-
|
|
27516
|
-
|
|
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);
|
|
28128
|
+
const answers = decision.options.map((option) => option.key).join(" or ");
|
|
28129
|
+
return `${on}: ${decision.state === "open" ? answers : `${decision.result ?? "no answer"}, ${decision.closedWhy ?? "closed"}`} ${decision.decision}`;
|
|
27517
28130
|
};
|
|
27518
|
-
/**
|
|
27519
|
-
|
|
27520
|
-
|
|
27521
|
-
|
|
27522
|
-
|
|
28131
|
+
/**
|
|
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.
|
|
28134
|
+
*/
|
|
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";
|
|
28140
|
+
return [
|
|
28141
|
+
...yours,
|
|
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}.`
|
|
28145
|
+
].join("\n");
|
|
27523
28146
|
};
|
|
27524
28147
|
//#endregion
|
|
27525
28148
|
//#region src/commands/plans.ts
|
|
@@ -27534,7 +28157,10 @@ const checkPlan = Schema.decodeUnknownEffect(PlanInput);
|
|
|
27534
28157
|
const encodePlan = Schema.encodeEffect(PlanInput);
|
|
27535
28158
|
const decodeRecipient = Schema.decodeUnknownOption(CommentRecipient);
|
|
27536
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));
|
|
27537
|
-
|
|
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));
|
|
27538
28164
|
const version = Argument.Int("version").pipe(Argument.withDescription("The plan’s version"));
|
|
27539
28165
|
const decision = Argument.String("decision").pipe(Argument.withDescription("The decision’s ID"));
|
|
27540
28166
|
/** Where a version's approval stands: who accepted it, or its check. */
|
|
@@ -27695,27 +28321,37 @@ const comments = Command.make("comments", {
|
|
|
27695
28321
|
].filter((line) => line !== "").join("\n");
|
|
27696
28322
|
yield* printAnswer(answer.json, asJson, words);
|
|
27697
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
|
+
});
|
|
27698
28342
|
/** Plans' and decisions' commands. */
|
|
27699
28343
|
const planCommands = [
|
|
27700
|
-
|
|
27701
|
-
list$4,
|
|
27702
|
-
write,
|
|
27703
|
-
onVersion("show"),
|
|
27704
|
-
onVersion("propose"),
|
|
27705
|
-
comment,
|
|
27706
|
-
comments
|
|
27707
|
-
])),
|
|
28344
|
+
plan,
|
|
27708
28345
|
Command.make("waiting", {
|
|
27709
|
-
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),
|
|
27710
28347
|
json
|
|
27711
|
-
}, ({ project:
|
|
28348
|
+
}, ({ project: named, json: asJson }) => Effect.gen(function* () {
|
|
28349
|
+
const address = Option.isSome(named) ? named.value : yield* sessionScope;
|
|
27712
28350
|
const answer = yield* callEndpoint(routed(address, {
|
|
27713
28351
|
endpoint: "decisions.waiting",
|
|
27714
28352
|
params: scopeParams(address)
|
|
27715
28353
|
}));
|
|
27716
|
-
|
|
27717
|
-
const words = decisions.length === 0 ? "Nothing waits on you here." : decisions.map((each) => decisionLines(each)).join("\n");
|
|
27718
|
-
yield* printAnswer(answer.json, asJson, words);
|
|
28354
|
+
yield* printAnswer(answer.json, asJson, waitingLines(answer.value.decisions, address));
|
|
27719
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")),
|
|
27720
28356
|
Command.make("decision", {
|
|
27721
28357
|
project: project$2,
|
|
@@ -27729,7 +28365,7 @@ const planCommands = [
|
|
|
27729
28365
|
decision: id
|
|
27730
28366
|
}
|
|
27731
28367
|
}));
|
|
27732
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28368
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
27733
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")),
|
|
27734
28370
|
Command.make("answer", {
|
|
27735
28371
|
project: project$2,
|
|
@@ -27752,7 +28388,7 @@ const planCommands = [
|
|
|
27752
28388
|
})
|
|
27753
28389
|
})
|
|
27754
28390
|
}));
|
|
27755
|
-
yield* printAnswer(answer.json, asJson, decisionLines(answer.value));
|
|
28391
|
+
yield* printAnswer(answer.json, asJson, decisionLines(answer.value, address));
|
|
27756
28392
|
}).pipe(Effect.catchTag("CallFailed", (failed) => sayFailed(failed.nextStep)))).pipe(Command.withDescription("Answer a decision that waits on you"))
|
|
27757
28393
|
];
|
|
27758
28394
|
//#endregion
|
|
@@ -27792,9 +28428,14 @@ const endedLines = (view) => {
|
|
|
27792
28428
|
const why = view.closedWhy === null ? "" : closedWhyWords[view.closedWhy];
|
|
27793
28429
|
return [` ${view.state.padEnd(8)} ${at}${why}`];
|
|
27794
28430
|
};
|
|
27795
|
-
|
|
27796
|
-
|
|
27797
|
-
|
|
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}`];
|
|
27798
28439
|
};
|
|
27799
28440
|
/** `ref0 pr show`: one pull request, a line for each thing an agent acts on. */
|
|
27800
28441
|
const pullRequestLines = (view, repo) => {
|
|
@@ -27805,7 +28446,7 @@ const pullRequestLines = (view, repo) => {
|
|
|
27805
28446
|
`${repo}#${view.pullRequest}, thread #${view.threadNumber}: ${view.state}${repair}`,
|
|
27806
28447
|
` head ${headWords(head)}`,
|
|
27807
28448
|
` current ${versionWords(current)}, on ${base}`,
|
|
27808
|
-
|
|
28449
|
+
...openedLines(view),
|
|
27809
28450
|
...endedLines(view)
|
|
27810
28451
|
].join("\n");
|
|
27811
28452
|
};
|
|
@@ -28791,7 +29432,7 @@ const waitOnBlockers = Effect.fnUntraced(function* (asJson) {
|
|
|
28791
29432
|
const { thread, blockers } = first.value;
|
|
28792
29433
|
if (blockers.length === 0) return yield* printAnswer(first.json, asJson, `Nothing blocks #${thread} now: it may merge.`);
|
|
28793
29434
|
const them = blockers.length === 1 ? "it" : "them";
|
|
28794
|
-
yield*
|
|
29435
|
+
yield* printBlocks([[`${blockedSentence(thread, blockers)} Waiting up to 9 minutes for ${them} to clear…`]]);
|
|
28795
29436
|
let renewedAt = Number.NEGATIVE_INFINITY;
|
|
28796
29437
|
for (;;) {
|
|
28797
29438
|
const now = yield* Clock.currentTimeMillis;
|
|
@@ -29684,7 +30325,7 @@ const enable = Command.make("enable", {
|
|
|
29684
30325
|
const why = found.why === void 0 ? "" : ` (${found.why})`;
|
|
29685
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.`);
|
|
29686
30327
|
}
|
|
29687
|
-
yield*
|
|
30328
|
+
yield* printBlocks([[enablingLines(workflow, found.version, found.manifest, found)]]);
|
|
29688
30329
|
const answer = yield* callEndpoint({
|
|
29689
30330
|
endpoint: "workflows.enable",
|
|
29690
30331
|
params: repoParts(address),
|
|
@@ -29717,7 +30358,7 @@ const start = Command.make("start", {
|
|
|
29717
30358
|
source: source.value
|
|
29718
30359
|
}
|
|
29719
30360
|
});
|
|
29720
|
-
yield*
|
|
30361
|
+
yield* printBlocks([[attachingLines(written, compiled.value)]]);
|
|
29721
30362
|
const answer = yield* callEndpoint({
|
|
29722
30363
|
endpoint: "threadWorkflows.start",
|
|
29723
30364
|
params,
|
|
@@ -32677,11 +33318,11 @@ const startHarnessSession = Effect.fnUntraced(function* (input) {
|
|
|
32677
33318
|
* started that thread, `REF0_EVENT`. Both, each as ref0 writes it, or neither. `REF0_TITLE` is
|
|
32678
33319
|
* someone else's words, and nothing here reads it.
|
|
32679
33320
|
*/
|
|
32680
|
-
const decodeThread = Schema.decodeUnknownOption(WrittenThread);
|
|
33321
|
+
const decodeThread$1 = Schema.decodeUnknownOption(WrittenThread);
|
|
32681
33322
|
const decodeEvent = Schema.decodeUnknownOption(EventId);
|
|
32682
33323
|
const optional = (name) => Config.String(name).pipe(Config.option);
|
|
32683
33324
|
const watchedHere = Effect.gen(function* () {
|
|
32684
|
-
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread);
|
|
33325
|
+
const thread = Option.flatMap(yield* optional(watchVariables.thread), decodeThread$1);
|
|
32685
33326
|
const event = Option.flatMap(yield* optional(watchVariables.event), decodeEvent);
|
|
32686
33327
|
return Option.zipWith(thread, event, (on, by) => ({
|
|
32687
33328
|
thread: on,
|
|
@@ -34059,6 +34700,19 @@ const harnessSessionToken = Layer.effect(SessionToken, Effect.gen(function* () {
|
|
|
34059
34700
|
});
|
|
34060
34701
|
return SessionToken.of({ current });
|
|
34061
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
|
+
}));
|
|
34062
34716
|
//#endregion
|
|
34063
34717
|
//#region src/harness/server-check.ts
|
|
34064
34718
|
/**
|
|
@@ -34168,6 +34822,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
|
|
|
34168
34822
|
CLAUDE_CONFIG_DIR: configFolder,
|
|
34169
34823
|
...checked
|
|
34170
34824
|
};
|
|
34825
|
+
const withheld = Object.keys(yield* fromHere(keychainVariables)).some((variable) => env[variable] === void 0);
|
|
34171
34826
|
return yield* Effect.scoped(Effect.gen(function* () {
|
|
34172
34827
|
const handle = yield* spawner.spawn(ChildProcess.make(server.value.command, server.value.args, {
|
|
34173
34828
|
cwd: yield* emptyFolder(fs),
|
|
@@ -34182,6 +34837,7 @@ const startServer = Effect.fnUntraced(function* (harness, configFolder, plugin,
|
|
|
34182
34837
|
const said = yield* handle.stderr.pipe(Stream.decodeText(), Stream.mkString, Effect.forkScoped);
|
|
34183
34838
|
const lines = yield* handle.stdout.pipe(Stream.decodeText(), Stream.splitLines, Stream.takeUntil((line) => Option.isSome(decodeAnswer(line))), Stream.runCollect);
|
|
34184
34839
|
if (lines.some((line) => Option.isSome(decodeAnswer(line)))) return { kind: "answered" };
|
|
34840
|
+
if (withheld) return { kind: "withoutKeychain" };
|
|
34185
34841
|
return {
|
|
34186
34842
|
kind: "stopped",
|
|
34187
34843
|
said: firstLine(yield* Fiber.join(said)) || firstLine(lines.join("\n"))
|
|
@@ -34252,6 +34908,7 @@ const serverCheckOf = (started, configFolder) => {
|
|
|
34252
34908
|
switch (started.kind) {
|
|
34253
34909
|
case "answered": return checkOf("server", "yes", "starts");
|
|
34254
34910
|
case "stopped": return checkOf("server", "no", started.said === "" ? "can’t start, and said nothing" : `can’t start: ${started.said}`, installLine(configFolder));
|
|
34911
|
+
case "withoutKeychain": return checkOf("server", "no", "can’t reach the keychain, since an older ref0 installed it", installLine(configFolder));
|
|
34255
34912
|
case "unnamed": return checkOf("server", "no", "the plugin names no ref0 server", installLine(configFolder));
|
|
34256
34913
|
case "unanswered": return checkOf("server", "unknown", `didn’t answer in ${Duration.toSeconds(serverLimit)} s`, "ref0 status");
|
|
34257
34914
|
}
|
|
@@ -34417,7 +35074,8 @@ const ref0 = Command.make("ref0").pipe(Command.withDescription("ref0 from the te
|
|
|
34417
35074
|
...organizationCommands,
|
|
34418
35075
|
...grantCommands,
|
|
34419
35076
|
...agentCommands,
|
|
34420
|
-
...attachmentCommands
|
|
35077
|
+
...attachmentCommands,
|
|
35078
|
+
...identityTokenCommands
|
|
34421
35079
|
]));
|
|
34422
35080
|
const keychain = Keychain.layer.pipe(Layer.provide([NodeServices.layer, Ref0Config.layer]));
|
|
34423
35081
|
const harnessProcess = HarnessProcess.layer.pipe(Layer.provide(NodeServices.layer));
|
|
@@ -34432,7 +35090,7 @@ const services = Layer.mergeAll(cliOutput, Ref0.layer.pipe(Layer.provide([
|
|
|
34432
35090
|
Ref0Config.layer,
|
|
34433
35091
|
sessionToken,
|
|
34434
35092
|
FetchHttpClient.layer
|
|
34435
|
-
])), 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));
|
|
34436
35094
|
//#endregion
|
|
34437
35095
|
//#region src/main.ts
|
|
34438
35096
|
ref0.pipe(Command.run({ version: cliVersion }), Effect.provide(services), NodeRuntime.runMain);
|