@defdone/annon 0.1.0-alpha.1 → 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 (87) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/CLI.md +173 -13
  3. package/README.md +47 -10
  4. package/dist/api/annon-types.d.ts +2 -1
  5. package/dist/api/annon.js +9 -2
  6. package/dist/browser/index.d.ts +1 -1
  7. package/dist/browser/login-bundle.js +61 -60
  8. package/dist/browser/login-dom.d.ts +13 -0
  9. package/dist/browser/login-dom.js +71 -0
  10. package/dist/browser/login-dropdown.d.ts +32 -0
  11. package/dist/browser/login-dropdown.js +129 -0
  12. package/dist/browser/login-format.d.ts +16 -0
  13. package/dist/browser/login-format.js +52 -0
  14. package/dist/browser/login-status.d.ts +70 -0
  15. package/dist/browser/login-status.js +85 -0
  16. package/dist/browser/login-steps.d.ts +6 -0
  17. package/dist/browser/login-steps.js +35 -0
  18. package/dist/browser/login-terms.d.ts +10 -0
  19. package/dist/browser/login-terms.js +69 -0
  20. package/dist/browser/login-view.d.ts +34 -0
  21. package/dist/browser/login-view.js +227 -0
  22. package/dist/browser/login-wallet.d.ts +28 -0
  23. package/dist/browser/login-wallet.js +179 -0
  24. package/dist/browser/login.js +154 -142
  25. package/dist/browser/purchase-bundle.js +29 -29
  26. package/dist/browser/purchases.d.ts +4 -0
  27. package/dist/browser/purchases.js +14 -0
  28. package/dist/browser/reconcile.d.ts +41 -0
  29. package/dist/browser/reconcile.js +77 -0
  30. package/dist/node/adapter-checks.d.ts +3 -1
  31. package/dist/node/adapter-checks.js +3 -1
  32. package/dist/node/adapter.js +10 -3
  33. package/dist/node/agent.js +2 -1
  34. package/dist/node/approval-status.d.ts +34 -0
  35. package/dist/node/approval-status.js +71 -0
  36. package/dist/node/backup-secret-init.d.ts +9 -0
  37. package/dist/node/backup-secret-init.js +103 -0
  38. package/dist/node/backup-secret.d.ts +35 -0
  39. package/dist/node/backup-secret.js +104 -0
  40. package/dist/node/browser-login.d.ts +2 -0
  41. package/dist/node/browser-login.js +95 -16
  42. package/dist/node/buy-commands.js +17 -9
  43. package/dist/node/buy-flow.d.ts +18 -0
  44. package/dist/node/buy-flow.js +89 -0
  45. package/dist/node/cli-flags.d.ts +10 -0
  46. package/dist/node/cli-flags.js +68 -0
  47. package/dist/node/cli.d.ts +9 -1
  48. package/dist/node/cli.js +87 -95
  49. package/dist/node/command-context.js +1 -1
  50. package/dist/node/config.d.ts +4 -0
  51. package/dist/node/config.js +6 -2
  52. package/dist/node/core-adapter.js +4 -0
  53. package/dist/node/login-page.d.ts +7 -0
  54. package/dist/node/login-page.js +77 -0
  55. package/dist/node/main.js +20 -2
  56. package/dist/node/output/discovery.d.ts +23 -0
  57. package/dist/node/output/discovery.js +52 -0
  58. package/dist/node/output/human.d.ts +10 -0
  59. package/dist/node/output/human.js +46 -0
  60. package/dist/node/output/purchase.d.ts +10 -0
  61. package/dist/node/output/purchase.js +78 -0
  62. package/dist/node/output/text.d.ts +10 -0
  63. package/dist/node/output/text.js +43 -0
  64. package/dist/node/purchase-commands.d.ts +2 -2
  65. package/dist/node/purchase-commands.js +14 -6
  66. package/dist/node/purchase-ref.d.ts +15 -0
  67. package/dist/node/purchase-ref.js +93 -0
  68. package/dist/node/saved-state.d.ts +4 -0
  69. package/dist/node/saved-state.js +50 -0
  70. package/dist/node/shell/complete.d.ts +2 -0
  71. package/dist/node/shell/complete.js +17 -0
  72. package/dist/node/shell/session.d.ts +20 -0
  73. package/dist/node/shell/session.js +219 -0
  74. package/dist/node/shell/welcome.d.ts +9 -0
  75. package/dist/node/shell/welcome.js +39 -0
  76. package/dist/node/shell/words.d.ts +1 -0
  77. package/dist/node/shell/words.js +36 -0
  78. package/dist/node/shops.d.ts +5 -0
  79. package/dist/node/shops.js +22 -0
  80. package/dist/node/version.d.ts +1 -0
  81. package/dist/node/version.js +3 -0
  82. package/dist/node/wallets.d.ts +1 -1
  83. package/dist/node/wallets.js +2 -10
  84. package/dist/pay/purchase.d.ts +2 -1
  85. package/dist/wallet/uncertain.d.ts +6 -3
  86. package/dist/wallet/uncertain.js +4 -1
  87. package/package.json +1 -1
package/dist/node/cli.js CHANGED
@@ -1,62 +1,14 @@
1
1
  import { parseArgs } from 'node:util';
2
- import { publicError, usage } from './errors.js';
3
- const common = [
4
- 'no-open',
5
- 'json',
6
- 'config',
7
- 'contract-id',
8
- 'near-rpc',
9
- 'base-rpc',
10
- 'network',
11
- 'state-dir',
12
- 'relayer',
13
- 'trust-contract',
14
- 'wallet',
15
- 'evm-key-file',
16
- 'solana-key-file',
17
- 'near-key-file',
18
- 'near-account',
19
- 'near-public-key',
20
- 'backup-secret-file',
21
- ];
22
- const perCommand = {
23
- shops: ['search', 'limit', 'offset'],
24
- shop: ['id'],
25
- catalog: ['shop', 'search', 'limit', 'offset'],
26
- fund: [],
27
- budget: [],
28
- login: [],
29
- init: [
30
- 'signer',
31
- 'credential-env',
32
- 'credential-keychain',
33
- 'agent',
34
- 'recovery',
35
- 'key-env',
36
- 'keychain-entry',
37
- 'new-key',
38
- 'daily-budget',
39
- ],
40
- share: ['resume', 'price', 'shop', 'title', 'description', 'storage-budget', 'listing-storage'],
41
- sell: ['resume', 'price', 'split', 'base', 'moderator', 'output', 'storage-budget'],
42
- buy: [
43
- 'force',
44
- 'origin',
45
- 'route',
46
- 'from-chain',
47
- 'from-asset',
48
- 'refund-to',
49
- 'recovery',
50
- 'max-price',
51
- 'moderator',
52
- 'url',
53
- ],
54
- open: ['purchase', 'file', 'url', 'output'],
55
- recover: ['recovery', 'purchase'],
56
- stop: [],
57
- verify: [],
58
- status: [],
59
- };
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';
7
+ import { version } from './version.js';
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'];
60
12
  export function parseCli(argv) {
61
13
  try {
62
14
  const keys = [...new Set([...common, ...Object.values(perCommand).flat()])];
@@ -73,18 +25,15 @@ export function parseCli(argv) {
73
25
  (command === 'fund' || command === 'shop' ? extra.length !== 1 : extra.length !== 0))
74
26
  throw usage();
75
27
  const cmd = command;
76
- if ((['recover', 'login', 'init', 'shops', 'catalog'].includes(cmd) && target !== undefined) ||
77
- (!['recover', 'login', 'init', 'shops', 'catalog'].includes(cmd) && !target))
28
+ if ((noTarget.includes(cmd) && target !== undefined) ||
29
+ (![...noTarget, ...optionalTarget].includes(cmd) && !target))
78
30
  throw usage();
79
31
  if (cmd === 'shop' && target !== 'add')
80
32
  throw usage();
81
33
  const allowed = [...common, ...perCommand[cmd]];
82
34
  if (Object.keys(parsed.values).some((k) => !allowed.includes(k)))
83
35
  throw usage();
84
- const values = Object.fromEntries(Object.entries(parsed.values).map(([k, v]) => [
85
- k.replace(/-([a-z])/g, (_, c) => c.toUpperCase()),
86
- v,
87
- ]));
36
+ const values = Object.fromEntries(Object.entries(parsed.values).map(([k, v]) => [camel(k), v]));
88
37
  for (const key of ['limit', 'offset']) {
89
38
  if (values[key] !== undefined) {
90
39
  if (typeof values[key] !== 'string' || !/^(0|[1-9][0-9]*)$/.test(values[key]))
@@ -117,10 +66,18 @@ export function parseCli(argv) {
117
66
  else if (!values.price || !values.split)
118
67
  throw usage();
119
68
  }
120
- if (cmd === 'init' && values.agent !== true)
69
+ // Bare init only sets up the backup secret; signer and deployment options need --agent.
70
+ if (cmd === 'init' &&
71
+ values.agent !== true &&
72
+ Object.keys(values).some((k) => !bareInit.includes(k)))
121
73
  throw usage();
122
74
  if (cmd === 'recover' && !values.recovery)
123
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();
124
81
  if (values.route && !['near', 'base', '1click'].includes(String(values.route)))
125
82
  throw usage();
126
83
  if (values.wallet && !['evm', 'solana', 'near'].includes(String(values.wallet)))
@@ -136,10 +93,13 @@ export function parseCli(argv) {
136
93
  throw usage();
137
94
  }
138
95
  }
96
+ const versionFlags = ['--version', '-v'];
139
97
  const HELP = `annon <command> [options]
98
+ annon (no arguments, in a terminal) or annon shell: interactive session; type help inside
140
99
  shops [--search <q>] [--limit <n>] [--offset <n>]
141
100
  shop add <url> [--id <alias>]
142
101
  catalog [--shop <id>] [--search <q>] [--limit <n>] [--offset <n>]
102
+ init [--backup-secret-file <file>] [--state-dir <dir>] [--config <file>]
143
103
  login [--wallet evm|near] [--no-open]
144
104
  init --agent --daily-budget <USDC> [--recovery <kind:id>] [--key-env <name> | --keychain-entry <name> | --new-key | --signer <https origin> (--credential-env <name> | --credential-keychain <name>)]
145
105
  share <file> --price <USDC> [--shop <id>] [--title <title>] [--description <text>] [--listing-storage <NEAR>] [--storage-budget <NEAR>]
@@ -148,54 +108,86 @@ sell <file> --resume <file-id> [--output <PAY2>]
148
108
  fund <file-id> <NEAR>
149
109
  budget <file-id>
150
110
  sell <file> [--storage-budget <NEAR>] --price <USDC> --split <kind:id:bps,...> [--base] [--moderator <account>] [--output <PAY2>]
151
- 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>]
152
- 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>
153
113
  recover --recovery <kind:id> [--purchase <id>]
154
114
  stop <file-id>
155
115
  verify <PAY2|url>
156
- 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.
157
119
  Common: --json --config <file> --contract-id <account> --near-rpc <url> --base-rpc <url>
158
120
  --network <near:mainnet> --state-dir <dir> --relayer <url> --trust-contract
159
121
  --wallet evm|solana|near --evm-key-file <file> --solana-key-file <file> --near-key-file <file>
160
122
  --near-account <account> --near-public-key <ed25519:key> --backup-secret-file <file>
161
- Secrets: env ANNON_EVM_PRIVATE_KEY, ANNON_SOLANA_KEYPAIR, ANNON_NEAR_CREDENTIALS,
162
- ANNON_BACKUP_SECRET (64 hex); or explicitly named files. See packages/annon/CLI.md.
163
- Exit: 0 success (buy may need deposit), 1 operation failed, 2 arguments/config, 3 trust required, 4 purchase pending.
123
+ Backup secret: run init once; it creates ~/.config/annon/backup-secret (owner-only) and never
124
+ overwrites. --backup-secret-file or ANNON_BACKUP_SECRET_FILE, then ANNON_BACKUP_SECRET, override it.
125
+ Secrets: env ANNON_EVM_PRIVATE_KEY, ANNON_SOLANA_KEYPAIR, ANNON_NEAR_CREDENTIALS; or explicitly
126
+ named files. See packages/annon/CLI.md. --version prints the installed version.
127
+ Exit: 0 success (buy may need deposit), 1 operation failed, 2 arguments/config, 3 trust required, 4 purchase pending or uncertain.
164
128
  `;
165
- 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 = {}) {
166
163
  const json = argv.includes('--json') || argv.includes('--json=true');
164
+ const human = options.human === true && !json;
167
165
  if (argv.length === 0 || argv.includes('--help')) {
168
166
  write(HELP);
169
167
  return 0;
170
168
  }
169
+ // Answered before any config, agent policy, wallet, network or backup-secret step.
170
+ if (argv.some((a) => versionFlags.includes(a)) &&
171
+ argv.every((a) => [...versionFlags, '--json'].includes(a))) {
172
+ write(json ? JSON.stringify({ ok: true, result: { version } }) : version);
173
+ return 0;
174
+ }
171
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);
172
178
  const input = parseCli(argv);
173
- const result = await execute(input);
174
- if (!json && ['shops', 'catalog', 'share'].includes(input.command)) {
175
- const show = (value) => {
176
- const r = value;
177
- const rules = r.info?.rules ?? r.rules;
178
- if (rules)
179
- write(`Shop rules (${r.shop?.manifest ?? r.manifest}):\n${rules.text}`);
180
- };
181
- show(result);
182
- const items = result.items;
183
- if (Array.isArray(items))
184
- for (const item of items)
185
- show(item);
186
- }
187
- write(JSON.stringify({ ok: true, result }, null, json ? undefined : 2));
188
- return input.command === 'status' &&
189
- result &&
190
- typeof result === 'object' &&
191
- 'state' in result &&
192
- result.state === 'pending'
193
- ? 4
194
- : 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);
195
185
  }
196
186
  catch (error) {
197
187
  const safe = publicError(error);
198
- 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));
199
191
  return safe.exitCode;
200
192
  }
201
193
  }
@@ -65,7 +65,7 @@ async function pendingSetup(ctx, purchaseId) {
65
65
  return {
66
66
  purchaseId,
67
67
  state: 'pending',
68
- next: 'Check the payment; configure readable saved state and the matching backup secret to resume Base unlock. 1Click SUCCESS alone is not acceptance.',
68
+ next: 'Check the payment. To resume Base unlock, keep the saved state readable and restore its matching backup secret (or select its existing copy with ANNON_BACKUP_SECRET_FILE). 1Click SUCCESS alone is not acceptance.',
69
69
  };
70
70
  }
71
71
  /** Loads the command's wallet once; later calls reuse it. */
@@ -32,6 +32,10 @@ export interface Config extends DiscoveryConfig {
32
32
  bps: number;
33
33
  };
34
34
  }
35
+ /** Human default for saved records and backups; agent and explicit settings override it. */
36
+ export declare const defaultStateDir: () => string;
35
37
  export declare function loadConfig(o: Input, env: Environment, login?: boolean, forcedAgent?: boolean): Promise<Config>;
36
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;
37
41
  export declare function trustContract(c: Config, explicit: boolean, prompt?: TrustPrompt): Promise<void>;
@@ -8,6 +8,8 @@ import { validateDiscoveryConfig } from '../shops/discovery.js';
8
8
  import { agentPolicy, configPath, validateSigner } from './agent.js';
9
9
  import { AdapterError } from './errors.js';
10
10
  import { resourceAllowed } from './resources.js';
11
+ /** Human default for saved records and backups; agent and explicit settings override it. */
12
+ export const defaultStateDir = () => join(homedir(), '.local', 'share', 'annon');
11
13
  export async function loadConfig(o, env, login = false, forcedAgent = false) {
12
14
  const pinnedAgent = await agentPolicy(o, env, forcedAgent);
13
15
  try {
@@ -41,7 +43,7 @@ export async function loadConfig(o, env, login = false, forcedAgent = false) {
41
43
  stateDir: o.stateDir ??
42
44
  (agent ? undefined : env.ANNON_STATE_DIR) ??
43
45
  file.stateDir ??
44
- join(homedir(), '.local', 'share', 'annon'),
46
+ defaultStateDir(),
45
47
  };
46
48
  Object.assign(c, validateDiscoveryConfig(file));
47
49
  const workDir = (env.ANNON_AGENT === 'true' ? env.ANNON_WORK_DIR : undefined) ?? file.workDir;
@@ -175,11 +177,13 @@ export async function loadConfig(o, env, login = false, forcedAgent = false) {
175
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);
176
178
  }
177
179
  }
180
+ /** Trust is recorded per network, NEAR RPC and contract. */
181
+ export const trustId = (c) => JSON.stringify([c.network, c.nearRpc, c.contractId]);
178
182
  export async function trustContract(c, explicit, prompt) {
179
183
  const dir = join(c.stateDir, 'trust');
180
184
  await mkdir(dir, { recursive: true, mode: 0o700 });
181
185
  const store = fileStore(dir);
182
- const id = JSON.stringify([c.network, c.nearRpc, c.contractId]);
186
+ const id = trustId(c);
183
187
  if (await store.load(id))
184
188
  return;
185
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);
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The approval page shell and its stylesheet. Both are static: the page script fills in every
3
+ * request value as text. The stylesheet is served from the page's own origin (`style-src 'self'`),
4
+ * with no inline styles, web fonts or other resources.
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<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";
@@ -0,0 +1,77 @@
1
+ /**
2
+ * The approval page shell and its stylesheet. Both are static: the page script fills in every
3
+ * request value as text. The stylesheet is served from the page's own origin (`style-src 'self'`),
4
+ * with no inline styles, web fonts or other resources.
5
+ */
6
+ /** A small arch: the Annon mark. Inline SVG, so it needs no image source. */
7
+ const MARK = '<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>';
8
+ export 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">
9
+ <header class="brand">${MARK}<p class="eyebrow">ANNON CLI</p></header>
10
+ <h1 id="title">Waiting for the CLI</h1>
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>
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>
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>
16
+ <details id="current-technical" class="details"><summary>Technical details</summary><pre id="action"></pre></details>
17
+ </main></body></html>`;
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}
19
+ *{box-sizing:border-box;border-radius:0}
20
+ html{background:var(--paper)}
21
+ body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.5 var(--serif);-webkit-text-size-adjust:100%}
22
+ .card{max-width:640px;margin:0 auto;padding:40px 16px 56px}
23
+ .brand{display:flex;align-items:center;gap:10px;margin-bottom:20px}
24
+ .mark{fill:var(--ink);flex:none}
25
+ .eyebrow,.label,dt,.details summary,button{font-family:var(--mono);letter-spacing:.08em;text-transform:uppercase;font-size:12px}
26
+ .eyebrow{margin:0;color:var(--soft)}
27
+ h1{font-weight:500;font-size:clamp(28px,6vw,36px);line-height:1.15;margin:0 0 24px;letter-spacing:-.01em}
28
+ .label{margin:0 0 6px;color:var(--muted)}
29
+ .hint{margin:6px 0 0;color:var(--muted);font-size:15px}
30
+ .pair{border:1px solid var(--ink);background:var(--alt);padding:16px 20px;margin-bottom:28px}
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}
36
+ .summary{border-top:1px solid var(--line);padding-top:20px}
37
+ .amount{margin:0 0 12px;font-size:clamp(30px,8vw,42px);line-height:1.1}
38
+ .rows{margin:0}
39
+ .row{display:grid;grid-template-columns:minmax(0,11em) minmax(0,1fr);gap:4px 16px;padding:9px 0;border-bottom:1px solid var(--line)}
40
+ dt{color:var(--muted);padding-top:3px}
41
+ dd{margin:0;color:var(--soft);overflow-wrap:anywhere}
42
+ dd.mono{font:14px/1.5 var(--mono)}
43
+ .message,.terms{margin-top:20px}
44
+ pre{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}
45
+ .warnings{margin:20px 0 0;padding:12px 16px 12px 32px;border:1px solid var(--red);color:var(--red)}
46
+ .note{margin:20px 0 0;color:var(--soft)}
47
+ .act{margin-top:28px;border-top:1px solid var(--line);padding-top:20px}
48
+ .field{margin:0 0 16px;display:flex;flex-wrap:wrap;align-items:center;gap:8px 16px}
49
+ .picker{position:relative;flex:0 1 20em;min-width:0}
50
+ .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}
51
+ .picker-button::after{content:"";width:0;height:0;border:5px solid transparent;border-top-color:var(--ink);border-bottom:0;flex:none}
52
+ .picker-button[aria-expanded="true"]::after{border-top:0;border-bottom:5px solid var(--ink)}
53
+ .picker-button[aria-disabled="true"]{opacity:.4;cursor:not-allowed}
54
+ .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}
55
+ .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)}
56
+ .option+.option{border-top:1px solid var(--line)}
57
+ .option.active{background:var(--alt);color:var(--ink)}
58
+ .option[aria-selected="true"]{color:var(--ink);font-weight:600}
59
+ .option[aria-selected="true"]::before{content:"";width:6px;height:6px;background:var(--ink);flex:none;align-self:center}
60
+ .option[aria-disabled="true"]{color:var(--muted);cursor:not-allowed;opacity:.6}
61
+ .option-note{margin-left:auto;font-size:11px;font-weight:400;letter-spacing:.06em;text-transform:uppercase;color:var(--muted)}
62
+ .buttons{display:flex;flex-wrap:wrap;gap:12px}
63
+ button{padding:14px 22px;border:1px solid var(--ink);cursor:pointer}
64
+ .primary{background:var(--ink);color:var(--paper)}
65
+ .secondary{background:transparent;color:var(--ink)}
66
+ button:disabled{cursor:not-allowed;opacity:.4}
67
+ button:focus-visible,.picker-button:focus-visible,summary:focus-visible{outline:2px solid var(--red);outline-offset:2px}
68
+ .status{min-height:1.5em;margin:16px 0 0;color:var(--soft)}
69
+ .status.wait{color:var(--muted)}
70
+ .status.ok{color:var(--ink);border-left:3px solid var(--ink);padding-left:12px}
71
+ .status.error{color:var(--red);border-left:3px solid var(--red);padding-left:12px}
72
+ .details{margin-top:32px;border-top:1px solid var(--line);padding-top:12px}
73
+ .details summary{cursor:pointer;color:var(--muted);padding:6px 0}
74
+ .details pre{margin-top:10px}
75
+ [hidden]{display:none!important}
76
+ @media (max-width:480px){.row{grid-template-columns:1fr}.card{padding-top:28px}button{flex:1 1 100%}.picker{flex-basis:100%}}
77
+ `;
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 {};