@getcofund/cli 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Cofund
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,228 @@
1
- # Temporary Holding Version
1
+ # @getcofund/cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ `cofund-chain`: the Cofund protocol from the command line, on
4
+ [`@getcofund/protocol`](https://www.npmjs.com/package/@getcofund/protocol) alone. It reads from and sends to the
5
+ chain and nowhere else: no Cofund API, no server. Co-signers pass signatures
6
+ and PSBTs to each other as files, over whatever channel they choose.
7
+
8
+ ```sh
9
+ npm i -g @getcofund/cli
10
+ cofund-chain --help
11
+ ```
12
+
13
+ Node 20 or later.
14
+
15
+ ## Settings
16
+
17
+ | Setting | Flag | Environment |
18
+ |---|---|---|
19
+ | The keystore | `--keystore <file>` | `COFUND_KEYSTORE` (default `~/.cofund/keystore.json`) |
20
+ | The EVM deployment: a `deployments/<chainId>.json` | `--deployment <file>` | `COFUND_DEPLOYMENT` |
21
+ | The EVM chain's RPC | `--rpc <url>` | `COFUND_RPC_URL` |
22
+ | An Esplora API, for Bitcoin | `--esplora <url>` | `COFUND_ESPLORA_URL` |
23
+
24
+ A flag wins over its variable; a command that needs a setting neither gives
25
+ says which is missing. The deployment is checked against its chain before
26
+ anything is signed under it (`loadEvmDeployment`): its domain, the chain id,
27
+ and every helper State registers.
28
+
29
+ `--via direct` (the default) sends a call from the keystore's EVM key, which
30
+ pays gas. `--via guard --sponsor-key-file <file>` wraps it in the
31
+ deployment's guard (`sponsorGuard.relay`) and sends it from the sponsor's key
32
+ (a hex private key, or a keystore), which the guard must accept. Founding a
33
+ cofund (`createClient`) and seating a member (`addUser`) only accept the guard
34
+ or a Cofund admin as their sender, so they go through the guard.
35
+
36
+ ## The keystore
37
+
38
+ The same file as the API CLI `cofund`'s (`Keystore` from
39
+ `@getcofund/protocol/keys`), with mode 0600: one EVM key and one BIP-86
40
+ Bitcoin account, generated locally, so one key serves both CLIs. It pins the
41
+ company each cofund's messages carry: `found` and `join` pin it, and a command
42
+ acts in the keystore's only pinned cofund unless `--company` names another.
43
+ A key signs a file for a cofund it has not pinned only when `--company` names
44
+ that cofund.
45
+
46
+ `COFUND_BTC_NETWORK` (`mainnet`, `testnet` or `regtest`) picks the network of
47
+ new keys and vaults; regtest uses testnet keys.
48
+
49
+ ## Signature files
50
+
51
+ A signature is a JSON file:
52
+
53
+ ```json
54
+ {
55
+ "version": 1,
56
+ "type": "Transfer",
57
+ "domain": { "name": "Cofund", "version": "1", "chainId": 1, "verifyingContract": "0xD59e…02C0" },
58
+ "message": { "company": "acme", "policyId": "…", "messageType": "Operational_Expense", "amount": "50000000", "token": "0x…", "recipient": "0x…", "authId": "…" },
59
+ "signer": "0x…",
60
+ "signature": "0x…"
61
+ }
62
+ ```
63
+
64
+ `type` is one of the protocol's EIP-712 types (`InviteUser`, `RemoveUser`,
65
+ `RotateUser`, `CompleteOnboarding`, `TransferPolicyActivation`,
66
+ `TransactionPolicyActivation`, `PolicyDeactivation`, `Transfer`,
67
+ `Transaction`); a `Transaction` file also carries the `instructions` it signs
68
+ by hash. Every file is checked when it is loaded: the message is exactly its
69
+ type's fields (the types are always the package's, never the file's), the
70
+ domain is the deployment's, the signature recovers to the `signer` the file
71
+ claims, and every file a command takes is of a type it takes and over the
72
+ same message. Files are never overwritten.
73
+
74
+ ## Commands
75
+
76
+ Every command that signs or sends prints what it is about to do, checked by
77
+ the package's builders, and asks before it acts unless `--yes` is given. A
78
+ call is also run as an `eth_call` from its sender before anyone is asked, so
79
+ the checks that turn on the sender (the guard's sponsors, the platform gate,
80
+ the vault's broadcasters) are shown too. A refusal is shown with the
81
+ contract's error and why, and nothing is sent.
82
+
83
+ | Command | Does |
84
+ |---|---|
85
+ | `keys init [--force]` | Make the keystore |
86
+ | `keys show` | Its EVM address, its Bitcoin account xpub, the cofunds it pins |
87
+ | `found --name <name> [--company <company>] [--tier <tier>]` | Sign the founder's own seat and send `createClient`; the company is the name normalized unless `--company` gives it, the tier `BUSINESS` unless `--tier` names another |
88
+ | `show` | The cofund, its vault, its phase, its admins, its policies, this key's seat |
89
+ | `members` | Everyone ever seated, as State holds them now |
90
+ | `policies show <id>` | A policy, its signers by user id |
91
+ | `invite --user-id <id> --position <title> [--admin] --out <file>` | An admin signs a seat |
92
+ | `invite cosign <file>… --out <file>` | Another admin co-signs an admin seat |
93
+ | `join <file>…` | The invitee signs its half and sends `addUser` with the files' signatures |
94
+ | `members remove <user-id> [<file>…] [--out <file>]` | With `--out`, sign; without, send with the files' signatures, this key's added if it is an admin's not among them |
95
+ | `members rotate [<file>…] [--new-address <address>] [--user-id <id>] [--out <file>]` | The current key (or, for a lost key, admins) signs with `--out`; the new key adds its own and sends |
96
+ | `policies sign (--name … --signers <id,…> --threshold <n> (--token <address> --max-amount <amount> [--recipients <address,…>] \| --wrapper <address>) [--type <policy type>] \| <file>…) --out <file>` | An admin signs a policy's activation, new or the files' |
97
+ | `policies activate <file>…` | Send it with the admins' signatures |
98
+ | `policies deactivate <id> [<file>] [--out <file>]` | One admin's signature, this key's or the file's, sent |
99
+ | `transfers sign (--policy <id> --to <address> --amount <amount> \| <file>…) --out <file>` | A signer of a transfer policy signs a transfer |
100
+ | `transfers send <file>…` | Send it from the vault once signed often enough |
101
+ | `calls sign (--policy <id> --instructions <hex> [--allowance <token>:<amount>]… \| <file>…) --out <file>` | A signer of a call policy signs a call through its wrapper |
102
+ | `calls send <file>…` | Send it from the vault |
103
+ | `calls simulate <file>…` | Its effect on every balance it moves, simulated (`eth_simulateV1`) before the threshold is met |
104
+ | `onboarding complete [<file>] [--out <file>]` | One admin's `CompleteOnboarding`, sent |
105
+ | `btc vault create --xpubs <xpub,…> --threshold <k> [--type ALL\|ADMINS_INCLUDED\|ADMINS_REQUIRED] [--admins <xpub,…>] --out <file>` | A vault's definition from its members' account xpubs, and its address |
106
+ | `btc spend build --vault <file> --to <address> --amount <sats> [--fee-rate <sat/vB>] --out <file>` | The vault's confirmed coins selected through Esplora, and the unsigned PSBT |
107
+ | `btc spend sign <psbt> --vault <file> --to <address> --amount <sats> --out <file>` | Sign it, after checking it pays exactly that and spends only the vault |
108
+ | `btc spend combine <psbt>… --out <file>` | One PSBT with every signature |
109
+ | `btc spend finalize <psbt> --vault <file> --out <file>` | The witnesses, once a leaf has its threshold; writes the transaction's hex |
110
+ | `btc spend broadcast <hex or file>` | Send it through Esplora |
111
+
112
+ On EVM, amounts are whole tokens, converted with the token's own decimals,
113
+ and every preview shows the base units too. On Bitcoin they are sats. A vault
114
+ file holds `{ id, address, definition }`; every command that takes one
115
+ rebuilds the vault from its definition (`verifyBtcVault`) and refuses one
116
+ that does not derive its address. A PSBT file is base64.
117
+
118
+ ## A cofund, end to end
119
+
120
+ Three people, three machines. The deployment is one the founder trusts, its
121
+ guard accepts the sponsor's key, and each person's shell has
122
+ `COFUND_DEPLOYMENT` and `COFUND_RPC_URL` set.
123
+
124
+ ```sh
125
+ # Each of them.
126
+ cofund-chain keys init
127
+
128
+ # Ada founds Acme through the guard, and invites Ben as an admin and Cy as a member.
129
+ cofund-chain found --name Acme --via guard --sponsor-key-file sponsor.key
130
+ cofund-chain invite --user-id ben --position CFO --admin --out ben.json
131
+ cofund-chain invite --user-id cy --position Engineer --out cy.json
132
+
133
+ # Ben and Cy join with their invites (and pin "acme").
134
+ cofund-chain join ben.json --via guard --sponsor-key-file sponsor.key
135
+ cofund-chain join cy.json --via guard --sponsor-key-file sponsor.key
136
+
137
+ # A transfer policy: Ben and Cy, two of two, up to 1000 USDC a transfer.
138
+ cofund-chain policies sign --name Operations --signers ben,cy --threshold 2 \
139
+ --token 0xA0b8…eB48 --max-amount 1000 --out ops-ada.json # Ada
140
+ cofund-chain policies sign ops-ada.json --out ops-ben.json # Ben
141
+ cofund-chain policies activate ops-ada.json ops-ben.json # anyone
142
+ cofund-chain show # lists its id
143
+
144
+ # A transfer: both signers sign, either sends it from the vault.
145
+ cofund-chain transfers sign --policy <id> --to 0x… --amount 50 --out t-ben.json # Ben
146
+ cofund-chain transfers sign t-ben.json --out t-cy.json # Cy
147
+ cofund-chain transfers send t-ben.json t-cy.json # Cy
148
+
149
+ # Cy moves to a new key: the old key signs, the new keystore adds its own and sends.
150
+ cofund-chain members rotate --new-address 0x… --out rotate.json
151
+ cofund-chain members rotate rotate.json --company acme --keystore new.json
152
+
153
+ # Ada closes the founding phase: from now on admin seats and policies take the admins' consensus.
154
+ cofund-chain onboarding complete
155
+ ```
156
+
157
+ ## A Bitcoin vault, end to end
158
+
159
+ ```sh
160
+ # Each member: COFUND_BTC_NETWORK=testnet cofund-chain keys init, and shares the xpub keys show prints.
161
+ cofund-chain btc vault create --xpubs tpubA…,tpubB…,tpubC… --threshold 2 --out vault.json
162
+ # Fund the address it prints. Then, with COFUND_ESPLORA_URL set:
163
+ cofund-chain btc spend build --vault vault.json --to tb1q… --amount 700000 --out spend.psbt
164
+ # Two members, each on their machine, sign what they agreed it pays:
165
+ cofund-chain btc spend sign spend.psbt --vault vault.json --to tb1q… --amount 700000 --out b.psbt
166
+ cofund-chain btc spend sign spend.psbt --vault vault.json --to tb1q… --amount 700000 --out c.psbt
167
+ # Whoever collects them:
168
+ cofund-chain btc spend combine b.psbt c.psbt --out signed.psbt
169
+ cofund-chain btc spend finalize signed.psbt --vault vault.json --out spend.hex
170
+ cofund-chain btc spend broadcast spend.hex
171
+ ```
172
+
173
+ ## Local stack
174
+
175
+ From this repo, with Foundry, a cofund-solidity checkout and (for `--btc`)
176
+ Docker:
177
+
178
+ ```sh
179
+ # 1. Up: an Anvil with cofund-solidity's e2e contracts and a guard sponsor's key.
180
+ # Progress goes to stderr, the exports to stdout. A rerun reuses what is up.
181
+ pnpm -s --filter @getcofund/cli local-up # [--port <n>] [--btc]
182
+
183
+ # 2. The exports in this shell (fish: `pnpm -s --filter @getcofund/cli local-up | source`).
184
+ eval "$(pnpm -s --filter @getcofund/cli local-up)"
185
+
186
+ # 3. cofund-chain against it (here: `node packages/cli/src/cli.ts`).
187
+ cofund-chain keys init
188
+ cast rpc anvil_setBalance <its evm address> 0x56bc75e2d63100000 --rpc-url "$COFUND_RPC_URL"
189
+ cofund-chain found --name Acme --via guard --sponsor-key-file ~/.cofund/local/sponsor.key
190
+
191
+ # Done: stops only what local-up started.
192
+ pnpm --filter @getcofund/cli local-down
193
+ ```
194
+
195
+ `local-up` starts Anvil on a free port (or `--port`, or `LOCAL_ANVIL_PORT`),
196
+ never on one where a node it did not start answers, runs cofund-solidity's
197
+ `script/e2e-deploy.sh` on it, and writes `<chainId>.json` and `sponsor.key`
198
+ (Anvil account 10, one of the guard's sponsors) into its state dir,
199
+ `COFUND_LOCAL_DIR` (default `~/.cofund/local`). For Bitcoin it uses the shared
200
+ regtest Esplora at `http://127.0.0.1:3002` when it answers; otherwise `--btc`
201
+ starts a bitcoind and an electrs of its own in Docker. cofund-solidity is
202
+ `COFUND_SOLIDITY_DIR`, else the sibling of this repo's main checkout (as
203
+ `apps/cofund/scripts/e2e-up.sh` finds it).
204
+
205
+ ## Tests
206
+
207
+ `pnpm --filter @getcofund/cli test` runs the unit tests: each command's
208
+ arguments, previews and refusals against an in-memory chain, the signature
209
+ file checks, and the settings' precedence. Two integration tests run the
210
+ `cofund-chain` binary, one process per command, against real chains:
211
+
212
+ ```sh
213
+ # The EVM: cofund-solidity's e2e deploy script on a throwaway Anvil, a cofund's life
214
+ # sent directly and through the guard. Needs Foundry.
215
+ CLI_ANVIL=1 COFUND_SOLIDITY_DIR=../cofund-solidity pnpm --filter @getcofund/cli test
216
+
217
+ # Bitcoin: a bitcoind -regtest and an electrs of its own in Docker, a 2-of-3 vault
218
+ # created, funded, spent through PSBTs signed by two keystores, broadcast and mined.
219
+ CLI_REGTEST=1 pnpm --filter @getcofund/cli test
220
+ ```
221
+
222
+ `pnpm --filter @getcofund/cli smoke` packs the package, installs it outside
223
+ the repo next to the packed protocol, and runs `cofund-chain --help` and
224
+ `keys init`.
225
+
226
+ ## License
227
+
228
+ MIT
package/dist/chain.js ADDED
@@ -0,0 +1,115 @@
1
+ import { formatUnitsAmount, readContract, readUser, readUserIdByAddress, stateAbi, } from "@getcofund/protocol/evm";
2
+ import { decodeEventLog, encodeEventTopics, erc20Abi, keccak256, stringToHex, } from "viem";
3
+ /** The seat `address` holds in the cofund, active or not; null when none. */
4
+ export async function seatOf(evm, clientId, address) {
5
+ const userId = await readUserIdByAddress(evm.reader, evm.deployment, clientId, address);
6
+ if (userId === null) {
7
+ return null;
8
+ }
9
+ const user = await readUser(evm.reader, evm.deployment, clientId, userId);
10
+ return user ? { userId, user } : null;
11
+ }
12
+ /** `address`'s seat, which must be an active admin's, or why not. */
13
+ export async function requireAdmin(evm, clientId, address) {
14
+ const seat = await seatOf(evm, clientId, address);
15
+ if (!seat) {
16
+ throw new Error(`${address} holds no seat in this cofund`);
17
+ }
18
+ if (!(seat.user.active && seat.user.isAdmin)) {
19
+ throw new Error(`${address} (${seat.userId}) is not an active admin of this cofund`);
20
+ }
21
+ return seat;
22
+ }
23
+ /** A seat as previews name it: `id (admin, CFO, 0x…)`. */
24
+ export function seatText({ userId, user }) {
25
+ const role = user.isAdmin ? "admin" : "member";
26
+ const state = user.active ? "" : ", removed";
27
+ return `${userId} (${role}, ${user.position}, ${user.address}${state})`;
28
+ }
29
+ /** A symbol a token claims, shown only when it is a plain one. */
30
+ const SYMBOL = /^[\w.$-]{1,32}$/;
31
+ export async function tokenInfo(evm, address) {
32
+ let decimals;
33
+ try {
34
+ decimals = await readContract(evm.reader, {
35
+ address,
36
+ abi: erc20Abi,
37
+ functionName: "decimals",
38
+ args: [],
39
+ });
40
+ }
41
+ catch {
42
+ throw new Error(`${address} reports no decimals: it is not a token this CLI can take an amount in`);
43
+ }
44
+ const symbol = await readContract(evm.reader, {
45
+ address,
46
+ abi: erc20Abi,
47
+ functionName: "symbol",
48
+ args: [],
49
+ }).catch(() => null);
50
+ return {
51
+ address,
52
+ symbol: symbol && SYMBOL.test(symbol) ? symbol : null,
53
+ decimals,
54
+ };
55
+ }
56
+ const AMOUNT = /^(\d*)\.?(\d*)$/;
57
+ const NOT_ZERO = /[^0]/;
58
+ /**
59
+ * `--flag`'s whole-token amount in base units, exactly: positive, and no
60
+ * finer than the token's decimals (never rounded).
61
+ */
62
+ export function baseUnits(amount, token, flag) {
63
+ const match = AMOUNT.exec(amount);
64
+ const fraction = match?.[2] ?? "";
65
+ if (!match ||
66
+ match[1].length + fraction.length === 0 ||
67
+ NOT_ZERO.test(fraction.slice(token.decimals))) {
68
+ throw new Error(`${flag} takes whole tokens, to at most ${token.decimals} decimals, not ${amount}`);
69
+ }
70
+ const units = BigInt(`${match[1] || "0"}${fraction.padEnd(token.decimals, "0").slice(0, token.decimals)}`);
71
+ if (units <= 0n) {
72
+ throw new Error(`${flag} must be positive`);
73
+ }
74
+ return units;
75
+ }
76
+ /** `units` of `token` as every preview prints them: whole tokens and base units. */
77
+ export function amountText(units, token) {
78
+ const symbol = token.symbol ? `"${token.symbol}"` : "tokens";
79
+ return `${formatUnitsAmount(units, token.decimals)} ${symbol} (${units} base units of ${token.address})`;
80
+ }
81
+ /** State's `events` for the cofund, oldest first: there is no index, so they are read from the chain. */
82
+ async function stateEvents(evm, clientId, events) {
83
+ const logs = (await evm.reader.request({
84
+ method: "eth_getLogs",
85
+ params: [
86
+ {
87
+ address: evm.deployment.state,
88
+ fromBlock: "earliest",
89
+ toBlock: "latest",
90
+ topics: [
91
+ events.map((eventName) => encodeEventTopics({ abi: stateAbi, eventName })[0]),
92
+ clientId,
93
+ ],
94
+ },
95
+ ],
96
+ }));
97
+ return logs.map((log) => decodeEventLog({ abi: stateAbi, data: log.data, topics: log.topics }));
98
+ }
99
+ /** Every user id ever seated in the cofund, the founder first. */
100
+ export async function memberIds(evm, clientId) {
101
+ const ids = (await stateEvents(evm, clientId, ["NewClientCreated", "UserAdded"])).map((event) => event.eventName === "NewClientCreated"
102
+ ? event.args.adminId
103
+ : event.args.userId);
104
+ return [...new Set(ids)];
105
+ }
106
+ /** Every policy id ever activated in the cofund. */
107
+ export async function policyIds(evm, clientId) {
108
+ const ids = (await stateEvents(evm, clientId, ["PolicyActivated"])).map((event) => event.args.policyId);
109
+ return [...new Set(ids)];
110
+ }
111
+ /** A policy's signer ids (State keeps `keccak256(userId)`) named by the members they hash. */
112
+ export function signerNames(signerIds, members) {
113
+ const byHash = new Map(members.map((id) => [keccak256(stringToHex(id)), id]));
114
+ return signerIds.map((hash) => byHash.get(hash) ?? hash);
115
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,153 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * `cofund-chain`: the Cofund protocol from the command line, on
4
+ * `@getcofund/protocol` alone. It reads and sends to the chain and nowhere
5
+ * else; co-signers pass signatures and PSBTs as files. `--help` lists the
6
+ * commands.
7
+ */
8
+ import { realpathSync } from "node:fs";
9
+ import { createInterface } from "node:readline/promises";
10
+ import { pathToFileURL } from "node:url";
11
+ import { parseArgs } from "node:util";
12
+ import { btcSpendBroadcast, btcSpendBuild, btcSpendCombine, btcSpendFinalize, btcSpendSign, btcVaultCreate, } from "./commands/btc.js";
13
+ import { callsSend, callsSign, callsSimulate } from "./commands/calls.js";
14
+ import { found } from "./commands/found.js";
15
+ import { invite, inviteCosign, join } from "./commands/invite.js";
16
+ import { keysInit, keysShow } from "./commands/keys.js";
17
+ import { membersRemove, membersRotate } from "./commands/members.js";
18
+ import { onboardingComplete } from "./commands/onboarding.js";
19
+ import { policiesActivate, policiesDeactivate, policiesSign, } from "./commands/policies.js";
20
+ import { members, policiesShow, show } from "./commands/reads.js";
21
+ import { transfersSend, transfersSign } from "./commands/transfers.js";
22
+ import { errorLines } from "./context.js";
23
+ import { USAGE } from "./usage.js";
24
+ const OPTIONS = {
25
+ keystore: { type: "string" },
26
+ deployment: { type: "string" },
27
+ rpc: { type: "string" },
28
+ esplora: { type: "string" },
29
+ via: { type: "string" },
30
+ "sponsor-key-file": { type: "string" },
31
+ company: { type: "string" },
32
+ out: { type: "string" },
33
+ yes: { type: "boolean", short: "y" },
34
+ force: { type: "boolean" },
35
+ help: { type: "boolean", short: "h" },
36
+ name: { type: "string" },
37
+ tier: { type: "string" },
38
+ "user-id": { type: "string" },
39
+ position: { type: "string" },
40
+ admin: { type: "boolean" },
41
+ "new-address": { type: "string" },
42
+ signers: { type: "string" },
43
+ threshold: { type: "string" },
44
+ token: { type: "string" },
45
+ "max-amount": { type: "string" },
46
+ recipients: { type: "string" },
47
+ wrapper: { type: "string" },
48
+ type: { type: "string" },
49
+ policy: { type: "string" },
50
+ to: { type: "string" },
51
+ amount: { type: "string" },
52
+ instructions: { type: "string" },
53
+ allowance: { type: "string", multiple: true },
54
+ xpubs: { type: "string" },
55
+ admins: { type: "string" },
56
+ vault: { type: "string" },
57
+ "fee-rate": { type: "string" },
58
+ };
59
+ /** A command that takes no positionals. */
60
+ const bare = (run) => (io, flags, args) => args.length === 0 ? run(io, flags) : usage(io);
61
+ /** Every command by its name, the longest name a line starts with winning. */
62
+ const COMMANDS = {
63
+ "keys init": bare(keysInit),
64
+ "keys show": bare(keysShow),
65
+ found: bare(found),
66
+ show: bare(show),
67
+ members: bare(members),
68
+ "members remove": (io, flags, [userId, ...files]) => membersRemove(io, flags, userId, files),
69
+ "members rotate": membersRotate,
70
+ invite: bare(invite),
71
+ "invite cosign": inviteCosign,
72
+ join,
73
+ "policies show": (io, flags, [id, ...rest]) => rest.length === 0 ? policiesShow(io, flags, id) : usage(io),
74
+ "policies sign": policiesSign,
75
+ "policies activate": policiesActivate,
76
+ "policies deactivate": (io, flags, [id, ...files]) => policiesDeactivate(io, flags, id, files),
77
+ "transfers sign": transfersSign,
78
+ "transfers send": transfersSend,
79
+ "calls sign": callsSign,
80
+ "calls send": callsSend,
81
+ "calls simulate": callsSimulate,
82
+ "onboarding complete": onboardingComplete,
83
+ "btc vault create": bare(btcVaultCreate),
84
+ "btc spend build": bare(btcSpendBuild),
85
+ "btc spend sign": btcSpendSign,
86
+ "btc spend combine": btcSpendCombine,
87
+ "btc spend finalize": btcSpendFinalize,
88
+ "btc spend broadcast": btcSpendBroadcast,
89
+ };
90
+ function dispatch(io, flags, positionals) {
91
+ for (const words of [3, 2, 1]) {
92
+ const name = positionals.slice(0, words).join(" ");
93
+ if (positionals.length >= words && Object.hasOwn(COMMANDS, name)) {
94
+ return COMMANDS[name](io, flags, positionals.slice(words));
95
+ }
96
+ }
97
+ return usage(io);
98
+ }
99
+ function usage(io) {
100
+ io.err(USAGE);
101
+ return 1;
102
+ }
103
+ export async function runCli(argv, io) {
104
+ let parsed;
105
+ try {
106
+ parsed = parseArgs({
107
+ args: argv,
108
+ allowPositionals: true,
109
+ options: OPTIONS,
110
+ });
111
+ }
112
+ catch (error) {
113
+ io.err(error instanceof Error ? error.message : String(error));
114
+ io.err("`cofund-chain --help` lists the commands and their flags");
115
+ return 1;
116
+ }
117
+ const { values, positionals } = parsed;
118
+ if (values.help) {
119
+ io.out(USAGE);
120
+ return 0;
121
+ }
122
+ try {
123
+ return await dispatch(io, values, positionals);
124
+ }
125
+ catch (error) {
126
+ for (const text of errorLines(error)) {
127
+ io.err(text);
128
+ }
129
+ return 1;
130
+ }
131
+ }
132
+ if (process.argv[1] &&
133
+ import.meta.url === pathToFileURL(realpathSync(process.argv[1])).href) {
134
+ const tty = process.stdin.isTTY && process.stdout.isTTY;
135
+ runCli(process.argv.slice(2), {
136
+ env: process.env,
137
+ out: (text) => process.stdout.write(`${text}\n`),
138
+ err: (text) => process.stderr.write(`${text}\n`),
139
+ confirm: tty
140
+ ? async (question) => {
141
+ const rl = createInterface({
142
+ input: process.stdin,
143
+ output: process.stdout,
144
+ });
145
+ const answer = await rl.question(`${question} [y/N] `);
146
+ rl.close();
147
+ return answer.trim().toLowerCase() === "y";
148
+ }
149
+ : undefined,
150
+ }).then((code) => {
151
+ process.exitCode = code;
152
+ });
153
+ }