@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.
Files changed (51) hide show
  1. package/CHANGELOG.md +18 -0
  2. package/CLI.md +58 -4
  3. package/README.md +6 -1
  4. package/dist/browser/login-bundle.js +51 -51
  5. package/dist/browser/login-dom.d.ts +1 -1
  6. package/dist/browser/login-dom.js +5 -1
  7. package/dist/browser/login-status.d.ts +1 -1
  8. package/dist/browser/login-status.js +1 -1
  9. package/dist/browser/login-steps.d.ts +6 -0
  10. package/dist/browser/login-steps.js +35 -0
  11. package/dist/browser/login.js +41 -4
  12. package/dist/node/adapter-checks.d.ts +3 -1
  13. package/dist/node/adapter-checks.js +3 -1
  14. package/dist/node/adapter.js +5 -1
  15. package/dist/node/agent.js +2 -1
  16. package/dist/node/browser-login.js +28 -3
  17. package/dist/node/buy-commands.js +9 -8
  18. package/dist/node/buy-flow.d.ts +18 -0
  19. package/dist/node/buy-flow.js +89 -0
  20. package/dist/node/cli-flags.d.ts +10 -0
  21. package/dist/node/cli-flags.js +68 -0
  22. package/dist/node/cli.d.ts +9 -1
  23. package/dist/node/cli.js +68 -92
  24. package/dist/node/config.d.ts +2 -0
  25. package/dist/node/config.js +3 -1
  26. package/dist/node/core-adapter.js +4 -0
  27. package/dist/node/login-page.d.ts +2 -2
  28. package/dist/node/login-page.js +7 -1
  29. package/dist/node/main.js +20 -2
  30. package/dist/node/output/discovery.d.ts +23 -0
  31. package/dist/node/output/discovery.js +52 -0
  32. package/dist/node/output/human.d.ts +10 -0
  33. package/dist/node/output/human.js +46 -0
  34. package/dist/node/output/purchase.d.ts +10 -0
  35. package/dist/node/output/purchase.js +78 -0
  36. package/dist/node/output/text.d.ts +10 -0
  37. package/dist/node/output/text.js +43 -0
  38. package/dist/node/purchase-commands.js +4 -0
  39. package/dist/node/purchase-ref.d.ts +15 -0
  40. package/dist/node/purchase-ref.js +93 -0
  41. package/dist/node/shell/complete.d.ts +2 -0
  42. package/dist/node/shell/complete.js +17 -0
  43. package/dist/node/shell/session.d.ts +20 -0
  44. package/dist/node/shell/session.js +219 -0
  45. package/dist/node/shell/welcome.d.ts +9 -0
  46. package/dist/node/shell/welcome.js +39 -0
  47. package/dist/node/shell/words.d.ts +1 -0
  48. package/dist/node/shell/words.js +36 -0
  49. package/dist/node/shops.d.ts +5 -0
  50. package/dist/node/shops.js +22 -0
  51. package/package.json +1 -1
package/dist/node/cli.js CHANGED
@@ -1,64 +1,14 @@
1
1
  import { parseArgs } from 'node:util';
2
- import { publicError, usage } from './errors.js';
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 ((['recover', 'login', 'init', 'shops', 'catalog'].includes(cmd) && target !== undefined) ||
79
- (!['recover', 'login', 'init', 'shops', 'catalog'].includes(cmd) && !target))
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 <purchase-id|backup|PAY2> [--purchase <id|backup>] [--file <PAY2> | --url <url>] --output <file>
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 <purchase-id>
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
- export async function runCli(argv, execute, write) {
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 execute(input);
189
- if (!json && ['shops', 'catalog', 'share'].includes(input.command)) {
190
- const show = (value) => {
191
- const r = value;
192
- const rules = r.info?.rules ?? r.rules;
193
- if (rules)
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(JSON.stringify({ ok: false, error: safe }, null, json ? undefined : 2));
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
  }
@@ -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>;
@@ -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 = JSON.stringify([c.network, c.nearRpc, c.contractId]);
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";
@@ -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: (message) => process.stderr.write(`${message}\n`),
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;