@trucore/atf 1.3.0 → 1.3.1
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 +72 -28
- package/dist/index.js +427 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## One-Liner
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
|
-
npx @trucore/atf@1.3.
|
|
8
|
+
npx @trucore/atf@1.3.1 simulate --preset swap_small --verify --pretty
|
|
9
9
|
```
|
|
10
10
|
|
|
11
11
|
That's it. No install, no config, no API key required.
|
|
@@ -22,31 +22,37 @@ That's it. No install, no config, no API key required.
|
|
|
22
22
|
## Install (optional)
|
|
23
23
|
|
|
24
24
|
```bash
|
|
25
|
-
npm install -g @trucore/atf@1.3.
|
|
25
|
+
npm install -g @trucore/atf@1.3.1
|
|
26
26
|
```
|
|
27
27
|
|
|
28
28
|
Or use directly with `npx` (always pin the version):
|
|
29
29
|
|
|
30
30
|
```bash
|
|
31
|
-
npx @trucore/atf@1.3.
|
|
31
|
+
npx @trucore/atf@1.3.1 simulate --preset swap_small
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Quickstart: Doctor (Dev Environment Check)
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
npx @trucore/atf@1.3.1 doctor --pretty
|
|
32
38
|
```
|
|
33
39
|
|
|
34
40
|
## Quickstart: Devnet Burner
|
|
35
41
|
|
|
36
42
|
```bash
|
|
37
|
-
npx @trucore/atf@1.3.
|
|
43
|
+
npx @trucore/atf@1.3.1 swap --burner --devnet --in SOL --out USDC --amount-in 0.01 --execute --yes --confirm
|
|
38
44
|
```
|
|
39
45
|
|
|
40
46
|
## Quickstart: Helius Profile Setup
|
|
41
47
|
|
|
42
48
|
```bash
|
|
43
|
-
npx @trucore/atf@1.3.
|
|
44
|
-
npx @trucore/atf@1.3.
|
|
45
|
-
npx @trucore/atf@1.3.
|
|
46
|
-
npx @trucore/atf@1.3.
|
|
47
|
-
npx @trucore/atf@1.3.
|
|
49
|
+
npx @trucore/atf@1.3.1 config init --yes
|
|
50
|
+
npx @trucore/atf@1.3.1 profile create devnet
|
|
51
|
+
npx @trucore/atf@1.3.1 profile use devnet
|
|
52
|
+
npx @trucore/atf@1.3.1 config set solana_cluster devnet
|
|
53
|
+
npx @trucore/atf@1.3.1 config set rpc_url helius
|
|
48
54
|
export HELIUS_API_KEY=your_key_here
|
|
49
|
-
npx @trucore/atf@1.3.
|
|
55
|
+
npx @trucore/atf@1.3.1 rpc ping
|
|
50
56
|
```
|
|
51
57
|
|
|
52
58
|
## Commands
|
|
@@ -56,9 +62,9 @@ npx @trucore/atf@1.3.0 rpc ping
|
|
|
56
62
|
Check API health and round-trip latency.
|
|
57
63
|
|
|
58
64
|
```bash
|
|
59
|
-
npx @trucore/atf@1.3.
|
|
60
|
-
npx @trucore/atf@1.3.
|
|
61
|
-
npx @trucore/atf@1.3.
|
|
65
|
+
npx @trucore/atf@1.3.1 health
|
|
66
|
+
npx @trucore/atf@1.3.1 health --pretty
|
|
67
|
+
npx @trucore/atf@1.3.1 health --base-url http://localhost:3000
|
|
62
68
|
```
|
|
63
69
|
|
|
64
70
|
**Output shape:**
|
|
@@ -72,9 +78,9 @@ npx @trucore/atf@1.3.0 health --base-url http://localhost:3000
|
|
|
72
78
|
Run a transaction simulation against the ATF API.
|
|
73
79
|
|
|
74
80
|
```bash
|
|
75
|
-
npx @trucore/atf@1.3.
|
|
76
|
-
npx @trucore/atf@1.3.
|
|
77
|
-
npx @trucore/atf@1.3.
|
|
81
|
+
npx @trucore/atf@1.3.1 simulate --preset swap_small --verify
|
|
82
|
+
npx @trucore/atf@1.3.1 simulate --preset swap_too_large --pretty
|
|
83
|
+
npx @trucore/atf@1.3.1 simulate --json '{"chain_id":1,"value_eth":"0.5"}' --base-url http://localhost:3000
|
|
78
84
|
```
|
|
79
85
|
|
|
80
86
|
**Output shape:**
|
|
@@ -88,8 +94,8 @@ npx @trucore/atf@1.3.0 simulate --json '{"chain_id":1,"value_eth":"0.5"}' --base
|
|
|
88
94
|
Approve a pending intent (requires authentication).
|
|
89
95
|
|
|
90
96
|
```bash
|
|
91
|
-
npx @trucore/atf@1.3.
|
|
92
|
-
npx @trucore/atf@1.3.
|
|
97
|
+
npx @trucore/atf@1.3.1 approve --intent abc123 --token mytoken
|
|
98
|
+
npx @trucore/atf@1.3.1 approve --intent abc123 --token mytoken --pretty
|
|
93
99
|
```
|
|
94
100
|
|
|
95
101
|
**Output shape:**
|
|
@@ -103,14 +109,14 @@ npx @trucore/atf@1.3.0 approve --intent abc123 --token mytoken --pretty
|
|
|
103
109
|
Show rich version information (CLI version, Node version, build info).
|
|
104
110
|
|
|
105
111
|
```bash
|
|
106
|
-
npx @trucore/atf@1.3.
|
|
107
|
-
npx @trucore/atf@1.3.
|
|
112
|
+
npx @trucore/atf@1.3.1 version
|
|
113
|
+
npx @trucore/atf@1.3.1 version --pretty
|
|
108
114
|
```
|
|
109
115
|
|
|
110
116
|
**Output shape:**
|
|
111
117
|
|
|
112
118
|
```json
|
|
113
|
-
{ "ok": true, "cli_version": "1.3.
|
|
119
|
+
{ "ok": true, "cli_version": "1.3.1", "node_version": "v22.0.0", "platform": "linux", "arch": "x64", "default_base_url": "https://api.trucore.xyz", "build_commit": "abc1234", "build_date": "2025-01-01T00:00:00Z" }
|
|
114
120
|
```
|
|
115
121
|
|
|
116
122
|
### `swap`
|
|
@@ -123,7 +129,7 @@ Execute a **real on-chain Solana swap** (Jupiter DEX) gated by ATF policy evalua
|
|
|
123
129
|
**Dry run (no funds move):**
|
|
124
130
|
|
|
125
131
|
```bash
|
|
126
|
-
npx @trucore/atf@1.3.
|
|
132
|
+
npx @trucore/atf@1.3.1 swap \
|
|
127
133
|
--in SOL --out USDC \
|
|
128
134
|
--amount-in 0.001 \
|
|
129
135
|
--slippage-bps 50 \
|
|
@@ -134,7 +140,7 @@ npx @trucore/atf@1.3.0 swap \
|
|
|
134
140
|
**Real transaction (mainnet):**
|
|
135
141
|
|
|
136
142
|
```bash
|
|
137
|
-
npx @trucore/atf@1.3.
|
|
143
|
+
npx @trucore/atf@1.3.1 swap \
|
|
138
144
|
--in SOL --out USDC \
|
|
139
145
|
--amount-in 0.001 \
|
|
140
146
|
--slippage-bps 50 \
|
|
@@ -146,7 +152,7 @@ npx @trucore/atf@1.3.0 swap \
|
|
|
146
152
|
**Devnet burner one-liner (generates ephemeral keypair, no files needed):**
|
|
147
153
|
|
|
148
154
|
```bash
|
|
149
|
-
npx @trucore/atf@1.3.
|
|
155
|
+
npx @trucore/atf@1.3.1 swap \
|
|
150
156
|
--burner --devnet \
|
|
151
157
|
--in SOL --out USDC \
|
|
152
158
|
--amount-in 0.01 \
|
|
@@ -316,7 +322,7 @@ atf whoami --profile staging
|
|
|
316
322
|
**Output shape:**
|
|
317
323
|
|
|
318
324
|
```json
|
|
319
|
-
{ "ok": true, "profile": "default", "atf_base_url": "https://api.trucore.xyz", "solana_cluster": "mainnet-beta", "rpc_host": "mainnet.helius-rpc.com", "commitment": "confirmed", "explorer": "solscan", "cli_version": "1.3.
|
|
325
|
+
{ "ok": true, "profile": "default", "atf_base_url": "https://api.trucore.xyz", "solana_cluster": "mainnet-beta", "rpc_host": "mainnet.helius-rpc.com", "commitment": "confirmed", "explorer": "solscan", "cli_version": "1.3.1" }
|
|
320
326
|
```
|
|
321
327
|
|
|
322
328
|
### `ls`
|
|
@@ -339,6 +345,43 @@ atf completion fish > ~/.config/fish/completions/atf.fish
|
|
|
339
345
|
atf completion powershell >> $PROFILE
|
|
340
346
|
```
|
|
341
347
|
|
|
348
|
+
### `doctor`
|
|
349
|
+
|
|
350
|
+
Run a comprehensive dev environment health check — ATF API, RPC, config, keypair, and explorer.
|
|
351
|
+
|
|
352
|
+
```bash
|
|
353
|
+
npx @trucore/atf@1.3.1 doctor --pretty
|
|
354
|
+
npx @trucore/atf@1.3.1 doctor --profile devnet --pretty
|
|
355
|
+
npx @trucore/atf@1.3.1 doctor --rpc https://custom-rpc.example.com
|
|
356
|
+
npx @trucore/atf@1.3.1 doctor --verbose
|
|
357
|
+
```
|
|
358
|
+
|
|
359
|
+
**Output shape (JSON, default):**
|
|
360
|
+
|
|
361
|
+
```json
|
|
362
|
+
{
|
|
363
|
+
"ok": true,
|
|
364
|
+
"summary": { "status": "ok", "passed": 5, "warned": 0, "failed": 0 },
|
|
365
|
+
"env": { "cli_version": "1.3.1", "node_version": "v22.0.0", "platform": "linux", "arch": "x64" },
|
|
366
|
+
"effective": { "profile": "default", "atf_base_url": "https://api.trucore.xyz", "solana_cluster": "mainnet" },
|
|
367
|
+
"checks": [
|
|
368
|
+
{ "name": "atf_health", "status": "pass", "latency_ms": 42, "details": { "http_status": 200 }, "actions": [] },
|
|
369
|
+
{ "name": "simulate_sanity", "status": "pass", "latency_ms": 55, "details": { "decision": "allowed" }, "actions": [] },
|
|
370
|
+
{ "name": "rpc_health", "status": "pass", "latency_ms": 30, "details": { "solana_version": "2.2.1" }, "actions": [] },
|
|
371
|
+
{ "name": "keypair", "status": "pass", "details": { "configured": true, "valid": true }, "actions": [] },
|
|
372
|
+
{ "name": "explorer_link", "status": "pass", "details": { "explorer": "solscan" }, "actions": [] }
|
|
373
|
+
]
|
|
374
|
+
}
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
**Exit codes:**
|
|
378
|
+
|
|
379
|
+
| Code | Meaning |
|
|
380
|
+
|------|---------|
|
|
381
|
+
| 0 | All checks pass (or warn-only) |
|
|
382
|
+
| 1 | User/actionable misconfiguration (missing Helius key, invalid config) |
|
|
383
|
+
| 2 | Network/server failure (ATF API or RPC unreachable) |
|
|
384
|
+
|
|
342
385
|
## Global Options
|
|
343
386
|
|
|
344
387
|
| Flag | Description | Default |
|
|
@@ -525,9 +568,10 @@ npm pack --dry-run
|
|
|
525
568
|
npm publish --access public
|
|
526
569
|
|
|
527
570
|
# 5. Verify install (from a clean machine or CI)
|
|
528
|
-
npx @trucore/atf@1.3.
|
|
529
|
-
npx @trucore/atf@1.3.
|
|
530
|
-
npx @trucore/atf@1.3.
|
|
571
|
+
npx @trucore/atf@1.3.1 version
|
|
572
|
+
npx @trucore/atf@1.3.1 health
|
|
573
|
+
npx @trucore/atf@1.3.1 whoami
|
|
574
|
+
npx @trucore/atf@1.3.1 doctor --pretty
|
|
531
575
|
```
|
|
532
576
|
|
|
533
577
|
## License
|
package/dist/index.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
"use strict";
|
|
3
3
|
|
|
4
|
-
// @trucore/atf v1.3.
|
|
5
|
-
// Built: 2026-02-
|
|
4
|
+
// @trucore/atf v1.3.1 — Agent Transaction Firewall CLI
|
|
5
|
+
// Built: 2026-02-28T17:33:25.811Z
|
|
6
6
|
// Commit: 1bf6915
|
|
7
7
|
|
|
8
8
|
// ---- src/constants.mjs ----
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* by build.mjs during the bundling step.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
const VERSION = "1.3.
|
|
16
|
+
const VERSION = "1.3.1";
|
|
17
17
|
const DEFAULT_BASE_URL = "https://api.trucore.xyz";
|
|
18
18
|
const BUILD_COMMIT = "1bf6915";
|
|
19
|
-
const BUILD_DATE = "2026-02-
|
|
19
|
+
const BUILD_DATE = "2026-02-28T17:33:25.811Z";
|
|
20
20
|
const SIMULATE_PATHS = ["/api/simulate", "/v1/simulate"];
|
|
21
21
|
|
|
22
22
|
// ---- src/redact.mjs ----
|
|
@@ -3490,6 +3490,422 @@ async function runCompletion(args) {
|
|
|
3490
3490
|
process.stdout.write(script);
|
|
3491
3491
|
}
|
|
3492
3492
|
|
|
3493
|
+
// ---- src/doctor.mjs ----
|
|
3494
|
+
/**
|
|
3495
|
+
* doctor.mjs — `atf doctor` command
|
|
3496
|
+
*
|
|
3497
|
+
* Runs a comprehensive, fail-soft set of dev environment checks and returns
|
|
3498
|
+
* a single JSON envelope. Each check collects pass/warn/fail status and
|
|
3499
|
+
* actionable suggestions. Secret values are never printed.
|
|
3500
|
+
*
|
|
3501
|
+
* Exit codes:
|
|
3502
|
+
* 0 — all checks pass (or warn-only)
|
|
3503
|
+
* 1 — user/actionable misconfiguration
|
|
3504
|
+
* 2 — network/server failure
|
|
3505
|
+
*/
|
|
3506
|
+
|
|
3507
|
+
async function runDoctor(args) {
|
|
3508
|
+
const format = args.format;
|
|
3509
|
+
const verbose = args.verbose;
|
|
3510
|
+
const timeoutMs = args.timeoutMs || 10000;
|
|
3511
|
+
const profileName = args.profileFlag || null;
|
|
3512
|
+
const isDevnet = args.devnet || false;
|
|
3513
|
+
|
|
3514
|
+
// ── 1) Resolve effective config ──────────────────────────────────
|
|
3515
|
+
const { name: pName, profile } = resolveEffectiveProfile(profileName);
|
|
3516
|
+
|
|
3517
|
+
// Apply devnet override from flag
|
|
3518
|
+
const cluster = isDevnet ? "devnet" : (profile.solana_cluster || "mainnet");
|
|
3519
|
+
|
|
3520
|
+
// Apply CLI flag overrides for base URL and RPC
|
|
3521
|
+
const effectiveBaseUrl = args.baseUrl || profile.atf_base_url || DEFAULT_BASE_URL;
|
|
3522
|
+
|
|
3523
|
+
// Config sources (informational)
|
|
3524
|
+
const configSources = [];
|
|
3525
|
+
const { existsSync } = require("node:fs");
|
|
3526
|
+
if (existsSync(resolveGlobalConfigPath())) configSources.push("global");
|
|
3527
|
+
if (existsSync(resolveProjectConfigPath())) configSources.push("project");
|
|
3528
|
+
if (process.env.ATF_BASE_URL) configSources.push("env:ATF_BASE_URL");
|
|
3529
|
+
if (args.baseUrl !== DEFAULT_BASE_URL && !process.env.ATF_BASE_URL) configSources.push("flag:--base-url");
|
|
3530
|
+
if (args.profileFlag) configSources.push("flag:--profile");
|
|
3531
|
+
|
|
3532
|
+
// ── Collect checks array ────────────────────────────────────────
|
|
3533
|
+
const checks = [];
|
|
3534
|
+
|
|
3535
|
+
// ── 2) CLI runtime info (env block) ────────────────────────────
|
|
3536
|
+
const commit = BUILD_COMMIT === "__BUILD_COMMIT__" ? null : BUILD_COMMIT;
|
|
3537
|
+
const buildTime = BUILD_DATE === "__BUILD_DATE__" ? null : BUILD_DATE;
|
|
3538
|
+
const env = {
|
|
3539
|
+
cli_version: VERSION,
|
|
3540
|
+
node_version: process.version,
|
|
3541
|
+
platform: process.platform,
|
|
3542
|
+
arch: process.arch,
|
|
3543
|
+
build_commit: commit,
|
|
3544
|
+
build_date: buildTime,
|
|
3545
|
+
};
|
|
3546
|
+
|
|
3547
|
+
// ── Resolve RPC URL (safe — don't exit on failure) ─────────────
|
|
3548
|
+
let rpcUrl = null;
|
|
3549
|
+
let rpcHost = null;
|
|
3550
|
+
let heliusKeySource = null;
|
|
3551
|
+
let rpcResolutionError = null;
|
|
3552
|
+
|
|
3553
|
+
// Determine RPC setting
|
|
3554
|
+
const rpcSetting = args.rpcUrl || profile.rpc_url;
|
|
3555
|
+
const isHeliusMode = rpcSetting === "helius" || args.rpcUrl === "helius";
|
|
3556
|
+
|
|
3557
|
+
if (isHeliusMode) {
|
|
3558
|
+
// Check key sources without exiting
|
|
3559
|
+
const envKey = process.env.HELIUS_API_KEY;
|
|
3560
|
+
const secretKey = getSecret("helius", pName);
|
|
3561
|
+
if (envKey && envKey.length > 0) {
|
|
3562
|
+
heliusKeySource = "env";
|
|
3563
|
+
const url = buildHeliusRpcUrl(cluster, envKey);
|
|
3564
|
+
rpcUrl = url;
|
|
3565
|
+
rpcHost = extractRpcHost(url);
|
|
3566
|
+
} else if (secretKey) {
|
|
3567
|
+
heliusKeySource = "secrets";
|
|
3568
|
+
const url = buildHeliusRpcUrl(cluster, secretKey);
|
|
3569
|
+
rpcUrl = url;
|
|
3570
|
+
rpcHost = extractRpcHost(url);
|
|
3571
|
+
} else {
|
|
3572
|
+
heliusKeySource = "missing";
|
|
3573
|
+
rpcResolutionError = "Helius API key not found";
|
|
3574
|
+
}
|
|
3575
|
+
} else if (rpcSetting) {
|
|
3576
|
+
rpcUrl = rpcSetting;
|
|
3577
|
+
rpcHost = extractRpcHost(rpcSetting);
|
|
3578
|
+
} else if (args.rpcUrl) {
|
|
3579
|
+
rpcUrl = args.rpcUrl;
|
|
3580
|
+
rpcHost = extractRpcHost(args.rpcUrl);
|
|
3581
|
+
} else {
|
|
3582
|
+
// Use defaults
|
|
3583
|
+
if (isDevnet) {
|
|
3584
|
+
const devnetDefault = process.env.ATF_DEVNET_RPC || DEVNET_RPC_DEFAULT;
|
|
3585
|
+
rpcUrl = devnetDefault;
|
|
3586
|
+
rpcHost = extractRpcHost(devnetDefault);
|
|
3587
|
+
} else {
|
|
3588
|
+
rpcUrl = "https://api.mainnet-beta.solana.com";
|
|
3589
|
+
rpcHost = "api.mainnet-beta.solana.com";
|
|
3590
|
+
}
|
|
3591
|
+
}
|
|
3592
|
+
|
|
3593
|
+
const effective = {
|
|
3594
|
+
profile: pName,
|
|
3595
|
+
atf_base_url: effectiveBaseUrl,
|
|
3596
|
+
solana_cluster: cluster,
|
|
3597
|
+
rpc_host: rpcHost ? redactApiKeyInUrl(rpcHost) : null,
|
|
3598
|
+
commitment: profile.commitment || "confirmed",
|
|
3599
|
+
explorer: profile.explorer || "solscan",
|
|
3600
|
+
config_sources: configSources,
|
|
3601
|
+
};
|
|
3602
|
+
|
|
3603
|
+
// Helper: verbose log to stderr
|
|
3604
|
+
function vlog(msg) {
|
|
3605
|
+
if (verbose) process.stderr.write(` [doctor] ${msg}\n`);
|
|
3606
|
+
}
|
|
3607
|
+
|
|
3608
|
+
// ── 3) ATF API health check ────────────────────────────────────
|
|
3609
|
+
vlog(`Checking ATF API at ${redactApiKeyInUrl(effectiveBaseUrl)}/health ...`);
|
|
3610
|
+
{
|
|
3611
|
+
const check = { name: "atf_health", status: "pass", latency_ms: null, details: {}, actions: [] };
|
|
3612
|
+
const start = Date.now();
|
|
3613
|
+
try {
|
|
3614
|
+
const response = await getHealth(effectiveBaseUrl, timeoutMs);
|
|
3615
|
+
check.latency_ms = Date.now() - start;
|
|
3616
|
+
check.details.http_status = response.status;
|
|
3617
|
+
check.details.request_id = response.requestId || null;
|
|
3618
|
+
check.details.path = "/health";
|
|
3619
|
+
if (!response.ok) {
|
|
3620
|
+
check.status = "fail";
|
|
3621
|
+
check.details.http_status = response.status;
|
|
3622
|
+
check.actions.push(`Check base URL: atf config set atf_base_url ${DEFAULT_BASE_URL}`);
|
|
3623
|
+
check.actions.push("Run: atf health --pretty");
|
|
3624
|
+
}
|
|
3625
|
+
} catch (err) {
|
|
3626
|
+
check.latency_ms = Date.now() - start;
|
|
3627
|
+
check.status = "fail";
|
|
3628
|
+
check.details.error = err.message || String(err);
|
|
3629
|
+
check.details.path = "/health";
|
|
3630
|
+
check.actions.push(`Cannot reach ${redactApiKeyInUrl(effectiveBaseUrl)}/health`);
|
|
3631
|
+
check.actions.push(`Check base URL: atf config set atf_base_url ${DEFAULT_BASE_URL}`);
|
|
3632
|
+
}
|
|
3633
|
+
checks.push(check);
|
|
3634
|
+
}
|
|
3635
|
+
|
|
3636
|
+
// ── 4) Simulate sanity check ───────────────────────────────────
|
|
3637
|
+
vlog("Running simulate sanity check (swap_small preset) ...");
|
|
3638
|
+
{
|
|
3639
|
+
const check = { name: "simulate_sanity", status: "pass", latency_ms: null, details: {}, actions: [] };
|
|
3640
|
+
const body = PRESETS.swap_small.transaction;
|
|
3641
|
+
const start = Date.now();
|
|
3642
|
+
try {
|
|
3643
|
+
let response;
|
|
3644
|
+
const attemptedPaths = [];
|
|
3645
|
+
for (const path of SIMULATE_PATHS) {
|
|
3646
|
+
attemptedPaths.push(path);
|
|
3647
|
+
response = await postSimulate(effectiveBaseUrl, body, args.apiKey, timeoutMs, path);
|
|
3648
|
+
if (response.status !== 404) break;
|
|
3649
|
+
if (isAtfErrorEnvelope(response.data)) break;
|
|
3650
|
+
}
|
|
3651
|
+
check.latency_ms = Date.now() - start;
|
|
3652
|
+
check.details.http_status = response.status;
|
|
3653
|
+
check.details.request_id = response.requestId || null;
|
|
3654
|
+
check.details.paths_tried = attemptedPaths;
|
|
3655
|
+
|
|
3656
|
+
if (response.status === 404) {
|
|
3657
|
+
// Known: some deployments disable simulate → WARN not FAIL
|
|
3658
|
+
check.status = "warn";
|
|
3659
|
+
check.details.note = "Simulate endpoint returned 404 — may be disabled on this deployment";
|
|
3660
|
+
check.actions.push("If simulate is expected, check your ATF deployment config");
|
|
3661
|
+
} else if (!response.ok) {
|
|
3662
|
+
check.status = "warn";
|
|
3663
|
+
check.details.note = `Simulate returned HTTP ${response.status}`;
|
|
3664
|
+
check.actions.push("Run: atf simulate --preset swap_small --pretty");
|
|
3665
|
+
} else {
|
|
3666
|
+
// Check response shape
|
|
3667
|
+
const data = response.data || {};
|
|
3668
|
+
check.details.decision = data.decision || null;
|
|
3669
|
+
if (data.decision !== "allowed") {
|
|
3670
|
+
check.status = "warn";
|
|
3671
|
+
check.details.note = `swap_small preset returned ${data.decision || "unknown"} instead of allowed`;
|
|
3672
|
+
check.actions.push("Run: atf simulate --preset swap_small --verify --pretty");
|
|
3673
|
+
}
|
|
3674
|
+
}
|
|
3675
|
+
} catch (err) {
|
|
3676
|
+
check.latency_ms = Date.now() - start;
|
|
3677
|
+
check.status = "warn";
|
|
3678
|
+
check.details.error = err.message || String(err);
|
|
3679
|
+
check.actions.push("Simulate check failed — this is non-fatal if /health passed");
|
|
3680
|
+
}
|
|
3681
|
+
checks.push(check);
|
|
3682
|
+
}
|
|
3683
|
+
|
|
3684
|
+
// ── 5) RPC checks ─────────────────────────────────────────────
|
|
3685
|
+
vlog("Checking Solana RPC connectivity ...");
|
|
3686
|
+
|
|
3687
|
+
// 5a) Helius key source check (if helius mode)
|
|
3688
|
+
if (isHeliusMode) {
|
|
3689
|
+
const check = { name: "helius_key", status: "pass", latency_ms: 0, details: {}, actions: [] };
|
|
3690
|
+
check.details.key_source = heliusKeySource;
|
|
3691
|
+
if (heliusKeySource === "missing") {
|
|
3692
|
+
check.status = "fail";
|
|
3693
|
+
check.actions.push("Export HELIUS_API_KEY or run: atf secret set helius --profile " + pName);
|
|
3694
|
+
check.actions.push("Docs: https://docs.helius.dev/");
|
|
3695
|
+
}
|
|
3696
|
+
checks.push(check);
|
|
3697
|
+
}
|
|
3698
|
+
|
|
3699
|
+
// 5b) RPC getHealth + getVersion
|
|
3700
|
+
if (rpcUrl && !rpcResolutionError) {
|
|
3701
|
+
const check = { name: "rpc_health", status: "pass", latency_ms: null, details: {}, actions: [] };
|
|
3702
|
+
check.details.rpc_host = rpcHost;
|
|
3703
|
+
const start = Date.now();
|
|
3704
|
+
try {
|
|
3705
|
+
// getHealth
|
|
3706
|
+
const healthRes = await fetch(rpcUrl, {
|
|
3707
|
+
method: "POST",
|
|
3708
|
+
headers: { "Content-Type": "application/json" },
|
|
3709
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 1, method: "getHealth", params: [] }),
|
|
3710
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3711
|
+
});
|
|
3712
|
+
const healthData = await healthRes.json();
|
|
3713
|
+
check.latency_ms = Date.now() - start;
|
|
3714
|
+
|
|
3715
|
+
if (healthData.error) {
|
|
3716
|
+
// Some nodes return error for getHealth if not validators
|
|
3717
|
+
// Try getVersion as fallback
|
|
3718
|
+
check.details.get_health_error = healthData.error.message || "unknown";
|
|
3719
|
+
} else {
|
|
3720
|
+
check.details.health_result = healthData.result || null;
|
|
3721
|
+
}
|
|
3722
|
+
|
|
3723
|
+
// getVersion
|
|
3724
|
+
const versionRes = await fetch(rpcUrl, {
|
|
3725
|
+
method: "POST",
|
|
3726
|
+
headers: { "Content-Type": "application/json" },
|
|
3727
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: 2, method: "getVersion", params: [] }),
|
|
3728
|
+
signal: AbortSignal.timeout(timeoutMs),
|
|
3729
|
+
});
|
|
3730
|
+
const versionData = await versionRes.json();
|
|
3731
|
+
if (versionData.result) {
|
|
3732
|
+
check.details.solana_version = versionData.result["solana-core"] || null;
|
|
3733
|
+
}
|
|
3734
|
+
} catch (err) {
|
|
3735
|
+
check.latency_ms = Date.now() - start;
|
|
3736
|
+
check.status = "fail";
|
|
3737
|
+
check.details.error = err.message || String(err);
|
|
3738
|
+
check.actions.push("Check your RPC URL and network connectivity");
|
|
3739
|
+
check.actions.push("Run: atf rpc ping");
|
|
3740
|
+
}
|
|
3741
|
+
checks.push(check);
|
|
3742
|
+
} else if (rpcResolutionError) {
|
|
3743
|
+
// Already handled by helius_key check above; skip rpc_health
|
|
3744
|
+
}
|
|
3745
|
+
|
|
3746
|
+
// ── 6) Keypair readiness ───────────────────────────────────────
|
|
3747
|
+
vlog("Checking keypair readiness ...");
|
|
3748
|
+
{
|
|
3749
|
+
const check = { name: "keypair", status: "pass", latency_ms: 0, details: {}, actions: [] };
|
|
3750
|
+
const kpPath = args.keypairPath || profile.keypair_path;
|
|
3751
|
+
if (!kpPath) {
|
|
3752
|
+
check.status = "warn";
|
|
3753
|
+
check.details.configured = false;
|
|
3754
|
+
check.actions.push("Run: atf config set keypair_path <path>");
|
|
3755
|
+
check.actions.push("If devnet, run: atf swap --burner --devnet --faucet");
|
|
3756
|
+
} else {
|
|
3757
|
+
check.details.configured = true;
|
|
3758
|
+
check.details.path = kpPath;
|
|
3759
|
+
try {
|
|
3760
|
+
const { readFileSync } = require("node:fs");
|
|
3761
|
+
const raw = readFileSync(kpPath, "utf8");
|
|
3762
|
+
const arr = JSON.parse(raw);
|
|
3763
|
+
if (Array.isArray(arr) && arr.length === 64) {
|
|
3764
|
+
check.details.valid = true;
|
|
3765
|
+
} else {
|
|
3766
|
+
check.status = "warn";
|
|
3767
|
+
check.details.valid = false;
|
|
3768
|
+
check.details.note = `Expected JSON array of length 64, got ${Array.isArray(arr) ? arr.length : typeof arr}`;
|
|
3769
|
+
check.actions.push("Verify keypair file format: JSON array of 64 byte values");
|
|
3770
|
+
}
|
|
3771
|
+
} catch (err) {
|
|
3772
|
+
check.status = "warn";
|
|
3773
|
+
check.details.valid = false;
|
|
3774
|
+
check.details.note = err.code === "ENOENT" ? "File not found" : (err.message || String(err));
|
|
3775
|
+
if (err.code === "ENOENT") {
|
|
3776
|
+
check.actions.push(`File not found: ${kpPath}`);
|
|
3777
|
+
check.actions.push("Run: atf config set keypair_path <valid-path>");
|
|
3778
|
+
} else {
|
|
3779
|
+
check.actions.push("Keypair file exists but could not be parsed");
|
|
3780
|
+
}
|
|
3781
|
+
}
|
|
3782
|
+
}
|
|
3783
|
+
checks.push(check);
|
|
3784
|
+
}
|
|
3785
|
+
|
|
3786
|
+
// ── 7) Explorer link builder sanity ────────────────────────────
|
|
3787
|
+
vlog("Checking explorer link builder ...");
|
|
3788
|
+
{
|
|
3789
|
+
const check = { name: "explorer_link", status: "pass", latency_ms: 0, details: {}, actions: [] };
|
|
3790
|
+
const explorerPref = profile.explorer || "solscan";
|
|
3791
|
+
const dummySig = "5" + "A".repeat(86);
|
|
3792
|
+
const url = buildExplorerUrl(dummySig, cluster, explorerPref);
|
|
3793
|
+
check.details.explorer = explorerPref;
|
|
3794
|
+
check.details.sample_url = url;
|
|
3795
|
+
if (!url || !url.startsWith("https://")) {
|
|
3796
|
+
check.status = "warn";
|
|
3797
|
+
check.details.note = "Explorer URL does not look valid";
|
|
3798
|
+
check.actions.push(`Check explorer config: atf config set explorer solscan`);
|
|
3799
|
+
}
|
|
3800
|
+
checks.push(check);
|
|
3801
|
+
}
|
|
3802
|
+
|
|
3803
|
+
// ── Build envelope ─────────────────────────────────────────────
|
|
3804
|
+
let passed = 0;
|
|
3805
|
+
let warned = 0;
|
|
3806
|
+
let failed = 0;
|
|
3807
|
+
for (const c of checks) {
|
|
3808
|
+
if (c.status === "pass") passed++;
|
|
3809
|
+
else if (c.status === "warn") warned++;
|
|
3810
|
+
else failed++;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
const ok = failed === 0;
|
|
3814
|
+
const summaryStatus = failed > 0 ? "error" : (warned > 0 ? "warn" : "ok");
|
|
3815
|
+
|
|
3816
|
+
const envelope = {
|
|
3817
|
+
ok,
|
|
3818
|
+
summary: { status: summaryStatus, passed, warned, failed },
|
|
3819
|
+
env,
|
|
3820
|
+
effective,
|
|
3821
|
+
checks,
|
|
3822
|
+
};
|
|
3823
|
+
|
|
3824
|
+
// ── Determine exit code ────────────────────────────────────────
|
|
3825
|
+
let exitCode = 0;
|
|
3826
|
+
if (failed > 0) {
|
|
3827
|
+
// Distinguish user misconfiguration from network failure
|
|
3828
|
+
const hasNetworkFail = checks.some(
|
|
3829
|
+
(c) => c.status === "fail" && (c.name === "atf_health" || c.name === "rpc_health")
|
|
3830
|
+
);
|
|
3831
|
+
const hasUserFail = checks.some(
|
|
3832
|
+
(c) => c.status === "fail" && c.name !== "atf_health" && c.name !== "rpc_health"
|
|
3833
|
+
);
|
|
3834
|
+
if (hasNetworkFail && !hasUserFail) {
|
|
3835
|
+
exitCode = 2;
|
|
3836
|
+
} else if (hasUserFail && !hasNetworkFail) {
|
|
3837
|
+
exitCode = 1;
|
|
3838
|
+
} else {
|
|
3839
|
+
// Both — prefer exit 2 (network) as it's the more severe issue
|
|
3840
|
+
exitCode = 2;
|
|
3841
|
+
}
|
|
3842
|
+
}
|
|
3843
|
+
|
|
3844
|
+
// ── Output ─────────────────────────────────────────────────────
|
|
3845
|
+
// Redact any stray secrets from the entire output
|
|
3846
|
+
let output;
|
|
3847
|
+
if (format === "pretty") {
|
|
3848
|
+
output = formatDoctorPretty(envelope, args.noColor);
|
|
3849
|
+
} else {
|
|
3850
|
+
output = JSON.stringify(envelope, null, 2) + "\n";
|
|
3851
|
+
}
|
|
3852
|
+
// Defense-in-depth: scrub any HELIUS_API_KEY value that might leak
|
|
3853
|
+
const heliusKey = process.env.HELIUS_API_KEY;
|
|
3854
|
+
if (heliusKey && heliusKey.length > 0) {
|
|
3855
|
+
output = output.split(heliusKey).join("***REDACTED***");
|
|
3856
|
+
}
|
|
3857
|
+
output = output.replace(/api-key=[^&\s"]+/gi, "api-key=***REDACTED***");
|
|
3858
|
+
process.stdout.write(output);
|
|
3859
|
+
|
|
3860
|
+
process.exit(exitCode);
|
|
3861
|
+
}
|
|
3862
|
+
|
|
3863
|
+
/**
|
|
3864
|
+
* Format doctor output for --pretty mode.
|
|
3865
|
+
*/
|
|
3866
|
+
function formatDoctorPretty(envelope, noColor) {
|
|
3867
|
+
const c = noColor ? { reset: "", bold: "", dim: "", green: "", red: "", yellow: "", cyan: "", gray: "" } : COLORS;
|
|
3868
|
+
const lines = [];
|
|
3869
|
+
|
|
3870
|
+
lines.push("");
|
|
3871
|
+
lines.push(`${c.bold}${c.cyan} @trucore/atf doctor${c.reset}`);
|
|
3872
|
+
lines.push(`${c.dim} CLI ${envelope.env.cli_version} | Node ${envelope.env.node_version} | ${envelope.env.platform}/${envelope.env.arch}${c.reset}`);
|
|
3873
|
+
lines.push("");
|
|
3874
|
+
|
|
3875
|
+
const S = envelope.summary;
|
|
3876
|
+
const statusIcon = S.status === "ok" ? `${c.green}\u2713` : S.status === "warn" ? `${c.yellow}\u26a0` : `${c.red}\u2717`;
|
|
3877
|
+
lines.push(` ${c.bold}${statusIcon} ${S.status.toUpperCase()}${c.reset} (${S.passed} passed, ${S.warned} warned, ${S.failed} failed)`);
|
|
3878
|
+
lines.push("");
|
|
3879
|
+
|
|
3880
|
+
// Effective config
|
|
3881
|
+
lines.push(`${c.dim} Profile: ${c.reset}${envelope.effective.profile}`);
|
|
3882
|
+
lines.push(`${c.dim} Base URL: ${c.reset}${envelope.effective.atf_base_url}`);
|
|
3883
|
+
lines.push(`${c.dim} Cluster: ${c.reset}${envelope.effective.solana_cluster}`);
|
|
3884
|
+
lines.push(`${c.dim} RPC Host: ${c.reset}${envelope.effective.rpc_host || "(not configured)"}`);
|
|
3885
|
+
lines.push(`${c.dim} Commitment: ${c.reset}${envelope.effective.commitment}`);
|
|
3886
|
+
lines.push(`${c.dim} Explorer: ${c.reset}${envelope.effective.explorer}`);
|
|
3887
|
+
lines.push("");
|
|
3888
|
+
|
|
3889
|
+
// Checks
|
|
3890
|
+
for (const chk of envelope.checks) {
|
|
3891
|
+
const icon =
|
|
3892
|
+
chk.status === "pass" ? `${c.green}\u2713` :
|
|
3893
|
+
chk.status === "warn" ? `${c.yellow}\u26a0` :
|
|
3894
|
+
`${c.red}\u2717`;
|
|
3895
|
+
const latency = chk.latency_ms !== null ? ` (${chk.latency_ms}ms)` : "";
|
|
3896
|
+
lines.push(` ${icon} ${c.bold}${chk.name}${c.reset}${latency}`);
|
|
3897
|
+
|
|
3898
|
+
if (chk.actions && chk.actions.length > 0) {
|
|
3899
|
+
for (const a of chk.actions) {
|
|
3900
|
+
lines.push(` ${c.dim}\u2192 ${a}${c.reset}`);
|
|
3901
|
+
}
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
lines.push("");
|
|
3905
|
+
|
|
3906
|
+
return lines.join("\n") + "\n";
|
|
3907
|
+
}
|
|
3908
|
+
|
|
3493
3909
|
// ---- src/cli.mjs ----
|
|
3494
3910
|
/**
|
|
3495
3911
|
* cli.mjs — argument parsing and entry point (zero deps)
|
|
@@ -3528,6 +3944,7 @@ COMMANDS
|
|
|
3528
3944
|
tx status Check transaction confirmation status
|
|
3529
3945
|
receipts verify Verify content_hash / receipt_hash integrity
|
|
3530
3946
|
completion Generate shell completion (bash|zsh|fish|powershell)
|
|
3947
|
+
doctor Run dev environment health checks
|
|
3531
3948
|
|
|
3532
3949
|
ALIASES
|
|
3533
3950
|
ls Alias for config list
|
|
@@ -3616,7 +4033,9 @@ EXAMPLES
|
|
|
3616
4033
|
npx @trucore/atf@${VERSION} tx send --tx-base64 ... --keypair ~/.config/solana/id.json --confirm
|
|
3617
4034
|
npx @trucore/atf@${VERSION} tx status --sig <signature>
|
|
3618
4035
|
npx @trucore/atf@${VERSION} receipts verify --file response.json
|
|
3619
|
-
npx @trucore/atf@${VERSION} completion bash
|
|
4036
|
+
npx @trucore/atf@${VERSION} completion bash
|
|
4037
|
+
npx @trucore/atf@${VERSION} doctor --pretty
|
|
4038
|
+
npx @trucore/atf@${VERSION} doctor --profile devnet --pretty`;
|
|
3620
4039
|
|
|
3621
4040
|
function parseArgs(argv) {
|
|
3622
4041
|
const defaultTimeout = (() => {
|
|
@@ -3948,6 +4367,9 @@ async function main() {
|
|
|
3948
4367
|
case "completion":
|
|
3949
4368
|
await runCompletion(args);
|
|
3950
4369
|
break;
|
|
4370
|
+
case "doctor":
|
|
4371
|
+
await runDoctor(args);
|
|
4372
|
+
break;
|
|
3951
4373
|
default:
|
|
3952
4374
|
exitWithError(ERROR_CODES.USER_ERROR, `Unknown command: ${args.command}`, "Run with --help for usage.", args.format);
|
|
3953
4375
|
}
|