arkiv-cli 0.0.0-stage → 0.1.0
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/LICENSE +21 -0
- package/README.md +512 -2
- package/dist/args.d.ts +19 -0
- package/dist/args.js +124 -0
- package/dist/arkiv.d.ts +77 -0
- package/dist/arkiv.js +158 -0
- package/dist/cli.d.ts +9 -0
- package/dist/cli.js +343 -0
- package/dist/commands/cost.d.ts +22 -0
- package/dist/commands/cost.js +144 -0
- package/dist/commands/create.d.ts +27 -0
- package/dist/commands/create.js +239 -0
- package/dist/commands/doctor.d.ts +23 -0
- package/dist/commands/doctor.js +190 -0
- package/dist/commands/entity.d.ts +7 -0
- package/dist/commands/entity.js +91 -0
- package/dist/commands/init.d.ts +13 -0
- package/dist/commands/init.js +205 -0
- package/dist/commands/query.d.ts +24 -0
- package/dist/commands/query.js +111 -0
- package/dist/commands/schema.d.ts +55 -0
- package/dist/commands/schema.js +397 -0
- package/dist/commands/watch.d.ts +16 -0
- package/dist/commands/watch.js +124 -0
- package/dist/context.d.ts +25 -0
- package/dist/context.js +31 -0
- package/dist/env.d.ts +51 -0
- package/dist/env.js +136 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +19 -0
- package/dist/limits.d.ts +21 -0
- package/dist/limits.js +28 -0
- package/dist/render.d.ts +26 -0
- package/dist/render.js +77 -0
- package/dist/ui.d.ts +41 -0
- package/dist/ui.js +144 -0
- package/dist/values.d.ts +26 -0
- package/dist/values.js +132 -0
- package/package.json +46 -4
package/dist/arkiv.d.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain access for the CLI: network resolution, a quiet client factory and
|
|
3
|
+
* helpers that turn block numbers into wall-clock answers.
|
|
4
|
+
*/
|
|
5
|
+
import { type WalletArkivClient } from "@arkiv-network/sdk";
|
|
6
|
+
import { type Chain } from "viem";
|
|
7
|
+
import { type PrivateKeyAccount } from "viem/accounts";
|
|
8
|
+
import type { Entity } from "./render.js";
|
|
9
|
+
export interface NetworkInfo {
|
|
10
|
+
name: string;
|
|
11
|
+
chain: Chain;
|
|
12
|
+
explorer?: string;
|
|
13
|
+
}
|
|
14
|
+
export declare const NETWORKS: Record<string, {
|
|
15
|
+
explorer?: string;
|
|
16
|
+
}>;
|
|
17
|
+
export declare function networkNames(): string[];
|
|
18
|
+
export interface ClientOptions {
|
|
19
|
+
network: string;
|
|
20
|
+
accessKey?: string;
|
|
21
|
+
rpcUrl?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Build a chain definition, optionally overriding the RPC endpoint. */
|
|
24
|
+
export declare function resolveChain(options: ClientOptions): Chain;
|
|
25
|
+
export declare function explorerUrl(network: string, key?: string): string | null;
|
|
26
|
+
export declare function takeNotice(): string;
|
|
27
|
+
/**
|
|
28
|
+
* The SDK's own client types are narrower than the client it actually returns:
|
|
29
|
+
* `where()` is typed as taking an Expression even though a rendered predicate
|
|
30
|
+
* string works at runtime, and the generic projection types reject dynamic
|
|
31
|
+
* SELECT lists. Rather than spraying casts through every command, the CLI
|
|
32
|
+
* declares the surface it uses once, here, and casts at the factory boundary.
|
|
33
|
+
*/
|
|
34
|
+
export interface QueryResultLike {
|
|
35
|
+
entities: Entity[];
|
|
36
|
+
blockNumber?: bigint;
|
|
37
|
+
skipped?: number;
|
|
38
|
+
count?: bigint;
|
|
39
|
+
}
|
|
40
|
+
export interface QueryBuilderLike {
|
|
41
|
+
where(expression: string | unknown): QueryBuilderLike;
|
|
42
|
+
orderBy(term: unknown): QueryBuilderLike;
|
|
43
|
+
limit(n: number): QueryBuilderLike;
|
|
44
|
+
offset(n: number): QueryBuilderLike;
|
|
45
|
+
withCount(): QueryBuilderLike;
|
|
46
|
+
fetch(): Promise<QueryResultLike>;
|
|
47
|
+
}
|
|
48
|
+
export interface ArkivPublic {
|
|
49
|
+
select(selection?: Record<string, boolean>): QueryBuilderLike;
|
|
50
|
+
getEntity(key: `0x${string}`): Promise<unknown>;
|
|
51
|
+
getBlockTiming(): Promise<unknown>;
|
|
52
|
+
getBalance(args: {
|
|
53
|
+
address: `0x${string}`;
|
|
54
|
+
}): Promise<bigint>;
|
|
55
|
+
getChainId(): Promise<number>;
|
|
56
|
+
getBlock(args?: {
|
|
57
|
+
blockNumber?: bigint;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
timestamp?: bigint | number;
|
|
60
|
+
}>;
|
|
61
|
+
getGasPrice(): Promise<bigint>;
|
|
62
|
+
}
|
|
63
|
+
export declare function makeClient(options: ClientOptions): ArkivPublic;
|
|
64
|
+
export declare function makeWallet(options: ClientOptions, privateKey: `0x${string}`): {
|
|
65
|
+
client: WalletArkivClient;
|
|
66
|
+
account: PrivateKeyAccount;
|
|
67
|
+
chain: Chain;
|
|
68
|
+
};
|
|
69
|
+
export interface Timing {
|
|
70
|
+
currentBlock: number;
|
|
71
|
+
currentBlockTime: number;
|
|
72
|
+
blockDuration: number;
|
|
73
|
+
}
|
|
74
|
+
export declare function getTiming(client: ArkivPublic): Promise<Timing>;
|
|
75
|
+
export declare function blockToUnix(timing: Timing, block: number): number;
|
|
76
|
+
export declare function unixToBlock(timing: Timing, unixSeconds: number): number;
|
|
77
|
+
export declare function explainError(error: unknown): string;
|
package/dist/arkiv.js
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Chain access for the CLI: network resolution, a quiet client factory and
|
|
3
|
+
* helpers that turn block numbers into wall-clock answers.
|
|
4
|
+
*/
|
|
5
|
+
import { createPublicClient, createWalletClient, } from "@arkiv-network/sdk";
|
|
6
|
+
import * as chains from "@arkiv-network/sdk/chains";
|
|
7
|
+
import { http } from "viem";
|
|
8
|
+
import { privateKeyToAccount } from "viem/accounts";
|
|
9
|
+
import { assertSafeRpcUrl } from "./env.js";
|
|
10
|
+
import { safeText } from "./values.js";
|
|
11
|
+
export const NETWORKS = {
|
|
12
|
+
rabdi: { explorer: "https://rabdi.explorer.arkiv.network" },
|
|
13
|
+
localhost: {},
|
|
14
|
+
};
|
|
15
|
+
export function networkNames() {
|
|
16
|
+
return Object.keys(NETWORKS);
|
|
17
|
+
}
|
|
18
|
+
/** Build a chain definition, optionally overriding the RPC endpoint. */
|
|
19
|
+
export function resolveChain(options) {
|
|
20
|
+
const base = chains[options.network];
|
|
21
|
+
if (!base) {
|
|
22
|
+
throw new Error(`unknown network "${options.network}" (available: ${networkNames().join(", ")})`);
|
|
23
|
+
}
|
|
24
|
+
const chain = base({ accessKey: options.accessKey ?? "" });
|
|
25
|
+
if (!options.rpcUrl)
|
|
26
|
+
return chain;
|
|
27
|
+
const safe = assertSafeRpcUrl(options.rpcUrl);
|
|
28
|
+
if (!safe.ok)
|
|
29
|
+
throw new Error(safe.reason);
|
|
30
|
+
return {
|
|
31
|
+
...chain,
|
|
32
|
+
rpcUrls: { default: { http: [safe.url] } },
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* The SDK prints a rate-limit notice when no access key is configured. That is
|
|
37
|
+
* useful advice, but it must not pollute stdout in --json mode, so the notice
|
|
38
|
+
* is captured here and re-emitted on stderr by the caller once.
|
|
39
|
+
*/
|
|
40
|
+
let capturedNotice = "";
|
|
41
|
+
export function explorerUrl(network, key) {
|
|
42
|
+
const base = NETWORKS[network]?.explorer;
|
|
43
|
+
if (!base)
|
|
44
|
+
return null;
|
|
45
|
+
return key ? `${base}/entity/${key}` : base;
|
|
46
|
+
}
|
|
47
|
+
export function takeNotice() {
|
|
48
|
+
const notice = capturedNotice;
|
|
49
|
+
capturedNotice = "";
|
|
50
|
+
return notice;
|
|
51
|
+
}
|
|
52
|
+
function quietWarn() {
|
|
53
|
+
const originals = {
|
|
54
|
+
log: console.log,
|
|
55
|
+
warn: console.warn,
|
|
56
|
+
info: console.info,
|
|
57
|
+
debug: console.debug,
|
|
58
|
+
};
|
|
59
|
+
const capture = (...args) => {
|
|
60
|
+
const text = args.map(String).join(" ");
|
|
61
|
+
if (text.includes("heavily rate limited")) {
|
|
62
|
+
capturedNotice = text.trim();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
originals.log(...args);
|
|
66
|
+
};
|
|
67
|
+
console.log = capture;
|
|
68
|
+
console.info = capture;
|
|
69
|
+
console.debug = capture;
|
|
70
|
+
console.warn = capture;
|
|
71
|
+
return () => {
|
|
72
|
+
console.log = originals.log;
|
|
73
|
+
console.warn = originals.warn;
|
|
74
|
+
console.info = originals.info;
|
|
75
|
+
console.debug = originals.debug;
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
export function makeClient(options) {
|
|
79
|
+
const restore = quietWarn();
|
|
80
|
+
const chain = resolveChain(options);
|
|
81
|
+
try {
|
|
82
|
+
return createPublicClient({ chain, transport: http() });
|
|
83
|
+
}
|
|
84
|
+
finally {
|
|
85
|
+
restore();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
export function makeWallet(options, privateKey) {
|
|
89
|
+
const restore = quietWarn();
|
|
90
|
+
const chain = resolveChain(options);
|
|
91
|
+
const account = privateKeyToAccount(privateKey);
|
|
92
|
+
try {
|
|
93
|
+
const client = createWalletClient({ chain, transport: http(), account });
|
|
94
|
+
return { client, account, chain };
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
restore();
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
export async function getTiming(client) {
|
|
101
|
+
try {
|
|
102
|
+
const raw = (await client.getBlockTiming());
|
|
103
|
+
return {
|
|
104
|
+
currentBlock: Number(raw.currentBlock),
|
|
105
|
+
currentBlockTime: Number(raw.currentBlockTime),
|
|
106
|
+
blockDuration: Number(raw.blockDuration) || 2,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
const block = (await client.getBlock());
|
|
111
|
+
return {
|
|
112
|
+
currentBlock: Number(block.number ?? 0),
|
|
113
|
+
currentBlockTime: Number(block.timestamp ?? 0),
|
|
114
|
+
blockDuration: 2,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export function blockToUnix(timing, block) {
|
|
119
|
+
if (!Number.isFinite(block) || block <= 0)
|
|
120
|
+
return 0;
|
|
121
|
+
return timing.currentBlockTime + (block - timing.currentBlock) * timing.blockDuration;
|
|
122
|
+
}
|
|
123
|
+
export function unixToBlock(timing, unixSeconds) {
|
|
124
|
+
return (timing.currentBlock +
|
|
125
|
+
Math.ceil((unixSeconds - timing.currentBlockTime) / timing.blockDuration));
|
|
126
|
+
}
|
|
127
|
+
/* ------------------------------------------------------------------ errors */
|
|
128
|
+
const HINTS = [
|
|
129
|
+
[
|
|
130
|
+
/heavily rate limited|rate limit|429|too many requests/i,
|
|
131
|
+
"The public endpoint is rate limited. Get a free key at https://hub.arkiv.network/access-keys and set ARKIV_ACCESS_KEY.",
|
|
132
|
+
],
|
|
133
|
+
[
|
|
134
|
+
/missing access key|invalid access key|unauthori[sz]ed|401|403/i,
|
|
135
|
+
"The access key was rejected. Check ARKIV_ACCESS_KEY, or create a new key at https://hub.arkiv.network/access-keys.",
|
|
136
|
+
],
|
|
137
|
+
[/insufficient funds|insufficient balance|exceeds balance/i, "The signer has no tGLM. Use the Rabdi faucet: https://hub.arkiv.network/faucet."],
|
|
138
|
+
[
|
|
139
|
+
/not queryable/i,
|
|
140
|
+
"That field can be selected but not filtered on. Drop it from the where clause; see `arkiv limits`.",
|
|
141
|
+
],
|
|
142
|
+
[
|
|
143
|
+
/Query rejected/i,
|
|
144
|
+
"The query expression was rejected. Check syntax against https://docs.arkiv.network and try `arkiv query \"arkiv.schema = str('name')\"`.",
|
|
145
|
+
],
|
|
146
|
+
[/no entity|not found|NoEntityFound/i, "No entity with that key on this network. Keys are network specific."],
|
|
147
|
+
[/expired/i, "The entity has expired. Its record no longer exists on chain."],
|
|
148
|
+
[
|
|
149
|
+
/ENOTFOUND|ECONNREFUSED|fetch failed|network error/i,
|
|
150
|
+
"The RPC endpoint is unreachable. Check connectivity and --rpc/--network.",
|
|
151
|
+
],
|
|
152
|
+
[/entity too large|MAX_ENTITY_BYTES/i, "The entity exceeds the size limit. See `arkiv limits`."],
|
|
153
|
+
];
|
|
154
|
+
export function explainError(error) {
|
|
155
|
+
const message = error instanceof Error ? `${error.name}: ${error.message}` : String(error ?? "unknown error");
|
|
156
|
+
const hint = HINTS.find(([re]) => re.test(message))?.[1];
|
|
157
|
+
return hint ? `${safeText(message)}\n hint: ${hint}` : safeText(message);
|
|
158
|
+
}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The command surface.
|
|
4
|
+
*
|
|
5
|
+
* Every flag is declared, so a typo becomes a usage error instead of a silently
|
|
6
|
+
* different query. Nothing here prints a credential: values come through
|
|
7
|
+
* readSecret(), which registers them with the redactor in ui.ts.
|
|
8
|
+
*/
|
|
9
|
+
export declare function main(argv?: string[]): Promise<number>;
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The command surface.
|
|
4
|
+
*
|
|
5
|
+
* Every flag is declared, so a typo becomes a usage error instead of a silently
|
|
6
|
+
* different query. Nothing here prints a credential: values come through
|
|
7
|
+
* readSecret(), which registers them with the redactor in ui.ts.
|
|
8
|
+
*/
|
|
9
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
10
|
+
/*
|
|
11
|
+
* Node 26 emits an ExperimentalWarning the first time anything touches
|
|
12
|
+
* localStorage, and the SDK does that while building a client. That warning is
|
|
13
|
+
* not about this program and it corrupts piped output and --json, so it is
|
|
14
|
+
* dropped here rather than left for every caller to filter.
|
|
15
|
+
*/
|
|
16
|
+
const emitWarning = process.emitWarning.bind(process);
|
|
17
|
+
process.emitWarning = ((warning, ...rest) => {
|
|
18
|
+
const text = typeof warning === "string" ? warning : warning?.message ?? "";
|
|
19
|
+
if (/localstorage/i.test(text))
|
|
20
|
+
return;
|
|
21
|
+
return emitWarning(warning, ...rest);
|
|
22
|
+
});
|
|
23
|
+
import { parseArgs, UsageError } from "./args.js";
|
|
24
|
+
import { buildContext } from "./context.js";
|
|
25
|
+
import { setColour, bold, dim, green, out, red, sanitize } from "./ui.js";
|
|
26
|
+
import { networkNames, explainError, takeNotice } from "./arkiv.js";
|
|
27
|
+
import { runDoctor, printDoctor } from "./commands/doctor.js";
|
|
28
|
+
import { runQuery } from "./commands/query.js";
|
|
29
|
+
import { runEntity } from "./commands/entity.js";
|
|
30
|
+
import { runSchemaList, runSchemaDetail, sampleSchemas, renderAtlas } from "./commands/schema.js";
|
|
31
|
+
import { runCost, describeLimits, parseLifetime } from "./commands/cost.js";
|
|
32
|
+
import { runCreate } from "./commands/create.js";
|
|
33
|
+
import { runInit } from "./commands/init.js";
|
|
34
|
+
import { runWatch } from "./commands/watch.js";
|
|
35
|
+
const VERSION = readVersion();
|
|
36
|
+
function readVersion() {
|
|
37
|
+
try {
|
|
38
|
+
const raw = readFileSync(new URL("../package.json", import.meta.url), "utf8");
|
|
39
|
+
return JSON.parse(raw).version ?? "0.0.0";
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return "0.0.0";
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
const HELP = `arkiv - a terminal toolkit for the Arkiv network
|
|
46
|
+
|
|
47
|
+
arkiv init [dir] scaffold a runnable project
|
|
48
|
+
arkiv doctor check node, keys, balance, network and .env safety
|
|
49
|
+
arkiv query <expression> run a query
|
|
50
|
+
arkiv entity <key> read one entity in full
|
|
51
|
+
arkiv schema [name] discover schemas, lint their names, or open one
|
|
52
|
+
arkiv create write one entity
|
|
53
|
+
arkiv watch <expression> poll a query and print changes
|
|
54
|
+
arkiv cost size a write before you send it
|
|
55
|
+
arkiv limits protocol limits in one table
|
|
56
|
+
arkiv help this page
|
|
57
|
+
|
|
58
|
+
Global flags
|
|
59
|
+
--network <name> ${networkNames().join(" | ")} (default rabdi)
|
|
60
|
+
--rpc <url> override the RPC endpoint
|
|
61
|
+
--env <path> use a different .env file
|
|
62
|
+
--json machine-readable output where supported
|
|
63
|
+
--no-colour plain text
|
|
64
|
+
--verbose extra detail
|
|
65
|
+
-h, --help help for a command
|
|
66
|
+
-v, --version version
|
|
67
|
+
|
|
68
|
+
Credentials
|
|
69
|
+
ARKIV_ACCESS_KEY and ARKIV_PRIVATE_KEY are read from .env or the environment.
|
|
70
|
+
The CLI never generates a wallet and never prints a key.
|
|
71
|
+
|
|
72
|
+
Examples
|
|
73
|
+
arkiv doctor
|
|
74
|
+
arkiv query "arkiv.schema = str('notes/v1')" --limit 5
|
|
75
|
+
arkiv schema --atlas atlas.html
|
|
76
|
+
arkiv create --schema notes/v1 --set title=str:Hello --set body=bytes:@./README.md
|
|
77
|
+
arkiv watch "arkiv.schema = str('notes/v1')" --interval 5
|
|
78
|
+
`;
|
|
79
|
+
const GLOBAL_SPEC = {
|
|
80
|
+
help: "bool",
|
|
81
|
+
version: "bool",
|
|
82
|
+
network: "string",
|
|
83
|
+
rpc: "string",
|
|
84
|
+
env: "string",
|
|
85
|
+
json: "bool",
|
|
86
|
+
colour: "bool",
|
|
87
|
+
"no-colour": "bool",
|
|
88
|
+
verbose: "bool",
|
|
89
|
+
quiet: "bool",
|
|
90
|
+
};
|
|
91
|
+
function exitCode() {
|
|
92
|
+
return typeof process.exitCode === "number" ? process.exitCode : 0;
|
|
93
|
+
}
|
|
94
|
+
function strFlag(flags, name) {
|
|
95
|
+
const value = flags[name];
|
|
96
|
+
return typeof value === "string" ? value : undefined;
|
|
97
|
+
}
|
|
98
|
+
function numFlag(flags, name, fallback) {
|
|
99
|
+
const value = flags[name];
|
|
100
|
+
return typeof value === "number" ? value : fallback;
|
|
101
|
+
}
|
|
102
|
+
function boolFlag(flags, name) {
|
|
103
|
+
return flags[name] === true;
|
|
104
|
+
}
|
|
105
|
+
function listFlag(flags, name) {
|
|
106
|
+
const value = flags[name];
|
|
107
|
+
if (value === undefined)
|
|
108
|
+
return [];
|
|
109
|
+
return Array.isArray(value) ? value.map(String) : [String(value)];
|
|
110
|
+
}
|
|
111
|
+
function globals(flags) {
|
|
112
|
+
const noColour = boolFlag(flags, "no-colour");
|
|
113
|
+
return {
|
|
114
|
+
json: boolFlag(flags, "json"),
|
|
115
|
+
colour: !noColour,
|
|
116
|
+
network: strFlag(flags, "network"),
|
|
117
|
+
rpc: strFlag(flags, "rpc"),
|
|
118
|
+
env: strFlag(flags, "env"),
|
|
119
|
+
verbose: boolFlag(flags, "verbose"),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
123
|
+
if (argv.length === 0 || argv[0] === "help" || argv[0] === "--help" || argv[0] === "-h") {
|
|
124
|
+
process.stdout.write(HELP);
|
|
125
|
+
return 0;
|
|
126
|
+
}
|
|
127
|
+
if (argv[0] === "--version" || argv[0] === "-v" || argv[0] === "version") {
|
|
128
|
+
process.stdout.write(`arkiv ${VERSION}\n`);
|
|
129
|
+
return 0;
|
|
130
|
+
}
|
|
131
|
+
const command = argv[0];
|
|
132
|
+
const rest = argv.slice(1);
|
|
133
|
+
try {
|
|
134
|
+
switch (command) {
|
|
135
|
+
case "init": {
|
|
136
|
+
const parsed = parseArgs(rest, { ...GLOBAL_SPEC, force: "bool", install: "bool" }, { f: "force" });
|
|
137
|
+
setColour(globals(parsed.flags).colour);
|
|
138
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
139
|
+
process.stdout.write("arkiv init [dir]\n\n scaffold a project that connects, writes and reads\n\n --force write into an existing directory (never overwrites files)\n --install run npm install afterwards\n");
|
|
140
|
+
return 0;
|
|
141
|
+
}
|
|
142
|
+
await runInit({
|
|
143
|
+
directory: parsed.positionals[0] ?? ".",
|
|
144
|
+
force: boolFlag(parsed.flags, "force"),
|
|
145
|
+
install: boolFlag(parsed.flags, "install"),
|
|
146
|
+
});
|
|
147
|
+
return exitCode();
|
|
148
|
+
}
|
|
149
|
+
case "doctor": {
|
|
150
|
+
const parsed = parseArgs(rest, GLOBAL_SPEC, {});
|
|
151
|
+
const g = globals(parsed.flags);
|
|
152
|
+
setColour(g.colour);
|
|
153
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
154
|
+
process.stdout.write("arkiv doctor\n\n checks node, .env permissions, key formats, balance, clock and RPC\n");
|
|
155
|
+
return 0;
|
|
156
|
+
}
|
|
157
|
+
const result = await runDoctor(buildContext(g));
|
|
158
|
+
printDoctor(result.checks, g.json === true);
|
|
159
|
+
return result.ok ? 0 : 1;
|
|
160
|
+
}
|
|
161
|
+
case "query": {
|
|
162
|
+
const parsed = parseArgs(rest, {
|
|
163
|
+
...GLOBAL_SPEC,
|
|
164
|
+
schema: "string",
|
|
165
|
+
limit: "number",
|
|
166
|
+
offset: "number",
|
|
167
|
+
sort: "string",
|
|
168
|
+
type: "string",
|
|
169
|
+
desc: "bool",
|
|
170
|
+
asc: "bool",
|
|
171
|
+
count: "bool",
|
|
172
|
+
key: "bool",
|
|
173
|
+
select: "string",
|
|
174
|
+
}, { n: "limit", s: "schema", j: "json", q: "quiet" });
|
|
175
|
+
const g = globals(parsed.flags);
|
|
176
|
+
setColour(g.colour);
|
|
177
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
178
|
+
process.stdout.write("arkiv query <expression>\n\n --schema <name> add arkiv.schema = str(name)\n --limit <n> rows per page (max 100, default 10)\n --offset <n> skip rows\n --sort <field> order by a queryable attribute\n --type <t> value type for the sort field\n --desc descending\n --count count matches instead of listing\n --key print keys only\n --select a,b project specific attributes\n");
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const select = strFlag(parsed.flags, "select");
|
|
182
|
+
await runQuery(buildContext(g), {
|
|
183
|
+
expression: parsed.positionals[0],
|
|
184
|
+
schema: strFlag(parsed.flags, "schema"),
|
|
185
|
+
limit: numFlag(parsed.flags, "limit", 10),
|
|
186
|
+
offset: numFlag(parsed.flags, "offset", 0),
|
|
187
|
+
countOnly: boolFlag(parsed.flags, "count"),
|
|
188
|
+
showKey: boolFlag(parsed.flags, "key"),
|
|
189
|
+
sort: strFlag(parsed.flags, "sort"),
|
|
190
|
+
sortType: strFlag(parsed.flags, "type"),
|
|
191
|
+
descending: boolFlag(parsed.flags, "desc"),
|
|
192
|
+
select: select ? select.split(",").map((s) => s.trim()) : undefined,
|
|
193
|
+
});
|
|
194
|
+
return exitCode();
|
|
195
|
+
}
|
|
196
|
+
case "entity": {
|
|
197
|
+
const parsed = parseArgs(rest, GLOBAL_SPEC, { j: "json" });
|
|
198
|
+
const g = globals(parsed.flags);
|
|
199
|
+
setColour(g.colour);
|
|
200
|
+
if (boolFlag(parsed.flags, "help") || parsed.positionals.length === 0) {
|
|
201
|
+
process.stdout.write("arkiv entity <key>\n\n read one entity, including its attributes and expiry\n");
|
|
202
|
+
return boolFlag(parsed.flags, "help") ? 0 : 2;
|
|
203
|
+
}
|
|
204
|
+
await runEntity(buildContext(g), parsed.positionals[0]);
|
|
205
|
+
return exitCode();
|
|
206
|
+
}
|
|
207
|
+
case "schema": {
|
|
208
|
+
const parsed = parseArgs(rest, { ...GLOBAL_SPEC, sample: "number", atlas: "string", min: "number" }, { n: "sample", j: "json" });
|
|
209
|
+
const g = globals(parsed.flags);
|
|
210
|
+
setColour(g.colour);
|
|
211
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
212
|
+
process.stdout.write("arkiv schema [name]\n\n list every schema found in a live sample, with lint findings\n name one schema: fields, types, expiry, findings\n\n --sample <n> entities to sample (default 300, max 1000)\n --atlas <file> write a shareable HTML atlas\n --min <n> hide schemas with fewer than n entities\n");
|
|
213
|
+
return 0;
|
|
214
|
+
}
|
|
215
|
+
const ctx = buildContext(g);
|
|
216
|
+
const atlas = strFlag(parsed.flags, "atlas");
|
|
217
|
+
const sample = numFlag(parsed.flags, "sample", atlas ? 1000 : 300);
|
|
218
|
+
const name = parsed.positionals[0];
|
|
219
|
+
if (name) {
|
|
220
|
+
await runSchemaDetail(ctx, name);
|
|
221
|
+
return exitCode();
|
|
222
|
+
}
|
|
223
|
+
if (atlas) {
|
|
224
|
+
const { schemas, scanned, block } = await sampleSchemas(ctx, { sample });
|
|
225
|
+
writeFileSync(atlas, renderAtlas(schemas, { scanned, block, network: ctx.network }));
|
|
226
|
+
out(green(`wrote ${atlas}`));
|
|
227
|
+
out(dim(`${schemas.size} schemas from ${scanned} sampled entities at block ${block.toLocaleString("en-US")}`));
|
|
228
|
+
return 0;
|
|
229
|
+
}
|
|
230
|
+
await runSchemaList(ctx, { sample });
|
|
231
|
+
return exitCode();
|
|
232
|
+
}
|
|
233
|
+
case "create": {
|
|
234
|
+
const parsed = parseArgs(rest, {
|
|
235
|
+
...GLOBAL_SPEC,
|
|
236
|
+
schema: "string",
|
|
237
|
+
set: "string",
|
|
238
|
+
index: "string",
|
|
239
|
+
expires: "string",
|
|
240
|
+
"dry-run": "bool",
|
|
241
|
+
yes: "bool",
|
|
242
|
+
}, { s: "schema", j: "json" });
|
|
243
|
+
const g = globals(parsed.flags);
|
|
244
|
+
setColour(g.colour);
|
|
245
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
246
|
+
process.stdout.write("arkiv create --schema <name> --set name=type:value [...]\n\n types str bytes u64 i32 u256 bool addr key dec\n bytes @./file, ./file or 0x hex\n --index <name> mark an attribute indexed (repeatable)\n --expires <30d> lifetime, default 30d\n --dry-run print the plan, sign nothing\n");
|
|
247
|
+
return 0;
|
|
248
|
+
}
|
|
249
|
+
const schema = strFlag(parsed.flags, "schema");
|
|
250
|
+
if (!schema)
|
|
251
|
+
throw new UsageError("create needs --schema <name>");
|
|
252
|
+
const sets = listFlag(parsed.flags, "set");
|
|
253
|
+
if (sets.length === 0)
|
|
254
|
+
throw new UsageError("create needs at least one --set name=type:value");
|
|
255
|
+
await runCreate(buildContext(g), {
|
|
256
|
+
schema,
|
|
257
|
+
sets,
|
|
258
|
+
indexed: listFlag(parsed.flags, "index"),
|
|
259
|
+
expires: strFlag(parsed.flags, "expires"),
|
|
260
|
+
dryRun: boolFlag(parsed.flags, "dry-run"),
|
|
261
|
+
});
|
|
262
|
+
return exitCode();
|
|
263
|
+
}
|
|
264
|
+
case "cost": {
|
|
265
|
+
const parsed = parseArgs(rest, { ...GLOBAL_SPEC, bytes: "number", file: "string", attributes: "number", expires: "string", avg: "number" }, { j: "json" });
|
|
266
|
+
const g = globals(parsed.flags);
|
|
267
|
+
setColour(g.colour);
|
|
268
|
+
if (boolFlag(parsed.flags, "help")) {
|
|
269
|
+
process.stdout.write("arkiv cost [--file path] [--bytes n] [--attributes n] [--expires 30d]\n\n sizes a write against the protocol limits and reads the live gas price\n");
|
|
270
|
+
return 0;
|
|
271
|
+
}
|
|
272
|
+
let lifetimeDays = 30;
|
|
273
|
+
try {
|
|
274
|
+
lifetimeDays = parseLifetime(strFlag(parsed.flags, "expires"), 30);
|
|
275
|
+
}
|
|
276
|
+
catch (error) {
|
|
277
|
+
throw new UsageError(error.message);
|
|
278
|
+
}
|
|
279
|
+
await runCost(buildContext(g), {
|
|
280
|
+
attributes: numFlag(parsed.flags, "attributes", 3),
|
|
281
|
+
bytes: numFlag(parsed.flags, "bytes", 0),
|
|
282
|
+
file: strFlag(parsed.flags, "file"),
|
|
283
|
+
lifetimeDays,
|
|
284
|
+
averageAttributeBytes: numFlag(parsed.flags, "avg", 32),
|
|
285
|
+
});
|
|
286
|
+
return exitCode();
|
|
287
|
+
}
|
|
288
|
+
case "limits": {
|
|
289
|
+
const parsed = parseArgs(rest, GLOBAL_SPEC, {});
|
|
290
|
+
setColour(globals(parsed.flags).colour);
|
|
291
|
+
out(bold("protocol limits"));
|
|
292
|
+
out(describeLimits());
|
|
293
|
+
return 0;
|
|
294
|
+
}
|
|
295
|
+
case "watch": {
|
|
296
|
+
const parsed = parseArgs(rest, { ...GLOBAL_SPEC, limit: "number", interval: "number", once: "bool" }, { n: "limit" });
|
|
297
|
+
const g = globals(parsed.flags);
|
|
298
|
+
setColour(g.colour);
|
|
299
|
+
if (boolFlag(parsed.flags, "help") || parsed.positionals.length === 0) {
|
|
300
|
+
process.stdout.write("arkiv watch <expression>\n\n --limit <n> window size (default 200, max 1000)\n --interval <s> seconds between polls (default 5)\n --once print the current window and exit\n");
|
|
301
|
+
return boolFlag(parsed.flags, "help") ? 0 : 2;
|
|
302
|
+
}
|
|
303
|
+
await runWatch(buildContext(g), {
|
|
304
|
+
expression: parsed.positionals[0],
|
|
305
|
+
limit: numFlag(parsed.flags, "limit", 200),
|
|
306
|
+
intervalSeconds: numFlag(parsed.flags, "interval", 5),
|
|
307
|
+
once: boolFlag(parsed.flags, "once"),
|
|
308
|
+
});
|
|
309
|
+
return exitCode();
|
|
310
|
+
}
|
|
311
|
+
default: {
|
|
312
|
+
process.stderr.write(`${red(`unknown command: ${command}`)}\n`);
|
|
313
|
+
process.stderr.write(HELP);
|
|
314
|
+
return 2;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
if (error instanceof UsageError) {
|
|
320
|
+
process.stderr.write(`${red("usage")}: ${error.message}\n`);
|
|
321
|
+
return 2;
|
|
322
|
+
}
|
|
323
|
+
process.stderr.write(`${red("error")}: ${explainError(error)}\n`);
|
|
324
|
+
return 1;
|
|
325
|
+
}
|
|
326
|
+
finally {
|
|
327
|
+
const notice = takeNotice();
|
|
328
|
+
if (notice)
|
|
329
|
+
process.stderr.write(`${dim(sanitize(notice))}\n`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
const isEntry = process.argv[1] !== undefined &&
|
|
333
|
+
(import.meta.url === `file://${process.argv[1]}` || import.meta.url.endsWith("/dist/cli.js"));
|
|
334
|
+
if (isEntry) {
|
|
335
|
+
main()
|
|
336
|
+
.then((code) => {
|
|
337
|
+
process.exitCode = code;
|
|
338
|
+
})
|
|
339
|
+
.catch((error) => {
|
|
340
|
+
process.stderr.write(`${red("fatal")}: ${error.message}\n`);
|
|
341
|
+
process.exitCode = 1;
|
|
342
|
+
});
|
|
343
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `arkiv cost` - the pre-flight sizing check.
|
|
3
|
+
*
|
|
4
|
+
* Arkiv charges for storage at write time and the exact fee is whatever the
|
|
5
|
+
* create transaction consumes, so this command does not invent a price. What it
|
|
6
|
+
* does is the part people get wrong: work out how many bytes and blocks a write
|
|
7
|
+
* really needs, whether it fits the protocol limits, how many creates that takes,
|
|
8
|
+
* and what the chain is charging for gas right now.
|
|
9
|
+
*/
|
|
10
|
+
import type { Context } from "../context.js";
|
|
11
|
+
export interface CostOptions {
|
|
12
|
+
attributes: number;
|
|
13
|
+
bytes: number;
|
|
14
|
+
file?: string;
|
|
15
|
+
lifetimeDays: number;
|
|
16
|
+
averageAttributeBytes: number;
|
|
17
|
+
}
|
|
18
|
+
export declare function parseLifetime(input: string | undefined, fallbackDays: number): number;
|
|
19
|
+
export declare function runCost(ctx: Context, options: CostOptions): Promise<void>;
|
|
20
|
+
export declare function formatBytes(bytes: number): string;
|
|
21
|
+
export declare function describeLimits(): string;
|
|
22
|
+
export declare function limitSummary(): string;
|