deveco-harness 0.1.0 → 0.1.1-test.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.
@@ -0,0 +1,401 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const path = require('path');
5
+ const fs = require('fs');
6
+
7
+ // --- Argument parsing ---
8
+
9
+ function parseArgs(argv) {
10
+ const args = { project: '', files: [] };
11
+ let i = 2;
12
+ while (i < argv.length) {
13
+ if (argv[i] === '--project' && argv[i + 1]) {
14
+ args.project = path.resolve(argv[++i]);
15
+ } else if (argv[i] === '--files') {
16
+ i++;
17
+ while (i < argv.length && !argv[i].startsWith('--')) {
18
+ args.files.push(argv[i++]);
19
+ }
20
+ continue;
21
+ } else if (!argv[i].startsWith('--')) {
22
+ args.files.push(argv[i]);
23
+ }
24
+ i++;
25
+ }
26
+ return args;
27
+ }
28
+
29
+ // --- DevEco SDK detection ---
30
+
31
+ function findDevecoHome() {
32
+ const envHome = (process.env.DEVECO_HOME || '').trim();
33
+ if (envHome && fs.existsSync(envHome)) return envHome;
34
+
35
+ const candidates = [];
36
+ if (process.platform === 'win32') {
37
+ const userHome = (process.env.USERPROFILE || '').trim();
38
+ candidates.push(
39
+ 'C:\\Program Files\\Huawei\\DevEco Studio',
40
+ 'C:\\Program Files\\DevEco Studio',
41
+ 'C:\\Program Files (x86)\\DevEco Studio',
42
+ userHome ? path.join(userHome, 'DevEco Studio') : '',
43
+ );
44
+ } else if (process.platform === 'darwin') {
45
+ candidates.push('/Applications/DevEco-Studio.app/Contents');
46
+ } else {
47
+ const home = (process.env.HOME || '').trim();
48
+ if (home) {
49
+ candidates.push(path.join(home, 'devecostudio/Contents'));
50
+ candidates.push(path.join(home, 'DevEco-Studio/Contents'));
51
+ }
52
+ }
53
+ for (const c of candidates.filter(Boolean)) {
54
+ if (fs.existsSync(c)) return c;
55
+ }
56
+ return null;
57
+ }
58
+ function findEtsLoader(devecoHome) {
59
+ const candidates = [
60
+ path.join(devecoHome, 'sdk', 'default', 'openharmony', 'ets', 'build-tools', 'ets-loader'),
61
+ path.join(devecoHome, 'sdk', 'openharmony', 'ets', 'build-tools', 'ets-loader'),
62
+ ];
63
+ for (const c of candidates) {
64
+ if (fs.existsSync(path.join(c, 'lib', 'ets_checker.js'))) return c;
65
+ }
66
+ return null;
67
+ }
68
+
69
+ // --- Collect .ets files from project ---
70
+
71
+ function collectEtsFiles(projectPath) {
72
+ const results = [];
73
+ const srcDir = path.join(projectPath, 'entry', 'src', 'main', 'ets');
74
+ if (!fs.existsSync(srcDir)) return results;
75
+
76
+ function walk(dir) {
77
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
78
+ const full = path.join(dir, entry.name);
79
+ if (entry.isDirectory()) {
80
+ if (entry.name === 'node_modules' || entry.name === 'oh_modules' || entry.name === 'build') continue;
81
+ walk(full);
82
+ } else if (entry.name.endsWith('.ets') && !entry.name.endsWith('.d.ets')) {
83
+ results.push(full);
84
+ }
85
+ }
86
+ }
87
+ walk(srcDir);
88
+ return results;
89
+ }
90
+
91
+ // --- Diagnostic output capture ---
92
+
93
+ function parseDiagnosticLine(line) {
94
+ const errorMatch = line.match(/ArkTS:(ERROR|WARN)\s+File:\s+(.+?):(\d+):(\d+)/);
95
+ if (errorMatch) {
96
+ return { severity: errorMatch[1].toLowerCase(), file: errorMatch[2], line: parseInt(errorMatch[3]), column: parseInt(errorMatch[4]) };
97
+ }
98
+ return null;
99
+ }
100
+
101
+ function parseMessageLine(line) {
102
+ const trimmed = line.trim();
103
+ const ruleMatch = trimmed.match(/^(.+?)\s*\(([a-z][\w-]+)\)\s*$/);
104
+ if (ruleMatch) {
105
+ return { message: ruleMatch[1].trim(), rule: ruleMatch[2] };
106
+ }
107
+ return { message: trimmed, rule: '' };
108
+ }
109
+
110
+ // --- Project-level validation (A-class checks) ---
111
+
112
+ function loadSystemResourceNames(devecoHome) {
113
+ const candidates = [
114
+ path.join(devecoHome, 'sdk', 'default', 'openharmony', 'previewer', 'common', 'resources', 'entry', 'resources.txt'),
115
+ path.join(devecoHome, 'sdk', 'openharmony', 'previewer', 'common', 'resources', 'entry', 'resources.txt'),
116
+ ];
117
+ let resFile = '';
118
+ for (const c of candidates) {
119
+ if (fs.existsSync(c)) { resFile = c; break; }
120
+ }
121
+ if (!resFile) return null;
122
+
123
+ const names = new Set();
124
+ const content = fs.readFileSync(resFile, 'utf-8');
125
+ const linePattern = /^id:\d+,\s*'[^']*'\s+'([^']+)'/;
126
+ for (const line of content.split('\n')) {
127
+ const m = line.match(linePattern);
128
+ if (m) names.add(m[1]);
129
+ }
130
+ return names;
131
+ }
132
+
133
+ function validateSystemResources(files, devecoHome, projectPath) {
134
+ const validNames = loadSystemResourceNames(devecoHome);
135
+ if (!validNames) return [];
136
+
137
+ const diagnostics = [];
138
+ const refPattern = /\$r\(\s*['"]sys\.(media|symbol)\.([^'"]+)['"]\s*\)/g;
139
+
140
+ for (const filePath of files) {
141
+ if (!fs.existsSync(filePath)) continue;
142
+ const content = fs.readFileSync(filePath, 'utf-8');
143
+ const fileLines = content.split('\n');
144
+ for (let i = 0; i < fileLines.length; i++) {
145
+ let match;
146
+ refPattern.lastIndex = 0;
147
+ while ((match = refPattern.exec(fileLines[i])) !== null) {
148
+ const resName = match[2];
149
+ if (!validNames.has(resName)) {
150
+ diagnostics.push({
151
+ file: path.relative(projectPath, filePath),
152
+ line: i + 1,
153
+ column: match.index + 1,
154
+ severity: 'error',
155
+ message: `Unknown resource name '${resName}'. No matching sys.${match[1]} resource found in SDK.`,
156
+ rule: 'resource-name-check',
157
+ });
158
+ }
159
+ }
160
+ }
161
+ }
162
+ return diagnostics;
163
+ }
164
+
165
+ function validateRouterPages(projectPath) {
166
+ const candidates = [
167
+ path.join(projectPath, 'entry', 'src', 'main', 'resources', 'base', 'profile', 'main_pages.json'),
168
+ path.join(projectPath, 'src', 'main', 'resources', 'base', 'profile', 'main_pages.json'),
169
+ ];
170
+ let mainPagesPath = '';
171
+ for (const c of candidates) {
172
+ if (fs.existsSync(c)) { mainPagesPath = c; break; }
173
+ }
174
+ if (!mainPagesPath) return [];
175
+
176
+ let config;
177
+ try {
178
+ config = JSON.parse(fs.readFileSync(mainPagesPath, 'utf-8'));
179
+ } catch { return []; }
180
+
181
+ const pages = config.src || [];
182
+ const diagnostics = [];
183
+ const etsBase = path.join(projectPath, 'entry', 'src', 'main', 'ets');
184
+
185
+ for (let i = 0; i < pages.length; i++) {
186
+ const pagePath = pages[i];
187
+ const etsFile = path.join(etsBase, pagePath + '.ets');
188
+ if (!fs.existsSync(etsFile)) {
189
+ diagnostics.push({
190
+ file: path.relative(projectPath, mainPagesPath),
191
+ line: i + 2,
192
+ column: 1,
193
+ severity: 'error',
194
+ message: `Page '${pagePath}.ets' does not exist. Registered in main_pages.json but file not found at entry/src/main/ets/${pagePath}.ets`,
195
+ rule: 'page-file-exists',
196
+ });
197
+ }
198
+ }
199
+ return diagnostics;
200
+ }
201
+
202
+ function validateModelVersion(projectPath) {
203
+ const hvigorPath = path.join(projectPath, 'hvigor', 'hvigor-config.json5');
204
+ const ohPkgPath = path.join(projectPath, 'oh-package.json5');
205
+ if (!fs.existsSync(hvigorPath) || !fs.existsSync(ohPkgPath)) return [];
206
+
207
+ const extractVersion = (file) => {
208
+ const content = fs.readFileSync(file, 'utf-8');
209
+ const m = content.match(/["']?modelVersion["']?\s*:\s*["']([^"']+)["']/);
210
+ return m ? m[1] : null;
211
+ };
212
+
213
+ const hvigorVer = extractVersion(hvigorPath);
214
+ const ohPkgVer = extractVersion(ohPkgPath);
215
+
216
+ if (hvigorVer && ohPkgVer && hvigorVer !== ohPkgVer) {
217
+ return [{
218
+ file: 'hvigor/hvigor-config.json5',
219
+ line: 1,
220
+ column: 1,
221
+ severity: 'error',
222
+ message: `modelVersion mismatch: hvigor-config.json5 has '${hvigorVer}' but oh-package.json5 has '${ohPkgVer}'. They must be consistent.`,
223
+ rule: 'model-version-consistency',
224
+ }];
225
+ }
226
+ return [];
227
+ }
228
+
229
+ // --- Main ---
230
+
231
+ function main() {
232
+ const args = parseArgs(process.argv);
233
+
234
+ if (!args.project) {
235
+ process.stdout.write(JSON.stringify({ success: false, error: 'Missing --project argument', errors: [], summary: { errorCount: 0, warnCount: 0 } }));
236
+ process.exit(1);
237
+ }
238
+
239
+ if (!fs.existsSync(args.project)) {
240
+ process.stdout.write(JSON.stringify({ success: false, error: `Project path not found: ${args.project}`, errors: [], summary: { errorCount: 0, warnCount: 0 } }));
241
+ process.exit(1);
242
+ }
243
+
244
+ const devecoHome = findDevecoHome();
245
+ if (!devecoHome) {
246
+ process.stdout.write(JSON.stringify({ success: false, error: 'Cannot find DevEco Studio. Set DEVECO_HOME environment variable.', errors: [], summary: { errorCount: 0, warnCount: 0 } }));
247
+ process.exit(1);
248
+ }
249
+
250
+ const etsLoaderPath = findEtsLoader(devecoHome);
251
+ if (!etsLoaderPath) {
252
+ process.stdout.write(JSON.stringify({ success: false, error: `Cannot find ets-loader in DevEco SDK at: ${devecoHome}`, errors: [], summary: { errorCount: 0, warnCount: 0 } }));
253
+ process.exit(1);
254
+ }
255
+ let files = args.files.map(f => path.isAbsolute(f) ? f : path.resolve(args.project, f));
256
+ if (files.length === 0) {
257
+ files = collectEtsFiles(args.project);
258
+ }
259
+
260
+ if (files.length === 0) {
261
+ process.stdout.write(JSON.stringify({ success: true, errors: [], summary: { errorCount: 0, warnCount: 0 } }));
262
+ process.exit(0);
263
+ }
264
+
265
+ const fileMap = {};
266
+ files.forEach((f, i) => { fileMap[`file_${i}`] = f; });
267
+
268
+ const moduleJsonCandidates = [
269
+ path.join(args.project, 'entry', 'src', 'main', 'module.json5'),
270
+ path.join(args.project, 'src', 'main', 'module.json5'),
271
+ path.join(args.project, 'entry', 'module.json5'),
272
+ ];
273
+ let aceModuleJsonPath = '';
274
+ for (const candidate of moduleJsonCandidates) {
275
+ if (fs.existsSync(candidate)) {
276
+ aceModuleJsonPath = candidate;
277
+ break;
278
+ }
279
+ }
280
+
281
+ const captured = [];
282
+ const origLog = console.log;
283
+ const origError = console.error;
284
+ const origWarn = console.warn;
285
+ const capture = (...args) => { captured.push(args.map(String).join(' ')); };
286
+ console.log = capture;
287
+ console.error = capture;
288
+ console.warn = capture;
289
+
290
+ // Set externalApiPaths so main.js discovers HMS SDK modules (@hms.*, @kit.*)
291
+ const hmsSdkEts = path.join(devecoHome, 'sdk', 'default', 'hms', 'ets');
292
+ if (fs.existsSync(hmsSdkEts)) {
293
+ const existing = process.env.externalApiPaths || '';
294
+ process.env.externalApiPaths = existing
295
+ ? existing + path.delimiter + hmsSdkEts
296
+ : hmsSdkEts;
297
+ }
298
+
299
+ try {
300
+ const etsChecker = require(path.join(etsLoaderPath, 'lib', 'ets_checker.js'));
301
+ const mainModule = require(path.join(etsLoaderPath, 'main.js'));
302
+
303
+ Object.assign(mainModule.partialUpdateConfig, {
304
+ executeArkTSLinter: true,
305
+ standardArkTSLinter: true,
306
+ });
307
+
308
+ process.env.compileMode = 'moduleJson';
309
+
310
+ const projectConfig = {
311
+ projectPath: args.project,
312
+ projectRootPath: args.project,
313
+ modulePath: args.project,
314
+ cachePath: path.join(args.project, '.cache', 'arkts-check'),
315
+ aceModuleJsonPath: aceModuleJsonPath,
316
+ compileMode: 'esmodule',
317
+ etsLoaderPath: etsLoaderPath,
318
+ packageManagerType: 'ohpm',
319
+ packageDir: 'oh_modules',
320
+ runtimeOS: 'OpenHarmony',
321
+ sdkInfo: '5.0.0',
322
+ compatibleSdkVersion: 12,
323
+ bundleType: '',
324
+ compilerTypes: [],
325
+ resolveModulePaths: [],
326
+ };
327
+
328
+ const cacheDir = projectConfig.cachePath;
329
+ if (!fs.existsSync(cacheDir)) {
330
+ fs.mkdirSync(cacheDir, { recursive: true });
331
+ }
332
+
333
+ const logger = {
334
+ debug: capture,
335
+ info: capture,
336
+ warn: capture,
337
+ error: capture,
338
+ };
339
+
340
+ etsChecker.etsStandaloneChecker(fileMap, logger, projectConfig);
341
+ } catch (e) {
342
+ captured.push(`Internal error: ${e.message}`);
343
+ } finally {
344
+ console.log = origLog;
345
+ console.error = origError;
346
+ console.warn = origWarn;
347
+ }
348
+
349
+ const diagnostics = [];
350
+ let current = null;
351
+
352
+ const lines = captured.flatMap(entry => entry.split('\n'));
353
+
354
+ for (const line of lines) {
355
+ const clean = line.replace(/\x1b\[\d+m/g, '').replace(/\[(\d+)m/g, '');
356
+ const loc = parseDiagnosticLine(clean);
357
+ if (loc) {
358
+ current = loc;
359
+ continue;
360
+ }
361
+ if (current && clean.trim() && !clean.includes('ArkTS:') && !clean.includes('For details about')) {
362
+ const { message, rule } = parseMessageLine(clean);
363
+ diagnostics.push({
364
+ file: path.relative(args.project, current.file),
365
+ line: current.line,
366
+ column: current.column,
367
+ severity: current.severity === 'error' ? 'error' : 'warning',
368
+ message,
369
+ rule,
370
+ });
371
+ current = null;
372
+ }
373
+ }
374
+
375
+ // A-class project-level checks
376
+ const extraDiags = [
377
+ ...validateSystemResources(files, devecoHome, args.project),
378
+ ...validateRouterPages(args.project),
379
+ ...validateModelVersion(args.project),
380
+ ];
381
+ diagnostics.push(...extraDiags);
382
+
383
+ const isStageProject = aceModuleJsonPath !== '';
384
+ const filtered = isStageProject
385
+ ? diagnostics.filter(d => !d.message.includes('the current Mode is FA'))
386
+ : diagnostics;
387
+
388
+ const errorCount = filtered.filter(d => d.severity === 'error').length;
389
+ const warnCount = filtered.filter(d => d.severity === 'warning').length;
390
+
391
+ const result = {
392
+ success: errorCount === 0,
393
+ errors: filtered,
394
+ summary: { errorCount, warnCount },
395
+ };
396
+
397
+ process.stdout.write(JSON.stringify(result, null, 2));
398
+ process.exit(errorCount > 0 ? 1 : 0);
399
+ }
400
+
401
+ main();
package/dist/index.js CHANGED
@@ -180,6 +180,7 @@ function applySkillConfig(hostConfig, pluginConfig, directory) {
180
180
  }
181
181
 
182
182
  // src/tool/arkts-check.ts
183
+ import fs from "node:fs";
183
184
  import path6 from "node:path";
184
185
  import { tool } from "@opencode-ai/plugin";
185
186
 
@@ -313,6 +314,17 @@ function runCommand(command, args, cwd, signal, env = process.env) {
313
314
  }
314
315
 
315
316
  // src/tool/arkts-check.ts
317
+ function arktsCheckScriptPath() {
318
+ const candidates = [
319
+ path6.join(packageRoot(), "dist", "arkts-check.cjs"),
320
+ path6.join(packageRoot(), "src", "tool", "arkts-check.cjs")
321
+ ];
322
+ for (const candidate of candidates) {
323
+ if (fs.existsSync(candidate))
324
+ return candidate;
325
+ }
326
+ throw new Error("deveco-harness arkts-check.cjs not found. Reinstall or rebuild the plugin package.");
327
+ }
316
328
  function createArktsCheckTool() {
317
329
  return tool({
318
330
  description: "Run ArkTS strict-mode static syntax/type check on .ets files. " + "Detects compiler-level violations such as `arkts-no-standalone-this`, `arkts-no-obj-literals-as-types`, and other ArkTS spec issues. " + "Returns structured diagnostics by file, line, column, and rule. " + "Call this after writing or editing any .ets file, before running build_project — catches most ArkTS errors faster than a full build. " + "Do NOT call for non-.ets source files, or when DevEco Studio is not installed locally.",
@@ -332,7 +344,7 @@ function createArktsCheckTool() {
332
344
  throw new Error("DevEco Studio not found. Set the DEVECO_HOME environment variable and retry.");
333
345
  }
334
346
  const node = nodePath(home);
335
- const script = path6.join(import.meta.dirname, "arkts-check.cjs");
347
+ const script = arktsCheckScriptPath();
336
348
  const cmdArgs = ["--project", projectPath];
337
349
  if (args.files?.length) {
338
350
  cmdArgs.push("--files", ...args.files);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deveco-harness",
3
- "version": "0.1.0",
3
+ "version": "0.1.1-test.0",
4
4
  "description": "OpenCode plugin for DevEco and HarmonyOS development workflows",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -17,7 +17,7 @@
17
17
  "deveco-harness.example.jsonc"
18
18
  ],
19
19
  "scripts": {
20
- "build": "bun build ./src/index.ts --outdir ./dist --target node --external '@deveco-codegenie/mcp-bridge' --external '@opencode-ai/plugin' --external jsonc-parser",
20
+ "build": "bun build ./src/index.ts --outdir ./dist --target node --external '@deveco-codegenie/mcp-bridge' --external '@opencode-ai/plugin' --external jsonc-parser && bun run script/copy-assets.ts",
21
21
  "prepublishOnly": "bun run build",
22
22
  "typecheck": "tsc --noEmit"
23
23
  },