@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.
Files changed (3) hide show
  1. package/README.md +72 -28
  2. package/dist/index.js +427 -5
  3. 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.0 simulate --preset swap_small --verify --pretty
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.0
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.0 simulate --preset swap_small
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.0 swap --burner --devnet --in SOL --out USDC --amount-in 0.01 --execute --yes --confirm
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.0 config init --yes
44
- npx @trucore/atf@1.3.0 profile create devnet
45
- npx @trucore/atf@1.3.0 profile use devnet
46
- npx @trucore/atf@1.3.0 config set solana_cluster devnet
47
- npx @trucore/atf@1.3.0 config set rpc_url helius
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.0 rpc ping
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.0 health
60
- npx @trucore/atf@1.3.0 health --pretty
61
- npx @trucore/atf@1.3.0 health --base-url http://localhost:3000
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.0 simulate --preset swap_small --verify
76
- npx @trucore/atf@1.3.0 simulate --preset swap_too_large --pretty
77
- npx @trucore/atf@1.3.0 simulate --json '{"chain_id":1,"value_eth":"0.5"}' --base-url http://localhost:3000
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.0 approve --intent abc123 --token mytoken
92
- npx @trucore/atf@1.3.0 approve --intent abc123 --token mytoken --pretty
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.0 version
107
- npx @trucore/atf@1.3.0 version --pretty
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.0", "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" }
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.0 swap \
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.0 swap \
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.0 swap \
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.0" }
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.0 version
529
- npx @trucore/atf@1.3.0 health
530
- npx @trucore/atf@1.3.0 whoami
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.0 — Agent Transaction Firewall CLI
5
- // Built: 2026-02-28T16:06:56.895Z
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.0";
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-28T16:06:56.895Z";
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trucore/atf",
3
- "version": "1.3.0",
3
+ "version": "1.3.1",
4
4
  "description": "Agent Transaction Firewall CLI — simulate, verify, and audit on-chain transactions trustlessly.",
5
5
  "license": "MIT",
6
6
  "repository": {