@hmharness/domain-harmony 0.1.0
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/dist/apikg.d.ts +34 -0
- package/dist/apikg.js +185 -0
- package/dist/apimatrix.d.ts +43 -0
- package/dist/apimatrix.js +63 -0
- package/dist/builddoctor.d.ts +24 -0
- package/dist/builddoctor.js +99 -0
- package/dist/cangjie.d.ts +4 -0
- package/dist/cangjie.js +129 -0
- package/dist/emulator.d.ts +11 -0
- package/dist/emulator.js +368 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +439 -0
- package/dist/lint.d.ts +2 -0
- package/dist/lint.js +65 -0
- package/dist/ondevice.d.ts +23 -0
- package/dist/ondevice.js +174 -0
- package/dist/profile.d.ts +22 -0
- package/dist/profile.js +159 -0
- package/dist/project.d.ts +22 -0
- package/dist/project.js +414 -0
- package/dist/schema.d.ts +22 -0
- package/dist/schema.js +215 -0
- package/dist/signing.d.ts +26 -0
- package/dist/signing.js +209 -0
- package/dist/uiregress.d.ts +27 -0
- package/dist/uiregress.js +147 -0
- package/package.json +31 -0
package/dist/apikg.d.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Tool } from '@hmharness/kernel';
|
|
2
|
+
export interface ApiSymbolEntry {
|
|
3
|
+
module: string;
|
|
4
|
+
kind: string;
|
|
5
|
+
kit: string;
|
|
6
|
+
file: string;
|
|
7
|
+
line: number;
|
|
8
|
+
snippet: string;
|
|
9
|
+
}
|
|
10
|
+
export interface ApiIndex {
|
|
11
|
+
builtAt: string;
|
|
12
|
+
sdkPath: string;
|
|
13
|
+
sdkMtime: number;
|
|
14
|
+
symbolCount: number;
|
|
15
|
+
symbols: Record<string, ApiSymbolEntry[]>;
|
|
16
|
+
}
|
|
17
|
+
export declare function sdkApiDir(devecoHome: string): string;
|
|
18
|
+
/** Parse one d.ts into symbols. Deliberately line-based and tolerant -
|
|
19
|
+
* d.ts is regular enough that declaration headers live on single lines. */
|
|
20
|
+
export declare function parseDeclaration(file: string, relModule: string, text: string): Array<[string, ApiSymbolEntry]>;
|
|
21
|
+
export declare function buildApiIndex(devecoHome: string, cacheDir: string): Promise<ApiIndex>;
|
|
22
|
+
/** Load the index (building it on first use / SDK change). */
|
|
23
|
+
export declare function loadApiIndex(devecoHome: string, home: string): Promise<ApiIndex | null>;
|
|
24
|
+
export interface LookupResult {
|
|
25
|
+
query: string;
|
|
26
|
+
exact: Array<ApiSymbolEntry>;
|
|
27
|
+
fuzzy: Array<{
|
|
28
|
+
name: string;
|
|
29
|
+
entry: ApiSymbolEntry;
|
|
30
|
+
}>;
|
|
31
|
+
totalSymbols: number;
|
|
32
|
+
}
|
|
33
|
+
export declare function lookupSymbol(index: ApiIndex, query: string, limit?: number): LookupResult;
|
|
34
|
+
export declare const harmonyApiLookup: Tool;
|
package/dist/apikg.js
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/domain-harmony - apikg (API knowledge graph, the codelin api_kg gap)
|
|
3
|
+
* The agent guesses HarmonyOS APIs because it cannot SEE the SDK. The
|
|
4
|
+
* declarations are right there on disk (ets/api/*.d.ts, 927 files) - this
|
|
5
|
+
* module indexes them ONCE into HMH_HOME/apikg and answers lookups with
|
|
6
|
+
* EVIDENCE: the declaration snippet + file:line + kit membership, so a
|
|
7
|
+
* wrong API name is caught by "not in the SDK" instead of hallucinated.
|
|
8
|
+
*
|
|
9
|
+
* Index shape (one JSON per run, rebuilt when the SDK mtime changes):
|
|
10
|
+
* symbols: { name -> [{ module, kind, kit, file, line, snippet }] }
|
|
11
|
+
* kinds: namespace(declare module), interface, class, enum, type, function,
|
|
12
|
+
* const, method, property. Methods index under "Class.method".
|
|
13
|
+
*/
|
|
14
|
+
import { mkdir, readdir, readFile, stat, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { join } from 'node:path';
|
|
16
|
+
export function sdkApiDir(devecoHome) {
|
|
17
|
+
return join(devecoHome, 'sdk', 'default', 'openharmony', 'ets', 'api');
|
|
18
|
+
}
|
|
19
|
+
/* ---------------- parsing ---------------- */
|
|
20
|
+
function extractKit(text) {
|
|
21
|
+
const m = text.match(/@kit\s+([A-Za-z0-9]+)/);
|
|
22
|
+
return m ? m[1] : '';
|
|
23
|
+
}
|
|
24
|
+
/** Parse one d.ts into symbols. Deliberately line-based and tolerant -
|
|
25
|
+
* d.ts is regular enough that declaration headers live on single lines. */
|
|
26
|
+
export function parseDeclaration(file, relModule, text) {
|
|
27
|
+
const kit = extractKit(text);
|
|
28
|
+
const out = [];
|
|
29
|
+
const lines = text.split('\n');
|
|
30
|
+
const declRe = /^(?:export\s+)?(?:declare\s+)?(?:default\s+)?(?:abstract\s+)?(interface|class|enum|type|const|namespace|module)\s+([A-Za-z_$][\w$]*)?/;
|
|
31
|
+
let currentName = '';
|
|
32
|
+
let currentKind = '';
|
|
33
|
+
let currentStart = -1;
|
|
34
|
+
const push = (end) => {
|
|
35
|
+
if (!currentName || currentStart < 0)
|
|
36
|
+
return;
|
|
37
|
+
const snippet = lines.slice(currentStart, Math.min(end, currentStart + 12)).join('\n').slice(0, 320);
|
|
38
|
+
out.push([currentName, { module: relModule, kind: currentKind, kit, file, line: currentStart + 1, snippet }]);
|
|
39
|
+
};
|
|
40
|
+
for (let i = 0; i < lines.length; i++) {
|
|
41
|
+
const m = declRe.exec(lines[i]);
|
|
42
|
+
if (m) {
|
|
43
|
+
push(i); // close previous
|
|
44
|
+
currentKind = m[1];
|
|
45
|
+
currentName = m[2] ?? '';
|
|
46
|
+
currentStart = i;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
// methods/props inside a class/interface/namespace: foo(...): X; / foo: X;
|
|
50
|
+
// and namespace-level "function foo(...)" - both indented members
|
|
51
|
+
if (currentName && currentStart >= 0 && i > currentStart) {
|
|
52
|
+
const mm = /^\s{2,}(?:static\s+|readonly\s+|function\s+|const\s+|get\s+|set\s+)?([A-Za-z_$][\w$]*)\s*\??\s*[(:=]/.exec(lines[i]);
|
|
53
|
+
if (mm && !/^(constructor|declare|export)$/.test(mm[1]) && !/^\/\//.test(lines[i])) {
|
|
54
|
+
const member = `${currentName}.${mm[1]}`;
|
|
55
|
+
const isFn = /^\s{2,}function\s/.test(lines[i]) || lines[i].includes('(');
|
|
56
|
+
const snippet = lines[i].trim().slice(0, 200);
|
|
57
|
+
out.push([member, { module: relModule, kind: isFn ? 'function' : 'member', kit, file, line: i + 1, snippet }]);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
// top-level (unindented) exported functions inside a namespace body get
|
|
61
|
+
// caught by the member branch above via their 4-space indent in d.ts
|
|
62
|
+
}
|
|
63
|
+
push(lines.length);
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
/* ---------------- index build ---------------- */
|
|
67
|
+
export async function buildApiIndex(devecoHome, cacheDir) {
|
|
68
|
+
const apiDir = sdkApiDir(devecoHome);
|
|
69
|
+
// SDK identity = newest mtime among the api dir's entries
|
|
70
|
+
let sdkMtime = 0;
|
|
71
|
+
try {
|
|
72
|
+
for (const e of await readdir(apiDir)) {
|
|
73
|
+
const m = (await stat(join(apiDir, e))).mtimeMs;
|
|
74
|
+
if (m > sdkMtime)
|
|
75
|
+
sdkMtime = m;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* no SDK */ }
|
|
79
|
+
const indexFile = join(cacheDir, 'apikg.json');
|
|
80
|
+
try {
|
|
81
|
+
const prev = JSON.parse(await readFile(indexFile, 'utf8'));
|
|
82
|
+
if (prev.sdkMtime === sdkMtime && prev.symbolCount > 0)
|
|
83
|
+
return prev; // fresh
|
|
84
|
+
}
|
|
85
|
+
catch { /* rebuild */ }
|
|
86
|
+
const symbols = {};
|
|
87
|
+
let files = 0;
|
|
88
|
+
try {
|
|
89
|
+
files = (await readdir(apiDir)).filter((f) => f.endsWith('.d.ts') || f.endsWith('.d.ets')).length;
|
|
90
|
+
}
|
|
91
|
+
catch { /* none */ }
|
|
92
|
+
for (const f of (await readdir(apiDir).catch(() => []))) {
|
|
93
|
+
if (!/\.d\.(ts|ets)$/.test(f))
|
|
94
|
+
continue;
|
|
95
|
+
const text = await readFile(join(apiDir, f), 'utf8');
|
|
96
|
+
const relModule = f.replace(/\.d\.(ts|ets)$/, '');
|
|
97
|
+
for (const [name, entry] of parseDeclaration(f, relModule, text)) {
|
|
98
|
+
(symbols[name] ??= []).push(entry);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
const index = {
|
|
102
|
+
builtAt: new Date().toISOString(),
|
|
103
|
+
sdkPath: apiDir,
|
|
104
|
+
sdkMtime,
|
|
105
|
+
symbolCount: Object.keys(symbols).length,
|
|
106
|
+
symbols,
|
|
107
|
+
};
|
|
108
|
+
await mkdir(cacheDir, { recursive: true });
|
|
109
|
+
await writeFile(indexFile, JSON.stringify(index), 'utf8');
|
|
110
|
+
return index;
|
|
111
|
+
}
|
|
112
|
+
/** Load the index (building it on first use / SDK change). */
|
|
113
|
+
export async function loadApiIndex(devecoHome, home) {
|
|
114
|
+
try {
|
|
115
|
+
return await buildApiIndex(devecoHome, join(home, 'apikg'));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
export function lookupSymbol(index, query, limit = 4) {
|
|
122
|
+
const q = query.trim();
|
|
123
|
+
const exact = (index.symbols[q] ?? []).slice(0, limit);
|
|
124
|
+
const fuzzy = [];
|
|
125
|
+
const lower = q.toLowerCase();
|
|
126
|
+
for (const [name, entries] of Object.entries(index.symbols)) {
|
|
127
|
+
if (name.toLowerCase().includes(lower) && name !== q) {
|
|
128
|
+
for (const entry of entries.slice(0, 1))
|
|
129
|
+
fuzzy.push({ name, entry });
|
|
130
|
+
if (fuzzy.length >= limit)
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return { query: q, exact, fuzzy, totalSymbols: index.symbolCount };
|
|
135
|
+
}
|
|
136
|
+
/* ---------------- tool ---------------- */
|
|
137
|
+
export const harmonyApiLookup = {
|
|
138
|
+
name: 'harmony_api_lookup',
|
|
139
|
+
description: 'Look up a HarmonyOS/ArkTS API symbol in the ACTUAL local SDK declarations (927 d.ts files indexed) - returns the declaration snippet with file:line evidence and kit membership. Use BEFORE writing code that calls any @ohos API: confirms the symbol exists, its exact signature context, and which kit it belongs to. A miss means "not in this SDK" - do not guess, reconsider the name or check the docs.',
|
|
140
|
+
parameters: {
|
|
141
|
+
type: 'object',
|
|
142
|
+
properties: {
|
|
143
|
+
symbol: { type: 'string', description: 'symbol name, e.g. "UIAbility", "hilog.info" (hierarchical too), "Want"' },
|
|
144
|
+
},
|
|
145
|
+
required: ['symbol'],
|
|
146
|
+
},
|
|
147
|
+
needsApproval: () => false, // read-only; first call builds the local index
|
|
148
|
+
async execute(args) {
|
|
149
|
+
const { homedir } = await import('node:os');
|
|
150
|
+
const deveco = process.env.HM_DEVECO_HOME ?? 'C:\\DevEco-Studio';
|
|
151
|
+
const home = process.env.HMH_HOME ?? join(homedir(), '.hmharness');
|
|
152
|
+
const index = await loadApiIndex(deveco, home);
|
|
153
|
+
if (!index || index.symbolCount === 0) {
|
|
154
|
+
return { output: `SDK declarations not found at ${sdkApiDir(deveco)} - set HM_DEVECO_HOME.`, isError: true };
|
|
155
|
+
}
|
|
156
|
+
const raw = String(args.symbol ?? '').trim();
|
|
157
|
+
if (!raw)
|
|
158
|
+
return { output: 'symbol required', isError: true };
|
|
159
|
+
// support "hilog.info" - namespace.method form: try full, then the tail
|
|
160
|
+
const tries = [raw];
|
|
161
|
+
if (raw.includes('.'))
|
|
162
|
+
tries.push(raw.split('.').pop());
|
|
163
|
+
let best = null;
|
|
164
|
+
for (const t of tries) {
|
|
165
|
+
const r = lookupSymbol(index, t);
|
|
166
|
+
if (r.exact.length > 0) {
|
|
167
|
+
best = r;
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
if (!best)
|
|
171
|
+
best = r;
|
|
172
|
+
}
|
|
173
|
+
const r = best;
|
|
174
|
+
if (r.exact.length === 0 && r.fuzzy.length === 0) {
|
|
175
|
+
return { output: `"${raw}" not found in the local SDK index (${r.totalSymbols} symbols). The symbol does not exist in THIS SDK version - do not guess; check the docs or reconsider the name.`, isError: true };
|
|
176
|
+
}
|
|
177
|
+
const fmt = (e, name) => ` ${name ? name + ' ' : ''}[${e.kind}${e.kit ? ' · ' + e.kit : ''}] ${e.module} :: ${e.file}:${e.line}\n${e.snippet}`;
|
|
178
|
+
const lines = [
|
|
179
|
+
`query: ${raw} (index: ${r.totalSymbols} symbols, built ${index.builtAt.slice(0, 10)})`,
|
|
180
|
+
...r.exact.map((e) => fmt(e)),
|
|
181
|
+
...(r.fuzzy.length ? ['', 'similar:', ...r.fuzzy.map((f) => fmt(f.entry, f.name))] : []),
|
|
182
|
+
];
|
|
183
|
+
return { output: lines.join('\n') };
|
|
184
|
+
},
|
|
185
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/domain-harmony - apimatrix (API-level capability matrix)
|
|
3
|
+
* SDK version strings come in TWO shapes and toolchain behavior differs
|
|
4
|
+
* across them:
|
|
5
|
+
* legacy: "6.1.1(24)" - <version>(<api-level>) up to API 25
|
|
6
|
+
* semver: "26.0.0" and later - pure SemVer; the API level IS the
|
|
7
|
+
* major (26.0.0 == API 26)
|
|
8
|
+
*
|
|
9
|
+
* Everything that branches on "which SDK / API level is this" (scaffold
|
|
10
|
+
* defaults, capability guards, compat warnings) goes through this module
|
|
11
|
+
* so the 26.0.0 switch happens in exactly one place.
|
|
12
|
+
*/
|
|
13
|
+
export interface SdkVersion {
|
|
14
|
+
raw: string;
|
|
15
|
+
apiLevel: number;
|
|
16
|
+
shape: 'legacy' | 'semver';
|
|
17
|
+
/** comparable numeric tuple for range math */
|
|
18
|
+
tuple: [number, number, number];
|
|
19
|
+
}
|
|
20
|
+
/** Parse "6.1.1(24)" / "5.0.5(17)" / "26.0.0" / "26.1.2". Throws on junk. */
|
|
21
|
+
export declare function parseSdkVersion(raw: string): SdkVersion;
|
|
22
|
+
/** Compare two SDK versions (tuple math; throws on unparseable input). */
|
|
23
|
+
export declare function compareSdk(a: string, b: string): number;
|
|
24
|
+
export interface CapabilityRule {
|
|
25
|
+
id: string;
|
|
26
|
+
/** minimum SDK version (inclusive) */
|
|
27
|
+
since: string;
|
|
28
|
+
/** maximum SDK version (exclusive); omit = open-ended */
|
|
29
|
+
until?: string;
|
|
30
|
+
note: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The capability matrix. Extend entries as toolchain knowledge lands; the
|
|
34
|
+
* radar/knowledge pipeline can propose additions through the normal skill
|
|
35
|
+
* gate. Keep notes one actionable line each.
|
|
36
|
+
*/
|
|
37
|
+
export declare const CAPABILITY_MATRIX: CapabilityRule[];
|
|
38
|
+
/** Which matrix rules apply to a given SDK version (sorted by since). */
|
|
39
|
+
export declare function capabilitiesFor(sdk: string): Array<CapabilityRule & {
|
|
40
|
+
available: boolean;
|
|
41
|
+
}>;
|
|
42
|
+
/** Format a version for build-profile compatibleSdkVersion: as-parsed. */
|
|
43
|
+
export declare function formatSdkVersion(v: SdkVersion): string;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/domain-harmony - apimatrix (API-level capability matrix)
|
|
3
|
+
* SDK version strings come in TWO shapes and toolchain behavior differs
|
|
4
|
+
* across them:
|
|
5
|
+
* legacy: "6.1.1(24)" - <version>(<api-level>) up to API 25
|
|
6
|
+
* semver: "26.0.0" and later - pure SemVer; the API level IS the
|
|
7
|
+
* major (26.0.0 == API 26)
|
|
8
|
+
*
|
|
9
|
+
* Everything that branches on "which SDK / API level is this" (scaffold
|
|
10
|
+
* defaults, capability guards, compat warnings) goes through this module
|
|
11
|
+
* so the 26.0.0 switch happens in exactly one place.
|
|
12
|
+
*/
|
|
13
|
+
/** Parse "6.1.1(24)" / "5.0.5(17)" / "26.0.0" / "26.1.2". Throws on junk. */
|
|
14
|
+
export function parseSdkVersion(raw) {
|
|
15
|
+
const s = raw.trim();
|
|
16
|
+
const legacy = /^(\d+)\.(\d+)\.(\d+)\((\d+)\)$/.exec(s);
|
|
17
|
+
if (legacy) {
|
|
18
|
+
const [, a, b, c, lvl] = legacy;
|
|
19
|
+
return { raw: s, apiLevel: Number(lvl), shape: 'legacy', tuple: [Number(a), Number(b), Number(c)] };
|
|
20
|
+
}
|
|
21
|
+
const semver = /^(\d+)\.(\d+)\.(\d+)$/.exec(s);
|
|
22
|
+
if (semver) {
|
|
23
|
+
const [, a, b, c] = semver;
|
|
24
|
+
const tuple = [Number(a), Number(b), Number(c)];
|
|
25
|
+
// from 26 on, the major IS the API level (HarmonyOS NEXT renumbering)
|
|
26
|
+
return { raw: s, apiLevel: tuple[0], shape: tuple[0] >= 26 ? 'semver' : 'legacy', tuple };
|
|
27
|
+
}
|
|
28
|
+
throw new Error(`unrecognized SDK version "${raw}" - expected "6.1.1(24)" or "26.0.0" style`);
|
|
29
|
+
}
|
|
30
|
+
/** Compare two SDK versions (tuple math; throws on unparseable input). */
|
|
31
|
+
export function compareSdk(a, b) {
|
|
32
|
+
const ta = parseSdkVersion(a).tuple;
|
|
33
|
+
const tb = parseSdkVersion(b).tuple;
|
|
34
|
+
for (let i = 0; i < 3; i++) {
|
|
35
|
+
if (ta[i] !== tb[i])
|
|
36
|
+
return ta[i] < tb[i] ? -1 : 1;
|
|
37
|
+
}
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* The capability matrix. Extend entries as toolchain knowledge lands; the
|
|
42
|
+
* radar/knowledge pipeline can propose additions through the normal skill
|
|
43
|
+
* gate. Keep notes one actionable line each.
|
|
44
|
+
*/
|
|
45
|
+
export const CAPABILITY_MATRIX = [
|
|
46
|
+
{ id: 'stage-model', since: '5.0.0(12)', note: 'stage model is the only supported model' },
|
|
47
|
+
{ id: 'har-module', since: '5.0.0(12)', note: 'har shared libraries' },
|
|
48
|
+
{ id: 'shared-module', since: '5.0.5(17)', note: 'shared (static) module type' },
|
|
49
|
+
{ id: '2in1-devicetype', since: '5.0.5(17)', note: '"2in1" deviceType token' },
|
|
50
|
+
{ id: 'semver-numbering', since: '26.0.0', note: 'version strings drop the (api) suffix; major == API level' },
|
|
51
|
+
];
|
|
52
|
+
/** Which matrix rules apply to a given SDK version (sorted by since). */
|
|
53
|
+
export function capabilitiesFor(sdk) {
|
|
54
|
+
const out = CAPABILITY_MATRIX.map((r) => ({
|
|
55
|
+
...r,
|
|
56
|
+
available: compareSdk(sdk, r.since) >= 0 && (r.until ? compareSdk(sdk, r.until) < 0 : true),
|
|
57
|
+
}));
|
|
58
|
+
return out.sort((a, b) => (a.available === b.available ? compareSdk(a.since, b.since) : a.available ? -1 : 1));
|
|
59
|
+
}
|
|
60
|
+
/** Format a version for build-profile compatibleSdkVersion: as-parsed. */
|
|
61
|
+
export function formatSdkVersion(v) {
|
|
62
|
+
return v.raw;
|
|
63
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/domain-harmony - builddoctor (compile-fix loop, the codelin icf gap)
|
|
3
|
+
* When hvigor fails, its log is a wall of stack traces. This tool parses
|
|
4
|
+
* the failure into ONE OF A SMALL SET OF KNOWN CAUSE CLASSES and returns
|
|
5
|
+
* the concrete fix for that class - so the agent (or user) repairs the
|
|
6
|
+
* actual cause instead of googling the tail of a stack trace.
|
|
7
|
+
*
|
|
8
|
+
* The classifier is deliberately regex-on-tail: hvigor's stable failure
|
|
9
|
+
* signatures are few (env/sdk/signing/deps/arkts/config), each with a
|
|
10
|
+
* known remedy. Unknown signatures pass through with the raw tail so
|
|
11
|
+
* nothing is hidden.
|
|
12
|
+
*/
|
|
13
|
+
import type { Tool } from '@hmharness/kernel';
|
|
14
|
+
export interface DiagnosedError {
|
|
15
|
+
kind: string;
|
|
16
|
+
fix: string;
|
|
17
|
+
evidence: string;
|
|
18
|
+
}
|
|
19
|
+
/** Diagnose a hvigor/build failure output; unknown returns null (caller
|
|
20
|
+
* shows the raw tail - never hide unclassified failures). */
|
|
21
|
+
export declare function diagnoseBuildFailure(log: string): DiagnosedError | null;
|
|
22
|
+
/** Extract the first ERROR block (file/line/position) from a build log. */
|
|
23
|
+
export declare function firstErrorBlock(log: string): string;
|
|
24
|
+
export declare const harmonyBuildDoctor: Tool;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/** Cause classes: signature -> fix. Order matters (first hit wins). */
|
|
2
|
+
const SIGNATURES = [
|
|
3
|
+
{
|
|
4
|
+
re: /Invalid value of DEVECO_SDK_HOME|sdk home|not find sdk|SdkHomePath/i,
|
|
5
|
+
kind: 'sdk-home',
|
|
6
|
+
fix: 'DEVECO_SDK_HOME is unset or wrong. Set HM_DEVECO_HOME (default C:\\DevEco-Studio) or export DEVECO_SDK_HOME=<DevEco>/sdk; our build wrapper already tries - set HM_DEVECO_HOME and retry.',
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
re: /signingConfigs|signing config|keystore|\.p12|\.cer|no signing/i,
|
|
10
|
+
kind: 'signing',
|
|
11
|
+
fix: 'Signing config missing/invalid. For a debug run on emulator this is usually a stale auto-signature: in DevEco re-enable auto-sign (File > Project Structure > Signing), or clear signingConfigs and build unsigned for install checks. Production signing needs real certs from AppGallery.',
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
re: /ohpm (install|ERROR)|ohos_modules|Failed to install dependencies|ERESOLVE|404 .*ohpm/i,
|
|
15
|
+
kind: 'ohpm-deps',
|
|
16
|
+
fix: 'ohpm dependency resolution failed. Run: ohpm install --all (in the project root) and check oh-package.json5 versions actually exist on the registry; .har file: deps must exist or be pre-built.',
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
re: /hvigor (daemon|wrapper)|node: not found|NODE_HOME|Cannot find module .*hvigor/i,
|
|
20
|
+
kind: 'hvigor-env',
|
|
21
|
+
fix: 'hvigor/node environment broken. Set HM_DEVECO_HOME so the bundled tools/node is found; delete the project .hvigor cache dir and retry (a corrupted daemon cache is common).',
|
|
22
|
+
},
|
|
23
|
+
{
|
|
24
|
+
re: /ERROR: ArkTS|arkts-\d+|Cannot find module '@|Struct.*must|expected component/i,
|
|
25
|
+
kind: 'arkts-source',
|
|
26
|
+
fix: 'ArkTS compile error (source). Open the file+line named in the log above this diagnosis; the fix is a code edit (missing import, wrong type, struct syntax) - not an environment issue. Read the first "ERROR" block for the exact location.',
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
re: /build-profile|module.json5|parse.*json|Expected.*json|module.*not found in/i,
|
|
30
|
+
kind: 'config',
|
|
31
|
+
fix: 'Project config problem. Run harmony_schema_check first - it names the exact broken field in module.json5/build-profile.json5 in milliseconds instead of this stack trace.',
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
re: /network|ECONN|timeout|registry|fetch failed/i,
|
|
35
|
+
kind: 'network',
|
|
36
|
+
fix: 'Network failure fetching deps/registry. Check proxy (or disable proxy for 127.0.0.1 registries) and retry; ohpm may need a mirror (ohpm config set registry https://ohpm.openharmony.cn/ohpm/).',
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
/** Diagnose a hvigor/build failure output; unknown returns null (caller
|
|
40
|
+
* shows the raw tail - never hide unclassified failures). */
|
|
41
|
+
export function diagnoseBuildFailure(log) {
|
|
42
|
+
// the interesting part is the LAST error block, not the whole log
|
|
43
|
+
const tail = log.slice(-8000);
|
|
44
|
+
for (const s of SIGNATURES) {
|
|
45
|
+
const m = tail.match(s.re);
|
|
46
|
+
if (m) {
|
|
47
|
+
const evidence = tail.split('\n').find((l) => s.re.test(l))?.trim().slice(0, 200) ?? m[0];
|
|
48
|
+
return { kind: s.kind, fix: s.fix, evidence };
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
/** Extract the first ERROR block (file/line/position) from a build log. */
|
|
54
|
+
export function firstErrorBlock(log) {
|
|
55
|
+
const i = log.search(/^.*\bERROR\b/m);
|
|
56
|
+
if (i < 0)
|
|
57
|
+
return '';
|
|
58
|
+
return log.slice(i, i + 700).split('\n').slice(0, 8).join('\n');
|
|
59
|
+
}
|
|
60
|
+
export const harmonyBuildDoctor = {
|
|
61
|
+
name: 'harmony_build_doctor',
|
|
62
|
+
description: 'Diagnose a failed HarmonyOS build log: classifies the failure into a known cause class (sdk-home / signing / ohpm-deps / hvigor-env / arkts-source / config / network) and returns the concrete fix for that class plus the first ERROR block. Pass the build output text; also runs harmony_build yourself and diagnoses when given a project path with run=true.',
|
|
63
|
+
parameters: {
|
|
64
|
+
type: 'object',
|
|
65
|
+
properties: {
|
|
66
|
+
log: { type: 'string', description: 'the failed build output to diagnose (from a previous harmony_build)' },
|
|
67
|
+
project: { type: 'string', description: 'alternative: run harmony_build on this path now and diagnose its output' },
|
|
68
|
+
},
|
|
69
|
+
required: [],
|
|
70
|
+
},
|
|
71
|
+
needsApproval: () => false,
|
|
72
|
+
async execute(args, ctx) {
|
|
73
|
+
let log = typeof args.log === 'string' ? args.log : '';
|
|
74
|
+
if (!log && typeof args.project === 'string') {
|
|
75
|
+
const { harmonyBuild } = await import("./index.js");
|
|
76
|
+
const r = await harmonyBuild.execute({ project: args.project }, ctx);
|
|
77
|
+
log = r.output;
|
|
78
|
+
if (!r.isError)
|
|
79
|
+
return { output: `build succeeded - nothing to diagnose.\n${log.split('\n').slice(0, 3).join('\n')}` };
|
|
80
|
+
}
|
|
81
|
+
if (!log.trim())
|
|
82
|
+
return { output: 'pass the failed build output in `log`, or a project path to run a fresh build.', isError: true };
|
|
83
|
+
const d = diagnoseBuildFailure(log);
|
|
84
|
+
const first = firstErrorBlock(log);
|
|
85
|
+
if (!d) {
|
|
86
|
+
return { output: `Unclassified failure (no known signature matched) - full context follows. If a pattern repeats, it belongs in the doctor's signature table.\n\nFirst ERROR block:\n${first || '(none found)'}\n\nTail:\n${log.slice(-1200)}` };
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
output: [
|
|
90
|
+
`kind: ${d.kind}`,
|
|
91
|
+
`evidence: ${d.evidence}`,
|
|
92
|
+
`fix: ${d.fix}`,
|
|
93
|
+
'',
|
|
94
|
+
'First ERROR block:',
|
|
95
|
+
first || '(no explicit ERROR line - see evidence above)',
|
|
96
|
+
].join('\n'),
|
|
97
|
+
};
|
|
98
|
+
},
|
|
99
|
+
};
|
package/dist/cangjie.js
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @hmharness/domain-harmony - cangjie (cjpm) tools
|
|
3
|
+
* Cangjie package-manager build/test for HarmonyOS native modules.
|
|
4
|
+
* Resolution: HM_CJPM env override > PATH > known DevEco-adjacent install
|
|
5
|
+
* roots. CANGJIE_HOME is derived from the cjpm.exe layout and injected -
|
|
6
|
+
* the #1 Windows pitfall recorded in the cjpm-build-repair skill.
|
|
7
|
+
*/
|
|
8
|
+
import { execFile } from 'node:child_process';
|
|
9
|
+
import { access } from 'node:fs/promises';
|
|
10
|
+
import { dirname, join, resolve } from 'node:path';
|
|
11
|
+
import { promisify } from 'node:util';
|
|
12
|
+
const exec = promisify(execFile);
|
|
13
|
+
const CJPM_CANDIDATE_ROOTS = [
|
|
14
|
+
'C:\\鸿蒙开发工具\\cangjie\\cangjie-1.1.0\\tools\\bin\\cjpm.exe',
|
|
15
|
+
'C:\\cangjie113\\tools\\bin\\cjpm.exe',
|
|
16
|
+
];
|
|
17
|
+
async function exists(p) {
|
|
18
|
+
try {
|
|
19
|
+
await access(p);
|
|
20
|
+
return true;
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export async function findCjpm() {
|
|
27
|
+
if (process.env.HM_CJPM)
|
|
28
|
+
return process.env.HM_CJPM;
|
|
29
|
+
const probe = await run('cjpm', ['--version'], 8000);
|
|
30
|
+
if (probe.ok)
|
|
31
|
+
return 'cjpm';
|
|
32
|
+
for (const p of CJPM_CANDIDATE_ROOTS)
|
|
33
|
+
if (await exists(p))
|
|
34
|
+
return p;
|
|
35
|
+
return '';
|
|
36
|
+
}
|
|
37
|
+
function cjpmHome(cjpm) {
|
|
38
|
+
// <root>/tools/bin/cjpm.exe -> <root>. Only meaningful for an absolute
|
|
39
|
+
// install path; the PATH alias already carries its own environment, and a
|
|
40
|
+
// wrong CANGJIE_HOME is precisely the Windows pitfall the skill warns of.
|
|
41
|
+
if (!/^[a-zA-Z]:[\\/]/.test(cjpm))
|
|
42
|
+
return null;
|
|
43
|
+
return resolve(cjpm, '..', '..', '..');
|
|
44
|
+
}
|
|
45
|
+
async function run(cmd, args, timeoutMs = 20_000, cwd, extraEnv) {
|
|
46
|
+
try {
|
|
47
|
+
const { stdout, stderr } = await exec(cmd, args, {
|
|
48
|
+
timeout: timeoutMs,
|
|
49
|
+
windowsHide: true,
|
|
50
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
51
|
+
cwd,
|
|
52
|
+
...(extraEnv ? { env: { ...process.env, ...extraEnv } } : {}),
|
|
53
|
+
});
|
|
54
|
+
return { ok: true, out: (stdout || stderr || '(no output)').trim() };
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
const e = err;
|
|
58
|
+
return { ok: false, out: [e.stdout, e.stderr, e.message].filter(Boolean).join('\n').slice(0, 4000) };
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/** Walk up looking for a cjpm project marker. */
|
|
62
|
+
async function findCjpmRoot(start) {
|
|
63
|
+
let dir = resolve(start);
|
|
64
|
+
for (;;) {
|
|
65
|
+
if (await exists(join(dir, 'cjpm.toml')))
|
|
66
|
+
return dir;
|
|
67
|
+
const parent = dirname(dir);
|
|
68
|
+
if (parent === dir)
|
|
69
|
+
return '';
|
|
70
|
+
dir = parent;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function cjpmEnv() {
|
|
74
|
+
const cjpm = await findCjpm();
|
|
75
|
+
if (!cjpm) {
|
|
76
|
+
return { error: 'cjpm not found. Install the Cangjie toolchain, add it to PATH, or set HM_CJPM to cjpm.exe.' };
|
|
77
|
+
}
|
|
78
|
+
const env = {};
|
|
79
|
+
const home = cjpmHome(cjpm);
|
|
80
|
+
if (home && !process.env.CANGJIE_HOME)
|
|
81
|
+
env.CANGJIE_HOME = home;
|
|
82
|
+
return { cjpm, env };
|
|
83
|
+
}
|
|
84
|
+
export const harmonyCjpmBuild = {
|
|
85
|
+
name: 'harmony_cjpm_build',
|
|
86
|
+
description: 'Build a Cangjie (cjpm) package with cjpm build. Looks for cjpm.toml at/above the given path (default cwd). Injects CANGJIE_HOME derived from the resolved cjpm.exe - the classic Windows pitfall. Long-running: allow minutes.',
|
|
87
|
+
parameters: {
|
|
88
|
+
type: 'object',
|
|
89
|
+
properties: {
|
|
90
|
+
project: { type: 'string', description: 'directory containing cjpm.toml (default: auto-detect from cwd)' },
|
|
91
|
+
release: { type: 'boolean', description: 'build in release mode (default false)' },
|
|
92
|
+
},
|
|
93
|
+
required: [],
|
|
94
|
+
},
|
|
95
|
+
async execute(args, ctx) {
|
|
96
|
+
const e = await cjpmEnv();
|
|
97
|
+
if ('error' in e)
|
|
98
|
+
return { output: e.error, isError: true };
|
|
99
|
+
const start = typeof args.project === 'string' && args.project ? resolve(ctx.cwd, args.project) : ctx.cwd;
|
|
100
|
+
const root = await findCjpmRoot(start);
|
|
101
|
+
if (!root)
|
|
102
|
+
return { output: `No cjpm project found at or above ${start} (looking for cjpm.toml).`, isError: true };
|
|
103
|
+
const r = await run(e.cjpm, ['build', ...(args.release === true ? ['--release'] : [])], 900_000, root, e.env);
|
|
104
|
+
const ok = r.ok && (/build finished/i.test(r.out) || !/error/i.test(r.out));
|
|
105
|
+
const tail = r.out.length > 4000 ? '...\n' + r.out.slice(-4000) : r.out;
|
|
106
|
+
return { output: `project: ${root}\ncjpm: ${e.cjpm}\n${tail}`, isError: !ok };
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
export const harmonyCjpmTest = {
|
|
110
|
+
name: 'harmony_cjpm_test',
|
|
111
|
+
description: 'Run Cangjie unit tests (cjpm test) in the project at/above the given path. Same CANGJIE_HOME injection as harmony_cjpm_build.',
|
|
112
|
+
parameters: {
|
|
113
|
+
type: 'object',
|
|
114
|
+
properties: { project: { type: 'string', description: 'directory containing cjpm.toml (default: auto-detect from cwd)' } },
|
|
115
|
+
required: [],
|
|
116
|
+
},
|
|
117
|
+
async execute(args, ctx) {
|
|
118
|
+
const e = await cjpmEnv();
|
|
119
|
+
if ('error' in e)
|
|
120
|
+
return { output: e.error, isError: true };
|
|
121
|
+
const start = typeof args.project === 'string' && args.project ? resolve(ctx.cwd, args.project) : ctx.cwd;
|
|
122
|
+
const root = await findCjpmRoot(start);
|
|
123
|
+
if (!root)
|
|
124
|
+
return { output: `No cjpm project found at or above ${start}.`, isError: true };
|
|
125
|
+
const r = await run(e.cjpm, ['test'], 900_000, root, e.env);
|
|
126
|
+
const tail = r.out.length > 4000 ? '...\n' + r.out.slice(-4000) : r.out;
|
|
127
|
+
return { output: `project: ${root}\n${tail}`, isError: !r.ok };
|
|
128
|
+
},
|
|
129
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Tool } from '@hmharness/kernel';
|
|
2
|
+
export declare function deployedDir(): string;
|
|
3
|
+
export declare function imageRoot(): string;
|
|
4
|
+
export declare const harmonyEmulatorList: Tool;
|
|
5
|
+
export declare const harmonyEmulatorStart: Tool;
|
|
6
|
+
export declare const harmonyEmulatorStop: Tool;
|
|
7
|
+
export declare const harmonyEmulatorCatalog: Tool;
|
|
8
|
+
export declare const harmonyEmulatorCreate: Tool;
|
|
9
|
+
export declare const harmonyEmulatorDelete: Tool;
|
|
10
|
+
export declare const harmonyImageDownloadCheck: Tool;
|
|
11
|
+
export declare const emulatorTools: Tool[];
|