@defdone/annon 0.1.0-alpha.2 → 0.1.0-alpha.3
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/CHANGELOG.md +18 -0
- package/CLI.md +58 -4
- package/README.md +6 -1
- package/dist/browser/login-bundle.js +51 -51
- package/dist/browser/login-dom.d.ts +1 -1
- package/dist/browser/login-dom.js +5 -1
- package/dist/browser/login-status.d.ts +1 -1
- package/dist/browser/login-status.js +1 -1
- package/dist/browser/login-steps.d.ts +6 -0
- package/dist/browser/login-steps.js +35 -0
- package/dist/browser/login.js +41 -4
- package/dist/node/adapter-checks.d.ts +3 -1
- package/dist/node/adapter-checks.js +3 -1
- package/dist/node/adapter.js +5 -1
- package/dist/node/agent.js +2 -1
- package/dist/node/browser-login.js +28 -3
- package/dist/node/buy-commands.js +9 -8
- package/dist/node/buy-flow.d.ts +18 -0
- package/dist/node/buy-flow.js +89 -0
- package/dist/node/cli-flags.d.ts +10 -0
- package/dist/node/cli-flags.js +68 -0
- package/dist/node/cli.d.ts +9 -1
- package/dist/node/cli.js +68 -92
- package/dist/node/config.d.ts +2 -0
- package/dist/node/config.js +3 -1
- package/dist/node/core-adapter.js +4 -0
- package/dist/node/login-page.d.ts +2 -2
- package/dist/node/login-page.js +7 -1
- package/dist/node/main.js +20 -2
- package/dist/node/output/discovery.d.ts +23 -0
- package/dist/node/output/discovery.js +52 -0
- package/dist/node/output/human.d.ts +10 -0
- package/dist/node/output/human.js +46 -0
- package/dist/node/output/purchase.d.ts +10 -0
- package/dist/node/output/purchase.js +78 -0
- package/dist/node/output/text.d.ts +10 -0
- package/dist/node/output/text.js +43 -0
- package/dist/node/purchase-commands.js +4 -0
- package/dist/node/purchase-ref.d.ts +15 -0
- package/dist/node/purchase-ref.js +93 -0
- package/dist/node/shell/complete.d.ts +2 -0
- package/dist/node/shell/complete.js +17 -0
- package/dist/node/shell/session.d.ts +20 -0
- package/dist/node/shell/session.js +219 -0
- package/dist/node/shell/welcome.d.ts +9 -0
- package/dist/node/shell/welcome.js +39 -0
- package/dist/node/shell/words.d.ts +1 -0
- package/dist/node/shell/words.js +36 -0
- package/dist/node/shops.d.ts +5 -0
- package/dist/node/shops.js +22 -0
- package/package.json +1 -1
package/dist/node/cli.js
CHANGED
|
@@ -1,64 +1,14 @@
|
|
|
1
1
|
import { parseArgs } from 'node:util';
|
|
2
|
-
import {
|
|
2
|
+
import { isRow } from './adapter-checks.js';
|
|
3
|
+
import { buyFlow } from './buy-flow.js';
|
|
4
|
+
import { camel, common, perCommand } from './cli-flags.js';
|
|
5
|
+
import { AdapterError, publicError, usage } from './errors.js';
|
|
6
|
+
import { humanError, humanResult } from './output/human.js';
|
|
3
7
|
import { version } from './version.js';
|
|
4
|
-
const common = [
|
|
5
|
-
'no-open',
|
|
6
|
-
'json',
|
|
7
|
-
'config',
|
|
8
|
-
'contract-id',
|
|
9
|
-
'near-rpc',
|
|
10
|
-
'base-rpc',
|
|
11
|
-
'network',
|
|
12
|
-
'state-dir',
|
|
13
|
-
'relayer',
|
|
14
|
-
'trust-contract',
|
|
15
|
-
'wallet',
|
|
16
|
-
'evm-key-file',
|
|
17
|
-
'solana-key-file',
|
|
18
|
-
'near-key-file',
|
|
19
|
-
'near-account',
|
|
20
|
-
'near-public-key',
|
|
21
|
-
'backup-secret-file',
|
|
22
|
-
];
|
|
23
|
-
const perCommand = {
|
|
24
|
-
shops: ['search', 'limit', 'offset'],
|
|
25
|
-
shop: ['id'],
|
|
26
|
-
catalog: ['shop', 'search', 'limit', 'offset'],
|
|
27
|
-
fund: [],
|
|
28
|
-
budget: [],
|
|
29
|
-
login: [],
|
|
30
|
-
init: [
|
|
31
|
-
'signer',
|
|
32
|
-
'credential-env',
|
|
33
|
-
'credential-keychain',
|
|
34
|
-
'agent',
|
|
35
|
-
'recovery',
|
|
36
|
-
'key-env',
|
|
37
|
-
'keychain-entry',
|
|
38
|
-
'new-key',
|
|
39
|
-
'daily-budget',
|
|
40
|
-
],
|
|
41
|
-
share: ['resume', 'price', 'shop', 'title', 'description', 'storage-budget', 'listing-storage'],
|
|
42
|
-
sell: ['resume', 'price', 'split', 'base', 'moderator', 'output', 'storage-budget'],
|
|
43
|
-
buy: [
|
|
44
|
-
'force',
|
|
45
|
-
'origin',
|
|
46
|
-
'route',
|
|
47
|
-
'from-chain',
|
|
48
|
-
'from-asset',
|
|
49
|
-
'refund-to',
|
|
50
|
-
'recovery',
|
|
51
|
-
'max-price',
|
|
52
|
-
'moderator',
|
|
53
|
-
'url',
|
|
54
|
-
],
|
|
55
|
-
open: ['purchase', 'file', 'url', 'output'],
|
|
56
|
-
recover: ['recovery', 'purchase'],
|
|
57
|
-
stop: [],
|
|
58
|
-
verify: [],
|
|
59
|
-
status: [],
|
|
60
|
-
};
|
|
61
8
|
const bareInit = ['json', 'config', 'stateDir', 'backupSecretFile'];
|
|
9
|
+
/** Without a target `status` and `open` use the latest purchase; these take none at all. */
|
|
10
|
+
const optionalTarget = ['status', 'open'];
|
|
11
|
+
const noTarget = ['recover', 'login', 'init', 'shops', 'catalog'];
|
|
62
12
|
export function parseCli(argv) {
|
|
63
13
|
try {
|
|
64
14
|
const keys = [...new Set([...common, ...Object.values(perCommand).flat()])];
|
|
@@ -75,18 +25,15 @@ export function parseCli(argv) {
|
|
|
75
25
|
(command === 'fund' || command === 'shop' ? extra.length !== 1 : extra.length !== 0))
|
|
76
26
|
throw usage();
|
|
77
27
|
const cmd = command;
|
|
78
|
-
if ((
|
|
79
|
-
(![
|
|
28
|
+
if ((noTarget.includes(cmd) && target !== undefined) ||
|
|
29
|
+
(![...noTarget, ...optionalTarget].includes(cmd) && !target))
|
|
80
30
|
throw usage();
|
|
81
31
|
if (cmd === 'shop' && target !== 'add')
|
|
82
32
|
throw usage();
|
|
83
33
|
const allowed = [...common, ...perCommand[cmd]];
|
|
84
34
|
if (Object.keys(parsed.values).some((k) => !allowed.includes(k)))
|
|
85
35
|
throw usage();
|
|
86
|
-
const values = Object.fromEntries(Object.entries(parsed.values).map(([k, v]) => [
|
|
87
|
-
k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()),
|
|
88
|
-
v,
|
|
89
|
-
]));
|
|
36
|
+
const values = Object.fromEntries(Object.entries(parsed.values).map(([k, v]) => [camel(k), v]));
|
|
90
37
|
for (const key of ['limit', 'offset']) {
|
|
91
38
|
if (values[key] !== undefined) {
|
|
92
39
|
if (typeof values[key] !== 'string' || !/^(0|[1-9][0-9]*)$/.test(values[key]))
|
|
@@ -126,6 +73,11 @@ export function parseCli(argv) {
|
|
|
126
73
|
throw usage();
|
|
127
74
|
if (cmd === 'recover' && !values.recovery)
|
|
128
75
|
throw usage();
|
|
76
|
+
// --shop and --search only select the catalog listing a row number refers to.
|
|
77
|
+
if (cmd === 'buy' &&
|
|
78
|
+
(values.shop !== undefined || values.search !== undefined) &&
|
|
79
|
+
!isRow(target))
|
|
80
|
+
throw usage();
|
|
129
81
|
if (values.route && !['near', 'base', '1click'].includes(String(values.route)))
|
|
130
82
|
throw usage();
|
|
131
83
|
if (values.wallet && !['evm', 'solana', 'near'].includes(String(values.wallet)))
|
|
@@ -143,6 +95,7 @@ export function parseCli(argv) {
|
|
|
143
95
|
}
|
|
144
96
|
const versionFlags = ['--version', '-v'];
|
|
145
97
|
const HELP = `annon <command> [options]
|
|
98
|
+
annon (no arguments, in a terminal) or annon shell: interactive session; type help inside
|
|
146
99
|
shops [--search <q>] [--limit <n>] [--offset <n>]
|
|
147
100
|
shop add <url> [--id <alias>]
|
|
148
101
|
catalog [--shop <id>] [--search <q>] [--limit <n>] [--offset <n>]
|
|
@@ -155,12 +108,14 @@ sell <file> --resume <file-id> [--output <PAY2>]
|
|
|
155
108
|
fund <file-id> <NEAR>
|
|
156
109
|
budget <file-id>
|
|
157
110
|
sell <file> [--storage-budget <NEAR>] --price <USDC> --split <kind:id:bps,...> [--base] [--moderator <account>] [--output <PAY2>]
|
|
158
|
-
buy <url|offer.json> [--route near|base|1click] [--origin <configured name>] [--from-chain <chain>] [--from-asset <1Click asset>] [--refund-to <address>] [--recovery <kind:id>] [--max-price <USDC> or configured cap] [--url <expected resource for offer.json>] [--moderator <trusted account>]
|
|
159
|
-
open
|
|
111
|
+
buy <url|offer.json|catalog row number --shop <id> [--search <q>]> [--open <file>] [--route near|base|1click] [--origin <configured name>] [--from-chain <chain>] [--from-asset <1Click asset>] [--refund-to <address>] [--recovery <kind:id>] [--max-price <USDC> or configured cap] [--url <expected resource for offer.json>] [--moderator <trusted account>]
|
|
112
|
+
open [purchase-id|id prefix|backup|PAY2] [--purchase <id|id prefix|backup>] [--file <PAY2> | --url <url>] --output <file>
|
|
160
113
|
recover --recovery <kind:id> [--purchase <id>]
|
|
161
114
|
stop <file-id>
|
|
162
115
|
verify <PAY2|url>
|
|
163
|
-
status
|
|
116
|
+
status [purchase-id|id prefix]
|
|
117
|
+
status and open without an id use this contract's latest purchase; an id prefix needs 8 or more
|
|
118
|
+
hex digits and a purchase saved for this contract and network.
|
|
164
119
|
Common: --json --config <file> --contract-id <account> --near-rpc <url> --base-rpc <url>
|
|
165
120
|
--network <near:mainnet> --state-dir <dir> --relayer <url> --trust-contract
|
|
166
121
|
--wallet evm|solana|near --evm-key-file <file> --solana-key-file <file> --near-key-file <file>
|
|
@@ -171,8 +126,42 @@ Secrets: env ANNON_EVM_PRIVATE_KEY, ANNON_SOLANA_KEYPAIR, ANNON_NEAR_CREDENTIALS
|
|
|
171
126
|
named files. See packages/annon/CLI.md. --version prints the installed version.
|
|
172
127
|
Exit: 0 success (buy may need deposit), 1 operation failed, 2 arguments/config, 3 trust required, 4 purchase pending or uncertain.
|
|
173
128
|
`;
|
|
174
|
-
|
|
129
|
+
/** 4: not settled. A pending status, or a buy whose payment may be on chain or is not accepted yet. */
|
|
130
|
+
function exitCode(input, result) {
|
|
131
|
+
if (!result)
|
|
132
|
+
return 0;
|
|
133
|
+
if (input.command === 'status' && result.state === 'pending')
|
|
134
|
+
return 4;
|
|
135
|
+
if (input.command !== 'buy')
|
|
136
|
+
return 0;
|
|
137
|
+
// A payment that may be on chain outranks a failed status check or open after it.
|
|
138
|
+
if (result.state === 'uncertain')
|
|
139
|
+
return 4;
|
|
140
|
+
const failed = (result.openError ?? result.statusError);
|
|
141
|
+
if (failed)
|
|
142
|
+
return failed.exitCode;
|
|
143
|
+
return result.state === 'uncertain' || result.settlement === 'pending' ? 4 : 0;
|
|
144
|
+
}
|
|
145
|
+
/** The script contract: shop rules as text in pretty mode, then one JSON envelope. */
|
|
146
|
+
function writeJson(input, result, compact, write) {
|
|
147
|
+
if (!compact && ['shops', 'catalog', 'share'].includes(input.command)) {
|
|
148
|
+
const show = (value) => {
|
|
149
|
+
const r = value;
|
|
150
|
+
const rules = r.info?.rules ?? r.rules;
|
|
151
|
+
if (rules)
|
|
152
|
+
write(`Shop rules (${r.shop?.manifest ?? r.manifest}):\n${rules.text}`);
|
|
153
|
+
};
|
|
154
|
+
show(result);
|
|
155
|
+
const items = result.items;
|
|
156
|
+
if (Array.isArray(items))
|
|
157
|
+
for (const item of items)
|
|
158
|
+
show(item);
|
|
159
|
+
}
|
|
160
|
+
write(JSON.stringify({ ok: true, result }, null, compact ? undefined : 2));
|
|
161
|
+
}
|
|
162
|
+
export async function runCli(argv, execute, write, options = {}) {
|
|
175
163
|
const json = argv.includes('--json') || argv.includes('--json=true');
|
|
164
|
+
const human = options.human === true && !json;
|
|
176
165
|
if (argv.length === 0 || argv.includes('--help')) {
|
|
177
166
|
write(HELP);
|
|
178
167
|
return 0;
|
|
@@ -184,34 +173,21 @@ export async function runCli(argv, execute, write) {
|
|
|
184
173
|
return 0;
|
|
185
174
|
}
|
|
186
175
|
try {
|
|
176
|
+
if (argv[0] === 'shell')
|
|
177
|
+
throw new AdapterError('invalid_arguments', 'The interactive session needs a terminal, takes no options and is not available in agent mode.', 2);
|
|
187
178
|
const input = parseCli(argv);
|
|
188
|
-
const result = await
|
|
189
|
-
if (
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
write(`Shop rules (${r.shop?.manifest ?? r.manifest}):\n${rules.text}`);
|
|
195
|
-
};
|
|
196
|
-
show(result);
|
|
197
|
-
const items = result.items;
|
|
198
|
-
if (Array.isArray(items))
|
|
199
|
-
for (const item of items)
|
|
200
|
-
show(item);
|
|
201
|
-
}
|
|
202
|
-
write(JSON.stringify({ ok: true, result }, null, json ? undefined : 2));
|
|
203
|
-
// 4: not settled. A pending status, or a buy whose payment may be on chain (uncertain).
|
|
204
|
-
return result &&
|
|
205
|
-
typeof result === 'object' &&
|
|
206
|
-
'state' in result &&
|
|
207
|
-
((input.command === 'status' && result.state === 'pending') ||
|
|
208
|
-
(input.command === 'buy' && result.state === 'uncertain'))
|
|
209
|
-
? 4
|
|
210
|
-
: 0;
|
|
179
|
+
const result = await buyFlow(input, execute, human, options);
|
|
180
|
+
if (human)
|
|
181
|
+
write(humanResult(input, result));
|
|
182
|
+
else
|
|
183
|
+
writeJson(input, result, json, write);
|
|
184
|
+
return exitCode(input, result);
|
|
211
185
|
}
|
|
212
186
|
catch (error) {
|
|
213
187
|
const safe = publicError(error);
|
|
214
|
-
write(
|
|
188
|
+
write(human
|
|
189
|
+
? humanError(safe)
|
|
190
|
+
: JSON.stringify({ ok: false, error: safe }, null, json ? undefined : 2));
|
|
215
191
|
return safe.exitCode;
|
|
216
192
|
}
|
|
217
193
|
}
|
package/dist/node/config.d.ts
CHANGED
|
@@ -36,4 +36,6 @@ export interface Config extends DiscoveryConfig {
|
|
|
36
36
|
export declare const defaultStateDir: () => string;
|
|
37
37
|
export declare function loadConfig(o: Input, env: Environment, login?: boolean, forcedAgent?: boolean): Promise<Config>;
|
|
38
38
|
export type TrustPrompt = (description: string) => Promise<boolean>;
|
|
39
|
+
/** Trust is recorded per network, NEAR RPC and contract. */
|
|
40
|
+
export declare const trustId: (c: Config) => string;
|
|
39
41
|
export declare function trustContract(c: Config, explicit: boolean, prompt?: TrustPrompt): Promise<void>;
|
package/dist/node/config.js
CHANGED
|
@@ -177,11 +177,13 @@ export async function loadConfig(o, env, login = false, forcedAgent = false) {
|
|
|
177
177
|
throw new AdapterError('invalid_config', 'Set contractId, network and nearRpc (plus baseRpc for Base operations) in the config file, environment or flags. Endpoints must use HTTPS (HTTP only on localhost).', 2);
|
|
178
178
|
}
|
|
179
179
|
}
|
|
180
|
+
/** Trust is recorded per network, NEAR RPC and contract. */
|
|
181
|
+
export const trustId = (c) => JSON.stringify([c.network, c.nearRpc, c.contractId]);
|
|
180
182
|
export async function trustContract(c, explicit, prompt) {
|
|
181
183
|
const dir = join(c.stateDir, 'trust');
|
|
182
184
|
await mkdir(dir, { recursive: true, mode: 0o700 });
|
|
183
185
|
const store = fileStore(dir);
|
|
184
|
-
const id =
|
|
186
|
+
const id = trustId(c);
|
|
185
187
|
if (await store.load(id))
|
|
186
188
|
return;
|
|
187
189
|
if (!explicit &&
|
|
@@ -46,6 +46,7 @@ import './origin.js';
|
|
|
46
46
|
import { buy, prepareBuy, verifyFile } from './buy-commands.js';
|
|
47
47
|
import { requestContext, } from './command-context.js';
|
|
48
48
|
import { open, recover, status, statusOnChain } from './purchase-commands.js';
|
|
49
|
+
import { withPurchaseRef } from './purchase-ref.js';
|
|
49
50
|
import { createSdk } from './relay-transport.js';
|
|
50
51
|
import { sell, share, stop } from './seller-commands.js';
|
|
51
52
|
import { fileBudget, fund, fundWithNearWallet } from './storage-commands.js';
|
|
@@ -59,6 +60,9 @@ function createCoreAdapter(env = process.env, d = {}) {
|
|
|
59
60
|
return async (o) => {
|
|
60
61
|
const c = await loadConfig(o, env, false, d.agent);
|
|
61
62
|
await trustContract(c, o.trustContract === true || c.trustContract === true, o.json || d.agent ? undefined : d.prompt);
|
|
63
|
+
// Agents always name the full purchase ID.
|
|
64
|
+
if ((o.command === 'status' || o.command === 'open') && !d.agent)
|
|
65
|
+
o = await withPurchaseRef(o, c);
|
|
62
66
|
const ctx = requestContext(env, d, fetcher, wait, o, c);
|
|
63
67
|
if (o.command === 'budget')
|
|
64
68
|
return fileBudget(ctx);
|
|
@@ -3,5 +3,5 @@
|
|
|
3
3
|
* request value as text. The stylesheet is served from the page's own origin (`style-src 'self'`),
|
|
4
4
|
* with no inline styles, web fonts or other resources.
|
|
5
5
|
*/
|
|
6
|
-
export declare const LOGIN_PAGE_HTML = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light\"><title>Annon wallet approval</title><link rel=\"stylesheet\" href=\"/app.css\"><script src=\"/app.js\" defer></script></head><body><main class=\"card\">\n<header class=\"brand\"><svg class=\"mark\" viewBox=\"0 0 24 28\" width=\"22\" height=\"26\" aria-hidden=\"true\"><path d=\"M2 27V12a10 10 0 0 1 20 0v15h-5V12a5 5 0 0 0-10 0v15z\"/></svg><p class=\"eyebrow\">ANNON CLI</p></header>\n<h1 id=\"title\">Waiting for the CLI</h1>\n<section class=\"pair\" aria-labelledby=\"pair-label\"><p class=\"label\" id=\"pair-label\">Pairing code</p><p id=\"pair\" class=\"code\">------</p><p class=\"hint\">Check that your terminal shows the same code.</p></section>\n<section id=\"summary\" class=\"summary\" hidden><p id=\"amount\" class=\"amount\" hidden></p><dl id=\"rows\" class=\"rows\"></dl><div id=\"message-box\" class=\"message\" hidden><p id=\"message-label\" class=\"label\"></p><pre id=\"message\"></pre></div><ul id=\"warnings\" class=\"warnings\" hidden></ul><div id=\"terms-box\" class=\"terms\" hidden><p class=\"label\">Terms from the CLI</p><p class=\"hint\">For review. Your wallet signs only the request above.</p><dl id=\"terms\" class=\"rows\"></dl></div><p id=\"note\" class=\"note\"></p></section>\n<section class=\"act\"><div id=\"wallet-field\" class=\"field\" hidden><span class=\"label\" id=\"near-wallet-label\">NEAR wallet</span><div class=\"picker\"><div id=\"near-wallet\" class=\"picker-button\" role=\"combobox\" tabindex=\"0\" aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-controls=\"near-wallet-list\" aria-labelledby=\"near-wallet-label near-wallet-value\"><span id=\"near-wallet-value\">Meteor Wallet</span></div><ul id=\"near-wallet-list\" class=\"picker-list\" role=\"listbox\" aria-labelledby=\"near-wallet-label\" hidden><li id=\"near-wallet-meteor\" class=\"option\" role=\"option\" aria-selected=\"true\">Meteor Wallet<span class=\"option-note\">Extension or web popup</span></li><li id=\"near-wallet-hot\" class=\"option\" role=\"option\" aria-selected=\"false\">HOT Wallet<span class=\"option-note\">Mainnet only</span></li></ul></div></div><div class=\"buttons\"><button id=\"approve\" class=\"primary\" disabled>Review in wallet</button><button id=\"cancel\" class=\"secondary\" hidden>Cancel</button></div><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></p></section>\n<details class=\"details\"><summary>Technical details</summary><pre id=\"action\"></pre></details>\n</main></body></html>";
|
|
7
|
-
export declare const LOGIN_PAGE_CSS = ":root{--paper:#FAF8F2;--alt:#F1EDE3;--ink:#111111;--soft:#2E2C28;--muted:#4A4741;--line:#D9D3C7;--red:#B3261E;--serif:Newsreader,\"Iowan Old Style\",Georgia,serif;--mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace}\n*{box-sizing:border-box;border-radius:0}\nhtml{background:var(--paper)}\nbody{margin:0;background:var(--paper);color:var(--ink);font:16px/1.5 var(--serif);-webkit-text-size-adjust:100%}\n.card{max-width:640px;margin:0 auto;padding:40px 16px 56px}\n.brand{display:flex;align-items:center;gap:10px;margin-bottom:20px}\n.mark{fill:var(--ink);flex:none}\n.eyebrow,.label,dt,.details summary,button{font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;font-size:12px}\n.eyebrow{margin:0;color:var(--soft)}\nh1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24px;letter-spacing:-.01em}\n.label{margin:0 0 6px;color:var(--muted)}\n.hint{margin:6px 0 0;color:var(--muted);font-size:15px}\n.pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}\n.code{margin:0;font:600 clamp(30px,9vw,40px)/1.1 var(--mono);letter-spacing:.18em;word-break:break-all}\n.summary{border-top:1px solid var(--line);padding-top:20px}\n.amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}\n.rows{margin:0}\n.row{display:grid;grid-template-columns:minmax(0,11em) minmax(0,1fr);gap:4px 16px;padding:9px 0;border-bottom:1px solid var(--line)}\ndt{color:var(--muted);padding-top:3px}\ndd{margin:0;color:var(--soft);overflow-wrap:anywhere}\ndd.mono{font:14px/1.5 var(--mono)}\n.message,.terms{margin-top:20px}\npre{margin:0;padding:14px 16px;background:var(--alt);border:1px solid var(--line);font:13px/1.55 var(--mono);color:var(--soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:22em;overflow:auto}\n.warnings{margin:20px 0 0;padding:12px 16px 12px 32px;border:1px solid var(--red);color:var(--red)}\n.note{margin:20px 0 0;color:var(--soft)}\n.act{margin-top:28px;border-top:1px solid var(--line);padding-top:20px}\n.field{margin:0 0 16px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}\n.picker{position:relative;flex:0 1 20em;min-width:0}\n.picker-button{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--paper);color:var(--ink);border:1px solid var(--ink);padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);user-select:none}\n.picker-button::after{content:\"\";width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);border-bottom:0;flex:none}\n.picker-button[aria-expanded=\"true\"]::after{border-top:0;border-bottom:5px solid var(--ink)}\n.picker-button[aria-disabled=\"true\"]{opacity:.4;cursor:not-allowed}\n.picker-list{position:absolute;z-index:2;left:0;right:0;top:100%;margin:0;padding:0;list-style:none;background:var(--paper);border:1px solid var(--ink);border-top:0}\n.option{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);color:var(--soft)}\n.option+.option{border-top:1px solid var(--line)}\n.option.active{background:var(--alt);color:var(--ink)}\n.option[aria-selected=\"true\"]{color:var(--ink);font-weight:600}\n.option[aria-selected=\"true\"]::before{content:\"\";width:6px;height:6px;background:var(--ink);flex:none;align-self:center}\n.option[aria-disabled=\"true\"]{color:var(--muted);cursor:not-allowed;opacity:.6}\n.option-note{margin-left:auto;font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}\n.buttons{display:flex;flex-wrap:wrap;gap:12px}\nbutton{padding:14px 22px;border:1px solid var(--ink);cursor:pointer}\n.primary{background:var(--ink);color:var(--paper)}\n.secondary{background:transparent;color:var(--ink)}\nbutton:disabled{cursor:not-allowed;opacity:.4}\nbutton:focus-visible,.picker-button:focus-visible,summary:focus-visible{outline:2px solid var(--red);outline-offset:2px}\n.status{min-height:1.5em;margin:16px 0 0;color:var(--soft)}\n.status.wait{color:var(--muted)}\n.status.ok{color:var(--ink);border-left:3px solid var(--ink);padding-left:12px}\n.status.error{color:var(--red);border-left:3px solid var(--red);padding-left:12px}\n.details{margin-top:32px;border-top:1px solid var(--line);padding-top:12px}\n.details summary{cursor:pointer;color:var(--muted);padding:6px 0}\n.details pre{margin-top:10px}\n[hidden]{display:none!important}\n@media (max-width:480px){.row{grid-template-columns:1fr}.card{padding-top:28px}button{flex:1 1 100%}.picker{flex-basis:100%}}\n";
|
|
6
|
+
export declare const LOGIN_PAGE_HTML = "<!doctype html><html lang=\"en\"><head><meta charset=\"utf-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"color-scheme\" content=\"light\"><title>Annon wallet approval</title><link rel=\"stylesheet\" href=\"/app.css\"><script src=\"/app.js\" defer></script></head><body><main class=\"card\">\n<header class=\"brand\"><svg class=\"mark\" viewBox=\"0 0 24 28\" width=\"22\" height=\"26\" aria-hidden=\"true\"><path d=\"M2 27V12a10 10 0 0 1 20 0v15h-5V12a5 5 0 0 0-10 0v15z\"/></svg><p class=\"eyebrow\">ANNON CLI</p></header>\n<h1 id=\"title\">Waiting for the CLI</h1>\n<section class=\"pair\" aria-labelledby=\"pair-label\"><p class=\"label\" id=\"pair-label\">Pairing code</p><p id=\"pair\" class=\"code\">------</p><p class=\"hint\">Check that your terminal shows the same code.</p></section>\n<div id=\"steps\" aria-label=\"Finished approvals\"></div>\n<p class=\"label\" id=\"step-state\">In progress</p>\n<section id=\"summary\" class=\"summary\" hidden><p id=\"amount\" class=\"amount\" hidden></p><dl id=\"rows\" class=\"rows\"></dl><div id=\"message-box\" class=\"message\" hidden><p id=\"message-label\" class=\"label\"></p><pre id=\"message\"></pre></div><ul id=\"warnings\" class=\"warnings\" hidden></ul><div id=\"terms-box\" class=\"terms\" hidden><p class=\"label\">Terms from the CLI</p><p class=\"hint\">For review. Your wallet signs only the request above.</p><dl id=\"terms\" class=\"rows\"></dl></div><p id=\"note\" class=\"note\"></p></section>\n<section class=\"act\"><div id=\"wallet-field\" class=\"field\" hidden><span class=\"label\" id=\"near-wallet-label\">NEAR wallet</span><div class=\"picker\"><div id=\"near-wallet\" class=\"picker-button\" role=\"combobox\" tabindex=\"0\" aria-haspopup=\"listbox\" aria-expanded=\"false\" aria-controls=\"near-wallet-list\" aria-labelledby=\"near-wallet-label near-wallet-value\"><span id=\"near-wallet-value\">Meteor Wallet</span></div><ul id=\"near-wallet-list\" class=\"picker-list\" role=\"listbox\" aria-labelledby=\"near-wallet-label\" hidden><li id=\"near-wallet-meteor\" class=\"option\" role=\"option\" aria-selected=\"true\">Meteor Wallet<span class=\"option-note\">Extension or web popup</span></li><li id=\"near-wallet-hot\" class=\"option\" role=\"option\" aria-selected=\"false\">HOT Wallet<span class=\"option-note\">Mainnet only</span></li></ul></div></div><div class=\"buttons\"><button id=\"approve\" class=\"primary\" disabled>Review in wallet</button><button id=\"cancel\" class=\"secondary\" hidden>Cancel</button></div><p id=\"status\" class=\"status\" role=\"status\" aria-live=\"polite\"></p></section>\n<details id=\"current-technical\" class=\"details\"><summary>Technical details</summary><pre id=\"action\"></pre></details>\n</main></body></html>";
|
|
7
|
+
export declare const LOGIN_PAGE_CSS = ":root{--paper:#FAF8F2;--alt:#F1EDE3;--ink:#111111;--soft:#2E2C28;--muted:#4A4741;--line:#D9D3C7;--red:#B3261E;--serif:Newsreader,\"Iowan Old Style\",Georgia,serif;--mono:\"IBM Plex Mono\",ui-monospace,\"SF Mono\",Menlo,monospace}\n*{box-sizing:border-box;border-radius:0}\nhtml{background:var(--paper)}\nbody{margin:0;background:var(--paper);color:var(--ink);font:16px/1.5 var(--serif);-webkit-text-size-adjust:100%}\n.card{max-width:640px;margin:0 auto;padding:40px 16px 56px}\n.brand{display:flex;align-items:center;gap:10px;margin-bottom:20px}\n.mark{fill:var(--ink);flex:none}\n.eyebrow,.label,dt,.details summary,button{font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;font-size:12px}\n.eyebrow{margin:0;color:var(--soft)}\nh1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24px;letter-spacing:-.01em}\n.label{margin:0 0 6px;color:var(--muted)}\n.hint{margin:6px 0 0;color:var(--muted);font-size:15px}\n.pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}\n.code{margin:0;font:600 clamp(30px,9vw,40px)/1.1 var(--mono);letter-spacing:.18em;word-break:break-all}\n.step{border-top:1px solid var(--line);padding:12px 0;margin-bottom:16px}\n.step>summary{cursor:pointer;display:flex;gap:16px;align-items:baseline}\n.done-mark{color:#23643B;font:12px var(--mono);white-space:nowrap}\n.step[open]>summary{margin-bottom:16px}\n.summary{border-top:1px solid var(--line);padding-top:20px}\n.amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}\n.rows{margin:0}\n.row{display:grid;grid-template-columns:minmax(0,11em) minmax(0,1fr);gap:4px 16px;padding:9px 0;border-bottom:1px solid var(--line)}\ndt{color:var(--muted);padding-top:3px}\ndd{margin:0;color:var(--soft);overflow-wrap:anywhere}\ndd.mono{font:14px/1.5 var(--mono)}\n.message,.terms{margin-top:20px}\npre{margin:0;padding:14px 16px;background:var(--alt);border:1px solid var(--line);font:13px/1.55 var(--mono);color:var(--soft);white-space:pre-wrap;overflow-wrap:anywhere;max-height:22em;overflow:auto}\n.warnings{margin:20px 0 0;padding:12px 16px 12px 32px;border:1px solid var(--red);color:var(--red)}\n.note{margin:20px 0 0;color:var(--soft)}\n.act{margin-top:28px;border-top:1px solid var(--line);padding-top:20px}\n.field{margin:0 0 16px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}\n.picker{position:relative;flex:0 1 20em;min-width:0}\n.picker-button{display:flex;align-items:center;justify-content:space-between;gap:12px;background:var(--paper);color:var(--ink);border:1px solid var(--ink);padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);user-select:none}\n.picker-button::after{content:\"\";width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);border-bottom:0;flex:none}\n.picker-button[aria-expanded=\"true\"]::after{border-top:0;border-bottom:5px solid var(--ink)}\n.picker-button[aria-disabled=\"true\"]{opacity:.4;cursor:not-allowed}\n.picker-list{position:absolute;z-index:2;left:0;right:0;top:100%;margin:0;padding:0;list-style:none;background:var(--paper);border:1px solid var(--ink);border-top:0}\n.option{display:flex;justify-content:space-between;align-items:baseline;gap:12px;padding:10px 12px;cursor:pointer;font:14px/1.4 var(--mono);color:var(--soft)}\n.option+.option{border-top:1px solid var(--line)}\n.option.active{background:var(--alt);color:var(--ink)}\n.option[aria-selected=\"true\"]{color:var(--ink);font-weight:600}\n.option[aria-selected=\"true\"]::before{content:\"\";width:6px;height:6px;background:var(--ink);flex:none;align-self:center}\n.option[aria-disabled=\"true\"]{color:var(--muted);cursor:not-allowed;opacity:.6}\n.option-note{margin-left:auto;font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}\n.buttons{display:flex;flex-wrap:wrap;gap:12px}\nbutton{padding:14px 22px;border:1px solid var(--ink);cursor:pointer}\n.primary{background:var(--ink);color:var(--paper)}\n.secondary{background:transparent;color:var(--ink)}\nbutton:disabled{cursor:not-allowed;opacity:.4}\nbutton:focus-visible,.picker-button:focus-visible,summary:focus-visible{outline:2px solid var(--red);outline-offset:2px}\n.status{min-height:1.5em;margin:16px 0 0;color:var(--soft)}\n.status.wait{color:var(--muted)}\n.status.ok{color:var(--ink);border-left:3px solid var(--ink);padding-left:12px}\n.status.error{color:var(--red);border-left:3px solid var(--red);padding-left:12px}\n.details{margin-top:32px;border-top:1px solid var(--line);padding-top:12px}\n.details summary{cursor:pointer;color:var(--muted);padding:6px 0}\n.details pre{margin-top:10px}\n[hidden]{display:none!important}\n@media (max-width:480px){.row{grid-template-columns:1fr}.card{padding-top:28px}button{flex:1 1 100%}.picker{flex-basis:100%}}\n";
|
package/dist/node/login-page.js
CHANGED
|
@@ -9,9 +9,11 @@ export const LOGIN_PAGE_HTML = `<!doctype html><html lang="en"><head><meta chars
|
|
|
9
9
|
<header class="brand">${MARK}<p class="eyebrow">ANNON CLI</p></header>
|
|
10
10
|
<h1 id="title">Waiting for the CLI</h1>
|
|
11
11
|
<section class="pair" aria-labelledby="pair-label"><p class="label" id="pair-label">Pairing code</p><p id="pair" class="code">------</p><p class="hint">Check that your terminal shows the same code.</p></section>
|
|
12
|
+
<div id="steps" aria-label="Finished approvals"></div>
|
|
13
|
+
<p class="label" id="step-state">In progress</p>
|
|
12
14
|
<section id="summary" class="summary" hidden><p id="amount" class="amount" hidden></p><dl id="rows" class="rows"></dl><div id="message-box" class="message" hidden><p id="message-label" class="label"></p><pre id="message"></pre></div><ul id="warnings" class="warnings" hidden></ul><div id="terms-box" class="terms" hidden><p class="label">Terms from the CLI</p><p class="hint">For review. Your wallet signs only the request above.</p><dl id="terms" class="rows"></dl></div><p id="note" class="note"></p></section>
|
|
13
15
|
<section class="act"><div id="wallet-field" class="field" hidden><span class="label" id="near-wallet-label">NEAR wallet</span><div class="picker"><div id="near-wallet" class="picker-button" role="combobox" tabindex="0" aria-haspopup="listbox" aria-expanded="false" aria-controls="near-wallet-list" aria-labelledby="near-wallet-label near-wallet-value"><span id="near-wallet-value">Meteor Wallet</span></div><ul id="near-wallet-list" class="picker-list" role="listbox" aria-labelledby="near-wallet-label" hidden><li id="near-wallet-meteor" class="option" role="option" aria-selected="true">Meteor Wallet<span class="option-note">Extension or web popup</span></li><li id="near-wallet-hot" class="option" role="option" aria-selected="false">HOT Wallet<span class="option-note">Mainnet only</span></li></ul></div></div><div class="buttons"><button id="approve" class="primary" disabled>Review in wallet</button><button id="cancel" class="secondary" hidden>Cancel</button></div><p id="status" class="status" role="status" aria-live="polite"></p></section>
|
|
14
|
-
<details class="details"><summary>Technical details</summary><pre id="action"></pre></details>
|
|
16
|
+
<details id="current-technical" class="details"><summary>Technical details</summary><pre id="action"></pre></details>
|
|
15
17
|
</main></body></html>`;
|
|
16
18
|
export const LOGIN_PAGE_CSS = `:root{--paper:#FAF8F2;--alt:#F1EDE3;--ink:#111111;--soft:#2E2C28;--muted:#4A4741;--line:#D9D3C7;--red:#B3261E;--serif:Newsreader,"Iowan Old Style",Georgia,serif;--mono:"IBM Plex Mono",ui-monospace,"SF Mono",Menlo,monospace}
|
|
17
19
|
*{box-sizing:border-box;border-radius:0}
|
|
@@ -27,6 +29,10 @@ h1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24
|
|
|
27
29
|
.hint{margin:6px 0 0;color:var(--muted);font-size:15px}
|
|
28
30
|
.pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}
|
|
29
31
|
.code{margin:0;font:600 clamp(30px,9vw,40px)/1.1 var(--mono);letter-spacing:.18em;word-break:break-all}
|
|
32
|
+
.step{border-top:1px solid var(--line);padding:12px 0;margin-bottom:16px}
|
|
33
|
+
.step>summary{cursor:pointer;display:flex;gap:16px;align-items:baseline}
|
|
34
|
+
.done-mark{color:#23643B;font:12px var(--mono);white-space:nowrap}
|
|
35
|
+
.step[open]>summary{margin-bottom:16px}
|
|
30
36
|
.summary{border-top:1px solid var(--line);padding-top:20px}
|
|
31
37
|
.amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}
|
|
32
38
|
.rows{margin:0}
|
package/dist/node/main.js
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
import { createInterface } from 'node:readline/promises';
|
|
3
3
|
import { createAdapter } from './adapter.js';
|
|
4
4
|
import { runCli } from './cli.js';
|
|
5
|
+
import { createSession } from './shell/session.js';
|
|
6
|
+
import { humanOutput, sessionAllowed, welcome } from './shell/welcome.js';
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
const notify = (message) => process.stderr.write(`${message}\n`);
|
|
9
|
+
if (await sessionAllowed(argv, process.stdin.isTTY === true && process.stdout.isTTY === true, process.env)) {
|
|
10
|
+
const session = createSession({
|
|
11
|
+
input: process.stdin,
|
|
12
|
+
output: process.stdout,
|
|
13
|
+
terminal: true,
|
|
14
|
+
welcome: await welcome(process.env),
|
|
15
|
+
adapter: (hooks) => createAdapter(process.env, hooks),
|
|
16
|
+
});
|
|
17
|
+
// A confirmed Ctrl-C leaves a command running; exiting ends it as it would end a one-shot run.
|
|
18
|
+
process.exit(await session.run());
|
|
19
|
+
}
|
|
5
20
|
const adapter = createAdapter(process.env, {
|
|
6
21
|
...(process.stdin.isTTY && process.stderr.isTTY
|
|
7
22
|
? {
|
|
@@ -16,6 +31,9 @@ const adapter = createAdapter(process.env, {
|
|
|
16
31
|
},
|
|
17
32
|
}
|
|
18
33
|
: {}),
|
|
19
|
-
notify
|
|
34
|
+
notify,
|
|
35
|
+
});
|
|
36
|
+
process.exitCode = await runCli(argv, adapter, (text) => process.stdout.write(`${text}\n`), {
|
|
37
|
+
human: await humanOutput(process.stdout.isTTY === true, process.env),
|
|
38
|
+
notify,
|
|
20
39
|
});
|
|
21
|
-
process.exitCode = await runCli(process.argv.slice(2), adapter, (text) => process.stdout.write(`${text}\n`));
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { CatalogEntry, Shop, ShopFailure, ShopRules } from '../../shops/discovery.js';
|
|
2
|
+
import type { Input } from '../cli.js';
|
|
3
|
+
type Page<T> = {
|
|
4
|
+
items: T[];
|
|
5
|
+
total: number;
|
|
6
|
+
nextOffset: number | null;
|
|
7
|
+
};
|
|
8
|
+
export declare function shopsText(page: Page<Shop | ShopFailure>): string;
|
|
9
|
+
export declare const shopAddedText: (r: {
|
|
10
|
+
manifest: string;
|
|
11
|
+
id?: string;
|
|
12
|
+
trusted: boolean;
|
|
13
|
+
}) => string;
|
|
14
|
+
/** Row numbers count from the start of the listing, so `buy <n>` means the same row on any page. */
|
|
15
|
+
export declare const rowNumber: (input: Input, index: number) => number;
|
|
16
|
+
export declare function catalogText(input: Input, page: Page<CatalogEntry> & {
|
|
17
|
+
shop?: {
|
|
18
|
+
manifest: string;
|
|
19
|
+
name: string;
|
|
20
|
+
};
|
|
21
|
+
rules?: ShopRules;
|
|
22
|
+
}): string;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** Human output for `shops`, `shop add` and `catalog`. */
|
|
2
|
+
import { usdc } from '../../settle-near/codec.js';
|
|
3
|
+
import { cleanText, short, table, truncate } from './text.js';
|
|
4
|
+
const more = (page) => page.nextOffset === null ? [] : [`More: add --offset ${page.nextOffset}`];
|
|
5
|
+
export function shopsText(page) {
|
|
6
|
+
if (page.items.length === 0)
|
|
7
|
+
return 'No shops configured. Add one with: annon shop add <shop url> --id <name>';
|
|
8
|
+
return [
|
|
9
|
+
`Shops (${page.total}):`,
|
|
10
|
+
table(page.items.map((shop) => [
|
|
11
|
+
` ${cleanText(shop.id ?? '-')}`,
|
|
12
|
+
'info' in shop ? cleanText(shop.info.name) : `unavailable (${shop.error})`,
|
|
13
|
+
'info' in shop ? shop.info.shopUrl : shop.manifest,
|
|
14
|
+
shop.trusted ? 'trusted profile' : '',
|
|
15
|
+
])),
|
|
16
|
+
...more(page),
|
|
17
|
+
].join('\n');
|
|
18
|
+
}
|
|
19
|
+
export const shopAddedText = (r) => `Shop added${r.id ? ` as ${cleanText(r.id)}` : ''}: ${r.manifest}\nList its files with: annon catalog --shop ${cleanText(r.id ?? r.manifest)}`;
|
|
20
|
+
/** Row numbers count from the start of the listing, so `buy <n>` means the same row on any page. */
|
|
21
|
+
export const rowNumber = (input, index) => (input.offset ?? 0) + index + 1;
|
|
22
|
+
const routes = (e) => e.takenDown
|
|
23
|
+
? 'taken down'
|
|
24
|
+
: e.salesStopped
|
|
25
|
+
? 'sales stopped'
|
|
26
|
+
: ['near', ...(e.terms.base ? ['base'] : []), '1click'].join(', ');
|
|
27
|
+
const day = (seconds) => seconds === null ? '-' : new Date(seconds * 1000).toISOString().slice(0, 10);
|
|
28
|
+
/** The shop's rules in one line; the full text stays in `--json` and is shown by `share`. */
|
|
29
|
+
const rulesLine = (shop, rules) => `Shop rules${shop ? ` (${cleanText(shop.name)})` : ''}: ${truncate(cleanText(rules.text).replace(/\s+/g, ' ').trim(), 160)}`;
|
|
30
|
+
export function catalogText(input, page) {
|
|
31
|
+
if (page.items.length === 0)
|
|
32
|
+
return page.total === 0 ? 'No files.' : 'No files on this page.';
|
|
33
|
+
const first = rowNumber(input, 0);
|
|
34
|
+
const shop = input.shop === undefined ? '' : ` --shop ${input.shop}`;
|
|
35
|
+
return [
|
|
36
|
+
`Files ${first} to ${first + page.items.length - 1} of ${page.total}:`,
|
|
37
|
+
table([
|
|
38
|
+
[' #', 'Title', 'Price', 'Routes', 'Seller', 'Expires'],
|
|
39
|
+
...page.items.map((e, i) => [
|
|
40
|
+
String(rowNumber(input, i)).padStart(2),
|
|
41
|
+
truncate(cleanText(e.title || e.name), 40),
|
|
42
|
+
`${usdc(e.terms.near.price)} USDC`,
|
|
43
|
+
routes(e),
|
|
44
|
+
short(cleanText(`${e.terms.seller.kind}:${e.terms.seller.id}`)),
|
|
45
|
+
day(e.expiresAt),
|
|
46
|
+
]),
|
|
47
|
+
]),
|
|
48
|
+
...(page.rules ? [rulesLine(page.shop, page.rules)] : []),
|
|
49
|
+
...more(page),
|
|
50
|
+
`Buy a row with: annon buy <#>${shop}${input.search === undefined ? '' : ` --search ${JSON.stringify(input.search)}`}`,
|
|
51
|
+
].join('\n');
|
|
52
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a person reads in a terminal in place of the JSON envelope. `--json`, pipes and agent mode
|
|
3
|
+
* never come here, so scripts keep the envelope.
|
|
4
|
+
*/
|
|
5
|
+
import type { PublicError } from '../../errors.js';
|
|
6
|
+
import type { Input } from '../cli.js';
|
|
7
|
+
type Result = Record<string, unknown>;
|
|
8
|
+
export declare function humanResult(input: Input, result: Result | undefined): string;
|
|
9
|
+
export declare function humanError(error: PublicError): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { catalogText, shopAddedText, shopsText } from './discovery.js';
|
|
2
|
+
import { buyText, failure, loginText, openText, recoverText, statusText } from './purchase.js';
|
|
3
|
+
import { cleanText, fields } from './text.js';
|
|
4
|
+
/** A seller accepts the shop's rules by sharing, so `share` shows them whole, with their source. */
|
|
5
|
+
function shareText(r) {
|
|
6
|
+
const { rules, shop, ...rest } = r;
|
|
7
|
+
return [
|
|
8
|
+
...(rules ? [`Shop rules (${shop?.manifest}):\n${cleanText(rules.text, true)}`] : []),
|
|
9
|
+
fields(rest),
|
|
10
|
+
].join('\n');
|
|
11
|
+
}
|
|
12
|
+
export function humanResult(input, result) {
|
|
13
|
+
const r = result ?? {};
|
|
14
|
+
switch (input.command) {
|
|
15
|
+
case 'login':
|
|
16
|
+
return loginText(r);
|
|
17
|
+
case 'shops':
|
|
18
|
+
return shopsText(r);
|
|
19
|
+
case 'shop':
|
|
20
|
+
return shopAddedText(r);
|
|
21
|
+
case 'catalog':
|
|
22
|
+
return catalogText(input, r);
|
|
23
|
+
case 'buy':
|
|
24
|
+
return buyText(r);
|
|
25
|
+
case 'status':
|
|
26
|
+
return statusText(r);
|
|
27
|
+
case 'open':
|
|
28
|
+
return openText(r);
|
|
29
|
+
case 'recover':
|
|
30
|
+
return recoverText(r);
|
|
31
|
+
case 'share':
|
|
32
|
+
return shareText(r);
|
|
33
|
+
case 'verify':
|
|
34
|
+
return `Verified file ${r.fileId} (${r.checked}) against ${r.contractId}.`;
|
|
35
|
+
default:
|
|
36
|
+
return fields(r);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
export function humanError(error) {
|
|
40
|
+
const { code: _code, message: _message, exitCode: _exitCode, retryAfterSeconds, ...facts } = error;
|
|
41
|
+
return [
|
|
42
|
+
`Error: ${failure(error)}`,
|
|
43
|
+
...(Object.keys(facts).length > 0 ? [fields(facts)] : []),
|
|
44
|
+
...(retryAfterSeconds === undefined ? [] : [`Try again in ${retryAfterSeconds} seconds.`]),
|
|
45
|
+
].join('\n');
|
|
46
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Human output for `login`, `buy`, `status`, `open` and `recover`. */
|
|
2
|
+
import type { PublicError } from '../../errors.js';
|
|
3
|
+
type Result = Record<string, unknown>;
|
|
4
|
+
export declare const loginText: (r: Result) => string;
|
|
5
|
+
export declare const failure: (e: PublicError) => string;
|
|
6
|
+
export declare function buyText(r: Result): string;
|
|
7
|
+
export declare function statusText(r: Result): string;
|
|
8
|
+
export declare const openText: (r: Result) => string;
|
|
9
|
+
export declare function recoverText(r: Result): string;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { cleanText, networkName, table } from './text.js';
|
|
2
|
+
export const loginText = (r) => {
|
|
3
|
+
const identity = r.identity;
|
|
4
|
+
return `Signed in as ${cleanText(identity.id)} on ${networkName(String(r.network))}.`;
|
|
5
|
+
};
|
|
6
|
+
const paid = (r) => {
|
|
7
|
+
const payment = r.payment;
|
|
8
|
+
return payment ? `${payment.amountUsdc} USDC, ${payment.route} route` : `${r.route} route`;
|
|
9
|
+
};
|
|
10
|
+
export const failure = (e) => `${cleanText(e.message)} (${e.code})`;
|
|
11
|
+
/** The 1Click deposit a person makes from their own wallet, with the cutoff and any memo. */
|
|
12
|
+
function depositText(deposit, source) {
|
|
13
|
+
const amount = source?.amountFormatted === undefined
|
|
14
|
+
? `${deposit.amount} base units`
|
|
15
|
+
: `${source.amountFormatted} ${source.symbol ?? 'tokens'} (${deposit.amount} base units)`;
|
|
16
|
+
return [
|
|
17
|
+
'Send this deposit from your own wallet:',
|
|
18
|
+
table([
|
|
19
|
+
[' Amount', amount],
|
|
20
|
+
[' Asset', deposit.asset],
|
|
21
|
+
[' Chain', deposit.blockchain ?? deposit.chain],
|
|
22
|
+
[' To', deposit.address],
|
|
23
|
+
...(deposit.memo === undefined ? [] : [[' Memo', `${deposit.memo} (required)`]]),
|
|
24
|
+
[' Before', deposit.depositBefore],
|
|
25
|
+
].map((row) => row.map((cell) => cleanText(cell)))),
|
|
26
|
+
].join('\n');
|
|
27
|
+
}
|
|
28
|
+
export function buyText(r) {
|
|
29
|
+
const id = String(r.purchaseId);
|
|
30
|
+
const lines = [`Purchase ${id} (${paid(r)})`];
|
|
31
|
+
if (r.state === 'opened') {
|
|
32
|
+
const opened = r.opened;
|
|
33
|
+
lines.push(`Accepted by NEAR. Saved ${opened.output} (${opened.bytes} bytes).`);
|
|
34
|
+
}
|
|
35
|
+
else if (r.openError)
|
|
36
|
+
lines.push(`Accepted by NEAR, but the file was not saved: ${failure(r.openError)}`, 'The purchase is kept. Retry with: annon open --output <file>');
|
|
37
|
+
else if (r.state === 'accepted')
|
|
38
|
+
lines.push('Accepted by NEAR.', 'Save the file with: annon open --output <file>');
|
|
39
|
+
else {
|
|
40
|
+
if (r.state === 'uncertain' && r.txHash)
|
|
41
|
+
lines.push(`Transaction: ${cleanText(r.txHash)}`);
|
|
42
|
+
if (r.statusError)
|
|
43
|
+
lines.push(`Status check failed: ${failure(r.statusError)}`);
|
|
44
|
+
if (r.instructions)
|
|
45
|
+
lines.push(depositText(r.instructions, r.source));
|
|
46
|
+
lines.push(String(r.next));
|
|
47
|
+
if (r.instructions)
|
|
48
|
+
lines.push('Then: annon status, and annon open --output <file> (both use this purchase).');
|
|
49
|
+
}
|
|
50
|
+
lines.push(`Backup: ${r.backup}`);
|
|
51
|
+
return lines.join('\n');
|
|
52
|
+
}
|
|
53
|
+
export function statusText(r) {
|
|
54
|
+
const id = String(r.purchaseId);
|
|
55
|
+
if (r.state === 'accepted')
|
|
56
|
+
return `Purchase ${id} is accepted.\nSave the file with: annon open ${id} --output <file>`;
|
|
57
|
+
return [
|
|
58
|
+
`Purchase ${id} is ${r.state}${r.submission === 'uncertain' ? ' (the payment may have been sent)' : ''}.`,
|
|
59
|
+
...(r.txHash ? [`Transaction: ${cleanText(r.txHash)}`] : []),
|
|
60
|
+
String(r.next),
|
|
61
|
+
...(r.retryAfterSeconds === undefined
|
|
62
|
+
? []
|
|
63
|
+
: [`Check again in ${r.retryAfterSeconds} seconds.`]),
|
|
64
|
+
].join('\n');
|
|
65
|
+
}
|
|
66
|
+
export const openText = (r) => `Saved ${r.output} (${r.bytes} bytes) from purchase ${r.purchaseId}.`;
|
|
67
|
+
export function recoverText(r) {
|
|
68
|
+
const recovered = r.recovered;
|
|
69
|
+
const errors = r.errors;
|
|
70
|
+
return [
|
|
71
|
+
recovered.length === 0
|
|
72
|
+
? 'No purchases recovered.'
|
|
73
|
+
: `Recovered purchases (${recovered.length}):`,
|
|
74
|
+
...recovered.map((p) => ` ${p.purchaseId} file ${p.fileId}\n backup: ${p.backup}`),
|
|
75
|
+
...errors.map((e) => ` ${e.purchaseId} failed: ${cleanText(e.message)} (${e.code})`),
|
|
76
|
+
...(recovered.length > 0 ? ['Open one with: annon open <purchase id> --output <file>'] : []),
|
|
77
|
+
].join('\n');
|
|
78
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Shared pieces of the human output: safe text, short IDs, aligned tables and field lists. */
|
|
2
|
+
export declare function cleanText(value: unknown, lines?: boolean): string;
|
|
3
|
+
/** `0x1234abcd...ef01` for a long ID or address; short values are returned whole. */
|
|
4
|
+
export declare const short: (id: string) => string;
|
|
5
|
+
export declare const truncate: (text: string, max: number) => string;
|
|
6
|
+
/** Left-aligned columns, two spaces apart; the last column is not padded. */
|
|
7
|
+
export declare function table(rows: string[][]): string;
|
|
8
|
+
/** Every field of a result as `name: value`, so a command without its own layout hides nothing. */
|
|
9
|
+
export declare function fields(result: Record<string, unknown>): string;
|
|
10
|
+
export declare const networkName: (network: string) => string;
|