global-app-skill 0.1.4 → 0.1.5

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
@@ -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 目录。安装器把该目录写入入口的 `--skill-dir` 参数,并在同一文件中记录 CLI 版本。已存在的 SKILL.md 会被原子替换,目录中的其他文件保留,符号链接入口拒绝覆盖。安装不下载 references,也不修改 MCP 或客户端配置。
13
+ 默认只安装 `~/.agents/skills/overseas-app-research/SKILL.md`。也可用 `--dir /absolute/skills/overseas-app-research` 指定当前客户端实际使用的 skill 目录。所有目录使用完全相同的薄入口,只记录 CLI 版本,不写入安装路径。安装位置记录在本机状态目录,供 CLI 后续自动发现;该记录不放进 skill。已存在的 SKILL.md 会被原子替换,目录中的其他文件保留,符号链接入口拒绝覆盖。安装不下载 references,也不修改 MCP 或客户端配置。
14
14
 
15
15
  ## 执行
16
16
 
@@ -20,9 +20,13 @@ npx -y --prefer-online global-app-skill@latest context resolve --mode research
20
20
 
21
21
  `--mode` 为 `download`、`research` 或 `ued`。CLI 每次查询 GitHub 最新正式 Release 的 `latest.json` 附件,验证清单、压缩包及逐文件 SHA-256,返回绝对 `resourceRoot` 和有序 `mustRead`。agent 读取这些文件后按原工作流编排任务。
22
22
 
23
- `context resolve` 和 `run` 执行前会将已安装的薄入口同步为当前 CLI 捆绑的版本。入口使用 `npx --prefer-online ...@latest`,因此发布新版 CLI 后,下一次执行会更新本地 SKILL.md。传入 `--skill-dir /absolute/skill-directory` 可精确定位安装目录;安装器已自动写入该参数。未传入时仅检查 `~/.agents/skills/overseas-app-research` 和 `${CODEX_HOME:-~/.codex}/skills/overseas-app-research` 中现有的 CLI 入口,不创建新 skill,不覆盖手工维护的其他实现或符号链接,也不降级更新的入口。
23
+ `context resolve` 和 `run` 执行前由 CLI 自动定位已安装的薄入口,更新为当前 CLI 捆绑的版本。入口只调用通用命令,不传安装路径。入口使用 `npx --prefer-online ...@latest`,因此发布新版 CLI 后,下一次执行会更新本地 SKILL.md。
24
24
 
25
- 自定义目录中的入口若未包含 `--skill-dir`,重新执行一次 `skill install --dir ...` 写入准确目录。`context resolve` 的 `skillUpdate` 单独报告入口同步结果;同步失败会明确显示,资源解析仍可独立进行。脚本 stdout 和退出码保持不变,同步日志写入 stderr。
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 仓库内时,项目级发现只检查当前目录,不遍历其他工作区。
26
+
27
+ 自定义安装目录通过 `skill install --dir ...` 登记在 `${XDG_STATE_HOME:-~/.local/state}/global-app-skill/installations/`,不需要把路径写进 SKILL.md。复制到另一台机器的标准 skill 目录后,CLI 会按该机器环境重新发现;移到标准目录以外的位置时,再运行一次安装命令登记新位置。`--skill-dir` 仅保留为手动指定本次更新目标的可选参数,薄入口无需使用。
28
+
29
+ 自动同步只更新已经存在且属于本 CLI 的入口,不创建其他客户端的 skill,不覆盖手工维护的其他实现或符号链接,也不降级更新的入口。`context resolve` 的 `skillUpdate` 单独报告同步结果;失败会明确显示,资源解析仍可独立进行。脚本 stdout 和退出码保持不变,同步日志写入 stderr。
26
30
 
27
31
  本地文件更新后,新入口在客户端重新加载 skill 时采用;已经读入当前任务的上下文不因磁盘替换自动改写。[Codex 官方文档](https://learn.chatgpt.com/docs/build-skills)说明会自动检测 skill 变更;如果没有显示更新,可重启 Codex。其他客户端遵循各自的重新加载规则。
28
32
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "global-app-skill",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Thin skill bootstrap and GitHub resource runner for overseas Android app research",
5
5
  "type": "module",
6
6
  "license": "UNLICENSED",
@@ -10,9 +10,9 @@ description: 调研海外 Android App 的业务流程、功能入口、字段权
10
10
  按目标选择 `download`(仅取包)、`research`(业务调研,含同时需要图册)或 `ued`(仅界面图册),执行:
11
11
 
12
12
  ```sh
13
- npx -y --prefer-online global-app-skill@latest context resolve --skill-dir SKILL_DIRECTORY --mode MODE
13
+ npx -y --prefer-online global-app-skill@latest context resolve --mode MODE
14
14
  ```
15
15
 
16
- `SKILL_DIRECTORY` 为本 SKILL.md 所在绝对目录。CLI 同步本地入口,检查 `skillUpdate` 并报告失败;新入口在客户端重新加载后采用。依次读取全部 `mustRead`,以 `resourceRoot` 解析资源并按流程执行。新任务重新 resolve,同一任务沿用已读快照。资源更新或校验失败时停止依赖工作,不静默使用旧资源。
16
+ CLI 自动发现已安装的 skill 并同步入口,检查 `skillUpdate` 并报告失败;新入口在客户端重新加载后采用。依次读取全部 `mustRead`,以 `resourceRoot` 解析资源并按流程执行。新任务重新 resolve,同一任务沿用已读快照。资源更新或校验失败时停止依赖工作,不静默使用旧资源。
17
17
 
18
- 脚本通过 `npx -y --prefer-online global-app-skill@latest run SCRIPT --skill-dir SKILL_DIRECTORY --mode MODE --resource-root RESOURCE_ROOT -- 参数` 执行;参数见解析后的 CLI 文档。业务判断与页面操作由 agent 按文档编排,CLI 成功不代表调研完成;关键进度仍须输出到聊天正文。
18
+ 脚本通过 `npx -y --prefer-online global-app-skill@latest run SCRIPT --mode MODE --resource-root RESOURCE_ROOT -- 参数` 执行;参数见解析后的 CLI 文档。业务判断与页面操作由 agent 按文档编排,CLI 成功不代表调研完成;关键进度仍须输出到聊天正文。
package/src/cli.mjs CHANGED
@@ -13,6 +13,7 @@ const usage = `global-app-skill ${VERSION}
13
13
 
14
14
  skill install writes only SKILL.md; default: ~/.agents/skills/overseas-app-research.
15
15
  context resolve and run synchronize existing managed skill entries from this CLI bundle.
16
+ Skill locations are discovered from client user/project directories and CLI installation records.
16
17
  run uses a verified pinned snapshot when --resource-root is supplied; otherwise it resolves current resources.
17
18
  Script stdout/stderr and exit code are preserved. JSON command results go to stdout; progress goes to stderr.
18
19
  `;
@@ -42,6 +43,7 @@ export async function main(argv) {
42
43
  throw new Error('--resource-root cannot be combined with --feed or --cache-dir');
43
44
  }
44
45
  const skillUpdate = command === 'skill' ? undefined : await syncInstalledSkills({ directory: values['skill-dir'] });
46
+ if (skillUpdate?.error) process.stderr.write(`[global-app-skill] Skill discovery failed: ${skillUpdate.error}\n`);
45
47
  for (const entry of skillUpdate?.installations || []) {
46
48
  if (entry.status === 'updated') process.stderr.write(`[global-app-skill] Updated local entry: ${entry.skillPath}; reload the skill to use new discovery instructions\n`);
47
49
  if (entry.status === 'failed') process.stderr.write(`[global-app-skill] Skill entry update failed: ${entry.skillPath}: ${entry.error}\n`);
@@ -0,0 +1,64 @@
1
+ import { lstat, mkdir, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { homedir } from 'node:os';
4
+ import path from 'node:path';
5
+ import { sha256 } from './protocol.mjs';
6
+
7
+ export const SKILL_ROOTS = ['.agents/skills', '.codex/skills', '.claude/skills', '.cursor/skills', '.gemini/skills', '.kiro/skills'];
8
+ const skillName = 'overseas-app-research';
9
+
10
+ function stateRoot({ home = homedir(), env = process.env, stateDirectory } = {}) {
11
+ return path.resolve(stateDirectory || path.join(env.XDG_STATE_HOME || path.join(home, '.local/state'), 'global-app-skill'));
12
+ }
13
+
14
+ async function exists(filename) {
15
+ try { await lstat(filename); return true; }
16
+ catch (error) { if (['ENOENT', 'ENOTDIR'].includes(error.code)) return false; throw error; }
17
+ }
18
+
19
+ export async function rememberInstallation(directory, options = {}) {
20
+ directory = path.resolve(directory);
21
+ const registry = path.join(stateRoot(options), 'installations');
22
+ await mkdir(registry, { recursive: true });
23
+ // Independent records avoid lost registrations when installs run concurrently.
24
+ const target = path.join(registry, `${sha256(directory)}.json`);
25
+ const temporary = path.join(registry, `.${randomUUID()}.tmp`);
26
+ try {
27
+ await writeFile(temporary, JSON.stringify({ directory }) + '\n', { flag: 'wx' });
28
+ await rename(temporary, target);
29
+ } finally { await rm(temporary, { force: true }); }
30
+ }
31
+
32
+ async function registeredDirectories(options) {
33
+ const registry = path.join(stateRoot(options), 'installations');
34
+ let names;
35
+ try { names = await readdir(registry); }
36
+ catch (error) { if (error.code === 'ENOENT') return []; throw error; }
37
+ return Promise.all(names.filter(name => name.endsWith('.json')).sort().map(async name => {
38
+ const { directory } = JSON.parse(await readFile(path.join(registry, name), 'utf8'));
39
+ if (typeof directory !== 'string' || !path.isAbsolute(directory) || name !== `${sha256(directory)}.json`) {
40
+ throw new Error(`Invalid skill installation record: ${path.join(registry, name)}`);
41
+ }
42
+ return directory;
43
+ }));
44
+ }
45
+
46
+ async function projectDirectories(cwd) {
47
+ const directories = [];
48
+ for (let directory = path.resolve(cwd); ; directory = path.dirname(directory)) {
49
+ directories.push(directory);
50
+ if (await exists(path.join(directory, '.git'))) return directories;
51
+ if (path.dirname(directory) === directory) return [path.resolve(cwd)];
52
+ }
53
+ }
54
+
55
+ export async function discoverSkillDirectories(options = {}) {
56
+ const { home = homedir(), env = process.env, cwd = process.cwd() } = options;
57
+ 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));
60
+ candidates.push(...await registeredDirectories(options));
61
+ const unique = [...new Set(candidates)];
62
+ const present = await Promise.all(unique.map(directory => exists(path.join(directory, 'SKILL.md'))));
63
+ return unique.filter((directory, index) => present[index]);
64
+ }
package/src/skill.mjs CHANGED
@@ -4,6 +4,7 @@ import { homedir } from 'node:os';
4
4
  import path from 'node:path';
5
5
  import { setTimeout as delay } from 'node:timers/promises';
6
6
  import { VERSION } from './protocol.mjs';
7
+ import { discoverSkillDirectories, rememberInstallation } from './installations.mjs';
7
8
 
8
9
  async function stat(filename) {
9
10
  try { return await lstat(filename); }
@@ -46,8 +47,7 @@ async function writeSkill(target, existingOnly) {
46
47
  }
47
48
  }
48
49
  const bundled = await readFile(new URL('../skills/overseas-app-research/SKILL.md', import.meta.url), 'utf8');
49
- const quotedDirectory = "'" + target.replaceAll("'", "'\\''") + "'";
50
- const content = bundled.replaceAll('SKILL_DIRECTORY', quotedDirectory).trimEnd() + `\n\n<!-- global-app-skill-version: ${VERSION} -->\n`;
50
+ const content = bundled.trimEnd() + `\n\n<!-- global-app-skill-version: ${VERSION} -->\n`;
51
51
  if (previous === content) return { skillPath, status: 'unchanged' };
52
52
  const temporary = path.join(target, `.SKILL-${randomUUID()}.tmp`);
53
53
  try {
@@ -58,15 +58,18 @@ async function writeSkill(target, existingOnly) {
58
58
  } finally { await rm(lock, { recursive: true, force: true }); }
59
59
  }
60
60
 
61
- export async function installSkill(directory) {
62
- return { ok: true, ...await writeSkill(path.resolve(directory || path.join(homedir(), '.agents/skills/overseas-app-research')), false) };
61
+ export async function installSkill(directory, options = {}) {
62
+ const target = path.resolve(directory || path.join(options.home || homedir(), '.agents/skills/overseas-app-research'));
63
+ const result = await writeSkill(target, false);
64
+ await rememberInstallation(target, options);
65
+ return { ok: true, ...result };
63
66
  }
64
67
 
65
- export async function syncInstalledSkills({ directory, directories = [
66
- path.join(homedir(), '.agents/skills/overseas-app-research'),
67
- path.join(process.env.CODEX_HOME || path.join(homedir(), '.codex'), 'skills/overseas-app-research'),
68
- ] } = {}) {
69
- const targets = [...new Set((directory ? [directory] : directories).map(target => path.resolve(target)))];
68
+ export async function syncInstalledSkills({ directory, directories, ...options } = {}) {
69
+ let targets;
70
+ try {
71
+ targets = [...new Set((directory ? [directory] : directories || await discoverSkillDirectories(options)).map(target => path.resolve(target)))];
72
+ } catch (error) { return { ok: false, installations: [], error: error.message }; }
70
73
  const installations = [];
71
74
  for (const target of targets) {
72
75
  try { installations.push(await writeSkill(target, true)); }