@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
@@ -4,7 +4,7 @@
4
4
  */
5
5
  import type { Status } from './login-status.js';
6
6
  import type { Summary } from './login-view.js';
7
- declare const IDS: readonly ["title", "pair", "summary", "amount", "rows", "message-box", "message-label", "message", "terms-box", "terms", "warnings", "note", "wallet-field", "approve", "status", "action"];
7
+ declare const IDS: readonly ["current-technical", "step-state", "title", "pair", "summary", "amount", "rows", "message-box", "message-label", "message", "terms-box", "terms", "warnings", "note", "wallet-field", "approve", "status", "action"];
8
8
  export type PageElements = Record<(typeof IDS)[number], HTMLElement>;
9
9
  export declare function pageElements(doc: Document): PageElements;
10
10
  /** Shows one request. Returns the element the caller updates with the time left. */
@@ -3,6 +3,8 @@
3
3
  * this module never parses markup.
4
4
  */
5
5
  const IDS = [
6
+ 'current-technical',
7
+ 'step-state',
6
8
  'title',
7
9
  'pair',
8
10
  'summary',
@@ -59,7 +61,9 @@ export function renderSummary(doc, el, s, technical) {
59
61
  el.note.textContent = s.note;
60
62
  el.action.textContent = technical;
61
63
  el.summary.hidden = false;
62
- return dds[dds.length - 1];
64
+ const expiry = dds[dds.length - 1];
65
+ expiry.setAttribute('data-expiry', '');
66
+ return expiry;
63
67
  }
64
68
  export function showStatus(el, status) {
65
69
  el.status.textContent = status.text;
@@ -48,7 +48,7 @@ export declare const STATUS: {
48
48
  };
49
49
  readonly signed: {
50
50
  readonly tone: "ok";
51
- readonly text: "Signed. Return to the terminal. Keep this tab open if the command asks for another approval.";
51
+ readonly text: "Signed. Return to the terminal. Keep this tab open until the command finishes.";
52
52
  };
53
53
  readonly redirected: {
54
54
  readonly tone: "ok";
@@ -60,7 +60,7 @@ export const STATUS = {
60
60
  verifying: { tone: 'wait', text: 'Signed. The CLI is checking the result.' },
61
61
  signed: {
62
62
  tone: 'ok',
63
- text: 'Signed. Return to the terminal. Keep this tab open if the command asks for another approval.',
63
+ text: 'Signed. Return to the terminal. Keep this tab open until the command finishes.',
64
64
  },
65
65
  redirected: {
66
66
  tone: 'ok',
@@ -0,0 +1,6 @@
1
+ import type { PageElements } from './login-dom.js';
2
+ /** Completed steps contain snapshots of text and review content, never live controls. */
3
+ export declare function approvalSteps(doc: Document, el: PageElements): {
4
+ start(): void;
5
+ finish(): void;
6
+ };
@@ -0,0 +1,35 @@
1
+ /** Completed steps contain snapshots of text and review content, never live controls. */
2
+ export function approvalSteps(doc, el) {
3
+ const list = doc.getElementById('steps');
4
+ return {
5
+ start() {
6
+ el['current-technical'].hidden = false;
7
+ el['step-state'].textContent = 'In progress';
8
+ },
9
+ finish() {
10
+ const step = doc.createElement('details');
11
+ step.className = 'step done';
12
+ const heading = doc.createElement('summary');
13
+ const mark = doc.createElement('span');
14
+ mark.className = 'done-mark';
15
+ mark.textContent = '✓ Done';
16
+ const title = doc.createElement('span');
17
+ title.textContent = el.title.textContent;
18
+ heading.append(mark, title);
19
+ const content = el.summary.cloneNode(true);
20
+ // Snapshots have no IDs, so only the current request owns page references.
21
+ content.removeAttribute('id');
22
+ for (const node of Array.from(content.querySelectorAll('[id]')))
23
+ node.removeAttribute('id');
24
+ for (const node of Array.from(content.querySelectorAll('[data-expiry]')))
25
+ node.textContent = 'Signed';
26
+ const technical = doc.createElement('pre');
27
+ technical.textContent = el.action.textContent;
28
+ step.append(heading, content, technical);
29
+ list.append(step);
30
+ el.summary.hidden = true;
31
+ el['current-technical'].hidden = true;
32
+ el['step-state'].textContent = 'Done';
33
+ },
34
+ };
35
+ }
@@ -3,6 +3,7 @@ import { pageElements, renderSummary, showStatus } from './login-dom.js';
3
3
  import { walletPicker } from './login-dropdown.js';
4
4
  import { expiryText } from './login-format.js';
5
5
  import { connected, SendUncertain, STATUS, walletFailure } from './login-status.js';
6
+ import { approvalSteps } from './login-steps.js';
6
7
  import { describeRequest, technicalDetails } from './login-view.js';
7
8
  import { pageWallets } from './login-wallet.js';
8
9
  Object.assign(globalThis, { Buffer });
@@ -13,7 +14,11 @@ const fragment = new URLSearchParams(location.hash.slice(1));
13
14
  const redirected = location.pathname === '/callback';
14
15
  let capability = redirected ? '' : location.hash.slice(1);
15
16
  history.replaceState(null, '', location.pathname);
16
- const api = async (path, body) => {
17
+ let paused = false;
18
+ let finished = false;
19
+ let pollTimer;
20
+ const steps = approvalSteps(document, el);
21
+ const api = async (path, body, accepted) => {
17
22
  const r = await fetch(path, {
18
23
  method: 'POST',
19
24
  headers: { authorization: `Bearer ${capability}`, 'content-type': 'application/json' },
@@ -24,11 +29,13 @@ const api = async (path, body) => {
24
29
  });
25
30
  if (!r.ok)
26
31
  throw new Error('Session expired or wallet result rejected. Return to the terminal.');
32
+ accepted?.();
27
33
  return r.json();
28
34
  };
29
35
  /** Requests whose result went to the CLI, or that ended here: never signed again. */
30
36
  const done = new Set();
31
37
  const callbacks = new Map();
38
+ const submitting = new Set();
32
39
  const wallets = pageWallets({
33
40
  expect: (state, id) => callbacks.set(state, id),
34
41
  forget: (state) => callbacks.delete(state),
@@ -60,12 +67,34 @@ function controls() {
60
67
  q && wallets.needsConnect(q, walletId()) ? 'Connect wallet' : 'Review in wallet';
61
68
  }
62
69
  async function submit(id, result) {
70
+ if (submitting.has(id))
71
+ return;
72
+ submitting.add(id);
73
+ paused = true;
74
+ clearTimeout(pollTimer);
63
75
  done.add(id);
64
76
  status(STATUS.verifying);
65
77
  controls();
66
78
  try {
67
- await api('/api/result', { id, result });
68
- status(STATUS.signed);
79
+ let accepted = false;
80
+ try {
81
+ const next = await api('/api/result', { id, result, follow: true }, () => {
82
+ accepted = true;
83
+ steps.finish();
84
+ finished = true;
85
+ status(STATUS.signed);
86
+ });
87
+ if (next.action && next.action.id !== id) {
88
+ show(next.action);
89
+ }
90
+ }
91
+ catch (error) {
92
+ if (!accepted)
93
+ throw error;
94
+ status(STATUS.signed);
95
+ }
96
+ paused = false;
97
+ pollTimer = setTimeout(() => void poll(), 1000);
69
98
  }
70
99
  catch {
71
100
  status(STATUS.rejected);
@@ -99,6 +128,8 @@ else {
99
128
  function show(q, sendStarted = false) {
100
129
  if (q && q.id !== current?.id) {
101
130
  current = q;
131
+ finished = false;
132
+ steps.start();
102
133
  expiry = renderSummary(document, el, describeRequest(q), technicalDetails(q));
103
134
  el['wallet-field'].hidden = !q.operation.startsWith('near.');
104
135
  picker.setDisabled('hot-wallet', q.chain !== 'near:mainnet');
@@ -125,19 +156,25 @@ function show(q, sendStarted = false) {
125
156
  controls();
126
157
  }
127
158
  async function poll() {
159
+ if (paused)
160
+ return;
128
161
  if (!capability) {
129
162
  status(STATUS.noCapability);
130
163
  return;
131
164
  }
132
165
  try {
133
166
  const r = await api('/api/action');
167
+ if (paused)
168
+ return;
134
169
  el.pair.textContent = String(r.pairingCode);
135
170
  show(r.action ?? undefined, r.sendStarted === true);
136
- setTimeout(() => {
171
+ pollTimer = setTimeout(() => {
137
172
  void poll();
138
173
  }, 1000);
139
174
  }
140
175
  catch (e) {
176
+ if (paused || finished)
177
+ return;
141
178
  status({ tone: 'error', text: e.message });
142
179
  capability = '';
143
180
  approve.disabled = true;
@@ -3,4 +3,6 @@ import type { Config } from './config.js';
3
3
  declare const fail: (code: AnnonErrorCode, message: string, exit?: PublicError["exitCode"]) => never;
4
4
  declare const isId: (id: string) => boolean;
5
5
  declare const backupPath: (c: Config, id: string) => string;
6
- export { backupPath, fail, isId };
6
+ /** A catalog row number in place of a URL: `buy 2 --shop demo`. */
7
+ declare const isRow: (target: string | undefined) => boolean;
8
+ export { backupPath, fail, isId, isRow };
@@ -10,4 +10,6 @@ const backupPath = (c, id) => {
10
10
  throw usage();
11
11
  return join(c.stateDir, 'backups', `${id.replaceAll(':', '-')}.json`);
12
12
  };
13
- export { backupPath, fail, isId };
13
+ /** A catalog row number in place of a URL: `buy 2 --shop demo`. */
14
+ const isRow = (target) => /^[1-9][0-9]{0,4}$/.test(target ?? '');
15
+ export { backupPath, fail, isId, isRow };
@@ -2,6 +2,7 @@
2
2
  import { createCoreAdapter } from './core-adapter.js';
3
3
  // core-adapter.js loads first: it imports the SDK modules in the order this file did, so they initialize as before.
4
4
  import { identityJson, parseIdentity } from '../settle-near/identity.js';
5
+ import { isRow } from './adapter-checks.js';
5
6
  import { agentPolicy, configPath, initAgent, savePublicConfig } from './agent.js';
6
7
  import { initBackupSecret, initPolicy } from './backup-secret-init.js';
7
8
  import { startBrowserSession } from './browser-login.js';
@@ -9,7 +10,7 @@ import { loadConfig } from './config.js';
9
10
  import { AdapterError } from './errors.js';
10
11
  import { osKeychain } from './keychain.js';
11
12
  import { confinePaths } from './paths.js';
12
- import { discoverShops, isDiscovery } from './shops.js';
13
+ import { catalogRow, discoverShops, isDiscovery } from './shops.js';
13
14
  import { assertSignerInput, configuredWallet } from './signer-wallet.js';
14
15
  import { signingRequest, verifySigningResult } from './signing.js';
15
16
  import { identity } from './wallets.js';
@@ -29,6 +30,9 @@ export function createAdapter(env = process.env, d = {}) {
29
30
  const c = await loadConfig(o, env, o.command === 'login', d.agent);
30
31
  if (agent || c.agent)
31
32
  o = await confinePaths(o, c.workDir);
33
+ // Agents buy allowed resource URLs only; a row number stays a refused target for them.
34
+ else if (o.command === 'buy' && isRow(o.target))
35
+ o = await catalogRow(o, env, d.notify, d.fetch);
32
36
  if (c.signer) {
33
37
  assertSignerInput(o);
34
38
  if (o.recovery && identityJson(identity(o.recovery)) !== identityJson(c.signer.identity))
@@ -47,6 +47,7 @@ export async function agentPolicy(o, env, forced = false) {
47
47
  o.command === 'login' ||
48
48
  (o.config !== undefined && resolve(o.config) !== resolve(pinned)) ||
49
49
  o.stateDir !== undefined ||
50
+ o.open !== undefined ||
50
51
  [
51
52
  o.contractId,
52
53
  o.nearRpc,
@@ -60,7 +61,7 @@ export async function agentPolicy(o, env, forced = false) {
60
61
  o.listingStorage,
61
62
  o.fundAmount,
62
63
  ].some((v) => v !== undefined)))
63
- throw new AdapterError('agent_policy', 'Agent mode pins deployment, trust, price and state policy; per-call overrides, init and login are refused.', 2);
64
+ throw new AdapterError('agent_policy', 'Agent mode pins deployment, trust, price and state policy; per-call overrides, buy --open, init and login are refused.', 2);
64
65
  return agent;
65
66
  }
66
67
  export async function savePublicConfig(path, patch) {
@@ -54,6 +54,12 @@ export async function startBrowserSession(o) {
54
54
  const sent = (p) => p.sending === true ||
55
55
  p.action.operation === 'near.signTransaction';
56
56
  let pending;
57
+ const followers = new Set();
58
+ const advance = (action = null) => {
59
+ for (const response of followers)
60
+ response.end(JSON.stringify({ action }));
61
+ followers.clear();
62
+ };
57
63
  const server = createServer((req, res) => {
58
64
  void (async () => {
59
65
  const headers = {
@@ -146,9 +152,16 @@ export async function startBrowserSession(o) {
146
152
  armIdle();
147
153
  }
148
154
  notify(APPROVED_LINE);
149
- p.resolve(b.result);
150
155
  complete(200);
151
- send(200, '{}');
156
+ if (b.follow === true) {
157
+ res.writeHead(200, { ...headers, 'content-type': 'application/json' });
158
+ res.flushHeaders();
159
+ followers.add(res);
160
+ res.on('close', () => followers.delete(res));
161
+ }
162
+ else
163
+ send(200, '{}');
164
+ p.resolve(b.result);
152
165
  }
153
166
  catch (error) {
154
167
  clearTimeout(p.timer);
@@ -198,8 +211,19 @@ export async function startBrowserSession(o) {
198
211
  pending.reject(new Error('session closed'));
199
212
  pending = undefined;
200
213
  }
214
+ const flushed = [...followers].map((response) => new Promise((resolve) => response.end(JSON.stringify({ action: null }), resolve)));
215
+ followers.clear();
216
+ const stopped = new Promise((resolve) => server.close(() => resolve()));
217
+ let flushTimer;
218
+ await Promise.race([
219
+ Promise.all(flushed),
220
+ new Promise((resolve) => {
221
+ flushTimer = setTimeout(resolve, 1000);
222
+ }),
223
+ ]);
224
+ clearTimeout(flushTimer);
201
225
  server.closeAllConnections();
202
- await new Promise((resolve) => server.close(() => resolve()));
226
+ await stopped;
203
227
  };
204
228
  let timeout;
205
229
  const armIdle = () => {
@@ -251,6 +275,7 @@ export async function startBrowserSession(o) {
251
275
  }, action.expiresAt - Date.now()),
252
276
  };
253
277
  pending = entry;
278
+ advance(entry.action);
254
279
  notify(waitingLine(action.expiresAt - Date.now()));
255
280
  }),
256
281
  };
@@ -1,5 +1,5 @@
1
1
  /** Buyer commands before a purchase exists: `verify` a file and `buy` it. */
2
- import { mkdir, readdir, readFile } from 'node:fs/promises';
2
+ import { mkdir, readFile } from 'node:fs/promises';
3
3
  import { join } from 'node:path';
4
4
  import { inspect, inspectHeader, MAX_HEADER, parseHeader } from '../envelope/pay2.js';
5
5
  import { AnnonError } from '../errors.js';
@@ -17,6 +17,7 @@ import { offer, resourceHeader, response } from './offers.js';
17
17
  import { originFor } from './origin.js';
18
18
  import { humanPaymentTerms, quotedSource, savedPayment, validatePaymentToken, } from './payment-terms.js';
19
19
  import { claimPurchase } from './purchase-guard.js';
20
+ import { rememberPurchase, savedRecordIds } from './purchase-ref.js';
20
21
  import { resourceAllowed } from './resources.js';
21
22
  /** `verify`: a local file's header and body, or a remote file's header, against its listing. */
22
23
  async function verifyFile(ctx) {
@@ -155,13 +156,10 @@ async function buy(ctx, preparedBuy) {
155
156
  const guardDir = join(c.stateDir, 'purchase-guards');
156
157
  await mkdir(guardDir, { recursive: true, mode: 0o700 });
157
158
  const known = [];
158
- for (const name of await readdir(storeDir)) {
159
- if (!/^[0-9a-f]{64}\.json$/.test(name))
159
+ for (const id of await savedRecordIds(storeDir)) {
160
+ if (!isId(id))
160
161
  continue;
161
- const envelope = JSON.parse(await readFile(join(storeDir, name), 'utf8'));
162
- if (!isId(envelope.id))
163
- continue;
164
- const saved = await sdk.store.load(purchaseKey(envelope.id));
162
+ const saved = await sdk.store.load(purchaseKey(id));
165
163
  if (saved?.purchase.file_id === found.fileId &&
166
164
  saved.purchase.contract === c.contractId &&
167
165
  saved.terms.network === (saved.purchase.route === 'base-usdc' ? 'eip155:8453' : c.network))
@@ -216,6 +214,7 @@ async function buy(ctx, preparedBuy) {
216
214
  if (renewalError)
217
215
  throw renewalError;
218
216
  await guard.saved(record.id);
217
+ await rememberPurchase(c, record.id);
219
218
  },
220
219
  recovery: buyer,
221
220
  ...(baseSponsorship ? { baseSponsorship } : {}),
@@ -288,7 +287,9 @@ async function buy(ctx, preparedBuy) {
288
287
  ...(source ? { source } : {}),
289
288
  next: bought.uncertain
290
289
  ? `The wallet may have sent this payment. Do not pay again. Run annon status ${bought.record.id} until it is accepted.`
291
- : 'Fund the deposit if instructed, then check status and open this purchase.',
290
+ : bought.instructions
291
+ ? 'Fund the deposit as instructed, then check status and open this purchase.'
292
+ : `Payment sent. Run annon status ${bought.record.id}, then annon open ${bought.record.id} --output <file>.`,
292
293
  };
293
294
  }
294
295
  catch (error) {
@@ -0,0 +1,18 @@
1
+ import type { Input } from './cli.js';
2
+ export interface BuyFollow {
3
+ notify?: (message: string) => void;
4
+ /** Pause between status checks; resolves early when `signal` aborts. */
5
+ wait?: (ms: number, signal?: AbortSignal) => Promise<void>;
6
+ /** Aborting stops the waiting only. The saved purchase is left as it is. */
7
+ signal?: AbortSignal;
8
+ /** Asked once the purchase is accepted and no `--open` path was given. */
9
+ savePath?: () => Promise<string | undefined>;
10
+ onWaiting?: (waiting: boolean) => void;
11
+ }
12
+ type Execute = (input: Input) => Promise<unknown>;
13
+ /**
14
+ * Runs one command. A `buy` is followed when the caller is a person (`follow`) or asked for
15
+ * `--open`; a deposit route returns its instructions without waiting.
16
+ */
17
+ export declare function buyFlow(input: Input, execute: Execute, follow: boolean, o?: BuyFollow): Promise<unknown>;
18
+ export {};
@@ -0,0 +1,89 @@
1
+ import { commonInput } from './cli-flags.js';
2
+ import { publicError } from './errors.js';
3
+ import { newOutput } from './io.js';
4
+ const CHECKS = 60;
5
+ const INTERVAL_MS = 3000;
6
+ const pause = (ms, signal) => new Promise((resolve) => {
7
+ const done = () => {
8
+ clearTimeout(timer);
9
+ resolve();
10
+ };
11
+ const timer = setTimeout(done, ms);
12
+ signal?.addEventListener('abort', done, { once: true });
13
+ });
14
+ /** Status checks until the purchase is accepted, the checks run out or the wait is cancelled. */
15
+ async function accepted(execute, input, id, o) {
16
+ o.onWaiting?.(true);
17
+ try {
18
+ for (let check = 0; check < CHECKS && !o.signal?.aborted; check++) {
19
+ if (check > 0)
20
+ await (o.wait ?? pause)(INTERVAL_MS, o.signal);
21
+ if (o.signal?.aborted)
22
+ break;
23
+ const status = (await execute({
24
+ ...commonInput(input),
25
+ command: 'status',
26
+ target: id,
27
+ }));
28
+ if (status.state === 'accepted')
29
+ return true;
30
+ }
31
+ return false;
32
+ }
33
+ finally {
34
+ o.onWaiting?.(false);
35
+ }
36
+ }
37
+ /**
38
+ * Runs one command. A `buy` is followed when the caller is a person (`follow`) or asked for
39
+ * `--open`; a deposit route returns its instructions without waiting.
40
+ */
41
+ export async function buyFlow(input, execute, follow, o = {}) {
42
+ if (input.command !== 'buy' || (!follow && input.open === undefined))
43
+ return execute(input);
44
+ // Refuse a taken output name before any payment step.
45
+ if (input.open !== undefined)
46
+ await newOutput(input.open);
47
+ const bought = (await execute(input));
48
+ const id = bought.purchaseId;
49
+ if (typeof id !== 'string' || !['submitted', 'uncertain'].includes(String(bought.state)))
50
+ return bought;
51
+ o.notify?.(bought.state === 'uncertain'
52
+ ? 'The wallet may have sent this payment. Do not pay again. Checking NEAR...'
53
+ : 'Payment sent. Waiting for NEAR to accept...');
54
+ // From here the payment exists: a later failure is reported next to the purchase, never instead.
55
+ const pending = (statusError) => ({
56
+ ...bought,
57
+ settlement: 'pending',
58
+ ...(statusError ? { statusError } : {}),
59
+ next: `Not accepted yet. Do not pay again. Run annon status ${id} until it is accepted, then annon open ${id} --output <file>.`,
60
+ });
61
+ try {
62
+ if (!(await accepted(execute, input, id, o)))
63
+ return pending();
64
+ }
65
+ catch (error) {
66
+ return pending(publicError(error));
67
+ }
68
+ const settled = { ...bought, state: 'accepted', next: `annon open ${id} --output <file>` };
69
+ const output = input.open ?? (await o.savePath?.());
70
+ if (!output)
71
+ return settled;
72
+ try {
73
+ const opened = (await execute({
74
+ ...commonInput(input),
75
+ command: 'open',
76
+ target: id,
77
+ output,
78
+ }));
79
+ return {
80
+ ...bought,
81
+ state: 'opened',
82
+ opened: { output: opened.output, bytes: opened.bytes },
83
+ next: 'The file is saved.',
84
+ };
85
+ }
86
+ catch (error) {
87
+ return { ...settled, openError: publicError(error) };
88
+ }
89
+ }
@@ -0,0 +1,10 @@
1
+ /** The flag tables of the CLI: what every command accepts, for parsing, completion and follow-ups. */
2
+ import type { Command, Input } from './cli.js';
3
+ export declare const common: string[];
4
+ export declare const perCommand: Record<Command, string[]>;
5
+ export declare const camel: (flag: string) => string;
6
+ export declare const commandNames: Command[];
7
+ /** Every flag a command accepts, as typed on the command line. */
8
+ export declare const commandFlags: (command: Command) => string[];
9
+ /** The deployment, wallet and output options of one command, for a follow-up command it starts. */
10
+ export declare const commonInput: (input: Input) => Partial<Input>;
@@ -0,0 +1,68 @@
1
+ export const common = [
2
+ 'no-open',
3
+ 'json',
4
+ 'config',
5
+ 'contract-id',
6
+ 'near-rpc',
7
+ 'base-rpc',
8
+ 'network',
9
+ 'state-dir',
10
+ 'relayer',
11
+ 'trust-contract',
12
+ 'wallet',
13
+ 'evm-key-file',
14
+ 'solana-key-file',
15
+ 'near-key-file',
16
+ 'near-account',
17
+ 'near-public-key',
18
+ 'backup-secret-file',
19
+ ];
20
+ export const perCommand = {
21
+ shops: ['search', 'limit', 'offset'],
22
+ shop: ['id'],
23
+ catalog: ['shop', 'search', 'limit', 'offset'],
24
+ fund: [],
25
+ budget: [],
26
+ login: [],
27
+ init: [
28
+ 'signer',
29
+ 'credential-env',
30
+ 'credential-keychain',
31
+ 'agent',
32
+ 'recovery',
33
+ 'key-env',
34
+ 'keychain-entry',
35
+ 'new-key',
36
+ 'daily-budget',
37
+ ],
38
+ share: ['resume', 'price', 'shop', 'title', 'description', 'storage-budget', 'listing-storage'],
39
+ sell: ['resume', 'price', 'split', 'base', 'moderator', 'output', 'storage-budget'],
40
+ buy: [
41
+ 'force',
42
+ 'origin',
43
+ 'route',
44
+ 'from-chain',
45
+ 'from-asset',
46
+ 'refund-to',
47
+ 'recovery',
48
+ 'max-price',
49
+ 'moderator',
50
+ 'url',
51
+ 'open',
52
+ 'shop',
53
+ 'search',
54
+ ],
55
+ open: ['purchase', 'file', 'url', 'output'],
56
+ recover: ['recovery', 'purchase'],
57
+ stop: [],
58
+ verify: [],
59
+ status: [],
60
+ };
61
+ export const camel = (flag) => flag.replace(/-([a-z])/g, (_, c) => c.toUpperCase());
62
+ export const commandNames = Object.keys(perCommand);
63
+ /** Every flag a command accepts, as typed on the command line. */
64
+ export const commandFlags = (command) => [...perCommand[command], ...common].map((flag) => `--${flag}`);
65
+ /** The deployment, wallet and output options of one command, for a follow-up command it starts. */
66
+ export const commonInput = (input) => Object.fromEntries(common
67
+ .map((flag) => [camel(flag), input[camel(flag)]])
68
+ .filter(([, v]) => v !== undefined));
@@ -1,4 +1,5 @@
1
1
  import type { BuyRoute } from '../routes.js';
2
+ import { type BuyFollow } from './buy-flow.js';
2
3
  export type Command = 'shops' | 'shop' | 'catalog' | 'fund' | 'budget' | 'share' | 'sell' | 'buy' | 'open' | 'recover' | 'stop' | 'verify' | 'status' | 'login' | 'init';
3
4
  export interface Input {
4
5
  command: Command;
@@ -55,6 +56,13 @@ export interface Input {
55
56
  file?: string;
56
57
  url?: string;
57
58
  output?: string;
59
+ /** `buy --open <path>`: wait for acceptance, then write the opened file there. */
60
+ open?: string;
58
61
  }
59
62
  export declare function parseCli(argv: string[]): Input;
60
- export declare function runCli(argv: string[], execute: (input: Input) => Promise<unknown>, write: (text: string) => void): Promise<number>;
63
+ /** How one command line is presented and followed up; the defaults are the script contract. */
64
+ export interface RunOptions extends BuyFollow {
65
+ /** Readable text in place of the JSON envelope. `--json` always wins. */
66
+ human?: boolean;
67
+ }
68
+ export declare function runCli(argv: string[], execute: (input: Input) => Promise<unknown>, write: (text: string) => void, options?: RunOptions): Promise<number>;