gamealgo-cli 0.1.2 → 0.1.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
@@ -40,7 +40,7 @@ gamealgo key create --name <runtime-name> --json
40
40
  gamealgo key reveal --name <runtime-name> --json
41
41
  ```
42
42
 
43
- 安装和更新 CLI 都使用 `npm install -g gamealgo-cli@latest`,不需要拉取 GameAlgo Client 仓库。`gamealgo docs` 从当前 Admin Host 获取最新 AI 文档;登录前显式传 `--host`,登录后 CLI 会使用已保存的 Host。需要网页阅读时使用 `gamealgo docs --open`。
43
+ 安装和更新 CLI 都使用 `npm install -g gamealgo-cli@latest`,不需要拉取 GameAlgo Client 仓库。请在每个游戏项目根目录执行 `gamealgo login`,凭据保存在项目的 `.gamealgo/cli.json`,不同游戏互不覆盖。`gamealgo docs` 从当前 Admin Host 获取最新 AI 文档;登录前显式传 `--host`,登录后 CLI 会使用已保存的 Host。需要网页阅读时使用 `gamealgo docs --open`。
44
44
 
45
45
  如果已有可用 Client Game Key,就复用或 reveal;如果没有,就创建。Client Game Key 格式为 `ga_live_*`,用于游戏运行时拉配置、拉配置文件、上报事件。
46
46
 
package/cli/README.md CHANGED
@@ -56,7 +56,9 @@ gamealgo login \
56
56
  --admin-key ga_admin_xxx
57
57
  ```
58
58
 
59
- 登录信息会写入本机 `~/.gamealgo/cli.json`,CLI 会把文件权限设置为 `600`。也可以通过环境变量临时传入:
59
+ 请在游戏项目根目录执行登录。登录信息会写入当前目录的 `.gamealgo/cli.json`,CLI 会把文件权限设置为 `600`,并自动忽略整个 `.gamealgo/` 目录,避免密钥被提交到 Git。后续命令会从当前目录逐级向上查找该文件,因此可以在项目子目录执行;不同游戏仓库的登录信息互不覆盖。
60
+
61
+ 旧版的 `~/.gamealgo/cli.json` 仍可作为未找到项目级配置时的兜底。也可以通过环境变量临时传入:
60
62
 
61
63
  ```bash
62
64
  GAMEALGO_ADMIN_HOST=<admin-host>
package/dist/gamealgo.js CHANGED
@@ -7,7 +7,9 @@ import { homedir } from "node:os";
7
7
  import { createHash } from "node:crypto";
8
8
  import { spawn } from "node:child_process";
9
9
  import YAML from "yaml";
10
- const CONFIG_PATH = join(homedir(), ".gamealgo", "cli.json");
10
+ const GLOBAL_CONFIG_PATH = join(homedir(), ".gamealgo", "cli.json");
11
+ const PROJECT_CONFIG_DIR = ".gamealgo";
12
+ const PROJECT_CONFIG_FILE = "cli.json";
11
13
  const DOCS_CACHE_ROOT = join(homedir(), ".gamealgo", "docs");
12
14
  const SCRIPT_EXTENSIONS = new Set([".js", ".lua"]);
13
15
  const FILE_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.-]*$/;
@@ -135,12 +137,13 @@ async function login(args, global) {
135
137
  if (session.principal.type !== "game_admin" || !session.principal.gameId) {
136
138
  throw new Error("admin key is not scoped to a game");
137
139
  }
138
- await saveConfig({ host, adminKey });
140
+ const configPath = await saveConfig({ host, adminKey });
139
141
  await printResult({
140
142
  ok: true,
141
143
  host,
142
144
  gameId: session.principal.gameId,
143
145
  keyName: session.principal.keyName,
146
+ configPath,
144
147
  }, global);
145
148
  }
146
149
  async function handleDocs(args, global) {
@@ -1398,18 +1401,59 @@ function requireFlagValue(args, index, flag) {
1398
1401
  return value;
1399
1402
  }
1400
1403
  async function loadConfig() {
1401
- try {
1402
- return JSON.parse(await readFile(CONFIG_PATH, "utf8"));
1404
+ const localPath = await findProjectConfigPath(process.cwd());
1405
+ const paths = localPath && localPath !== GLOBAL_CONFIG_PATH
1406
+ ? [localPath, GLOBAL_CONFIG_PATH]
1407
+ : [GLOBAL_CONFIG_PATH];
1408
+ for (const configPath of paths) {
1409
+ try {
1410
+ return JSON.parse(await readFile(configPath, "utf8"));
1411
+ }
1412
+ catch {
1413
+ // Try the next supported config location.
1414
+ }
1403
1415
  }
1404
- catch {
1405
- return null;
1416
+ return null;
1417
+ }
1418
+ async function findProjectConfigPath(startDir) {
1419
+ let currentDir = startDir;
1420
+ while (true) {
1421
+ const candidate = join(currentDir, PROJECT_CONFIG_DIR, PROJECT_CONFIG_FILE);
1422
+ try {
1423
+ await readFile(candidate, "utf8");
1424
+ return candidate;
1425
+ }
1426
+ catch {
1427
+ const parentDir = dirname(currentDir);
1428
+ if (parentDir === currentDir)
1429
+ return null;
1430
+ currentDir = parentDir;
1431
+ }
1406
1432
  }
1407
1433
  }
1408
1434
  async function saveConfig(config) {
1409
- await mkdir(dirname(CONFIG_PATH), { recursive: true, mode: 0o700 });
1410
- await chmod(dirname(CONFIG_PATH), 0o700).catch(() => undefined);
1411
- await writeFile(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
1412
- await chmod(CONFIG_PATH, 0o600).catch(() => undefined);
1435
+ const configDir = join(process.cwd(), PROJECT_CONFIG_DIR);
1436
+ const configPath = join(configDir, PROJECT_CONFIG_FILE);
1437
+ await mkdir(configDir, { recursive: true, mode: 0o700 });
1438
+ await chmod(configDir, 0o700).catch(() => undefined);
1439
+ await ensureProjectConfigIgnored(configDir);
1440
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", { encoding: "utf8", mode: 0o600 });
1441
+ await chmod(configPath, 0o600).catch(() => undefined);
1442
+ return configPath;
1443
+ }
1444
+ async function ensureProjectConfigIgnored(configDir) {
1445
+ const ignorePath = join(configDir, ".gitignore");
1446
+ try {
1447
+ const existing = await readFile(ignorePath, "utf8");
1448
+ const rules = existing.split(/\r?\n/).map((line) => line.trim());
1449
+ if (rules.includes("*") || rules.includes(PROJECT_CONFIG_FILE) || rules.includes(`/${PROJECT_CONFIG_FILE}`))
1450
+ return;
1451
+ const separator = existing && !existing.endsWith("\n") ? "\n" : "";
1452
+ await writeFile(ignorePath, `${existing}${separator}${PROJECT_CONFIG_FILE}\n`, { encoding: "utf8", mode: 0o600 });
1453
+ }
1454
+ catch {
1455
+ await writeFile(ignorePath, "*\n", { encoding: "utf8", mode: 0o600 });
1456
+ }
1413
1457
  }
1414
1458
  async function readObjectFile(filePath, label) {
1415
1459
  const text = await readFile(filePath, "utf8");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gamealgo-cli",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "GameAlgo command-line client for AI agents and game developers",
5
5
  "type": "module",
6
6
  "bin": {