global-app-skill 0.1.5 → 0.1.7

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
@@ -1,6 +1,6 @@
1
1
  # global-app-skill
2
2
 
3
- 海外 Android App 调研 skill 的薄入口、资源更新器和脚本 CLI。保持 `overseas-app-research` 的下载、业务调研与 UED 图册三个分支。
3
+ 海外 Android App 调研 skill 的薄入口、资源更新器和脚本 CLI。保持 `global-app-analysis` 的下载、业务调研与 UED 图册三个分支。
4
4
 
5
5
  ## 安装
6
6
 
@@ -10,7 +10,7 @@
10
10
  npx -y --prefer-online global-app-skill@latest skill install
11
11
  ```
12
12
 
13
- 默认只安装 `~/.agents/skills/overseas-app-research/SKILL.md`。也可用 `--dir /absolute/skills/overseas-app-research` 指定当前客户端实际使用的 skill 目录。所有目录使用完全相同的薄入口,只记录 CLI 版本,不写入安装路径。安装位置记录在本机状态目录,供 CLI 后续自动发现;该记录不放进 skill。已存在的 SKILL.md 会被原子替换,目录中的其他文件保留,符号链接入口拒绝覆盖。安装不下载 references,也不修改 MCP 或客户端配置。
13
+ 默认只安装 `~/.agents/skills/global-app-analysis/SKILL.md`。也可用 `--dir /absolute/skills/global-app-analysis` 指定当前客户端实际使用的 skill 目录。所有目录使用完全相同的薄入口,只记录 CLI 版本,不写入安装路径。安装位置记录在本机状态目录,供 CLI 后续自动发现;该记录不放进 skill。已存在的 SKILL.md 会被原子替换,目录中的其他文件保留,符号链接入口拒绝覆盖。安装不下载 references,也不修改 MCP 或客户端配置。
14
14
 
15
15
  ## 执行
16
16
 
@@ -22,7 +22,7 @@ npx -y --prefer-online global-app-skill@latest context resolve --mode research
22
22
 
23
23
  `context resolve` 和 `run` 执行前由 CLI 自动定位已安装的薄入口,更新为当前 CLI 捆绑的版本。入口只调用通用命令,不传安装路径。入口使用 `npx --prefer-online ...@latest`,因此发布新版 CLI 后,下一次执行会更新本地 SKILL.md。
24
24
 
25
- CLI 检查以下位置中的 `overseas-app-research/SKILL.md`:用户目录下的 `.agents/skills`、`.codex/skills`、`.claude/skills`、`.cursor/skills`、`.gemini/skills`、`.kiro/skills`;当前目录至最近 Git 仓库根目录之间的相同项目级目录;`CODEX_HOME/skills`;以及 CLI 安装时登记的目录。不在 Git 仓库内时,项目级发现只检查当前目录,不遍历其他工作区。
25
+ CLI 检查以下位置中的 `global-app-analysis/SKILL.md`:用户目录下的 `.agents/skills`、`.codex/skills`、`.claude/skills`、`.cursor/skills`、`.gemini/skills`、`.kiro/skills`;当前目录至最近 Git 仓库根目录之间的相同项目级目录;`CODEX_HOME/skills`;以及 CLI 安装时登记的目录。不在 Git 仓库内时,项目级发现只检查当前目录,不遍历其他工作区。
26
26
 
27
27
  自定义安装目录通过 `skill install --dir ...` 登记在 `${XDG_STATE_HOME:-~/.local/state}/global-app-skill/installations/`,不需要把路径写进 SKILL.md。复制到另一台机器的标准 skill 目录后,CLI 会按该机器环境重新发现;移到标准目录以外的位置时,再运行一次安装命令登记新位置。`--skill-dir` 仅保留为手动指定本次更新目标的可选参数,薄入口无需使用。
28
28
 
@@ -42,11 +42,11 @@ npx -y --prefer-online global-app-skill@latest run preflight --mode research --r
42
42
 
43
43
  | 路径 | 职责 |
44
44
  |---|---|
45
- | `skills/overseas-app-research/SKILL.md` | 安装到客户端的薄入口 |
45
+ | `skills/global-app-analysis/SKILL.md` | 安装到客户端的薄入口 |
46
46
  | `src/` | npm CLI:安装、资源解析、校验、脚本运行 |
47
- | `resources/overseas-app-research/references/` | 主流程和各阶段操作文档 |
48
- | `resources/overseas-app-research/scripts/` | 原有 Python 脚本及回归测试 |
49
- | `resources/overseas-app-research/examples/` | 业务报告模板 |
47
+ | `resources/global-app-analysis/references/` | 主流程和各阶段操作文档 |
48
+ | `resources/global-app-analysis/scripts/` | 原有 Python 脚本及回归测试 |
49
+ | `resources/global-app-analysis/examples/` | 业务报告模板 |
50
50
  | `scripts/build-resources.mjs` | 生成确定性的资源包与清单 |
51
51
  | `.github/workflows/publish.yml` | tag 触发的构建、GitHub Release 和 npm 发布 |
52
52
  | `scripts/release.mjs` | tag 校验、打包、校验和及发布资产核对 |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "global-app-skill",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Thin skill bootstrap and GitHub resource runner for overseas Android app research",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -14,7 +14,7 @@
14
14
  "publishConfig": { "registry": "https://registry.npmjs.org/", "access": "public" },
15
15
  "scripts": {
16
16
  "test": "node --test tests/*.test.mjs",
17
- "test:python": "python3 -m unittest discover -s resources/overseas-app-research/scripts -p 'test_*.py'",
17
+ "test:python": "python3 -m unittest discover -s resources/global-app-analysis/scripts -p 'test_*.py'",
18
18
  "resources:build": "node scripts/build-resources.mjs",
19
19
  "resources:check": "node scripts/build-resources.mjs --check",
20
20
  "release:prepare": "node scripts/release.mjs",
@@ -1,9 +1,9 @@
1
1
  ---
2
- name: overseas-app-research
2
+ name: global-app-analysis
3
3
  description: 调研海外 Android App 的业务流程、功能入口、字段权限,或为 UED 提取整款 App/指定范围的原始界面图册;也支持仅下载/导出 APK。
4
4
  ---
5
5
 
6
- # 海外 App 调研
6
+ # 海外 App 分析
7
7
 
8
8
  需要 Node.js >=22.12、`python3` >=3.9;设备/AVD 门槛由解析后的工作流检查。
9
9
 
@@ -16,3 +16,8 @@ npx -y --prefer-online global-app-skill@latest context resolve --mode MODE
16
16
  CLI 自动发现已安装的 skill 并同步入口,检查 `skillUpdate` 并报告失败;新入口在客户端重新加载后采用。依次读取全部 `mustRead`,以 `resourceRoot` 解析资源并按流程执行。新任务重新 resolve,同一任务沿用已读快照。资源更新或校验失败时停止依赖工作,不静默使用旧资源。
17
17
 
18
18
  脚本通过 `npx -y --prefer-online global-app-skill@latest run SCRIPT --mode MODE --resource-root RESOURCE_ROOT -- 参数` 执行;参数见解析后的 CLI 文档。业务判断与页面操作由 agent 按文档编排,CLI 成功不代表调研完成;关键进度仍须输出到聊天正文。
19
+
20
+ ## 必检入口
21
+
22
+ 解析资源后,先读取当前 `resourceRoot` 下的 `references/rules.md`。读取失败时暂停依赖该规则的工作。
23
+ 到达规则定义的检查点时执行对应校验并记录结果。通过后才可正式或阶段性交付,包括复用、重发已有报告或链接。校验未通过时按规则回退或说明阻塞与恢复条件。
package/src/cli.mjs CHANGED
@@ -3,7 +3,7 @@ import { parseArgs } from 'node:util';
3
3
  import { installSkill, syncInstalledSkills } from './skill.mjs';
4
4
  import { resolveContext } from './resources.mjs';
5
5
  import { runScript } from './run.mjs';
6
- import { validateMode, VERSION } from './protocol.mjs';
6
+ import { SKILL_NAME, validateMode, VERSION } from './protocol.mjs';
7
7
 
8
8
  const usage = `global-app-skill ${VERSION}
9
9
  skill install [--dir ABSOLUTE_SKILL_DIRECTORY]
@@ -11,7 +11,7 @@ const usage = `global-app-skill ${VERSION}
11
11
  run SCRIPT [--mode download|research|ued] [--skill-dir PATH] [--resource-root PATH] [--cache-dir PATH] [--feed URL] -- SCRIPT_ARGS
12
12
  --version
13
13
 
14
- skill install writes only SKILL.md; default: ~/.agents/skills/overseas-app-research.
14
+ skill install writes only SKILL.md; default: ~/.agents/skills/${SKILL_NAME}.
15
15
  context resolve and run synchronize existing managed skill entries from this CLI bundle.
16
16
  Skill locations are discovered from client user/project directories and CLI installation records.
17
17
  run uses a verified pinned snapshot when --resource-root is supplied; otherwise it resolves current resources.
@@ -2,10 +2,9 @@ import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/
2
2
  import { randomUUID } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
- import { sha256 } from './protocol.mjs';
5
+ import { sha256, SKILL_NAME } from './protocol.mjs';
6
6
 
7
7
  export const SKILL_ROOTS = ['.agents/skills', '.codex/skills', '.claude/skills', '.cursor/skills', '.gemini/skills', '.kiro/skills'];
8
- const skillName = 'overseas-app-research';
9
8
 
10
9
  function stateRoot({ home = homedir(), env = process.env, stateDirectory } = {}) {
11
10
  return path.resolve(stateDirectory || path.join(env.XDG_STATE_HOME || path.join(home, '.local/state'), 'global-app-skill'));
@@ -55,8 +54,8 @@ async function projectDirectories(cwd) {
55
54
  export async function discoverSkillDirectories(options = {}) {
56
55
  const { home = homedir(), env = process.env, cwd = process.cwd() } = options;
57
56
  const bases = [path.resolve(home), ...await projectDirectories(cwd)];
58
- const candidates = bases.flatMap(base => SKILL_ROOTS.map(root => path.join(base, root, skillName)));
59
- if (env.CODEX_HOME) candidates.push(path.resolve(env.CODEX_HOME, 'skills', skillName));
57
+ const candidates = bases.flatMap(base => SKILL_ROOTS.map(root => path.join(base, root, SKILL_NAME)));
58
+ if (env.CODEX_HOME) candidates.push(path.resolve(env.CODEX_HOME, 'skills', SKILL_NAME));
60
59
  candidates.push(...await registeredDirectories(options));
61
60
  const unique = [...new Set(candidates)];
62
61
  const present = await Promise.all(unique.map(directory => exists(path.join(directory, 'SKILL.md'))));
package/src/protocol.mjs CHANGED
@@ -2,10 +2,12 @@ import { createHash } from 'node:crypto';
2
2
  import { readFileSync } from 'node:fs';
3
3
 
4
4
  export const VERSION = JSON.parse(readFileSync(new URL('../package.json', import.meta.url))).version;
5
+ export const SKILL_NAME = 'global-app-analysis';
5
6
  export const RELEASE_BASE = 'https://github.com/shiguang-lab/global-app-skill/releases';
6
7
  export const FEED = `${RELEASE_BASE}/latest/download/latest.json`;
7
8
  export const RELEASE_TAG = /^(?:v|resources-v)(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)$/;
8
9
  export const MAX_BYTES = 16 * 1024 * 1024;
10
+ export const RULES_PATH = 'references/rules.md';
9
11
  export const MODES = {
10
12
  download: 'references/download-workflow.md',
11
13
  research: 'references/research-workflow.md',
@@ -20,7 +22,7 @@ export function validateMode(mode) {
20
22
  return mode;
21
23
  }
22
24
 
23
- export function validateManifest(manifest) {
25
+ export function validateManifest(manifest, { requireRules = true } = {}) {
24
26
  const hash = /^[a-f0-9]{64}$/;
25
27
  if (manifest?.schemaVersion !== 1 || !RELEASE_TAG.test(manifest.releaseTag) || !hash.test(manifest.resourceHash) ||
26
28
  !/^\d+\.\d+\.\d+$/.test(manifest.minCliVersion) ||
@@ -43,7 +45,9 @@ export function validateManifest(manifest) {
43
45
  if (identity(manifest.files) !== manifest.resourceHash) throw new Error('Resource manifest hash mismatch');
44
46
  const required = ['references/guide.md', 'references/cli.md', 'references/orchestration.md',
45
47
  ...Object.values(MODES), ...SCRIPTS.map(name => `scripts/${name}.py`), 'scripts/apk_utils.py', 'scripts/progress.py'];
46
- if (required.some(name => !seen.has(name))) throw new Error('Resource manifest is missing workflow files');
48
+ if (requireRules) required.unshift(RULES_PATH, 'references/delivery-record.md', 'scripts/check_delivery.py');
49
+ const missing = required.filter(name => !seen.has(name));
50
+ if (missing.length) throw new Error(`Resource manifest is missing workflow files: ${missing.join(', ')}`);
47
51
  const installed = VERSION.split('.').map(Number);
48
52
  const minimum = manifest.minCliVersion.split('.').map(Number);
49
53
  for (let i = 0; i < 3; i++) {
package/src/resources.mjs CHANGED
@@ -3,7 +3,7 @@ import { homedir } from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { setTimeout as delay } from 'node:timers/promises';
5
5
  import { gunzipSync } from 'node:zlib';
6
- import { bundleUrl, FEED, MAX_BYTES, MODES, sha256, validateManifest, validateMode } from './protocol.mjs';
6
+ import { bundleUrl, FEED, MAX_BYTES, MODES, RULES_PATH, sha256, validateManifest, validateMode } from './protocol.mjs';
7
7
 
8
8
  async function download(url, limit) {
9
9
  const response = await fetch(url, { cache: 'no-store', signal: AbortSignal.timeout(30000) });
@@ -20,12 +20,12 @@ async function download(url, limit) {
20
20
  return Buffer.concat(chunks);
21
21
  }
22
22
 
23
- export async function readSnapshot(resourceRoot) {
23
+ export async function readSnapshot(resourceRoot, options) {
24
24
  resourceRoot = path.resolve(resourceRoot);
25
25
  if (!(await lstat(resourceRoot)).isDirectory()) throw new Error('Resource root must be a real directory');
26
26
  const manifestPath = path.join(resourceRoot, 'manifest.json');
27
27
  if (!(await lstat(manifestPath)).isFile()) throw new Error('Resource manifest must be a regular file');
28
- const manifest = validateManifest(JSON.parse(await readFile(manifestPath, 'utf8')));
28
+ const manifest = validateManifest(JSON.parse(await readFile(manifestPath, 'utf8')), options);
29
29
  if (path.basename(resourceRoot) !== manifest.resourceHash) throw new Error('Resource root does not match its snapshot hash');
30
30
  for (const file of manifest.files) {
31
31
  let directory = resourceRoot;
@@ -107,7 +107,7 @@ export async function resolveContext({ mode, feed = FEED, cacheDir, progress = (
107
107
  }
108
108
  await readSnapshot(resourceRoot);
109
109
  progress(updated ? 'Resource snapshot ready' : 'Verified cached snapshot is current');
110
- const mustRead = ['references/cli.md', 'references/guide.md', 'references/orchestration.md', MODES[mode]]
110
+ const mustRead = [RULES_PATH, 'references/cli.md', 'references/guide.md', 'references/orchestration.md', MODES[mode]]
111
111
  .map(name => path.join(resourceRoot, name));
112
112
  return { ok: true, source: url.origin === 'https://github.com' ? 'github' : 'custom', feed: url.href,
113
113
  mode, releaseTag: manifest.releaseTag, resourceHash: manifest.resourceHash, resourceRoot,
package/src/run.mjs CHANGED
@@ -7,7 +7,8 @@ import { readSnapshot, resolveContext } from './resources.mjs';
7
7
  export async function runScript(script, args, options = {}) {
8
8
  if (!SCRIPTS.includes(script)) throw new Error(`Unknown script; choose ${SCRIPTS.join(', ')}`);
9
9
  const resourceRoot = options.resourceRoot ? path.resolve(options.resourceRoot) : (await resolveContext(options)).resourceRoot;
10
- await readSnapshot(resourceRoot);
10
+ // A task that already resolved an earlier snapshot may finish on it.
11
+ await readSnapshot(resourceRoot, { requireRules: !options.resourceRoot });
11
12
  await promisify(execFile)('python3', ['-c', 'import sys; sys.exit(0 if sys.version_info >= (3, 9) else 1)'])
12
13
  .catch(() => { throw new Error('Python >=3.9 must be available as python3'); });
13
14
  return new Promise((resolve, reject) => {
package/src/skill.mjs CHANGED
@@ -3,7 +3,7 @@ import { randomUUID } from 'node:crypto';
3
3
  import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
- import { VERSION } from './protocol.mjs';
6
+ import { SKILL_NAME, VERSION } from './protocol.mjs';
7
7
  import { discoverSkillDirectories, rememberInstallation } from './installations.mjs';
8
8
 
9
9
  async function stat(filename) {
@@ -35,7 +35,8 @@ async function writeSkill(target, existingOnly) {
35
35
  let previous;
36
36
  try { previous = await readFile(skillPath, 'utf8'); }
37
37
  catch (error) { if (error.code !== 'ENOENT') throw error; }
38
- if (existingOnly && !previous?.includes('global-app-skill@latest context resolve')) {
38
+ const installedName = previous?.match(/^name: ([a-z0-9-]+)$/m)?.[1];
39
+ if (existingOnly && (installedName !== SKILL_NAME || !previous?.includes('global-app-skill@latest context resolve'))) {
39
40
  return { skillPath, status: 'skipped', reason: 'not-managed' };
40
41
  }
41
42
  const installedVersion = previous?.match(/^<!-- global-app-skill-version: (\d+\.\d+\.\d+) -->$/m)?.[1];
@@ -46,7 +47,7 @@ async function writeSkill(target, existingOnly) {
46
47
  if (installed[i] > bundled[i]) return { skillPath, status: 'skipped', reason: 'newer-installed', installedVersion };
47
48
  }
48
49
  }
49
- const bundled = await readFile(new URL('../skills/overseas-app-research/SKILL.md', import.meta.url), 'utf8');
50
+ const bundled = await readFile(new URL(`../skills/${SKILL_NAME}/SKILL.md`, import.meta.url), 'utf8');
50
51
  const content = bundled.trimEnd() + `\n\n<!-- global-app-skill-version: ${VERSION} -->\n`;
51
52
  if (previous === content) return { skillPath, status: 'unchanged' };
52
53
  const temporary = path.join(target, `.SKILL-${randomUUID()}.tmp`);
@@ -59,7 +60,7 @@ async function writeSkill(target, existingOnly) {
59
60
  }
60
61
 
61
62
  export async function installSkill(directory, options = {}) {
62
- const target = path.resolve(directory || path.join(options.home || homedir(), '.agents/skills/overseas-app-research'));
63
+ const target = path.resolve(directory || path.join(options.home || homedir(), '.agents/skills', SKILL_NAME));
63
64
  const result = await writeSkill(target, false);
64
65
  await rememberInstallation(target, options);
65
66
  return { ok: true, ...result };