@parix/cli 0.1.6 → 0.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,10 @@ parix --help
23
23
 
24
24
  ## Commands
25
25
 
26
+ Agent skill:
27
+
28
+ - `parix skill`: print the embedded Parix agent skill as raw Markdown
29
+
26
30
  Auth:
27
31
 
28
32
  - `parix auth login`
@@ -90,7 +94,7 @@ Supported operations:
90
94
 
91
95
  TigerBeetle commands support both styles:
92
96
 
93
- - flag-driven payloads for common workflows such as `--from`, `--to`, `--amount`, `--ledger`, `--code`, repeated `--id`, and repeated `--flag`
97
+ - flag-driven payloads for common workflows such as `--from`, `--to`, `--amount`, `--ledger`, `--code`, `--timestamp`, `--user-data-128`, `--user-data-64`, `--user-data-32`, repeated `--id`, and repeated `--flag`
94
98
  - raw JSON overrides with `--payload '<json>'` or `--file ./payload.json`
95
99
 
96
100
  Operational notes:
@@ -98,6 +102,10 @@ Operational notes:
98
102
  - database and TB commands use the active organization from the current `parix` token
99
103
  - `db:read` and `db:write` OIDC scopes are enforced on the Worker API surface
100
104
  - newly provisioned databases may take a short time to become ready for TB operations; the CLI/API includes readiness retries for fresh databases
105
+ - `--timestamp` is required with the `imported` account or transfer flag, is expressed in nanoseconds, and must be greater than 0 and less than 2^63
106
+ - transfer `--timeout` values are expressed in seconds
107
+ - post-pending transfers require `--pending-id` and `--amount`; void-pending transfers require `--pending-id`; omitted account IDs, ledger, code, and void amount are sent as zero to inherit them
108
+ - linked account or transfer batches must use `--payload` or `--file`
101
109
 
102
110
  ## Development
103
111
 
@@ -148,8 +156,8 @@ Recommended release flow:
148
156
 
149
157
  ```bash
150
158
  # bump version in package.json
151
- git tag v0.1.6
159
+ git tag v0.1.7
152
160
  git push origin main --tags
153
161
  ```
154
162
 
155
- Then publish a GitHub Release from tag `v0.1.6`. The publish workflow verifies that the release tag matches `package.json`, packs the npm tarball (`parix-cli-<version>.tgz`), publishes that tarball to npm, and uploads the same `.tgz` as a GitHub Release asset.
163
+ Then publish a GitHub Release from tag `v0.1.7`. The publish workflow verifies that the release tag matches `package.json`, packs the npm tarball (`parix-cli-<version>.tgz`), publishes that tarball to npm, and uploads the same `.tgz` as a GitHub Release asset.
package/dist/cli.cjs CHANGED
@@ -7,6 +7,7 @@ const node_module_1 = require("node:module");
7
7
  const api_1 = require("./commands/api.cjs");
8
8
  const auth_1 = require("./commands/auth.cjs");
9
9
  const database_1 = require("./commands/database.cjs");
10
+ const skill_1 = require("./commands/skill.cjs");
10
11
  const tb_1 = require("./commands/tb.cjs");
11
12
  const loadPackageJson = (0, node_module_1.createRequire)(require("url").pathToFileURL(__filename));
12
13
  const packageJson = loadPackageJson('../package.json');
@@ -20,11 +21,18 @@ async function main() {
20
21
  .addCommand((0, auth_1.createAuthCommand)())
21
22
  .addCommand((0, api_1.createApiCommand)())
22
23
  .addCommand((0, database_1.createDatabaseCommand)())
24
+ .addCommand((0, skill_1.createSkillCommand)())
23
25
  .addCommand((0, tb_1.createTbCommand)());
24
26
  await program.parseAsync(process.argv);
25
27
  }
26
- main().catch((error) => {
28
+ main()
29
+ .then(() => {
30
+ // CLIs that open HTTP servers / use undici fetch can leave sockets that keep
31
+ // Node alive after the command is done. Exit explicitly once work is finished.
32
+ process.exit(process.exitCode ?? 0);
33
+ })
34
+ .catch((error) => {
27
35
  const message = error instanceof Error ? error.message : String(error);
28
36
  prompts_1.log.error(message);
29
- process.exitCode = 1;
37
+ process.exit(1);
30
38
  });
package/dist/cli.js CHANGED
@@ -5,6 +5,7 @@ import { createRequire } from 'node:module';
5
5
  import { createApiCommand } from "./commands/api.js";
6
6
  import { createAuthCommand } from "./commands/auth.js";
7
7
  import { createDatabaseCommand } from "./commands/database.js";
8
+ import { createSkillCommand } from "./commands/skill.js";
8
9
  import { createTbCommand } from "./commands/tb.js";
9
10
  const loadPackageJson = createRequire(import.meta.url);
10
11
  const packageJson = loadPackageJson('../package.json');
@@ -18,11 +19,18 @@ async function main() {
18
19
  .addCommand(createAuthCommand())
19
20
  .addCommand(createApiCommand())
20
21
  .addCommand(createDatabaseCommand())
22
+ .addCommand(createSkillCommand())
21
23
  .addCommand(createTbCommand());
22
24
  await program.parseAsync(process.argv);
23
25
  }
24
- main().catch((error) => {
26
+ main()
27
+ .then(() => {
28
+ // CLIs that open HTTP servers / use undici fetch can leave sockets that keep
29
+ // Node alive after the command is done. Exit explicitly once work is finished.
30
+ process.exit(process.exitCode ?? 0);
31
+ })
32
+ .catch((error) => {
25
33
  const message = error instanceof Error ? error.message : String(error);
26
34
  log.error(message);
27
- process.exitCode = 1;
35
+ process.exit(1);
28
36
  });
@@ -69,6 +69,9 @@ async function handleLogin(options) {
69
69
  waitSpinner.start('Waiting for browser callback');
70
70
  const callbackResult = await loopbackServer.waitForResult();
71
71
  waitSpinner.stop('Received browser callback');
72
+ // Shut down the loopback server before token exchange so browser keep-alive
73
+ // sockets cannot keep the process open after sign-in completes.
74
+ await loopbackServer.close().catch(() => { });
72
75
  const tokenSet = await (0, oauth_api_1.exchangeAuthorizationCode)({
73
76
  baseUrl,
74
77
  code: callbackResult.code,
@@ -66,6 +66,9 @@ async function handleLogin(options) {
66
66
  waitSpinner.start('Waiting for browser callback');
67
67
  const callbackResult = await loopbackServer.waitForResult();
68
68
  waitSpinner.stop('Received browser callback');
69
+ // Shut down the loopback server before token exchange so browser keep-alive
70
+ // sockets cannot keep the process open after sign-in completes.
71
+ await loopbackServer.close().catch(() => { });
69
72
  const tokenSet = await exchangeAuthorizationCode({
70
73
  baseUrl,
71
74
  code: callbackResult.code,
@@ -0,0 +1,77 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PARIX_AGENT_SKILL = void 0;
4
+ exports.PARIX_AGENT_SKILL = [
5
+ '---',
6
+ 'name: parix',
7
+ 'description: "Operate Parix through the `parix` CLI: inspect authentication and active organization context, manage database lifecycle, call authenticated Parix API endpoints, and run TigerBeetle queries or mutations. Use for Parix CLI setup, sessions, database catalog/list/info/create/remove work, Parix API requests, and TigerBeetle account or transfer operations."',
8
+ '---',
9
+ '',
10
+ '# Parix CLI',
11
+ '',
12
+ 'Use the installed `parix` binary. Prefer scoped `--help` over guessing flags.',
13
+ '',
14
+ '## Start safely',
15
+ '',
16
+ '1. Run `command -v parix`. If it is absent, ask the user to install it with `npm install -g @parix/cli`.',
17
+ '2. Run `parix auth status` before authenticated work. Confirm the base URL and active organization before any mutation. Status may refresh and persist tokens or organization context, so it is not strictly read-only. If no usable session exists, ask the user to complete `parix auth login`; login opens a browser OAuth flow.',
18
+ '3. Treat `https://parix.io` as production. Login resolves its target as explicit `--base-url`, then `PARIX_BASE_URL`, then production; it ignores any URL in the stored session. Later commands resolve explicit URL, then stored URL, then environment, then production. Pass `--base-url <url>` explicitly when the environment matters.',
19
+ '4. Use `parix database`, not `parix db`; no `db` alias exists.',
20
+ '',
21
+ 'Never read or print `~/.config/parix/session.json`; it contains access and refresh tokens.',
22
+ '',
23
+ '`parix auth logout` deletes the local session without confirmation; run it only when explicitly requested.',
24
+ '',
25
+ '## Discover before changing state',
26
+ '',
27
+ '```bash',
28
+ 'parix database catalog',
29
+ 'parix database list',
30
+ 'parix database info <database-id>',
31
+ 'parix database <command> --help',
32
+ 'parix tb <command> --help',
33
+ '```',
34
+ '',
35
+ 'Database and TigerBeetle commands operate in the active organization from the current session. Stop if that organization or target environment is not the intended one.',
36
+ '',
37
+ 'Use `--json` when a database or TigerBeetle command needs the complete response, but do not assume it is directly pipeable to `jq`: current output is wrapped by Clack formatting.',
38
+ '',
39
+ '## Manage databases',
40
+ '',
41
+ '- Run `parix database catalog` before creation and use provider, region, cluster, size, and storage identifiers returned by it.',
42
+ '- Treat `parix database create` as potentially billable. Run it only when the user explicitly requests the exact resource. `--storage-gb` must be a positive integer.',
43
+ '- Inspect the create response, including `success`, billing, and provisioning fields; exit code 0 alone does not prove provisioning started. Only when `success` is true and a database ID is returned, follow with `parix database info <database-id>`.',
44
+ '- Treat `parix database remove <database-id>` as destructive. Resolve the exact ID, obtain explicit confirmation, and only then use `--yes` for a non-interactive run. Removal is queued; verify the returned workflow/status rather than claiming immediate deletion.',
45
+ '',
46
+ '## Run TigerBeetle operations',
47
+ '',
48
+ 'Read operations are `lookup-accounts`, `lookup-transfers`, `get-account-transfers`, `get-account-balances`, `query-accounts`, and `query-transfers`. `create-accounts` and `create-transfers` mutate data; run them only when explicitly requested.',
49
+ '',
50
+ 'Choose exactly one payload style:',
51
+ '',
52
+ '- flags for a single/common operation;',
53
+ "- `--payload '<json>'` for raw JSON; or",
54
+ '- `--file <path>` for a JSON file.',
55
+ '',
56
+ '`--file` takes precedence over `--payload`, and either raw form bypasses flag-built payloads, defaults, and validation. Flag-built account creation requires `--ledger` and `--code`. Ordinary transfer creation requires `--from`, `--to`, `--amount`, `--ledger`, and `--code`. Post-pending transfer creation requires `--flag post_pending_transfer`, `--pending-id`, and `--amount`; debit/credit IDs, ledger, and code may be omitted to inherit them. Void-pending transfer creation requires `--flag void_pending_transfer` and `--pending-id`; amount and the same inherited fields may be omitted. Imported accounts and transfers also require `--timestamp` in nanoseconds, greater than 0 and less than 2^63; nonzero `--timeout` values are in seconds and limited to non-imported pending transfers. Linked batches require `--payload` or `--file`.',
57
+ '',
58
+ 'Supply stable explicit `--id` values for retryable mutations; omitted IDs are newly generated on each run. Flag-built queries and account filters default to `--limit 100`.',
59
+ '',
60
+ 'Newly provisioned databases may not be ready immediately. Check `parix database info` and retry read-only readiness checks before TigerBeetle mutations.',
61
+ '',
62
+ '## Call the authenticated API',
63
+ '',
64
+ '```bash',
65
+ 'parix api /api/v1/session',
66
+ 'parix api /api/v1/example -X POST -d \'{"key":"value"}\'',
67
+ '```',
68
+ '',
69
+ "Prefer relative `/api/...` paths. An absolute HTTP(S) URL also receives the stored bearer token, so use one only when the user explicitly trusts that host. Repeat `-H 'name:value'` for headers; a body defaults to `content-type: application/json` unless overridden.",
70
+ '',
71
+ 'Always inspect the printed HTTP status: `parix api` can exit successfully for an HTTP error response.',
72
+ '',
73
+ '## Verify outcomes',
74
+ '',
75
+ 'Report the target base URL and organization, the exact command class used, the response status/body, and any asynchronous provisioning or deletion state. Do not infer success from process exit alone.',
76
+ '',
77
+ ].join('\n');
@@ -0,0 +1 @@
1
+ export declare const PARIX_AGENT_SKILL: string;
@@ -0,0 +1 @@
1
+ export declare const PARIX_AGENT_SKILL: string;
@@ -0,0 +1,74 @@
1
+ export const PARIX_AGENT_SKILL = [
2
+ '---',
3
+ 'name: parix',
4
+ 'description: "Operate Parix through the `parix` CLI: inspect authentication and active organization context, manage database lifecycle, call authenticated Parix API endpoints, and run TigerBeetle queries or mutations. Use for Parix CLI setup, sessions, database catalog/list/info/create/remove work, Parix API requests, and TigerBeetle account or transfer operations."',
5
+ '---',
6
+ '',
7
+ '# Parix CLI',
8
+ '',
9
+ 'Use the installed `parix` binary. Prefer scoped `--help` over guessing flags.',
10
+ '',
11
+ '## Start safely',
12
+ '',
13
+ '1. Run `command -v parix`. If it is absent, ask the user to install it with `npm install -g @parix/cli`.',
14
+ '2. Run `parix auth status` before authenticated work. Confirm the base URL and active organization before any mutation. Status may refresh and persist tokens or organization context, so it is not strictly read-only. If no usable session exists, ask the user to complete `parix auth login`; login opens a browser OAuth flow.',
15
+ '3. Treat `https://parix.io` as production. Login resolves its target as explicit `--base-url`, then `PARIX_BASE_URL`, then production; it ignores any URL in the stored session. Later commands resolve explicit URL, then stored URL, then environment, then production. Pass `--base-url <url>` explicitly when the environment matters.',
16
+ '4. Use `parix database`, not `parix db`; no `db` alias exists.',
17
+ '',
18
+ 'Never read or print `~/.config/parix/session.json`; it contains access and refresh tokens.',
19
+ '',
20
+ '`parix auth logout` deletes the local session without confirmation; run it only when explicitly requested.',
21
+ '',
22
+ '## Discover before changing state',
23
+ '',
24
+ '```bash',
25
+ 'parix database catalog',
26
+ 'parix database list',
27
+ 'parix database info <database-id>',
28
+ 'parix database <command> --help',
29
+ 'parix tb <command> --help',
30
+ '```',
31
+ '',
32
+ 'Database and TigerBeetle commands operate in the active organization from the current session. Stop if that organization or target environment is not the intended one.',
33
+ '',
34
+ 'Use `--json` when a database or TigerBeetle command needs the complete response, but do not assume it is directly pipeable to `jq`: current output is wrapped by Clack formatting.',
35
+ '',
36
+ '## Manage databases',
37
+ '',
38
+ '- Run `parix database catalog` before creation and use provider, region, cluster, size, and storage identifiers returned by it.',
39
+ '- Treat `parix database create` as potentially billable. Run it only when the user explicitly requests the exact resource. `--storage-gb` must be a positive integer.',
40
+ '- Inspect the create response, including `success`, billing, and provisioning fields; exit code 0 alone does not prove provisioning started. Only when `success` is true and a database ID is returned, follow with `parix database info <database-id>`.',
41
+ '- Treat `parix database remove <database-id>` as destructive. Resolve the exact ID, obtain explicit confirmation, and only then use `--yes` for a non-interactive run. Removal is queued; verify the returned workflow/status rather than claiming immediate deletion.',
42
+ '',
43
+ '## Run TigerBeetle operations',
44
+ '',
45
+ 'Read operations are `lookup-accounts`, `lookup-transfers`, `get-account-transfers`, `get-account-balances`, `query-accounts`, and `query-transfers`. `create-accounts` and `create-transfers` mutate data; run them only when explicitly requested.',
46
+ '',
47
+ 'Choose exactly one payload style:',
48
+ '',
49
+ '- flags for a single/common operation;',
50
+ "- `--payload '<json>'` for raw JSON; or",
51
+ '- `--file <path>` for a JSON file.',
52
+ '',
53
+ '`--file` takes precedence over `--payload`, and either raw form bypasses flag-built payloads, defaults, and validation. Flag-built account creation requires `--ledger` and `--code`. Ordinary transfer creation requires `--from`, `--to`, `--amount`, `--ledger`, and `--code`. Post-pending transfer creation requires `--flag post_pending_transfer`, `--pending-id`, and `--amount`; debit/credit IDs, ledger, and code may be omitted to inherit them. Void-pending transfer creation requires `--flag void_pending_transfer` and `--pending-id`; amount and the same inherited fields may be omitted. Imported accounts and transfers also require `--timestamp` in nanoseconds, greater than 0 and less than 2^63; nonzero `--timeout` values are in seconds and limited to non-imported pending transfers. Linked batches require `--payload` or `--file`.',
54
+ '',
55
+ 'Supply stable explicit `--id` values for retryable mutations; omitted IDs are newly generated on each run. Flag-built queries and account filters default to `--limit 100`.',
56
+ '',
57
+ 'Newly provisioned databases may not be ready immediately. Check `parix database info` and retry read-only readiness checks before TigerBeetle mutations.',
58
+ '',
59
+ '## Call the authenticated API',
60
+ '',
61
+ '```bash',
62
+ 'parix api /api/v1/session',
63
+ 'parix api /api/v1/example -X POST -d \'{"key":"value"}\'',
64
+ '```',
65
+ '',
66
+ "Prefer relative `/api/...` paths. An absolute HTTP(S) URL also receives the stored bearer token, so use one only when the user explicitly trusts that host. Repeat `-H 'name:value'` for headers; a body defaults to `content-type: application/json` unless overridden.",
67
+ '',
68
+ 'Always inspect the printed HTTP status: `parix api` can exit successfully for an HTTP error response.',
69
+ '',
70
+ '## Verify outcomes',
71
+ '',
72
+ 'Report the target base URL and organization, the exact command class used, the response status/body, and any asynchronous provisioning or deletion state. Do not infer success from process exit alone.',
73
+ '',
74
+ ].join('\n');
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createSkillCommand = createSkillCommand;
4
+ const commander_1 = require("commander");
5
+ const skill_content_1 = require("./skill-content.cjs");
6
+ function createSkillCommand() {
7
+ return new commander_1.Command('skill').description('Print the agent skill file to stdout').action(() => {
8
+ process.stdout.write(skill_content_1.PARIX_AGENT_SKILL);
9
+ });
10
+ }
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createSkillCommand(): Command;
@@ -0,0 +1,2 @@
1
+ import { Command } from 'commander';
2
+ export declare function createSkillCommand(): Command;
@@ -0,0 +1,7 @@
1
+ import { Command } from 'commander';
2
+ import { PARIX_AGENT_SKILL } from "./skill-content.js";
3
+ export function createSkillCommand() {
4
+ return new Command('skill').description('Print the agent skill file to stdout').action(() => {
5
+ process.stdout.write(PARIX_AGENT_SKILL);
6
+ });
7
+ }
@@ -41,6 +41,7 @@ function addCreateAccountsCommand(parent) {
41
41
  .option('--ledger <ledger>', 'Ledger id')
42
42
  .option('--code <code>', 'Account code')
43
43
  .option('--flag <flag>', 'Account flag name or bitfield value', collectValues, [])
44
+ .option('--timestamp <nanoseconds>', 'Imported timestamp, greater than 0 and less than 2^63')
44
45
  .option('--user-data-128 <value>', 'user_data_128')
45
46
  .option('--user-data-64 <value>', 'user_data_64')
46
47
  .option('--user-data-32 <value>', 'user_data_32')
@@ -56,6 +57,7 @@ function addCreateAccountsCommand(parent) {
56
57
  flags: options.flag,
57
58
  id: options.id,
58
59
  ledger: options.ledger,
60
+ timestamp: options.timestamp,
59
61
  userData128: options.userData128,
60
62
  userData32: options.userData32,
61
63
  userData64: options.userData64,
@@ -80,11 +82,17 @@ function addCreateTransfersCommand(parent) {
80
82
  .option('--code <code>', 'Transfer code')
81
83
  .option('--flag <flag>', 'Transfer flag name or bitfield value', collectValues, [])
82
84
  .option('--pending-id <id>', 'Pending transfer id')
83
- .option('--timeout <ms>', 'Timeout value')
85
+ .option('--timeout <seconds>', 'Pending transfer timeout in seconds')
86
+ .option('--timestamp <nanoseconds>', 'Imported timestamp, greater than 0 and less than 2^63')
87
+ .option('--user-data-128 <value>', 'user_data_128')
88
+ .option('--user-data-64 <value>', 'user_data_64')
89
+ .option('--user-data-32 <value>', 'user_data_32')
84
90
  .addHelpText('after', [
85
91
  '',
86
92
  'Examples:',
87
93
  ' parix tb create-transfers db_123 --id 2000 --from 1000 --to 1001 --amount 1 --ledger 1 --code 1',
94
+ ' parix tb create-transfers db_123 --id 2001 --pending-id 2000 --amount 25 --flag post_pending_transfer',
95
+ ' parix tb create-transfers db_123 --id 2001 --pending-id 2000 --flag void_pending_transfer',
88
96
  ' parix tb create-transfers db_123 --file ./transfer.json',
89
97
  ].join('\n'))
90
98
  .action(async (databaseId, options) => {
@@ -97,7 +105,11 @@ function addCreateTransfersCommand(parent) {
97
105
  id: options.id,
98
106
  ledger: options.ledger,
99
107
  pendingId: options.pendingId,
108
+ timestamp: options.timestamp,
100
109
  timeout: options.timeout,
110
+ userData128: options.userData128,
111
+ userData32: options.userData32,
112
+ userData64: options.userData64,
101
113
  }));
102
114
  await executeTbOperation(databaseId, 'create_transfers', payload, options);
103
115
  });
@@ -38,6 +38,7 @@ function addCreateAccountsCommand(parent) {
38
38
  .option('--ledger <ledger>', 'Ledger id')
39
39
  .option('--code <code>', 'Account code')
40
40
  .option('--flag <flag>', 'Account flag name or bitfield value', collectValues, [])
41
+ .option('--timestamp <nanoseconds>', 'Imported timestamp, greater than 0 and less than 2^63')
41
42
  .option('--user-data-128 <value>', 'user_data_128')
42
43
  .option('--user-data-64 <value>', 'user_data_64')
43
44
  .option('--user-data-32 <value>', 'user_data_32')
@@ -53,6 +54,7 @@ function addCreateAccountsCommand(parent) {
53
54
  flags: options.flag,
54
55
  id: options.id,
55
56
  ledger: options.ledger,
57
+ timestamp: options.timestamp,
56
58
  userData128: options.userData128,
57
59
  userData32: options.userData32,
58
60
  userData64: options.userData64,
@@ -77,11 +79,17 @@ function addCreateTransfersCommand(parent) {
77
79
  .option('--code <code>', 'Transfer code')
78
80
  .option('--flag <flag>', 'Transfer flag name or bitfield value', collectValues, [])
79
81
  .option('--pending-id <id>', 'Pending transfer id')
80
- .option('--timeout <ms>', 'Timeout value')
82
+ .option('--timeout <seconds>', 'Pending transfer timeout in seconds')
83
+ .option('--timestamp <nanoseconds>', 'Imported timestamp, greater than 0 and less than 2^63')
84
+ .option('--user-data-128 <value>', 'user_data_128')
85
+ .option('--user-data-64 <value>', 'user_data_64')
86
+ .option('--user-data-32 <value>', 'user_data_32')
81
87
  .addHelpText('after', [
82
88
  '',
83
89
  'Examples:',
84
90
  ' parix tb create-transfers db_123 --id 2000 --from 1000 --to 1001 --amount 1 --ledger 1 --code 1',
91
+ ' parix tb create-transfers db_123 --id 2001 --pending-id 2000 --amount 25 --flag post_pending_transfer',
92
+ ' parix tb create-transfers db_123 --id 2001 --pending-id 2000 --flag void_pending_transfer',
85
93
  ' parix tb create-transfers db_123 --file ./transfer.json',
86
94
  ].join('\n'))
87
95
  .action(async (databaseId, options) => {
@@ -94,7 +102,11 @@ function addCreateTransfersCommand(parent) {
94
102
  id: options.id,
95
103
  ledger: options.ledger,
96
104
  pendingId: options.pendingId,
105
+ timestamp: options.timestamp,
97
106
  timeout: options.timeout,
107
+ userData128: options.userData128,
108
+ userData32: options.userData32,
109
+ userData64: options.userData64,
98
110
  }));
99
111
  await executeTbOperation(databaseId, 'create_transfers', payload, options);
100
112
  });
@@ -37,6 +37,8 @@ async function startLoopbackServer(options) {
37
37
  rejectResult = reject;
38
38
  });
39
39
  server.on('request', (request, response) => {
40
+ // One-shot OAuth loopback: do not keep browser sockets alive after the response.
41
+ response.setHeader('connection', 'close');
40
42
  const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
41
43
  if (requestUrl.pathname === '/favicon.ico') {
42
44
  response.writeHead(204);
@@ -120,17 +122,37 @@ function listen(server, port) {
120
122
  }
121
123
  function closeServer(server) {
122
124
  return new Promise((resolve, reject) => {
125
+ let settled = false;
126
+ const finish = (error) => {
127
+ if (settled) {
128
+ return;
129
+ }
130
+ settled = true;
131
+ if (error) {
132
+ reject(error);
133
+ return;
134
+ }
135
+ resolve();
136
+ };
123
137
  server.close((error) => {
124
138
  if (error) {
125
139
  if ('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING') {
126
- resolve();
140
+ finish();
127
141
  return;
128
142
  }
129
- reject(error);
143
+ finish(error);
130
144
  return;
131
145
  }
132
- resolve();
146
+ finish();
133
147
  });
148
+ // `server.close()` waits for existing connections. Browsers often keep the
149
+ // loopback socket open via HTTP keep-alive, which would hang login forever
150
+ // after the session is already stored. Force those sockets closed (Node >= 18.2).
151
+ server.closeAllConnections();
152
+ // Last resort: never let close block the CLI indefinitely.
153
+ setTimeout(() => {
154
+ finish();
155
+ }, 250).unref();
134
156
  });
135
157
  }
136
158
  function buildHtml(title, message) {
@@ -34,6 +34,8 @@ export async function startLoopbackServer(options) {
34
34
  rejectResult = reject;
35
35
  });
36
36
  server.on('request', (request, response) => {
37
+ // One-shot OAuth loopback: do not keep browser sockets alive after the response.
38
+ response.setHeader('connection', 'close');
37
39
  const requestUrl = new URL(request.url ?? '/', 'http://127.0.0.1');
38
40
  if (requestUrl.pathname === '/favicon.ico') {
39
41
  response.writeHead(204);
@@ -117,17 +119,37 @@ function listen(server, port) {
117
119
  }
118
120
  function closeServer(server) {
119
121
  return new Promise((resolve, reject) => {
122
+ let settled = false;
123
+ const finish = (error) => {
124
+ if (settled) {
125
+ return;
126
+ }
127
+ settled = true;
128
+ if (error) {
129
+ reject(error);
130
+ return;
131
+ }
132
+ resolve();
133
+ };
120
134
  server.close((error) => {
121
135
  if (error) {
122
136
  if ('code' in error && error.code === 'ERR_SERVER_NOT_RUNNING') {
123
- resolve();
137
+ finish();
124
138
  return;
125
139
  }
126
- reject(error);
140
+ finish(error);
127
141
  return;
128
142
  }
129
- resolve();
143
+ finish();
130
144
  });
145
+ // `server.close()` waits for existing connections. Browsers often keep the
146
+ // loopback socket open via HTTP keep-alive, which would hang login forever
147
+ // after the session is already stored. Force those sockets closed (Node >= 18.2).
148
+ server.closeAllConnections();
149
+ // Last resort: never let close block the CLI indefinitely.
150
+ setTimeout(() => {
151
+ finish();
152
+ }, 250).unref();
131
153
  });
132
154
  }
133
155
  function buildHtml(title, message) {
@@ -9,24 +9,56 @@ exports.buildAccountFilterPayload = buildAccountFilterPayload;
9
9
  exports.buildQueryPayload = buildQueryPayload;
10
10
  const promises_1 = require("node:fs/promises");
11
11
  const SPLIT_IDS_REGEX = /[\s,]+/;
12
- const FLAG_NAME_TO_BIT = new Map([
13
- ['linked', 1 << 0],
14
- ['debits_must_not_exceed_credits', 1 << 1],
15
- ['credits_must_not_exceed_debits', 1 << 2],
16
- ['history', 1 << 3],
17
- ['imported', 1 << 4],
18
- ['closed', 1 << 5],
19
- ['pending', 1 << 1],
20
- ['post_pending_transfer', 1 << 2],
21
- ['void_pending_transfer', 1 << 3],
22
- ['balancing_debit', 1 << 4],
23
- ['balancing_credit', 1 << 5],
24
- ['closing_debit', 1 << 6],
25
- ['closing_credit', 1 << 7],
26
- ['debits', 1 << 0],
27
- ['credits', 1 << 1],
28
- ['reversed', 1 << 2],
29
- ]);
12
+ const DECIMAL_INTEGER_REGEX = /^\d+$/;
13
+ const LINKED_FLAG = 1n;
14
+ const ACCOUNT_IMPORTED_FLAG = 16n;
15
+ const TRANSFER_PENDING_FLAG = 2n;
16
+ const TRANSFER_POST_PENDING_FLAG = 4n;
17
+ const TRANSFER_VOID_PENDING_FLAG = 8n;
18
+ const TRANSFER_IMPORTED_FLAG = 256n;
19
+ const TIGERBEETLE_TIMESTAMP_MAX_EXCLUSIVE = 1n << 63n;
20
+ const IMPORTED_TIMESTAMP_ERROR = '--timestamp must be a decimal integer greater than 0 and less than 2^63 when --flag imported is used.';
21
+ const ACCOUNT_FLAG_SET = {
22
+ allowedMask: 63n,
23
+ label: 'account',
24
+ nameToBit: new Map([
25
+ ['linked', LINKED_FLAG],
26
+ ['debits_must_not_exceed_credits', 2n],
27
+ ['credits_must_not_exceed_debits', 4n],
28
+ ['history', 8n],
29
+ ['imported', ACCOUNT_IMPORTED_FLAG],
30
+ ['closed', 32n],
31
+ ]),
32
+ };
33
+ const TRANSFER_FLAG_SET = {
34
+ allowedMask: 511n,
35
+ label: 'transfer',
36
+ nameToBit: new Map([
37
+ ['linked', LINKED_FLAG],
38
+ ['pending', TRANSFER_PENDING_FLAG],
39
+ ['post_pending_transfer', TRANSFER_POST_PENDING_FLAG],
40
+ ['void_pending_transfer', TRANSFER_VOID_PENDING_FLAG],
41
+ ['balancing_debit', 16n],
42
+ ['balancing_credit', 32n],
43
+ ['closing_debit', 64n],
44
+ ['closing_credit', 128n],
45
+ ['imported', TRANSFER_IMPORTED_FLAG],
46
+ ]),
47
+ };
48
+ const ACCOUNT_FILTER_FLAG_SET = {
49
+ allowedMask: 7n,
50
+ label: 'account filter',
51
+ nameToBit: new Map([
52
+ ['debits', 1n],
53
+ ['credits', 2n],
54
+ ['reversed', 4n],
55
+ ]),
56
+ };
57
+ const QUERY_FILTER_FLAG_SET = {
58
+ allowedMask: 1n,
59
+ label: 'query filter',
60
+ nameToBit: new Map([['reversed', 1n]]),
61
+ };
30
62
  exports.tbOperationNames = [
31
63
  'create_accounts',
32
64
  'create_transfers',
@@ -47,12 +79,19 @@ async function resolveTbPayload(options, buildFromFlags) {
47
79
  return buildFromFlags();
48
80
  }
49
81
  function buildCreateAccountsPayload(options) {
82
+ const flags = flagsBitfield(options.flags, ACCOUNT_FLAG_SET);
83
+ rejectLinkedSingleEvent(flags);
84
+ const isImported = flagIsSet(flags, ACCOUNT_IMPORTED_FLAG);
85
+ if (!isImported && valueIsNonZero(options.timestamp)) {
86
+ throw new Error('--timestamp requires --flag imported when nonzero.');
87
+ }
50
88
  return [
51
89
  omitEmpty({
52
90
  code: requiredValue(options.code, '--code'),
53
- flags: flagsBitfield(options.flags),
91
+ flags,
54
92
  id: options.id ?? randomTbId(),
55
93
  ledger: requiredValue(options.ledger, '--ledger'),
94
+ timestamp: isImported ? requiredImportedTimestamp(options.timestamp) : options.timestamp,
56
95
  user_data_128: options.userData128,
57
96
  user_data_32: options.userData32,
58
97
  user_data_64: options.userData64,
@@ -60,17 +99,50 @@ function buildCreateAccountsPayload(options) {
60
99
  ];
61
100
  }
62
101
  function buildCreateTransfersPayload(options) {
102
+ const flags = flagsBitfield(options.flags, TRANSFER_FLAG_SET);
103
+ rejectLinkedSingleEvent(flags);
104
+ const isPending = flagIsSet(flags, TRANSFER_PENDING_FLAG);
105
+ const isPostPending = flagIsSet(flags, TRANSFER_POST_PENDING_FLAG);
106
+ const isVoidPending = flagIsSet(flags, TRANSFER_VOID_PENDING_FLAG);
107
+ const isImported = flagIsSet(flags, TRANSFER_IMPORTED_FLAG);
108
+ const lifecycleModeCount = Number(isPending) + Number(isPostPending) + Number(isVoidPending);
109
+ if (lifecycleModeCount > 1) {
110
+ throw new Error('The pending, post_pending_transfer, and void_pending_transfer flags cannot be combined.');
111
+ }
112
+ const resolvesPending = isPostPending || isVoidPending;
113
+ if (!isImported && valueIsNonZero(options.timestamp)) {
114
+ throw new Error('--timestamp requires --flag imported when nonzero.');
115
+ }
116
+ if (valueIsNonZero(options.timeout)) {
117
+ if (isImported) {
118
+ throw new Error('--timeout must be zero or omitted with --flag imported.');
119
+ }
120
+ if (!isPending) {
121
+ throw new Error('--timeout requires --flag pending when nonzero.');
122
+ }
123
+ }
124
+ if (!resolvesPending && valueIsNonZero(options.pendingId)) {
125
+ throw new Error('--pending-id requires --flag post_pending_transfer or --flag void_pending_transfer when nonzero.');
126
+ }
63
127
  return [
64
128
  omitEmpty({
65
- amount: requiredValue(options.amount, '--amount'),
66
- code: requiredValue(options.code, '--code'),
67
- credit_account_id: requiredValue(options.creditAccountId, '--to'),
68
- debit_account_id: requiredValue(options.debitAccountId, '--from'),
69
- flags: flagsBitfield(options.flags),
129
+ amount: isVoidPending ? valueOrZero(options.amount) : requiredValue(options.amount, '--amount'),
130
+ code: resolvesPending ? valueOrZero(options.code) : requiredValue(options.code, '--code'),
131
+ credit_account_id: resolvesPending
132
+ ? valueOrZero(options.creditAccountId)
133
+ : requiredValue(options.creditAccountId, '--to'),
134
+ debit_account_id: resolvesPending
135
+ ? valueOrZero(options.debitAccountId)
136
+ : requiredValue(options.debitAccountId, '--from'),
137
+ flags,
70
138
  id: options.id ?? randomTbId(),
71
- ledger: requiredValue(options.ledger, '--ledger'),
72
- pending_id: options.pendingId,
139
+ ledger: resolvesPending ? valueOrZero(options.ledger) : requiredValue(options.ledger, '--ledger'),
140
+ pending_id: resolvesPending ? requiredValue(options.pendingId, '--pending-id') : options.pendingId,
141
+ timestamp: isImported ? requiredImportedTimestamp(options.timestamp) : options.timestamp,
73
142
  timeout: options.timeout,
143
+ user_data_128: options.userData128,
144
+ user_data_32: options.userData32,
145
+ user_data_64: options.userData64,
74
146
  }),
75
147
  ];
76
148
  }
@@ -85,7 +157,7 @@ function buildAccountFilterPayload(options) {
85
157
  return omitEmpty({
86
158
  account_id: requiredValue(options.accountId, '--account-id'),
87
159
  code: options.code,
88
- flags: flagsBitfield(options.flags),
160
+ flags: flagsBitfield(options.flags, ACCOUNT_FILTER_FLAG_SET),
89
161
  limit: requiredValue(options.limit, '--limit'),
90
162
  timestamp_max: options.timestampMax,
91
163
  timestamp_min: options.timestampMin,
@@ -97,7 +169,7 @@ function buildAccountFilterPayload(options) {
97
169
  function buildQueryPayload(options) {
98
170
  return omitEmpty({
99
171
  code: options.code,
100
- flags: flagsBitfield(options.flags),
172
+ flags: flagsBitfield(options.flags, QUERY_FILTER_FLAG_SET),
101
173
  ledger: options.ledger,
102
174
  limit: requiredValue(options.limit, '--limit'),
103
175
  timestamp_max: options.timestampMax,
@@ -107,29 +179,40 @@ function buildQueryPayload(options) {
107
179
  user_data_64: options.userData64,
108
180
  });
109
181
  }
110
- function flagsBitfield(flags) {
182
+ function flagsBitfield(flags, flagSet) {
111
183
  if (!flags || flags.length === 0) {
112
184
  return '0';
113
185
  }
114
- let bitfield = 0;
186
+ let bitfield = 0n;
115
187
  for (const flag of flags) {
116
188
  const normalized = flag.trim();
117
189
  if (!normalized) {
118
190
  continue;
119
191
  }
120
- const direct = Number.parseInt(normalized, 10);
121
- if (Number.isInteger(direct) && direct >= 0) {
192
+ if (DECIMAL_INTEGER_REGEX.test(normalized)) {
193
+ const direct = BigInt(normalized);
194
+ if ((direct & ~flagSet.allowedMask) !== 0n) {
195
+ throw new Error(`Unsupported ${flagSet.label} flag bitfield: ${flag}`);
196
+ }
122
197
  bitfield |= direct;
123
198
  continue;
124
199
  }
125
- const bit = FLAG_NAME_TO_BIT.get(normalized);
200
+ const bit = flagSet.nameToBit.get(normalized);
126
201
  if (bit === undefined) {
127
- throw new Error(`Unsupported flag: ${flag}`);
202
+ throw new Error(`Unsupported ${flagSet.label} flag: ${flag}`);
128
203
  }
129
204
  bitfield |= bit;
130
205
  }
131
206
  return bitfield.toString();
132
207
  }
208
+ function flagIsSet(flags, flag) {
209
+ return (BigInt(flags) & flag) !== 0n;
210
+ }
211
+ function rejectLinkedSingleEvent(flags) {
212
+ if (flagIsSet(flags, LINKED_FLAG)) {
213
+ throw new Error('--flag linked requires --payload or --file with a batch of events.');
214
+ }
215
+ }
133
216
  function omitEmpty(input) {
134
217
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value.trim().length > 0));
135
218
  }
@@ -142,6 +225,28 @@ function requiredValue(value, flagName) {
142
225
  }
143
226
  return value.trim();
144
227
  }
228
+ function requiredImportedTimestamp(value) {
229
+ const timestamp = requiredValue(value, '--timestamp');
230
+ if (!DECIMAL_INTEGER_REGEX.test(timestamp)) {
231
+ throw new Error(IMPORTED_TIMESTAMP_ERROR);
232
+ }
233
+ const parsed = BigInt(timestamp);
234
+ if (parsed === 0n || parsed >= TIGERBEETLE_TIMESTAMP_MAX_EXCLUSIVE) {
235
+ throw new Error(IMPORTED_TIMESTAMP_ERROR);
236
+ }
237
+ return timestamp;
238
+ }
239
+ function valueOrZero(value) {
240
+ const trimmed = value?.trim();
241
+ return trimmed || '0';
242
+ }
243
+ function valueIsNonZero(value) {
244
+ const trimmed = value?.trim();
245
+ if (!trimmed) {
246
+ return false;
247
+ }
248
+ return !DECIMAL_INTEGER_REGEX.test(trimmed) || BigInt(trimmed) !== 0n;
249
+ }
145
250
  function splitIds(value) {
146
251
  if (!value) {
147
252
  return [];
@@ -10,6 +10,7 @@ export declare function buildCreateAccountsPayload(options: {
10
10
  flags?: string[];
11
11
  id?: string;
12
12
  ledger?: string;
13
+ timestamp?: string;
13
14
  userData128?: string;
14
15
  userData32?: string;
15
16
  userData64?: string;
@@ -25,7 +26,11 @@ export declare function buildCreateTransfersPayload(options: {
25
26
  id?: string;
26
27
  ledger?: string;
27
28
  pendingId?: string;
29
+ timestamp?: string;
28
30
  timeout?: string;
31
+ userData128?: string;
32
+ userData32?: string;
33
+ userData64?: string;
29
34
  }): {
30
35
  [k: string]: string | undefined;
31
36
  }[];
@@ -10,6 +10,7 @@ export declare function buildCreateAccountsPayload(options: {
10
10
  flags?: string[];
11
11
  id?: string;
12
12
  ledger?: string;
13
+ timestamp?: string;
13
14
  userData128?: string;
14
15
  userData32?: string;
15
16
  userData64?: string;
@@ -25,7 +26,11 @@ export declare function buildCreateTransfersPayload(options: {
25
26
  id?: string;
26
27
  ledger?: string;
27
28
  pendingId?: string;
29
+ timestamp?: string;
28
30
  timeout?: string;
31
+ userData128?: string;
32
+ userData32?: string;
33
+ userData64?: string;
29
34
  }): {
30
35
  [k: string]: string | undefined;
31
36
  }[];
@@ -1,23 +1,55 @@
1
1
  import { readFile } from 'node:fs/promises';
2
2
  const SPLIT_IDS_REGEX = /[\s,]+/;
3
- const FLAG_NAME_TO_BIT = new Map([
4
- ['linked', 1 << 0],
5
- ['debits_must_not_exceed_credits', 1 << 1],
6
- ['credits_must_not_exceed_debits', 1 << 2],
7
- ['history', 1 << 3],
8
- ['imported', 1 << 4],
9
- ['closed', 1 << 5],
10
- ['pending', 1 << 1],
11
- ['post_pending_transfer', 1 << 2],
12
- ['void_pending_transfer', 1 << 3],
13
- ['balancing_debit', 1 << 4],
14
- ['balancing_credit', 1 << 5],
15
- ['closing_debit', 1 << 6],
16
- ['closing_credit', 1 << 7],
17
- ['debits', 1 << 0],
18
- ['credits', 1 << 1],
19
- ['reversed', 1 << 2],
20
- ]);
3
+ const DECIMAL_INTEGER_REGEX = /^\d+$/;
4
+ const LINKED_FLAG = 1n;
5
+ const ACCOUNT_IMPORTED_FLAG = 16n;
6
+ const TRANSFER_PENDING_FLAG = 2n;
7
+ const TRANSFER_POST_PENDING_FLAG = 4n;
8
+ const TRANSFER_VOID_PENDING_FLAG = 8n;
9
+ const TRANSFER_IMPORTED_FLAG = 256n;
10
+ const TIGERBEETLE_TIMESTAMP_MAX_EXCLUSIVE = 1n << 63n;
11
+ const IMPORTED_TIMESTAMP_ERROR = '--timestamp must be a decimal integer greater than 0 and less than 2^63 when --flag imported is used.';
12
+ const ACCOUNT_FLAG_SET = {
13
+ allowedMask: 63n,
14
+ label: 'account',
15
+ nameToBit: new Map([
16
+ ['linked', LINKED_FLAG],
17
+ ['debits_must_not_exceed_credits', 2n],
18
+ ['credits_must_not_exceed_debits', 4n],
19
+ ['history', 8n],
20
+ ['imported', ACCOUNT_IMPORTED_FLAG],
21
+ ['closed', 32n],
22
+ ]),
23
+ };
24
+ const TRANSFER_FLAG_SET = {
25
+ allowedMask: 511n,
26
+ label: 'transfer',
27
+ nameToBit: new Map([
28
+ ['linked', LINKED_FLAG],
29
+ ['pending', TRANSFER_PENDING_FLAG],
30
+ ['post_pending_transfer', TRANSFER_POST_PENDING_FLAG],
31
+ ['void_pending_transfer', TRANSFER_VOID_PENDING_FLAG],
32
+ ['balancing_debit', 16n],
33
+ ['balancing_credit', 32n],
34
+ ['closing_debit', 64n],
35
+ ['closing_credit', 128n],
36
+ ['imported', TRANSFER_IMPORTED_FLAG],
37
+ ]),
38
+ };
39
+ const ACCOUNT_FILTER_FLAG_SET = {
40
+ allowedMask: 7n,
41
+ label: 'account filter',
42
+ nameToBit: new Map([
43
+ ['debits', 1n],
44
+ ['credits', 2n],
45
+ ['reversed', 4n],
46
+ ]),
47
+ };
48
+ const QUERY_FILTER_FLAG_SET = {
49
+ allowedMask: 1n,
50
+ label: 'query filter',
51
+ nameToBit: new Map([['reversed', 1n]]),
52
+ };
21
53
  export const tbOperationNames = [
22
54
  'create_accounts',
23
55
  'create_transfers',
@@ -38,12 +70,19 @@ export async function resolveTbPayload(options, buildFromFlags) {
38
70
  return buildFromFlags();
39
71
  }
40
72
  export function buildCreateAccountsPayload(options) {
73
+ const flags = flagsBitfield(options.flags, ACCOUNT_FLAG_SET);
74
+ rejectLinkedSingleEvent(flags);
75
+ const isImported = flagIsSet(flags, ACCOUNT_IMPORTED_FLAG);
76
+ if (!isImported && valueIsNonZero(options.timestamp)) {
77
+ throw new Error('--timestamp requires --flag imported when nonzero.');
78
+ }
41
79
  return [
42
80
  omitEmpty({
43
81
  code: requiredValue(options.code, '--code'),
44
- flags: flagsBitfield(options.flags),
82
+ flags,
45
83
  id: options.id ?? randomTbId(),
46
84
  ledger: requiredValue(options.ledger, '--ledger'),
85
+ timestamp: isImported ? requiredImportedTimestamp(options.timestamp) : options.timestamp,
47
86
  user_data_128: options.userData128,
48
87
  user_data_32: options.userData32,
49
88
  user_data_64: options.userData64,
@@ -51,17 +90,50 @@ export function buildCreateAccountsPayload(options) {
51
90
  ];
52
91
  }
53
92
  export function buildCreateTransfersPayload(options) {
93
+ const flags = flagsBitfield(options.flags, TRANSFER_FLAG_SET);
94
+ rejectLinkedSingleEvent(flags);
95
+ const isPending = flagIsSet(flags, TRANSFER_PENDING_FLAG);
96
+ const isPostPending = flagIsSet(flags, TRANSFER_POST_PENDING_FLAG);
97
+ const isVoidPending = flagIsSet(flags, TRANSFER_VOID_PENDING_FLAG);
98
+ const isImported = flagIsSet(flags, TRANSFER_IMPORTED_FLAG);
99
+ const lifecycleModeCount = Number(isPending) + Number(isPostPending) + Number(isVoidPending);
100
+ if (lifecycleModeCount > 1) {
101
+ throw new Error('The pending, post_pending_transfer, and void_pending_transfer flags cannot be combined.');
102
+ }
103
+ const resolvesPending = isPostPending || isVoidPending;
104
+ if (!isImported && valueIsNonZero(options.timestamp)) {
105
+ throw new Error('--timestamp requires --flag imported when nonzero.');
106
+ }
107
+ if (valueIsNonZero(options.timeout)) {
108
+ if (isImported) {
109
+ throw new Error('--timeout must be zero or omitted with --flag imported.');
110
+ }
111
+ if (!isPending) {
112
+ throw new Error('--timeout requires --flag pending when nonzero.');
113
+ }
114
+ }
115
+ if (!resolvesPending && valueIsNonZero(options.pendingId)) {
116
+ throw new Error('--pending-id requires --flag post_pending_transfer or --flag void_pending_transfer when nonzero.');
117
+ }
54
118
  return [
55
119
  omitEmpty({
56
- amount: requiredValue(options.amount, '--amount'),
57
- code: requiredValue(options.code, '--code'),
58
- credit_account_id: requiredValue(options.creditAccountId, '--to'),
59
- debit_account_id: requiredValue(options.debitAccountId, '--from'),
60
- flags: flagsBitfield(options.flags),
120
+ amount: isVoidPending ? valueOrZero(options.amount) : requiredValue(options.amount, '--amount'),
121
+ code: resolvesPending ? valueOrZero(options.code) : requiredValue(options.code, '--code'),
122
+ credit_account_id: resolvesPending
123
+ ? valueOrZero(options.creditAccountId)
124
+ : requiredValue(options.creditAccountId, '--to'),
125
+ debit_account_id: resolvesPending
126
+ ? valueOrZero(options.debitAccountId)
127
+ : requiredValue(options.debitAccountId, '--from'),
128
+ flags,
61
129
  id: options.id ?? randomTbId(),
62
- ledger: requiredValue(options.ledger, '--ledger'),
63
- pending_id: options.pendingId,
130
+ ledger: resolvesPending ? valueOrZero(options.ledger) : requiredValue(options.ledger, '--ledger'),
131
+ pending_id: resolvesPending ? requiredValue(options.pendingId, '--pending-id') : options.pendingId,
132
+ timestamp: isImported ? requiredImportedTimestamp(options.timestamp) : options.timestamp,
64
133
  timeout: options.timeout,
134
+ user_data_128: options.userData128,
135
+ user_data_32: options.userData32,
136
+ user_data_64: options.userData64,
65
137
  }),
66
138
  ];
67
139
  }
@@ -76,7 +148,7 @@ export function buildAccountFilterPayload(options) {
76
148
  return omitEmpty({
77
149
  account_id: requiredValue(options.accountId, '--account-id'),
78
150
  code: options.code,
79
- flags: flagsBitfield(options.flags),
151
+ flags: flagsBitfield(options.flags, ACCOUNT_FILTER_FLAG_SET),
80
152
  limit: requiredValue(options.limit, '--limit'),
81
153
  timestamp_max: options.timestampMax,
82
154
  timestamp_min: options.timestampMin,
@@ -88,7 +160,7 @@ export function buildAccountFilterPayload(options) {
88
160
  export function buildQueryPayload(options) {
89
161
  return omitEmpty({
90
162
  code: options.code,
91
- flags: flagsBitfield(options.flags),
163
+ flags: flagsBitfield(options.flags, QUERY_FILTER_FLAG_SET),
92
164
  ledger: options.ledger,
93
165
  limit: requiredValue(options.limit, '--limit'),
94
166
  timestamp_max: options.timestampMax,
@@ -98,29 +170,40 @@ export function buildQueryPayload(options) {
98
170
  user_data_64: options.userData64,
99
171
  });
100
172
  }
101
- function flagsBitfield(flags) {
173
+ function flagsBitfield(flags, flagSet) {
102
174
  if (!flags || flags.length === 0) {
103
175
  return '0';
104
176
  }
105
- let bitfield = 0;
177
+ let bitfield = 0n;
106
178
  for (const flag of flags) {
107
179
  const normalized = flag.trim();
108
180
  if (!normalized) {
109
181
  continue;
110
182
  }
111
- const direct = Number.parseInt(normalized, 10);
112
- if (Number.isInteger(direct) && direct >= 0) {
183
+ if (DECIMAL_INTEGER_REGEX.test(normalized)) {
184
+ const direct = BigInt(normalized);
185
+ if ((direct & ~flagSet.allowedMask) !== 0n) {
186
+ throw new Error(`Unsupported ${flagSet.label} flag bitfield: ${flag}`);
187
+ }
113
188
  bitfield |= direct;
114
189
  continue;
115
190
  }
116
- const bit = FLAG_NAME_TO_BIT.get(normalized);
191
+ const bit = flagSet.nameToBit.get(normalized);
117
192
  if (bit === undefined) {
118
- throw new Error(`Unsupported flag: ${flag}`);
193
+ throw new Error(`Unsupported ${flagSet.label} flag: ${flag}`);
119
194
  }
120
195
  bitfield |= bit;
121
196
  }
122
197
  return bitfield.toString();
123
198
  }
199
+ function flagIsSet(flags, flag) {
200
+ return (BigInt(flags) & flag) !== 0n;
201
+ }
202
+ function rejectLinkedSingleEvent(flags) {
203
+ if (flagIsSet(flags, LINKED_FLAG)) {
204
+ throw new Error('--flag linked requires --payload or --file with a batch of events.');
205
+ }
206
+ }
124
207
  function omitEmpty(input) {
125
208
  return Object.fromEntries(Object.entries(input).filter(([, value]) => value !== undefined && value.trim().length > 0));
126
209
  }
@@ -133,6 +216,28 @@ function requiredValue(value, flagName) {
133
216
  }
134
217
  return value.trim();
135
218
  }
219
+ function requiredImportedTimestamp(value) {
220
+ const timestamp = requiredValue(value, '--timestamp');
221
+ if (!DECIMAL_INTEGER_REGEX.test(timestamp)) {
222
+ throw new Error(IMPORTED_TIMESTAMP_ERROR);
223
+ }
224
+ const parsed = BigInt(timestamp);
225
+ if (parsed === 0n || parsed >= TIGERBEETLE_TIMESTAMP_MAX_EXCLUSIVE) {
226
+ throw new Error(IMPORTED_TIMESTAMP_ERROR);
227
+ }
228
+ return timestamp;
229
+ }
230
+ function valueOrZero(value) {
231
+ const trimmed = value?.trim();
232
+ return trimmed || '0';
233
+ }
234
+ function valueIsNonZero(value) {
235
+ const trimmed = value?.trim();
236
+ if (!trimmed) {
237
+ return false;
238
+ }
239
+ return !DECIMAL_INTEGER_REGEX.test(trimmed) || BigInt(trimmed) !== 0n;
240
+ }
136
241
  function splitIds(value) {
137
242
  if (!value) {
138
243
  return [];
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@parix/cli",
3
3
  "description": "Parix command line interface",
4
4
  "type": "module",
5
- "version": "0.1.6",
5
+ "version": "0.1.8",
6
6
  "bin": {
7
7
  "parix": "dist/cli.cjs"
8
8
  },