android2harmony 0.1.1 → 0.1.2

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.
@@ -49,9 +49,6 @@ function defaultUserConfigPath() {
49
49
  const xdg = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
50
50
  return path.join(xdg, "hmos-precheck", USER_CONFIG_BASENAME);
51
51
  }
52
- function defaultHomeTransConfigPath() {
53
- return path.join(os.homedir(), ".hometrans", "config.json");
54
- }
55
52
  function commandlineToolsHelp() {
56
53
  return "Run `hmos-precheck init --scope user --commandline-tools /abs/path/to/command-line-tools`, or pass `--commandline-tools`.";
57
54
  }
@@ -135,10 +132,6 @@ async function resolveCommandlineToolsInfo(options) {
135
132
  const projectResult = await wrap(typeof value === "string" ? value : null, "project_config");
136
133
  if (projectResult) return projectResult;
137
134
  }
138
- const homeTransConfig = await loadJson(defaultHomeTransConfigPath());
139
- const homeTransValue = isObject(homeTransConfig?.env) ? homeTransConfig.env.DEVECO_SDK_HOME : void 0;
140
- const homeTransResult = await wrap(typeof homeTransValue === "string" ? homeTransValue : null, "hometrans_config");
141
- if (homeTransResult) return homeTransResult;
142
135
  const userConfig = await loadJson(options.userConfigPath ?? defaultUserConfigPath());
143
136
  const userValue = isObject(userConfig?.toolchain) ? userConfig.toolchain.commandline_tools : void 0;
144
137
  const userResult = await wrap(typeof userValue === "string" ? userValue : null, "user_config");
@@ -837,7 +830,6 @@ async function cmdDoctor(argv) {
837
830
  },
838
831
  config_paths: {
839
832
  project: projectRoot ? path.join(projectRoot, PROJECT_CONFIG_NAME) : null,
840
- hometrans: defaultHomeTransConfigPath(),
841
833
  user: defaultUserConfigPath()
842
834
  },
843
835
  profiles: {
package/dist/index.js CHANGED
@@ -20,67 +20,16 @@ function parseFrontmatter(content) {
20
20
  const match = content.match(/^---\s*\r?\n([\s\S]*?)\r?\n---/);
21
21
  if (!match) return null;
22
22
  const lines = match[1].split(/\r?\n/);
23
- let name = null;
24
- let hidden = false;
25
- let inMetadata = false;
26
- let metadataIndent = -1;
27
23
  for (const line of lines) {
28
24
  if (!line.trim() || line.trim().startsWith("#")) continue;
29
- const indent = line.length - line.trimStart().length;
30
- const trimmed = line.trim();
31
- const kv = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
32
- if (indent === 0) {
33
- inMetadata = false;
34
- if (!kv) continue;
35
- const value = kv[2].trim();
36
- if (kv[1] === "name") {
37
- name = value.replace(/^["']|["']$/g, "");
38
- } else if (kv[1] === "metadata") {
39
- if (value === "") {
40
- inMetadata = true;
41
- metadataIndent = -1;
42
- } else if (/visible\s*:\s*false/.test(value)) {
43
- hidden = true;
44
- }
45
- }
46
- } else if (inMetadata) {
47
- if (metadataIndent === -1) metadataIndent = indent;
48
- if (indent < metadataIndent) {
49
- inMetadata = false;
50
- continue;
51
- }
52
- if (kv && kv[1] === "visible" && kv[2].trim() === "false") {
53
- hidden = true;
54
- }
25
+ if (line.length - line.trimStart().length !== 0) continue;
26
+ const kv = line.trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/);
27
+ if (!kv) continue;
28
+ if (kv[1] === "name") {
29
+ return kv[2].trim().replace(/^["']|["']$/g, "");
55
30
  }
56
31
  }
57
- return name ? { name, hidden } : null;
58
- }
59
- function discoverA2HSkills(skillsRoot) {
60
- const hidden = [];
61
- const all = [];
62
- let entries;
63
- try {
64
- entries = fs.readdirSync(skillsRoot, { withFileTypes: true });
65
- } catch {
66
- return { hidden, all };
67
- }
68
- for (const entry of entries) {
69
- if (!entry.isDirectory()) continue;
70
- const skillFile = path2.join(skillsRoot, entry.name, "SKILL.md");
71
- let raw;
72
- try {
73
- raw = fs.readFileSync(skillFile, "utf-8");
74
- } catch {
75
- continue;
76
- }
77
- const info = parseFrontmatter(raw);
78
- if (info) {
79
- all.push(info.name);
80
- if (info.hidden) hidden.push(info.name);
81
- }
82
- }
83
- return { hidden, all };
32
+ return null;
84
33
  }
85
34
  function discoverA2HAgents(agentsRoot) {
86
35
  const result = /* @__PURE__ */ new Map();
@@ -99,9 +48,9 @@ function discoverA2HAgents(agentsRoot) {
99
48
  } catch {
100
49
  continue;
101
50
  }
102
- const info = parseFrontmatter(raw);
103
- if (!info) continue;
104
- result.set(info.name, stripFrontmatter(raw));
51
+ const name = parseFrontmatter(raw);
52
+ if (!name) continue;
53
+ result.set(name, stripFrontmatter(raw));
105
54
  }
106
55
  return result;
107
56
  }
@@ -131,7 +80,7 @@ function createServer(deps) {
131
80
  dispose: async () => {
132
81
  agentPrompts.clear();
133
82
  },
134
- // ── config hook: register skills search paths, hidden skills, and agents ──
83
+ // ── config hook: register skills search paths and agents ──
135
84
  config: async (cfg) => {
136
85
  const skills = cfg.skills || {};
137
86
  cfg.skills = skills;
@@ -140,15 +89,6 @@ function createServer(deps) {
140
89
  if (!paths.includes(skillsRoot)) {
141
90
  paths.push(skillsRoot);
142
91
  }
143
- const { hidden: a2hHidden, all: a2hAll } = discoverA2HSkills(skillsRoot);
144
- const currentHidden = skills.hidden || [];
145
- const cleaned = currentHidden.filter((name) => !a2hAll.includes(name));
146
- for (const name of a2hHidden) {
147
- if (!cleaned.includes(name)) {
148
- cleaned.push(name);
149
- }
150
- }
151
- skills.hidden = cleaned;
152
92
  const agent = cfg.agent || {};
153
93
  cfg.agent = agent;
154
94
  for (const [name, prompt] of agentPrompts) {
package/dist/index.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts", "../src/paths.ts", "../src/discovery.ts", "../src/env.ts"],
4
- "sourcesContent": ["import path from \"node:path\"\r\nimport type { A2HHostDeps, Plugin, PluginInput, PluginHooks } from \"./types\"\r\nimport { PKG_ROOT } from \"./paths\"\r\nimport { discoverA2HAgents, discoverA2HSkills } from \"./discovery\"\r\nimport { buildA2HEnvBase } from \"./env\"\r\n\r\n// Public re-exports \u2014 the package's external surface (dev eco code loads\r\n// dist/index.js at runtime and calls `createServer`; `A2HHostDeps` /\r\n// `PLUGIN_ID` are kept exported for any TS consumers).\r\nexport type { A2HHostDeps } from \"./types\"\r\nexport { PLUGIN_ID } from \"./paths\"\r\n\r\n/**\r\n * Create the A2H DevEco Code plugin.\r\n *\r\n * This is a self-contained plugin - skills/, agents/, and tools/\r\n * are bundled inside the package, NOT referenced from HomeTrans.\r\n * A2H-Plugin is the independently publishable \"plugin edition\" of HomeTrans.\r\n *\r\n * @param deps - host dependencies injected by DevEco Code's thin bridge.\r\n * The A2H Plugin defines what it needs (A2HHostDeps);\r\n * DevEco Code provides how to resolve it.\r\n * @returns A Plugin function ready to be registered as a built-in plugin.\r\n */\r\nexport function createServer(deps: A2HHostDeps): Plugin {\r\n return async (input: PluginInput): Promise<PluginHooks> => {\r\n // All A2H assets live inside this package (self-contained, no HomeTrans dep)\r\n const skillsRoot = path.join(PKG_ROOT, \"skills\")\r\n const agentsRoot = path.join(PKG_ROOT, \"agents\")\r\n\r\n // Auto-discover agents: scan top-level agents/*.md, parse `name:` from\r\n // frontmatter, strip frontmatter for prompt body. Adding/removing/renaming\r\n // an agent is just a file drop \u2014 no code change, no rebuild.\r\n const agentPrompts = discoverA2HAgents(agentsRoot)\r\n\r\n return {\r\n dispose: async () => {\r\n agentPrompts.clear()\r\n },\r\n\r\n // \u2500\u2500 config hook: register skills search paths, hidden skills, and agents \u2500\u2500\r\n config: async (cfg: Record<string, unknown>) => {\r\n // --- Skills ---\r\n const skills = (cfg.skills as Record<string, unknown>) || {}\r\n cfg.skills = skills\r\n \r\n\r\n // Register the bundled skills/ directory as an additional search path\r\n const paths = (skills.paths as string[]) || []\r\n skills.paths = paths\r\n if (!paths.includes(skillsRoot)) {\r\n paths.push(skillsRoot)\r\n }\r\n\r\n // Manage hidden skill names, discovered from each SKILL.md's\r\n // metadata.visible: false frontmatter. DevEco Code's available()\r\n // filters these out of the user-facing /skills menu; get()/require()\r\n // still resolve them so other skills can invoke them by name.\r\n //\r\n // We fully manage A2H entries in the hidden list: stale entries from\r\n // skills that are renamed, removed, or changed to visible are cleaned\r\n // up, while non-A2H entries (from other plugins) are left untouched.\r\n const { hidden: a2hHidden, all: a2hAll } = discoverA2HSkills(skillsRoot)\r\n const currentHidden = (skills.hidden as string[]) || []\r\n // Remove any A2H-managed skills that are no longer hidden / no longer exist\r\n const cleaned = currentHidden.filter((name) => !a2hAll.includes(name))\r\n // Add currently-hidden A2H skills\r\n for (const name of a2hHidden) {\r\n if (!cleaned.includes(name)) {\r\n cleaned.push(name)\r\n }\r\n }\r\n skills.hidden = cleaned\r\n\r\n // --- Agents ---\r\n const agent = (cfg.agent as Record<string, Record<string, unknown>>) || {}\r\n cfg.agent = agent\r\n\r\n for (const [name, prompt] of agentPrompts) {\r\n // Don't overwrite if the user already configured this agent\r\n if (!agent[name]) {\r\n agent[name] = {\r\n mode: \"subagent\",\r\n hidden: true,\r\n prompt,\r\n }\r\n }\r\n }\r\n\r\n },\r\n\r\n // \u2500\u2500 shell.env hook: inject DevEco Studio paths as env vars \u2500\u2500\r\n // NOTE: deliberately uses the *base* env \u2014 the multimodal model apiKey\r\n // is NOT injected into LLM shell sessions (would leak into every command\r\n // and the transcript).\r\n \"shell.env\": async (\r\n shellInput: { cwd: string; sessionID?: string; callID?: string },\r\n output: { env: Record<string, string> },\r\n ) => {\r\n const a2hEnv = await buildA2HEnvBase(deps)\r\n Object.assign(output.env, a2hEnv)\r\n },\r\n }\r\n }\r\n}\r\n", "import path from \"node:path\"\r\nimport { fileURLToPath } from \"node:url\"\r\n\r\n// Resolve package root (one directory above dist/ or src/). Computed once here\r\n// and imported everywhere else so there's a single source of truth. After\r\n// esbuild bundling, import.meta.url points at dist/index.js, so this still\r\n// resolves to the package root at runtime.\r\nconst __filename = fileURLToPath(import.meta.url)\r\nconst __dirname = path.dirname(__filename)\r\n\r\n/** Absolute path to the A2H-Plugin package root. */\r\nexport const PKG_ROOT = path.resolve(__dirname, \"..\")\r\n\r\n/** Public plugin id. */\r\nexport const PLUGIN_ID = \"android2harmony\"\r\n", "import path from \"node:path\"\r\nimport fs from \"node:fs\"\r\n\r\n/**\r\n * Strip YAML frontmatter (--- ... ---) from agent .md files.\r\n * Agent .md files have metadata in frontmatter; only the body\r\n * should be used as the LLM prompt.\r\n */\r\nexport function stripFrontmatter(content: string): string {\r\n const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/)\r\n return match ? content.slice(match[0].length).trim() : content\r\n}\r\n\r\n/**\r\n * Minimal YAML frontmatter extractor for A2H-owned `.md` files (SKILL.md\r\n * and agent `.md`). A2H controls the frontmatter format, so we only need\r\n * two values: the top-level `name` (a scalar) and whether `metadata.visible`\r\n * is false. This avoids pulling a YAML dependency into the plugin.\r\n *\r\n * Returns `{ name, hidden }` or null when the file has no usable frontmatter\r\n * (e.g. a README dropped into a directory that's also scanned for agents).\r\n */\r\nexport function parseFrontmatter(content: string): { name: string; hidden: boolean } | null {\r\n const match = content.match(/^---\\s*\\r?\\n([\\s\\S]*?)\\r?\\n---/)\r\n if (!match) return null\r\n const lines = match[1].split(/\\r?\\n/)\r\n\r\n let name: string | null = null\r\n let hidden = false\r\n let inMetadata = false\r\n let metadataIndent = -1\r\n\r\n for (const line of lines) {\r\n if (!line.trim() || line.trim().startsWith(\"#\")) continue\r\n\r\n const indent = line.length - line.trimStart().length\r\n const trimmed = line.trim()\r\n const kv = trimmed.match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/)\r\n\r\n if (indent === 0) {\r\n inMetadata = false\r\n if (!kv) continue\r\n const value = kv[2].trim()\r\n if (kv[1] === \"name\") {\r\n name = value.replace(/^[\"']|[\"']$/g, \"\")\r\n } else if (kv[1] === \"metadata\") {\r\n if (value === \"\") {\r\n // Block-style metadata: following indented lines belong to it\r\n inMetadata = true\r\n metadataIndent = -1\r\n } else if (/visible\\s*:\\s*false/.test(value)) {\r\n // Inline/flow-style metadata: metadata: { visible: false }\r\n hidden = true\r\n }\r\n }\r\n } else if (inMetadata) {\r\n if (metadataIndent === -1) metadataIndent = indent\r\n if (indent < metadataIndent) {\r\n inMetadata = false\r\n continue\r\n }\r\n if (kv && kv[1] === \"visible\" && kv[2].trim() === \"false\") {\r\n hidden = true\r\n }\r\n }\r\n }\r\n\r\n return name ? { name, hidden } : null\r\n}\r\n\r\n/**\r\n * Scan the bundled skills/ directory and return A2H skill visibility info.\r\n *\r\n * Returns two lists:\r\n * - `hidden`: names of skills whose SKILL.md frontmatter declares metadata.visible: false\r\n * - `all`: names of ALL A2H skill directories found (used to clean up stale hidden entries)\r\n *\r\n * Visibility lives with each skill definition (best practice): adding a new\r\n * hidden skill only needs a frontmatter edit, with no code change here. The\r\n * returned names are pushed into cfg.skills.hidden, which DevEco Code's\r\n * available() already filters on - while get()/require() still resolve\r\n * them so other skills can invoke them by name.\r\n */\r\nexport function discoverA2HSkills(skillsRoot: string): { hidden: string[]; all: string[] } {\r\n const hidden: string[] = []\r\n const all: string[] = []\r\n let entries: fs.Dirent[]\r\n try {\r\n entries = fs.readdirSync(skillsRoot, { withFileTypes: true })\r\n } catch {\r\n return { hidden, all }\r\n }\r\n for (const entry of entries) {\r\n if (!entry.isDirectory()) continue\r\n const skillFile = path.join(skillsRoot, entry.name, \"SKILL.md\")\r\n let raw: string\r\n try {\r\n raw = fs.readFileSync(skillFile, \"utf-8\")\r\n } catch {\r\n continue\r\n }\r\n const info = parseFrontmatter(raw)\r\n if (info) {\r\n all.push(info.name)\r\n if (info.hidden) hidden.push(info.name)\r\n }\r\n }\r\n return { hidden, all }\r\n}\r\n\r\n/**\r\n * Scan the bundled agents/ directory and return a `name \u2192 prompt body` map.\r\n *\r\n * Only top-level `.md` files are considered (subdirectories like `scripts/`\r\n * are skipped via `isFile()`). Files without a `name:` field in frontmatter\r\n * (e.g. a stray README) are silently skipped \u2014 the frontmatter name is the\r\n * source of truth, not the file basename. The body (frontmatter stripped)\r\n * becomes the LLM prompt.\r\n *\r\n * Adding a new agent is therefore a zero-code, zero-rebuild change: just\r\n * drop an `.md` with `name:` frontmatter into `agents/`.\r\n */\r\nexport function discoverA2HAgents(agentsRoot: string): Map<string, string> {\r\n const result = new Map<string, string>()\r\n let entries: fs.Dirent[]\r\n try {\r\n entries = fs.readdirSync(agentsRoot, { withFileTypes: true })\r\n } catch {\r\n return result\r\n }\r\n for (const entry of entries) {\r\n if (!entry.isFile() || !entry.name.endsWith(\".md\")) continue\r\n const file = path.join(agentsRoot, entry.name)\r\n let raw: string\r\n try {\r\n raw = fs.readFileSync(file, \"utf-8\")\r\n } catch {\r\n continue\r\n }\r\n const info = parseFrontmatter(raw)\r\n if (!info) continue // not a valid agent file (no `name:` frontmatter)\r\n result.set(info.name, stripFrontmatter(raw))\r\n }\r\n return result\r\n}\r\n", "import path from \"node:path\"\r\nimport type { A2HHostDeps } from \"./types\"\r\n\r\n/**\r\n * Build the A2H env var set from host-injected deps. Shared by the `shell.env`\r\n * hook (so every shell session sees these).\r\n * Env vars are the single source of truth in the plugin context.\r\n *\r\n * This base set contains NO secrets \u2014 only DevEco/SDK paths. The multimodal\r\n * model apiKey is intentionally NOT included: the `shell.env` hook uses this\r\n * base set so the key never leaks into every LLM shell command (and thus the\r\n * transcript).\r\n */\r\nexport async function buildA2HEnvBase(\r\n deps: A2HHostDeps,\r\n): Promise<Record<string, string>> {\r\n const env: Record<string, string> = {}\r\n\r\n const home = await deps.resolveDevEcoHome()\r\n if (home) {\r\n const sdk = deps.resolveSdkPath(home)\r\n env[\"DEVECO_HOME\"] = home\r\n env[\"DEVECO_SDK_HOME\"] = sdk\r\n env[\"OHOS_SDK_PATH\"] = path.join(sdk, \"default\", \"openharmony\", \"ets\")\r\n env[\"HMS_SDK_PATH\"] = path.join(sdk, \"default\", \"hms\", \"ets\")\r\n }\r\n\r\n return env\r\n}\r\n"],
5
- "mappings": "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",
4
+ "sourcesContent": ["import path from \"node:path\"\r\nimport type { A2HHostDeps, Plugin, PluginInput, PluginHooks } from \"./types\"\r\nimport { PKG_ROOT } from \"./paths\"\r\nimport { discoverA2HAgents } from \"./discovery\"\r\nimport { buildA2HEnvBase } from \"./env\"\r\n\r\n// Public re-exports \u2014 the package's external surface (dev eco code loads\r\n// dist/index.js at runtime and calls `createServer`; `A2HHostDeps` /\r\n// `PLUGIN_ID` are kept exported for any TS consumers).\r\nexport type { A2HHostDeps } from \"./types\"\r\nexport { PLUGIN_ID } from \"./paths\"\r\n\r\n/**\r\n * Create the A2H DevEco Code plugin.\r\n *\r\n * This is a self-contained plugin - skills/, agents/, and tools/\r\n * are bundled inside the package, NOT referenced from HomeTrans.\r\n * A2H-Plugin is the independently publishable \"plugin edition\" of HomeTrans.\r\n *\r\n * @param deps - host dependencies injected by DevEco Code's thin bridge.\r\n * The A2H Plugin defines what it needs (A2HHostDeps);\r\n * DevEco Code provides how to resolve it.\r\n * @returns A Plugin function ready to be registered as a built-in plugin.\r\n */\r\nexport function createServer(deps: A2HHostDeps): Plugin {\r\n return async (input: PluginInput): Promise<PluginHooks> => {\r\n // All A2H assets live inside this package (self-contained, no HomeTrans dep)\r\n const skillsRoot = path.join(PKG_ROOT, \"skills\")\r\n const agentsRoot = path.join(PKG_ROOT, \"agents\")\r\n\r\n // Auto-discover agents: scan top-level agents/*.md, parse `name:` from\r\n // frontmatter, strip frontmatter for prompt body. Adding/removing/renaming\r\n // an agent is just a file drop \u2014 no code change, no rebuild.\r\n const agentPrompts = discoverA2HAgents(agentsRoot)\r\n\r\n return {\r\n dispose: async () => {\r\n agentPrompts.clear()\r\n },\r\n\r\n // \u2500\u2500 config hook: register skills search paths and agents \u2500\u2500\r\n config: async (cfg: Record<string, unknown>) => {\r\n // --- Skills ---\r\n const skills = (cfg.skills as Record<string, unknown>) || {}\r\n cfg.skills = skills\r\n\r\n // Register the bundled skills/ directory as an additional search path.\r\n // All bundled skills are user-visible \u2014 there is no partial-visibility\r\n // (hidden) layer anymore, so we only register the search path here.\r\n const paths = (skills.paths as string[]) || []\r\n skills.paths = paths\r\n if (!paths.includes(skillsRoot)) {\r\n paths.push(skillsRoot)\r\n }\r\n\r\n // --- Agents ---\r\n const agent = (cfg.agent as Record<string, Record<string, unknown>>) || {}\r\n cfg.agent = agent\r\n\r\n for (const [name, prompt] of agentPrompts) {\r\n // Don't overwrite if the user already configured this agent\r\n if (!agent[name]) {\r\n agent[name] = {\r\n mode: \"subagent\",\r\n hidden: true,\r\n prompt,\r\n }\r\n }\r\n }\r\n\r\n },\r\n\r\n // \u2500\u2500 shell.env hook: inject DevEco Studio paths as env vars \u2500\u2500\r\n // NOTE: deliberately uses the *base* env \u2014 the multimodal model apiKey\r\n // is NOT injected into LLM shell sessions (would leak into every command\r\n // and the transcript).\r\n \"shell.env\": async (\r\n shellInput: { cwd: string; sessionID?: string; callID?: string },\r\n output: { env: Record<string, string> },\r\n ) => {\r\n const a2hEnv = await buildA2HEnvBase(deps)\r\n Object.assign(output.env, a2hEnv)\r\n },\r\n }\r\n }\r\n}\r\n", "import path from \"node:path\"\r\nimport { fileURLToPath } from \"node:url\"\r\n\r\n// Resolve package root (one directory above dist/ or src/). Computed once here\r\n// and imported everywhere else so there's a single source of truth. After\r\n// esbuild bundling, import.meta.url points at dist/index.js, so this still\r\n// resolves to the package root at runtime.\r\nconst __filename = fileURLToPath(import.meta.url)\r\nconst __dirname = path.dirname(__filename)\r\n\r\n/** Absolute path to the A2H-Plugin package root. */\r\nexport const PKG_ROOT = path.resolve(__dirname, \"..\")\r\n\r\n/** Public plugin id. */\r\nexport const PLUGIN_ID = \"android2harmony\"\r\n", "import path from \"node:path\"\r\nimport fs from \"node:fs\"\r\n\r\n/**\r\n * Strip YAML frontmatter (--- ... ---) from agent .md files.\r\n * Agent .md files have metadata in frontmatter; only the body\r\n * should be used as the LLM prompt.\r\n */\r\nexport function stripFrontmatter(content: string): string {\r\n const match = content.match(/^---\\s*\\n([\\s\\S]*?)\\n---\\s*\\n/)\r\n return match ? content.slice(match[0].length).trim() : content\r\n}\r\n\r\n/**\r\n * Minimal YAML frontmatter extractor for A2H-owned `.md` files (SKILL.md\r\n * and agent `.md`). A2H controls the frontmatter format, so we only need\r\n * the top-level `name` (a scalar). This avoids pulling a YAML dependency\r\n * into the plugin.\r\n *\r\n * Returns the parsed `name`, or null when the file has no usable frontmatter\r\n * (e.g. a README dropped into a directory that's also scanned for agents).\r\n */\r\nexport function parseFrontmatter(content: string): string | null {\r\n const match = content.match(/^---\\s*\\r?\\n([\\s\\S]*?)\\r?\\n---/)\r\n if (!match) return null\r\n const lines = match[1].split(/\\r?\\n/)\r\n\r\n for (const line of lines) {\r\n if (!line.trim() || line.trim().startsWith(\"#\")) continue\r\n if (line.length - line.trimStart().length !== 0) continue // skip indented (e.g. metadata block) lines\r\n\r\n const kv = line.trim().match(/^([a-zA-Z_][a-zA-Z0-9_-]*):\\s*(.*)$/)\r\n if (!kv) continue\r\n if (kv[1] === \"name\") {\r\n return kv[2].trim().replace(/^[\"']|[\"']$/g, \"\")\r\n }\r\n }\r\n\r\n return null\r\n}\r\n\r\n/**\r\n * Scan the bundled agents/ directory and return a `name \u2192 prompt body` map.\r\n *\r\n * Only top-level `.md` files are considered (subdirectories like `scripts/`\r\n * are skipped via `isFile()`). Files without a `name:` field in frontmatter\r\n * (e.g. a stray README) are silently skipped \u2014 the frontmatter name is the\r\n * source of truth, not the file basename. The body (frontmatter stripped)\r\n * becomes the LLM prompt.\r\n *\r\n * Adding a new agent is therefore a zero-code, zero-rebuild change: just\r\n * drop an `.md` with `name:` frontmatter into `agents/`.\r\n */\r\nexport function discoverA2HAgents(agentsRoot: string): Map<string, string> {\r\n const result = new Map<string, string>()\r\n let entries: fs.Dirent[]\r\n try {\r\n entries = fs.readdirSync(agentsRoot, { withFileTypes: true })\r\n } catch {\r\n return result\r\n }\r\n for (const entry of entries) {\r\n if (!entry.isFile() || !entry.name.endsWith(\".md\")) continue\r\n const file = path.join(agentsRoot, entry.name)\r\n let raw: string\r\n try {\r\n raw = fs.readFileSync(file, \"utf-8\")\r\n } catch {\r\n continue\r\n }\r\n const name = parseFrontmatter(raw)\r\n if (!name) continue // not a valid agent file (no `name:` frontmatter)\r\n result.set(name, stripFrontmatter(raw))\r\n }\r\n return result\r\n}\r\n", "import path from \"node:path\"\r\nimport type { A2HHostDeps } from \"./types\"\r\n\r\n/**\r\n * Build the A2H env var set from host-injected deps. Shared by the `shell.env`\r\n * hook (so every shell session sees these).\r\n * Env vars are the single source of truth in the plugin context.\r\n *\r\n * This base set contains NO secrets \u2014 only DevEco/SDK paths. The multimodal\r\n * model apiKey is intentionally NOT included: the `shell.env` hook uses this\r\n * base set so the key never leaks into every LLM shell command (and thus the\r\n * transcript).\r\n */\r\nexport async function buildA2HEnvBase(\r\n deps: A2HHostDeps,\r\n): Promise<Record<string, string>> {\r\n const env: Record<string, string> = {}\r\n\r\n const home = await deps.resolveDevEcoHome()\r\n if (home) {\r\n const sdk = deps.resolveSdkPath(home)\r\n env[\"DEVECO_HOME\"] = home\r\n env[\"DEVECO_SDK_HOME\"] = sdk\r\n env[\"OHOS_SDK_PATH\"] = path.join(sdk, \"default\", \"openharmony\", \"ets\")\r\n env[\"HMS_SDK_PATH\"] = path.join(sdk, \"default\", \"hms\", \"ets\")\r\n }\r\n\r\n return env\r\n}\r\n"],
5
+ "mappings": "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",
6
6
  "names": ["path", "path", "path", "path"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "android2harmony",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "A2H (Android-to-HarmonyOS) Plugin for DevEco Code — standalone plugin containing A2H skills, agents, and tools for converting Android apps to HarmonyOS",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -1,5 +1,5 @@
1
1
  ---
2
- name: spec-generator-skill
2
+ name: hmos-spec-generate
3
3
  description: "Generate requirement specification (spec) documents for Android-to-HarmonyOS migration. For each .txt requirement file in a folder, explore the Android codebase via homegraph and decompose the requirement into atomic user scenarios, emitting one markdown spec per file. Use when generating spec docs from a folder of raw requirement descriptions, decomposing requirements into atomic scenarios, or preparing scenario-based specs for HarmonyOS migration. Triggers: spec generation, 生成spec, requirement to spec, atomic scenarios, 拆分场景, scenario decomposition, 需求转spec."
4
4
  license: MIT
5
5
  allowed-tools: