@zan_team/cli 0.0.1 → 0.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,8 +28,7 @@ pnpm build
28
28
  node dist/cli.js --help
29
29
  ```
30
30
 
31
- 下文统一使用 `zan`。测试构建产物时,将 `zan` 替换为 `node dist/cli.js`;
32
- 本地源码联调推荐使用下方的 `cli:local` 入口,无需反复构建。
31
+ 下文统一使用 `zan`。运行源码构建产物时,将 `zan` 替换为 `node dist/cli.js`。
33
32
 
34
33
  ### 配置服务地址
35
34
 
@@ -37,74 +36,12 @@ CLI 未显式配置服务地址时默认连接生产环境 `https://zan.top`。
37
36
  环境变量覆盖 Auth/API Origin:
38
37
 
39
38
  ```sh
40
- export ZAN_ENV=dev
41
39
  export ZAN_AUTH_BASE_URL=https://auth.example.com
42
40
  export ZAN_API_BASE_URL=https://api.example.com
43
41
  ```
44
42
 
45
- 仓库提供 DEV、SIT、PRE 和 PROD 四套调试配置:
46
-
47
- | 环境 | 配置文件 | Origin |
48
- | ---- | ----------- | -------------------------------------- |
49
- | DEV | `.env.dev` | `http://127.0.0.1:8088` |
50
- | SIT | `.env.sit` | `https://zan.unchartedw3s.com` |
51
- | PRE | `.env.pre` | `https://pre.zan.top` |
52
- | PROD | `.env.prod` | `https://zan.top` |
53
-
54
- ### 本地源码联调
55
-
56
- 开发者可在 CLI 仓库中通过 `pnpm cli:local` 选择一次环境,后续命令会自动沿用;
57
- 源码修改后下一次调用立即生效:
58
-
59
- ```sh
60
- pnpm cli:local -- env use dev
61
- pnpm cli:local -- env status
62
- pnpm cli:local -- --json --no-interactive usage
63
-
64
- # 切换到 SIT
65
- pnpm cli:local -- env use sit
66
-
67
- # 仅本次使用 DEV,不改变保存的选择;--env 必须放在最前面
68
- pnpm cli:local -- --env dev --json --no-interactive usage
69
- ```
70
-
71
- 选择保存在当前工作目录的 `.zan-dev.json`,只包含 `environment`,支持
72
- `dev`、`sit`、`pre`。仅读取当前目录,不向上查找;未选择环境时会报错,
73
- 也可以直接用 `--env sit` 单次指定。`env status` 始终以 JSON 返回环境、
74
- 配置路径、工作目录、源码仓库和 Auth/API 地址,不包含凭据。
75
-
76
- 测试 Agent 应直接通过 Node 调用源码入口,避免 pnpm 的脚本横幅影响 JSON 解析。
77
- 在其他业务项目中,保留该项目的工作目录,使用源码入口的绝对路径:
78
-
79
- ```sh
80
- node /absolute/path/to/zan-cli/scripts/zan-dev.mjs env use sit
81
- node /absolute/path/to/zan-cli/scripts/zan-dev.mjs --json --no-interactive usage
82
- ```
83
-
84
- 该入口从 CLI 仓库读取依赖、源码和环境文件,保留调用方工作目录。
85
- 每个测试项目分别保存环境选择;登录、Project/Network 等仍是用户级、按环境共享的状态。
86
- 同目录并发测试不同环境时,各命令显式使用 `--env`。
87
-
88
- 此功能仅用于源码联调:正式 `zan` 和发布的 CLI 不读取 `.zan-dev.json`,
89
- 开发脚本不随 npm 包发布。给最终用户的 Skill 只保留正式 CLI 用法。
90
- Agent 入口约定、写文件命令和状态隔离说明见[本地开发指南](docs/local-development.md)。
91
-
92
- 也可以通过 DEV、SIT、PRE 和 PROD 脚本直接指定环境,`--` 后的参数会完整传给 CLI:
93
-
94
- ```sh
95
- pnpm cli:dev -- auth login
96
- pnpm cli:sit -- auth status --verify
97
- pnpm cli:pre -- --json project list
98
- pnpm cli:prod -- auth login
99
- pnpm cli:prod -- --json project list
100
- pnpm cli:prod -- usage
101
- ```
102
-
103
- 环境脚本会覆盖当前终端残留的 `ZAN_ENV`、`ZAN_AUTH_BASE_URL` 和
104
- `ZAN_API_BASE_URL`。DEV、SIT、PRE 分别使用独立的系统凭据 Profile、Session、
105
- Project/Network 设置和 Endpoint 缓存;PROD 继续使用兼容既有安装的
106
- `profile-default`、`session.json` 和 `settings.json`。非生产设置分别保存在
107
- `settings.dev.json`、`settings.sit.json` 和 `settings.pre.json`。
43
+ 上述 `example.com` 地址仅用于展示配置方式,不能直接使用。日常使用无需覆盖
44
+ 服务地址;内部环境地址和配置请通过授权渠道获取,不要写入公开文档或示例。
108
45
 
109
46
  ### 登录
110
47
 
@@ -400,27 +337,17 @@ zan project cache clear
400
337
  Project 使用不同缓存文件与密钥。普通设置文件仍只保存 Project ID 和 Network
401
338
  ID,不保存 Endpoint 或 API Key。
402
339
 
403
- #### SIT 调试与 RPC API Key 覆盖
404
-
405
- 源码调试时可以直接使用环境脚本:
406
-
407
- ```sh
408
- pnpm cli:sit -- project use <project-id>
409
- pnpm cli:sit -- evm networks
410
- pnpm cli:sit -- evm networks --refresh
411
- pnpm cli:sit -- project cache status
412
- pnpm cli:sit -- project cache clear
413
- ```
340
+ #### RPC API Key 覆盖
414
341
 
415
342
  需要临时替换当前 Project 的 RPC API Key 时,通过隐藏输入或 stdin 写入系统
416
343
  凭据库:
417
344
 
418
345
  ```sh
419
- pnpm cli:sit -- project rpc-key set
346
+ zan project rpc-key set
420
347
  printf '%s\n' "$TEST_RPC_API_KEY" \
421
- | pnpm cli:sit -- project rpc-key set --stdin
422
- pnpm cli:sit -- project rpc-key status
423
- pnpm cli:sit -- project rpc-key clear
348
+ | zan project rpc-key set --stdin
349
+ zan project rpc-key status
350
+ zan project rpc-key clear
424
351
  ```
425
352
 
426
353
  覆盖值不会出现在命令参数、普通配置、Endpoint 缓存或命令输出中。启用覆盖后,
@@ -430,20 +357,34 @@ RPC 401/403 不会自动换回服务端下发的 Key,便于稳定测试指定
430
357
 
431
358
  ```sh
432
359
  zan project rpc-key pull
433
- # 源码 SIT 调试:
434
- pnpm cli:sit -- project rpc-key pull
435
360
  ```
436
361
 
437
362
  命令写入当前工作目录中固定的 `ZAN_API_KEY`:已存在时替换,缺少时追加,文件
438
- 不存在时创建。实际生效的测试覆盖 Key 优先,否则从加密 Endpoint 缓存中提取
439
- 服务端下发的 Project Key;不要求选择 Network。命令不接受 Key、stdin、变量名
363
+ 不存在时创建。API Key 按环境和 Project ID 独立保存在系统凭据库(macOS Keychain
364
+ 等),与 Endpoint 缓存的加密密钥、手动覆盖值分别存储。`pull` 优先使用手动覆盖值,
365
+ 否则直接读取所选 Project 已保存的 API Key,不需要读取网络缓存或请求后端。
366
+
367
+ 首次缺失时,CLI 请求该 Project 的网络列表,从首个可识别的 RPC 地址提取 API Key
368
+ 并保存到系统凭据库;现有后端接口通过地址携带 Key。刷新网络列表时同步更新保存的
369
+ API Key,服务端不再返回可识别 Key 时移除旧记录。`zan project cache clear` 会同时
370
+ 清理当前项目保存的 API Key;退出登录会清理当前环境各项目的 Key 和覆盖值。
371
+
372
+ API Key 属于所选 Project,不要求选择 Network。未选择 Project 时,交互终端会
373
+ 列出项目供选择,非交互模式需使用 `--project <id>` 或先执行 `zan project use <id>`。
374
+ 命令不接受 Key、stdin、变量名
440
375
  或输出路径,也不会在人工输出、JSON 输出、错误或日志中包含 Key。新文件及更新
441
376
  后的文件在 POSIX 系统上使用 `0600` 权限,并通过文件锁和原子替换写入;符号链接
442
377
  或非普通 `.env` 会被拒绝。
443
378
 
444
- `pull` 只写 `.env`,不会修改 `.env.dev`、`.env.sit`、`.env.pre` 或
445
- `.env.prod`。它不会检查 Git 跟踪状态或自动更新 `.gitignore`,请自行确保 `.env`
446
- 不会被提交。写入后,任何拥有当前目录读取权限的进程或 Agent 都可能读取这个明文
379
+ `pull` 会确保 `.env` 被 Git 忽略:已有生效规则(包括父目录、通配符和全局规则)时
380
+ 不修改 `.gitignore`,否则在当前目录的 `.gitignore` 末尾追加 `/.env`。尚未初始化 Git
381
+ 仓库的目录也支持此行为,不会自动创建项目仓库。此检查使用本机 Git;Git 不可用时会提示风险。
382
+
383
+ 若 `.env` 已被 Git 跟踪,或无法检查、更新忽略规则,命令仍写入 Key,并在终端提示
384
+ 误提交 Key 的风险;JSON 结果通过 `warnings` 返回同样的提示。已跟踪时可执行
385
+ `git rm --cached -- .env` 取消跟踪,并在提交前检查暂存内容;CLI 不会自动修改 Git
386
+ 索引,忽略规则也不能清除历史提交中的内容。其他环境配置文件不会被修改。
387
+ 写入后,任何拥有当前目录读取权限的进程或 Agent 都可能读取这个明文
447
388
  文件;“不泄露”保证只覆盖该命令自身的参数、输出、错误和日志边界。
448
389
 
449
390
  管理默认 Project 和 Network:
@@ -518,7 +459,9 @@ printf '["0x0000000000000000000000000000000000000000","latest"]' \
518
459
  ~/.zan-cli/config.yaml
519
460
  ```
520
461
 
521
- `~` 由操作系统用户目录 API 解析。该文件是个人 CLI 策略,与 `.env.dev`、`.env.sit`、`.env.pre`、`.env.prod`、`ZAN_ENV` 及所选 ZAN 后端环境完全无关;`ZAN_HOME` 不会改变它的位置,DEV、SIT、PRE、PROD 读取同一个文件。
462
+ `~` 由操作系统用户目录 API 解析。该文件是个人 CLI 策略,与环境配置文件、
463
+ `ZAN_ENV` 及所选 ZAN 后端环境无关;`ZAN_HOME` 不会改变它的位置,所有环境
464
+ 读取同一个文件。
522
465
 
523
466
  YAML schema 支持两个顶层属性,分别控制 Raw JSON-RPC 和 Hyperliquid Info 方法:
524
467
 
@@ -683,17 +626,6 @@ Agent 可用 `inspect` 检查本地状态;`ready: false` 需结合实际命令
683
626
  Project/Network 选择、套餐与 Key 用量、EVM/Hyperliquid 查询、RPC Key 拉取及钱包签名指引。
684
627
  `skills/` 随 npm 包发布;安装 CLI 后仍需将技能目录放入 Agent 的发现路径。
685
628
 
686
- 本仓库已通过 `.agents/skills/zan-cli` 软链接指向 `skills/zan-cli`。在 Codex
687
- 中打开本仓库即可发现技能,修改源文件无需复制。若未显示,重启 Codex 后尝试:
688
-
689
- ```text
690
- 使用 $zan-cli 列出当前支持的命令。本任务把技能中的 zan 统一替换为
691
- node /absolute/path/to/zan-cli/scripts/zan-dev.mjs --env sit。
692
- ```
693
-
694
- 将示例路径替换为实际 CLI 仓库路径。该入口约定仅用于本地测试;
695
- 环境选择和状态隔离说明见[本地开发指南](docs/local-development.md)。
696
-
697
629
  npm 包发布后,安装 CLI 并选择 Agent 安装技能:
698
630
 
699
631
  ```sh
@@ -796,13 +728,6 @@ zan completions fish
796
728
  名称,或通过 `zan project list` 查找项目后执行 `zan project use <id>` 使用已有
797
729
  项目;此错误不可重试,仍使用 `ZAN_API_ERROR` 和退出码 `8`。
798
730
 
799
- 本地联调遇到 “No free Node Service plan is configured for this environment” 时,
800
- 需在 `webtservice` 的本地数据库中配置 `/default/AVAILABLE_FREE_SPEC`,其中
801
- `AVAILABLE_FREE_SPEC.NODE_SERVICE.specId` 必须指向有效的 `drw3_service_spec`
802
- 商品规格。只启动 MySQL、Redis 或重新登录不会生成这些商品数据;应导入环境
803
- 对应的测试商品配置,并确认该规格所需的套餐开通及回调链路可用。后端代码
804
- 修改后,需要重新编译并重启实际监听本地 API 端口的进程。
805
-
806
731
  已认证的 Project 与 Network 请求如果通过 HTTP 401 或已发布的 WebResult
807
732
  语义表明 Access Token 无效,会刷新一次 Access Token 并重放一次请求。第二次
808
733
  仍失败时返回 `AUTH_EXPIRED`,不会继续刷新。普通 HTTP 403 和 Scope 不足不会
@@ -810,6 +735,8 @@ zan completions fish
810
735
 
811
736
  ### 安全注意事项
812
737
 
738
+ - 对外文档、截图和问题反馈中不要包含内部开发、测试或预发域名、IP、端口及环境配置;
739
+ 示例统一使用 `example.com` 等占位地址,分享命令输出前先检查并脱敏。
813
740
  - 不要把 Access Token、Refresh Token、Project Token 或完整 Endpoint 写入脚本、日志和普通配置。
814
741
  - 不要使用 `--verbose` 输出处理结果中的敏感字段。
815
742
  - 完整 RPC Endpoint 只在当前进程内解密使用;本地仅保存 AES-256-GCM 密文,
package/dist/cli.js CHANGED
@@ -1809,6 +1809,10 @@ function registerProject(program, outputWriter, getRuntimeContext, input, rpcKey
1809
1809
  rpcKey.command("pull").description("Write the active Project RPC API Key to .env").action(async () => {
1810
1810
  const { rpcKeyPullService } = await getRuntimeContext();
1811
1811
  const result = await rpcKeyPullService.pull();
1812
+ for (const warning of result.warnings ?? []) {
1813
+ outputWriter.writeStderr(`Warning: ${warning}
1814
+ `);
1815
+ }
1812
1816
  outputWriter.success(
1813
1817
  result,
1814
1818
  "ZAN_API_KEY was written to .env"
@@ -1891,7 +1895,7 @@ function registerSign(program, outputWriter, getRuntimeContext) {
1891
1895
  }
1892
1896
 
1893
1897
  // src/version.ts
1894
- var CLI_VERSION = true ? "0.0.1" : pkg.version;
1898
+ var CLI_VERSION = true ? "0.0.3" : pkg.version;
1895
1899
 
1896
1900
  // src/commands/version.ts
1897
1901
  function registerVersion(program, outputWriter) {
@@ -2166,7 +2170,7 @@ var ReadlineProjectPrompt = class {
2166
2170
 
2167
2171
  // src/runtime/context.ts
2168
2172
  import { homedir as readHomedir2 } from "os";
2169
- import { join as join6 } from "path";
2173
+ import { join as join7 } from "path";
2170
2174
 
2171
2175
  // src/auth/session-metadata-store.ts
2172
2176
  import { z as z8 } from "zod";
@@ -2630,9 +2634,9 @@ var BrowserFlow = class {
2630
2634
  // src/auth/browser-opener.ts
2631
2635
  import { execFile as nodeExecFile } from "child_process";
2632
2636
  var BrowserOpener = class {
2633
- constructor(platform = process.platform, execFile = nodeExecFile) {
2637
+ constructor(platform = process.platform, execFile2 = nodeExecFile) {
2634
2638
  this.platform = platform;
2635
- this.execFile = execFile;
2639
+ this.execFile = execFile2;
2636
2640
  }
2637
2641
  platform;
2638
2642
  execFile;
@@ -5133,7 +5137,7 @@ var NetworkSelector = class {
5133
5137
  };
5134
5138
 
5135
5139
  // src/projects/dotenv-api-key-writer.ts
5136
- import { randomUUID as randomUUID3 } from "crypto";
5140
+ import { randomUUID as randomUUID4 } from "crypto";
5137
5141
  import {
5138
5142
  chmod as nodeChmod2,
5139
5143
  lstat as nodeLstat2,
@@ -5142,7 +5146,111 @@ import {
5142
5146
  rename as nodeRename,
5143
5147
  unlink as nodeUnlink
5144
5148
  } from "fs/promises";
5145
- import { isAbsolute as isAbsolute3, join as join4 } from "path";
5149
+ import { isAbsolute as isAbsolute3, join as join5 } from "path";
5150
+
5151
+ // src/projects/dotenv-gitignore.ts
5152
+ import { execFile } from "child_process";
5153
+ import { randomUUID as randomUUID3 } from "crypto";
5154
+ import { lstat as lstat4, mkdtemp as mkdtemp2, open as open2, readFile as readFile3, rename as rename2, rm as rm2 } from "fs/promises";
5155
+ import { tmpdir } from "os";
5156
+ import { join as join4 } from "path";
5157
+ import { promisify } from "util";
5158
+ var execute = promisify(execFile);
5159
+ var UNCONFIRMED_WARNING = "Unable to confirm .env is ignored by Git; there is a risk of committing the API Key. Check Git and .gitignore before committing.";
5160
+ function protectionFailure() {
5161
+ return new Error("Unable to verify Git ignore rules.");
5162
+ }
5163
+ async function appendRule(directory) {
5164
+ const filename = join4(directory, ".gitignore");
5165
+ let current = "";
5166
+ let mode = 420;
5167
+ try {
5168
+ const info = await lstat4(filename);
5169
+ if (!info.isFile() || info.isSymbolicLink()) throw protectionFailure();
5170
+ mode = info.mode & 511;
5171
+ current = await readFile3(filename, "utf8");
5172
+ } catch (error) {
5173
+ if (error.code !== "ENOENT") throw error;
5174
+ }
5175
+ const newline = current.includes("\r\n") ? "\r\n" : "\n";
5176
+ const separator = current.length === 0 || current.endsWith("\n") ? "" : newline;
5177
+ const temporary = join4(directory, `.gitignore.${randomUUID3()}.tmp`);
5178
+ try {
5179
+ const handle = await open2(temporary, "wx", mode);
5180
+ try {
5181
+ await handle.writeFile(`${current}${separator}/.env${newline}`, "utf8");
5182
+ await handle.sync();
5183
+ } finally {
5184
+ await handle.close();
5185
+ }
5186
+ await rename2(temporary, filename);
5187
+ } finally {
5188
+ await rm2(temporary, { force: true });
5189
+ }
5190
+ }
5191
+ async function ensureDotenvIgnored(directory) {
5192
+ const warnings = [];
5193
+ const env = { ...process.env, LC_ALL: "C" };
5194
+ for (const name of ["GIT_DIR", "GIT_WORK_TREE", "GIT_INDEX_FILE", "GIT_COMMON_DIR", "GIT_PREFIX"]) {
5195
+ delete env[name];
5196
+ }
5197
+ const git = async (args) => {
5198
+ try {
5199
+ return { code: 0, ...await execute("git", args, {
5200
+ cwd: directory,
5201
+ env,
5202
+ encoding: "utf8",
5203
+ timeout: 1e4,
5204
+ maxBuffer: 64 * 1024
5205
+ }) };
5206
+ } catch (error) {
5207
+ const failure = error;
5208
+ if (typeof failure.code !== "number") throw protectionFailure();
5209
+ return { code: failure.code, stdout: failure.stdout ?? "", stderr: failure.stderr ?? "" };
5210
+ }
5211
+ };
5212
+ let temporaryRepository;
5213
+ try {
5214
+ const repository = await git(["rev-parse", "--is-inside-work-tree"]);
5215
+ let prefix = [];
5216
+ if (repository.code === 0 && repository.stdout.trim() === "true") {
5217
+ const tracked = await git(["ls-files", "--error-unmatch", "--", ".env"]);
5218
+ if (tracked.code === 0) {
5219
+ warnings.push(
5220
+ ".env is already tracked by Git; there is a risk of committing the API Key even with .gitignore. Run: git rm --cached -- .env, and review staged changes before committing."
5221
+ );
5222
+ } else if (tracked.code !== 1) {
5223
+ throw protectionFailure();
5224
+ }
5225
+ } else if (repository.code === 128 && repository.stderr.includes("not a git repository")) {
5226
+ temporaryRepository = await mkdtemp2(join4(tmpdir(), "zan-ignore-"));
5227
+ const initialized = await git(["init", "--bare", "--quiet", "--template=", temporaryRepository]);
5228
+ if (initialized.code !== 0) throw protectionFailure();
5229
+ prefix = [`--git-dir=${temporaryRepository}`, `--work-tree=${directory}`, "-c", "core.bare=false"];
5230
+ } else {
5231
+ throw protectionFailure();
5232
+ }
5233
+ const ignored = await git([...prefix, "check-ignore", "--quiet", "--no-index", "--", ".env"]);
5234
+ if (ignored.code === 0) return warnings;
5235
+ if (ignored.code !== 1) throw protectionFailure();
5236
+ await appendRule(directory);
5237
+ const verified = await git([...prefix, "check-ignore", "--quiet", "--no-index", "--", ".env"]);
5238
+ if (verified.code !== 0) throw protectionFailure();
5239
+ } catch {
5240
+ warnings.push(UNCONFIRMED_WARNING);
5241
+ } finally {
5242
+ if (temporaryRepository !== void 0) {
5243
+ try {
5244
+ await rm2(temporaryRepository, { recursive: true, force: true });
5245
+ } catch {
5246
+ warnings.push("Temporary Git metadata could not be removed; review the system temporary directory.");
5247
+ }
5248
+ }
5249
+ }
5250
+ return warnings;
5251
+ }
5252
+
5253
+ // src/projects/dotenv-api-key-writer.ts
5146
5254
  var API_KEY_ASSIGNMENT = /^[ \t]*(?:export[ \t]+)?ZAN_API_KEY[ \t]*=/u;
5147
5255
  function unsafeKeyFailure() {
5148
5256
  return new CliFailure(
@@ -5220,7 +5328,7 @@ var DotenvApiKeyWriter = class {
5220
5328
  this.options = options;
5221
5329
  this.platform = options.platform ?? process.platform;
5222
5330
  this.fileSystem = options.fileSystem ?? nodeDotenvApiKeyFileSystem;
5223
- this.createTemporaryId = options.createTemporaryId ?? randomUUID3;
5331
+ this.createTemporaryId = options.createTemporaryId ?? randomUUID4;
5224
5332
  }
5225
5333
  options;
5226
5334
  platform;
@@ -5234,8 +5342,9 @@ var DotenvApiKeyWriter = class {
5234
5342
  if (!isAbsolute3(this.options.directory)) {
5235
5343
  throw unsafeDotenvPathFailure();
5236
5344
  }
5237
- const lockPath = join4(this.options.directory, ".env.zan.lock");
5345
+ const lockPath = join5(this.options.directory, ".env.zan.lock");
5238
5346
  let renamed = false;
5347
+ let warnings = [];
5239
5348
  try {
5240
5349
  await this.assertSafeOptionalPath(lockPath);
5241
5350
  const lock = new FileLock(lockPath, {
@@ -5244,11 +5353,15 @@ var DotenvApiKeyWriter = class {
5244
5353
  await lock.withLock(async () => {
5245
5354
  const current = await this.readExistingFile();
5246
5355
  const next = updateApiKeyAssignment(current, parsed.data);
5356
+ warnings = await ensureDotenvIgnored(this.options.directory);
5247
5357
  await this.replaceAtomically(next, () => {
5248
5358
  renamed = true;
5249
5359
  });
5250
5360
  });
5251
5361
  } catch (error) {
5362
+ if (error instanceof CliFailure) {
5363
+ throw error;
5364
+ }
5252
5365
  if (error instanceof UnsafeDotenvPathError) {
5253
5366
  throw unsafeDotenvPathFailure();
5254
5367
  }
@@ -5257,6 +5370,9 @@ var DotenvApiKeyWriter = class {
5257
5370
  }
5258
5371
  throw dotenvWriteFailure();
5259
5372
  }
5373
+ if (warnings.length > 0) {
5374
+ return { warnings };
5375
+ }
5260
5376
  }
5261
5377
  async assertSafeOptionalPath(path) {
5262
5378
  try {
@@ -5272,7 +5388,7 @@ var DotenvApiKeyWriter = class {
5272
5388
  }
5273
5389
  }
5274
5390
  async readExistingFile() {
5275
- const filename = join4(this.options.directory, ".env");
5391
+ const filename = join5(this.options.directory, ".env");
5276
5392
  await this.assertSafeOptionalPath(filename);
5277
5393
  try {
5278
5394
  return await this.fileSystem.readFile(filename, "utf8");
@@ -5284,8 +5400,8 @@ var DotenvApiKeyWriter = class {
5284
5400
  }
5285
5401
  }
5286
5402
  async replaceAtomically(content, onRenamed) {
5287
- const filename = join4(this.options.directory, ".env");
5288
- const temporaryFile = join4(
5403
+ const filename = join5(this.options.directory, ".env");
5404
+ const temporaryFile = join5(
5289
5405
  this.options.directory,
5290
5406
  `.env.${process.pid}.${this.createTemporaryId()}.tmp`
5291
5407
  );
@@ -5479,22 +5595,22 @@ import { posix, win32 } from "path";
5479
5595
  // src/storage/atomic-json.ts
5480
5596
  import {
5481
5597
  chmod as chmod3,
5482
- lstat as lstat4,
5598
+ lstat as lstat5,
5483
5599
  mkdir as mkdir3,
5484
- open as open2,
5485
- readFile as readFile3,
5486
- rename as rename2,
5600
+ open as open3,
5601
+ readFile as readFile4,
5602
+ rename as rename3,
5487
5603
  unlink as unlink2
5488
5604
  } from "fs/promises";
5489
5605
  import { dirname as dirname2 } from "path";
5490
- import { randomUUID as randomUUID4 } from "crypto";
5606
+ import { randomUUID as randomUUID5 } from "crypto";
5491
5607
  var nodeAtomicJsonFileSystem = Object.freeze({
5492
5608
  chmod: chmod3,
5493
- lstat: lstat4,
5609
+ lstat: lstat5,
5494
5610
  mkdir: mkdir3,
5495
- open: open2,
5496
- readFile: readFile3,
5497
- rename: rename2,
5611
+ open: open3,
5612
+ readFile: readFile4,
5613
+ rename: rename3,
5498
5614
  unlink: unlink2
5499
5615
  });
5500
5616
  function hasErrorCode5(error, code) {
@@ -5538,7 +5654,7 @@ var AtomicJsonStore = class {
5538
5654
  this.filename = filename;
5539
5655
  this.fileSystem = options.fileSystem ?? nodeAtomicJsonFileSystem;
5540
5656
  this.platform = options.platform ?? process.platform;
5541
- this.createTemporaryId = options.createTemporaryId ?? randomUUID4;
5657
+ this.createTemporaryId = options.createTemporaryId ?? randomUUID5;
5542
5658
  }
5543
5659
  schema;
5544
5660
  filename;
@@ -5840,6 +5956,13 @@ var ProjectEndpointCache = class {
5840
5956
  ).toString("base64url");
5841
5957
  return `endpoint-cache-key-${scope}`;
5842
5958
  }
5959
+ apiKeyAccount(projectId) {
5960
+ const scope = Buffer6.from(
5961
+ `${this.options.environment}:${projectId}`,
5962
+ "utf8"
5963
+ ).toString("base64url");
5964
+ return `project-rpc-key-${scope}`;
5965
+ }
5843
5966
  overrideAccount(projectId) {
5844
5967
  const scope = Buffer6.from(
5845
5968
  `${this.options.environment}:${projectId}`,
@@ -5895,6 +6018,18 @@ var ProjectEndpointCache = class {
5895
6018
  }
5896
6019
  throw error;
5897
6020
  }
6021
+ let apiKey;
6022
+ for (const network of networks) {
6023
+ apiKey = this.extractCredential(network.endpoints.https);
6024
+ if (apiKey !== void 0) {
6025
+ break;
6026
+ }
6027
+ }
6028
+ if (apiKey === void 0) {
6029
+ await this.options.secretStore.delete(this.apiKeyAccount(projectId));
6030
+ } else {
6031
+ await this.options.secretStore.write(this.apiKeyAccount(projectId), apiKey);
6032
+ }
5898
6033
  return networks;
5899
6034
  }
5900
6035
  status(projectId) {
@@ -5903,7 +6038,8 @@ var ProjectEndpointCache = class {
5903
6038
  async clear(projectId) {
5904
6039
  const results = await Promise.allSettled([
5905
6040
  this.options.store.delete(projectId),
5906
- this.options.secretStore.delete(this.cacheKeyAccount(projectId))
6041
+ this.options.secretStore.delete(this.cacheKeyAccount(projectId)),
6042
+ this.options.secretStore.delete(this.apiKeyAccount(projectId))
5907
6043
  ]);
5908
6044
  const failure = results.find(
5909
6045
  (result) => result.status === "rejected"
@@ -5962,20 +6098,23 @@ var ProjectEndpointCache = class {
5962
6098
  this.overrideAccount(projectId)
5963
6099
  );
5964
6100
  if (override !== null) {
5965
- const parsed = rpcApiKeySchema.safeParse(override);
5966
- if (!parsed.success) {
6101
+ const parsed2 = rpcApiKeySchema.safeParse(override);
6102
+ if (!parsed2.success) {
5967
6103
  throw unavailableApiKey();
5968
6104
  }
5969
- return parsed.data;
6105
+ return parsed2.data;
5970
6106
  }
5971
- const candidates = (await this.list(projectId)).map(
5972
- (network) => this.extractCredential(network)
5973
- );
5974
- const unique = new Set(candidates);
5975
- if (candidates.length === 0 || unique.size !== 1) {
6107
+ const account = this.apiKeyAccount(projectId);
6108
+ let key = await this.options.secretStore.read(account);
6109
+ if (key === null) {
6110
+ await this.refresh(projectId);
6111
+ key = await this.options.secretStore.read(account);
6112
+ }
6113
+ const parsed = rpcApiKeySchema.safeParse(key);
6114
+ if (!parsed.success) {
5976
6115
  throw unavailableApiKey();
5977
6116
  }
5978
- return candidates[0];
6117
+ return parsed.data;
5979
6118
  }
5980
6119
  async applyOverride(projectId, network) {
5981
6120
  const key = await this.options.secretStore.read(
@@ -6032,33 +6171,39 @@ var ProjectEndpointCache = class {
6032
6171
  );
6033
6172
  }
6034
6173
  }
6035
- extractCredential(network) {
6174
+ extractCredential(endpoint) {
6036
6175
  try {
6037
- const url = new URL(network.endpoints.https);
6176
+ const url = new URL(endpoint);
6038
6177
  const trustedHost = url.hostname === "zan.top" || url.hostname.endsWith(".zan.top");
6039
6178
  const segments = url.pathname.slice(1).split("/");
6040
6179
  if (url.protocol !== "https:" || !trustedHost || url.port.length > 0 || url.username.length > 0 || url.password.length > 0 || url.search.length > 0 || url.hash.length > 0 || !url.pathname.startsWith("/") || segments.some((segment) => segment.length === 0) || url.pathname !== `/${segments.join("/")}`) {
6041
6180
  throw new Error("unsafe");
6042
6181
  }
6043
- let candidate;
6044
- if (network.ecosystem === "hyperliquid") {
6045
- if (segments.length !== 6 || segments[0] !== "node" || segments[1] !== "v1" || segments[2] !== "hyperliquid" || segments[3] !== network.network || segments[5] !== "evm") {
6046
- throw new Error("unsafe");
6047
- }
6048
- candidate = segments[4];
6049
- } else {
6050
- if (segments.length !== 5 || segments[0] !== "node" || segments[1] !== "v1" || !networkIdSchema.safeParse(segments[2]).success || segments[3] !== network.network) {
6051
- throw new Error("unsafe");
6052
- }
6053
- candidate = segments[4];
6182
+ if (segments[0] !== "node" || segments[1] !== "v1" || !networkIdSchema.safeParse(segments[2]).success || !networkIdSchema.safeParse(segments[3]).success) {
6183
+ return void 0;
6184
+ }
6185
+ let suffix = "";
6186
+ switch (segments[2]) {
6187
+ case "hyperliquid":
6188
+ suffix = "evm";
6189
+ break;
6190
+ case "avax":
6191
+ suffix = "ext/bc/C/rpc";
6192
+ break;
6193
+ case "tron":
6194
+ suffix = "jsonrpc";
6195
+ break;
6054
6196
  }
6055
- const parsed = rpcApiKeySchema.safeParse(candidate);
6197
+ if (segments.slice(5).join("/") !== suffix) {
6198
+ return void 0;
6199
+ }
6200
+ const parsed = rpcApiKeySchema.safeParse(segments[4]);
6056
6201
  if (!parsed.success) {
6057
6202
  throw new Error("unsafe");
6058
6203
  }
6059
6204
  return parsed.data;
6060
6205
  } catch {
6061
- throw unavailableApiKey();
6206
+ return void 0;
6062
6207
  }
6063
6208
  }
6064
6209
  };
@@ -6072,21 +6217,22 @@ var RpcKeyPullService = class {
6072
6217
  async pull() {
6073
6218
  const projectId = await this.dependencies.projects.activeId();
6074
6219
  const key = await this.dependencies.keys.effectiveApiKey(projectId);
6075
- await this.dependencies.writer.write(key);
6220
+ const result = await this.dependencies.writer.write(key);
6076
6221
  return {
6077
6222
  file: ".env",
6078
6223
  variable: "ZAN_API_KEY",
6079
- updated: true
6224
+ updated: true,
6225
+ ...result?.warnings.length ? { warnings: result.warnings } : {}
6080
6226
  };
6081
6227
  }
6082
6228
  };
6083
6229
 
6084
6230
  // src/projects/project-service.ts
6085
- import { randomUUID as randomUUID5 } from "crypto";
6231
+ import { randomUUID as randomUUID6 } from "crypto";
6086
6232
  var ProjectService = class {
6087
6233
  constructor(options) {
6088
6234
  this.options = options;
6089
- this.createIdempotencyKey = options.createIdempotencyKey ?? randomUUID5;
6235
+ this.createIdempotencyKey = options.createIdempotencyKey ?? randomUUID6;
6090
6236
  }
6091
6237
  options;
6092
6238
  createIdempotencyKey;
@@ -6480,7 +6626,7 @@ var SettingsStore = class {
6480
6626
  };
6481
6627
 
6482
6628
  // src/settings/paths.ts
6483
- import { join as join5, posix as posix2, win32 as win322 } from "path";
6629
+ import { join as join6, posix as posix2, win32 as win322 } from "path";
6484
6630
  function nonEmpty(value) {
6485
6631
  return value === void 0 || value.length === 0 ? void 0 : value;
6486
6632
  }
@@ -6513,7 +6659,7 @@ function resolveZanHome(env, platform, homedir2) {
6513
6659
  }
6514
6660
  if (platform === "darwin") {
6515
6661
  return validateHomePath(
6516
- join5(readHomedir(homedir2), "Library", "Application Support", "zan"),
6662
+ join6(readHomedir(homedir2), "Library", "Application Support", "zan"),
6517
6663
  platform
6518
6664
  );
6519
6665
  }
@@ -7336,7 +7482,7 @@ function createRuntimeContext(input) {
7336
7482
  walletService ??= new WalletService({
7337
7483
  index: new AtomicJsonStore(
7338
7484
  walletIndexSchema,
7339
- join6(home, "wallets.json"),
7485
+ join7(home, "wallets.json"),
7340
7486
  { platform }
7341
7487
  ),
7342
7488
  secrets: secretStore,
@@ -7652,4 +7798,3 @@ export {
7652
7798
  isMainModule,
7653
7799
  runCli
7654
7800
  };
7655
- //# sourceMappingURL=cli.js.map
package/package.json CHANGED
@@ -1,13 +1,13 @@
1
1
  {
2
2
  "name": "@zan_team/cli",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Unified ZAN command-line interface for developers, automation, and AI agents",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "zan": "./dist/cli.js"
8
8
  },
9
9
  "files": [
10
- "dist",
10
+ "dist/cli.js",
11
11
  "skills",
12
12
  "README.md"
13
13
  ],