@expo-harmony/cli 55.0.26-harmony.1 → 55.0.26-harmony.10

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.
Files changed (185) hide show
  1. package/README.md +87 -38
  2. package/build/args.d.ts +19 -0
  3. package/build/args.js +27 -0
  4. package/build/bin/expo-harmony.d.ts +2 -0
  5. package/build/bin/expo-harmony.js +1 -1
  6. package/build/buildHap/build.d.ts +18 -0
  7. package/build/buildHap/build.js +38 -0
  8. package/build/buildHap/index.d.ts +2 -0
  9. package/build/buildHap/index.js +19 -0
  10. package/build/buildHap/options.d.ts +7 -0
  11. package/build/buildHap/options.js +30 -0
  12. package/build/cli.d.ts +3 -0
  13. package/build/cli.js +40 -0
  14. package/build/command.d.ts +9 -0
  15. package/build/command.js +20 -0
  16. package/build/development/manifest.d.ts +1 -0
  17. package/build/development/manifest.js +100 -0
  18. package/build/development/protocol.d.ts +2 -0
  19. package/build/development/protocol.js +26 -0
  20. package/build/development/session.d.ts +5 -0
  21. package/build/development/session.js +66 -0
  22. package/build/doctor/doctor.d.ts +20 -0
  23. package/build/doctor/doctor.js +226 -0
  24. package/build/doctor/index.d.ts +2 -0
  25. package/build/doctor/index.js +16 -0
  26. package/build/doctor/options.d.ts +5 -0
  27. package/build/doctor/options.js +15 -0
  28. package/build/entry.d.ts +2 -0
  29. package/build/entry.js +47 -0
  30. package/build/errors.d.ts +12 -0
  31. package/build/errors.js +16 -0
  32. package/build/expo.d.ts +19 -0
  33. package/build/expo.js +53 -0
  34. package/build/exportEmbed/export.d.ts +18 -0
  35. package/build/exportEmbed/export.js +124 -0
  36. package/build/exportEmbed/index.d.ts +2 -0
  37. package/build/exportEmbed/index.js +19 -0
  38. package/build/exportEmbed/manifest.d.ts +44 -0
  39. package/build/exportEmbed/manifest.js +171 -0
  40. package/build/exportEmbed/options.d.ts +7 -0
  41. package/build/exportEmbed/options.js +22 -0
  42. package/build/file.d.ts +10 -0
  43. package/build/file.js +110 -0
  44. package/build/help.d.ts +1 -0
  45. package/build/help.js +44 -0
  46. package/build/index.d.ts +14 -0
  47. package/build/index.js +6 -5
  48. package/build/internal/development.d.ts +1 -0
  49. package/build/internal/development.js +8 -0
  50. package/build/internal/export.d.ts +1 -0
  51. package/build/internal/export.js +80 -0
  52. package/build/internal/prebuild.d.ts +3 -0
  53. package/build/internal/prebuild.js +9 -0
  54. package/build/log.d.ts +4 -0
  55. package/build/log.js +6 -0
  56. package/build/modules/index.d.ts +2 -0
  57. package/build/modules/index.js +16 -0
  58. package/build/modules/modules.d.ts +42 -0
  59. package/build/modules/modules.js +84 -0
  60. package/build/modules/options.d.ts +10 -0
  61. package/build/modules/options.js +45 -0
  62. package/build/native/bare.d.ts +4 -0
  63. package/build/native/bare.js +130 -0
  64. package/build/native/build.d.ts +15 -0
  65. package/build/native/build.js +69 -0
  66. package/build/native/cache.d.ts +11 -0
  67. package/build/native/cache.js +73 -0
  68. package/build/native/hvigor.d.ts +3 -0
  69. package/build/native/hvigor.js +83 -0
  70. package/build/native/install.d.ts +5 -0
  71. package/build/native/install.js +13 -0
  72. package/build/native/prepareProject.d.ts +11 -0
  73. package/build/native/prepareProject.js +68 -0
  74. package/build/native/project.d.ts +8 -0
  75. package/build/native/project.js +58 -0
  76. package/build/native/toolchain.d.ts +16 -0
  77. package/build/native/toolchain.js +172 -0
  78. package/build/native/types.d.ts +47 -0
  79. package/build/native/types.js +2 -0
  80. package/build/path.d.ts +4 -0
  81. package/build/path.js +27 -0
  82. package/build/prebuild/check.d.ts +10 -0
  83. package/build/prebuild/check.js +151 -0
  84. package/build/prebuild/clean.d.ts +2 -0
  85. package/build/prebuild/clean.js +50 -0
  86. package/build/prebuild/index.d.ts +2 -0
  87. package/build/prebuild/index.js +51 -0
  88. package/build/prebuild/options.d.ts +10 -0
  89. package/build/prebuild/options.js +50 -0
  90. package/build/prebuild/prebuild.d.ts +6 -0
  91. package/build/prebuild/prebuild.js +70 -0
  92. package/build/prebuild/template.d.ts +8 -0
  93. package/build/prebuild/template.js +86 -0
  94. package/build/process.d.ts +41 -0
  95. package/build/process.js +324 -0
  96. package/build/profile.d.ts +1 -0
  97. package/build/profile.js +12 -0
  98. package/build/project.d.ts +2 -0
  99. package/build/project.js +23 -0
  100. package/build/projectLock.d.ts +2 -0
  101. package/build/projectLock.js +185 -0
  102. package/build/run/devices.d.ts +29 -0
  103. package/build/run/devices.js +216 -0
  104. package/build/run/emulators.d.ts +13 -0
  105. package/build/run/emulators.js +82 -0
  106. package/build/run/index.d.ts +2 -0
  107. package/build/run/index.js +25 -0
  108. package/build/run/metro.d.ts +21 -0
  109. package/build/run/metro.js +162 -0
  110. package/build/run/options.d.ts +14 -0
  111. package/build/run/options.js +62 -0
  112. package/build/run/run.d.ts +46 -0
  113. package/build/run/run.js +135 -0
  114. package/build/runtime/config.d.ts +35 -0
  115. package/build/runtime/config.js +118 -0
  116. package/build/runtime/contract.d.ts +5 -0
  117. package/build/runtime/contract.js +108 -0
  118. package/build/runtime/fingerprint.d.ts +1 -0
  119. package/build/runtime/fingerprint.js +44 -0
  120. package/build/runtime/index.d.ts +2 -0
  121. package/build/runtime/index.js +50 -0
  122. package/build/start/index.d.ts +2 -0
  123. package/build/start/index.js +38 -0
  124. package/build/start/options.d.ts +9 -0
  125. package/build/start/options.js +38 -0
  126. package/build/updates/export.d.ts +14 -0
  127. package/build/updates/export.js +46 -0
  128. package/build/updates/index.d.ts +2 -0
  129. package/build/updates/index.js +96 -0
  130. package/build/upstream.d.ts +9 -0
  131. package/build/upstream.js +17 -0
  132. package/package.json +21 -5
  133. package/src/bin/expo-harmony.ts +2 -2
  134. package/src/buildHap/build.ts +13 -105
  135. package/src/buildHap/index.ts +19 -0
  136. package/src/cli.ts +27 -225
  137. package/src/command.ts +24 -0
  138. package/src/development/manifest.ts +106 -0
  139. package/src/development/protocol.ts +37 -0
  140. package/src/development/session.ts +83 -0
  141. package/src/doctor/doctor.ts +67 -62
  142. package/src/doctor/index.ts +16 -0
  143. package/src/exportEmbed/export.ts +73 -72
  144. package/src/exportEmbed/index.ts +17 -0
  145. package/src/file.ts +20 -11
  146. package/src/help.ts +41 -0
  147. package/src/index.ts +5 -3
  148. package/src/internal/development.ts +8 -0
  149. package/src/internal/export.ts +82 -0
  150. package/src/internal/prebuild.ts +3 -0
  151. package/src/log.ts +5 -0
  152. package/src/modules/index.ts +16 -0
  153. package/src/native/bare.ts +145 -0
  154. package/src/native/build.ts +83 -0
  155. package/src/{run → native}/cache.ts +10 -9
  156. package/src/native/hvigor.ts +88 -0
  157. package/src/native/install.ts +17 -0
  158. package/src/native/prepareProject.ts +99 -0
  159. package/src/native/project.ts +88 -0
  160. package/src/{tools.ts → native/toolchain.ts} +38 -140
  161. package/src/native/types.ts +44 -0
  162. package/src/prebuild/check.ts +56 -31
  163. package/src/prebuild/clean.ts +3 -3
  164. package/src/prebuild/index.ts +56 -0
  165. package/src/prebuild/prebuild.ts +11 -1
  166. package/src/prebuild/template.ts +2 -2
  167. package/src/process.ts +175 -64
  168. package/src/profile.ts +13 -0
  169. package/src/projectLock.ts +9 -1
  170. package/src/run/devices.ts +16 -11
  171. package/src/run/emulators.ts +4 -6
  172. package/src/run/index.ts +25 -0
  173. package/src/run/metro.ts +10 -1
  174. package/src/run/options.ts +2 -0
  175. package/src/run/run.ts +63 -129
  176. package/src/runtime/config.ts +150 -0
  177. package/src/runtime/contract.ts +165 -0
  178. package/src/runtime/fingerprint.ts +39 -0
  179. package/src/runtime/index.ts +54 -0
  180. package/src/start/index.ts +37 -0
  181. package/src/start/options.ts +12 -0
  182. package/src/updates/export.ts +60 -0
  183. package/src/updates/index.ts +89 -0
  184. package/src/buildHap/common.ts +0 -145
  185. package/src/run/install.ts +0 -41
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.resolveProject = resolveProject;
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_path_1 = __importDefault(require("node:path"));
9
+ const errors_1 = require("./errors");
10
+ function resolveProject(start = process.cwd()) {
11
+ let current = node_path_1.default.resolve(start);
12
+ if (node_fs_1.default.existsSync(current) && node_fs_1.default.statSync(current).isFile())
13
+ current = node_path_1.default.dirname(current);
14
+ while (true) {
15
+ if (node_fs_1.default.existsSync(node_path_1.default.join(current, 'package.json')))
16
+ return node_fs_1.default.realpathSync(current);
17
+ const parent = node_path_1.default.dirname(current);
18
+ if (parent === current)
19
+ break;
20
+ current = parent;
21
+ }
22
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `No package.json was found from ${start}.`, { operation: 'resolve-project' });
23
+ }
@@ -0,0 +1,2 @@
1
+ declare function withHarmonyProjectLockAsync<T>(projectRoot: string, operation: string, callback: () => Promise<T> | T): Promise<T>;
2
+ export { withHarmonyProjectLockAsync };
@@ -0,0 +1,185 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.withHarmonyProjectLockAsync = withHarmonyProjectLockAsync;
7
+ const node_async_hooks_1 = require("node:async_hooks");
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
+ const node_fs_1 = __importDefault(require("node:fs"));
10
+ const node_path_1 = __importDefault(require("node:path"));
11
+ const errors_1 = require("./errors");
12
+ const DefaultMalformedLockGraceMs = 30_000;
13
+ const DefaultPollIntervalMs = 100;
14
+ const DefaultWaitTimeoutMs = 5_000;
15
+ const LockRelativePath = '.expo/harmony/native-operation.lock';
16
+ const ActiveLock = new node_async_hooks_1.AsyncLocalStorage();
17
+ async function canonicalProjectRootAsync(projectRoot) {
18
+ try {
19
+ return await node_fs_1.default.promises.realpath(node_path_1.default.resolve(projectRoot));
20
+ }
21
+ catch (cause) {
22
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_LOCK', 'Cannot resolve the application project before acquiring its Harmony native operation lock.', { cause, operation: 'project-lock' });
23
+ }
24
+ }
25
+ function projectLockPath(projectRoot) {
26
+ return node_path_1.default.join(projectRoot, ...LockRelativePath.split('/'));
27
+ }
28
+ async function processIsAliveAsync(pid) {
29
+ if (!Number.isInteger(pid) || Number(pid) <= 0)
30
+ return false;
31
+ try {
32
+ process.kill(Number(pid), 0);
33
+ return true;
34
+ }
35
+ catch (cause) {
36
+ return cause?.code !== 'ESRCH';
37
+ }
38
+ }
39
+ async function readLockOwnerAsync(lockPath) {
40
+ try {
41
+ const candidate = JSON.parse(await node_fs_1.default.promises.readFile(lockPath, 'utf8'));
42
+ if (!candidate
43
+ || typeof candidate !== 'object'
44
+ || !Number.isInteger(candidate.pid)
45
+ || candidate.pid <= 0
46
+ || typeof candidate.createdAt !== 'string'
47
+ || typeof candidate.operation !== 'string'
48
+ || typeof candidate.projectRoot !== 'string'
49
+ || typeof candidate.token !== 'string') {
50
+ return null;
51
+ }
52
+ return candidate;
53
+ }
54
+ catch (_cause) {
55
+ return null;
56
+ }
57
+ }
58
+ async function removeStaleProjectLockAsync(lockPath) {
59
+ let observed;
60
+ try {
61
+ observed = await node_fs_1.default.promises.lstat(lockPath);
62
+ }
63
+ catch (_cause) {
64
+ return false;
65
+ }
66
+ if (!observed.isFile() || observed.isSymbolicLink())
67
+ return false;
68
+ const owner = await readLockOwnerAsync(lockPath);
69
+ if (owner) {
70
+ if (await processIsAliveAsync(owner.pid))
71
+ return false;
72
+ }
73
+ else if (Date.now() - observed.mtimeMs < DefaultMalformedLockGraceMs) {
74
+ return false;
75
+ }
76
+ try {
77
+ const current = await node_fs_1.default.promises.lstat(lockPath);
78
+ if (!current.isFile() || current.isSymbolicLink()
79
+ || current.dev !== observed.dev
80
+ || current.ino !== observed.ino
81
+ || current.mtimeMs !== observed.mtimeMs
82
+ || current.size !== observed.size) {
83
+ return false;
84
+ }
85
+ await node_fs_1.default.promises.unlink(lockPath);
86
+ return true;
87
+ }
88
+ catch (_cause) {
89
+ return false;
90
+ }
91
+ }
92
+ async function releaseOwnedLockAsync(handle, lockPath, owned) {
93
+ await handle.close().catch(() => { });
94
+ if (!owned)
95
+ return false;
96
+ try {
97
+ const current = await node_fs_1.default.promises.lstat(lockPath);
98
+ if (!current.isFile() || current.isSymbolicLink()
99
+ || current.dev !== owned.dev || current.ino !== owned.ino) {
100
+ return false;
101
+ }
102
+ await node_fs_1.default.promises.unlink(lockPath);
103
+ return true;
104
+ }
105
+ catch (_cause) {
106
+ return false;
107
+ }
108
+ }
109
+ function delayAsync(milliseconds) {
110
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
111
+ }
112
+ async function acquireHarmonyProjectLockAsync(projectRoot, operation) {
113
+ const canonicalRoot = await canonicalProjectRootAsync(projectRoot);
114
+ const lockPath = projectLockPath(canonicalRoot);
115
+ const deadline = Date.now() + DefaultWaitTimeoutMs;
116
+ await node_fs_1.default.promises.mkdir(node_path_1.default.dirname(lockPath), { recursive: true });
117
+ while (true) {
118
+ try {
119
+ const handle = await node_fs_1.default.promises.open(lockPath, 'wx', 0o600);
120
+ let owned;
121
+ const owner = {
122
+ createdAt: new Date().toISOString(),
123
+ operation,
124
+ pid: process.pid,
125
+ projectRoot: canonicalRoot,
126
+ token: node_crypto_1.default.randomUUID(),
127
+ };
128
+ try {
129
+ owned = await handle.stat();
130
+ await handle.writeFile(`${JSON.stringify(owner)}\n`);
131
+ await handle.sync();
132
+ }
133
+ catch (cause) {
134
+ await releaseOwnedLockAsync(handle, lockPath, owned);
135
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_LOCK', 'Cannot initialize the Harmony native operation lock.', { cause, operation: 'project-lock' });
136
+ }
137
+ let released = false;
138
+ return {
139
+ async releaseAsync() {
140
+ if (released)
141
+ return;
142
+ released = true;
143
+ if (await releaseOwnedLockAsync(handle, lockPath, owned))
144
+ return;
145
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_LOCK', 'The Harmony native operation lock changed before it could be released safely.', { operation: 'project-lock' });
146
+ },
147
+ };
148
+ }
149
+ catch (cause) {
150
+ if (cause instanceof errors_1.HarmonyCliError) {
151
+ throw new errors_1.HarmonyCliError(cause.code, cause.message, {
152
+ cause,
153
+ exitCode: cause.exitCode,
154
+ operation: cause.operation,
155
+ });
156
+ }
157
+ if (cause?.code !== 'EEXIST') {
158
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_LOCK', 'Cannot acquire the Harmony native operation lock.', { cause, operation: 'project-lock' });
159
+ }
160
+ if (await removeStaleProjectLockAsync(lockPath))
161
+ continue;
162
+ if (Date.now() >= deadline) {
163
+ const owner = await readLockOwnerAsync(lockPath);
164
+ const detail = owner?.operation ? ` (${owner.operation}, pid ${owner.pid})` : '';
165
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_PROJECT_BUSY', `Another Harmony native operation is using this project${detail}. Retry after it finishes.`, { operation });
166
+ }
167
+ await delayAsync(Math.min(DefaultPollIntervalMs, Math.max(1, deadline - Date.now())));
168
+ }
169
+ }
170
+ }
171
+ async function withHarmonyProjectLockAsync(projectRoot, operation, callback) {
172
+ const canonicalRoot = await canonicalProjectRootAsync(projectRoot);
173
+ const active = ActiveLock.getStore();
174
+ if (active?.projectRoot === canonicalRoot && !active.released)
175
+ return await callback();
176
+ const lease = await acquireHarmonyProjectLockAsync(canonicalRoot, operation);
177
+ const context = { projectRoot: canonicalRoot, released: false };
178
+ try {
179
+ return await ActiveLock.run(context, callback);
180
+ }
181
+ finally {
182
+ context.released = true;
183
+ await lease.releaseAsync();
184
+ }
185
+ }
@@ -0,0 +1,29 @@
1
+ import { type HarmonyTool } from '../native/toolchain';
2
+ interface Device {
3
+ aliases: string[];
4
+ connectTool: string | null;
5
+ id: string;
6
+ location: string | null;
7
+ state: string;
8
+ transport: string;
9
+ }
10
+ interface HdcOptions {
11
+ allowFailure?: boolean;
12
+ code?: string;
13
+ cwd?: string;
14
+ devicePort?: number;
15
+ message?: string;
16
+ operation?: string;
17
+ outputLimit?: number;
18
+ timeoutMs?: number;
19
+ }
20
+ interface DeviceSelectionOptions extends HdcOptions {
21
+ emulator?: HarmonyTool;
22
+ emulatorLogFile?: string;
23
+ onProgress?: (message: string) => void;
24
+ }
25
+ declare function selectDeviceAsync(hdc: HarmonyTool, requested?: string, options?: DeviceSelectionOptions): Promise<Device>;
26
+ declare function installHapAsync(hdc: HarmonyTool, device: Device, hap: string, options?: HdcOptions): Promise<void>;
27
+ declare function configureMetroPortAsync(hdc: HarmonyTool, device: Device, port: number, options?: HdcOptions): Promise<void>;
28
+ declare function launchAppAsync(hdc: HarmonyTool, device: Device, bundleName: string, abilityName: string, options?: HdcOptions): Promise<void>;
29
+ export { configureMetroPortAsync, installHapAsync, launchAppAsync, selectDeviceAsync, };
@@ -0,0 +1,216 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.configureMetroPortAsync = configureMetroPortAsync;
4
+ exports.installHapAsync = installHapAsync;
5
+ exports.launchAppAsync = launchAppAsync;
6
+ exports.selectDeviceAsync = selectDeviceAsync;
7
+ const promises_1 = require("node:timers/promises");
8
+ const emulators_1 = require("./emulators");
9
+ const errors_1 = require("../errors");
10
+ const process_1 = require("../process");
11
+ function parseHdcTargets(output) {
12
+ const trimmed = String(output).trim();
13
+ if (!trimmed || trimmed === '[Empty]')
14
+ return [];
15
+ return trimmed.split(/\r?\n/u).filter(Boolean).map((line) => {
16
+ const fields = line.trim().split(/\s+/u);
17
+ if (fields.length < 3 || !fields[0]) {
18
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_OUTPUT', 'HDC returned an unsupported target-list format.', { operation: 'list-devices' });
19
+ }
20
+ return {
21
+ aliases: [fields[0]],
22
+ connectTool: fields[4] || null,
23
+ id: fields[0],
24
+ location: fields[3] || null,
25
+ state: fields[2],
26
+ transport: fields[1],
27
+ };
28
+ });
29
+ }
30
+ function hasCommandFailure(result) {
31
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
32
+ return result.code !== 0 || result.timedOut
33
+ // HDC can print failures to stdout while returning exit code 0.
34
+ || /\[(?:Fail|Error)\]|\b(?:Failure|failed)\b|(?:失败|错误)/iu.test(output);
35
+ }
36
+ async function runHdcAsync(hdc, args, options = {}) {
37
+ const result = await (0, process_1.spawnAsync)(hdc.command, [
38
+ ...hdc.args,
39
+ ...args,
40
+ ], {
41
+ capture: true,
42
+ cwd: options.cwd,
43
+ operation: options.operation || 'hdc',
44
+ outputLimit: options.outputLimit || 256 * 1024,
45
+ timeoutMs: options.timeoutMs || 60_000,
46
+ });
47
+ if (!options.allowFailure && hasCommandFailure(result)) {
48
+ const diagnostics = (0, process_1.formatDiagnostics)(result, 2_000);
49
+ throw new errors_1.HarmonyCliError(options.code || 'ERR_HARMONY_DEVICE_COMMAND', `${options.message || 'HDC command failed'}${diagnostics ? `: ${diagnostics}` : '.'}`, { exitCode: result.code || 1, operation: options.operation || 'hdc' });
50
+ }
51
+ return result;
52
+ }
53
+ async function listConnectedDevicesAsync(hdc, options) {
54
+ const result = await runHdcAsync(hdc, ['list', 'targets', '-v'], {
55
+ code: 'ERR_HARMONY_DEVICE_LIST',
56
+ cwd: options.cwd,
57
+ message: 'Cannot list Harmony devices',
58
+ operation: 'list-devices',
59
+ timeoutMs: options.timeoutMs || 15_000,
60
+ });
61
+ const targets = parseHdcTargets(result.stdout || result.stderr);
62
+ return targets.filter(target => target.state.toLowerCase() === 'connected');
63
+ }
64
+ async function selectDeviceAsync(hdc, requested, options = {}) {
65
+ if (requested !== undefined && (!requested || /[\0\r\n]/u.test(requested) || requested.trim() !== requested)) {
66
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--device must be a non-empty HDC id or exact emulator name.', { operation: 'select-device' });
67
+ }
68
+ const connected = await listConnectedDevicesAsync(hdc, { cwd: options.cwd });
69
+ if (requested) {
70
+ const matches = connected.filter(target => target.id === requested || target.aliases.includes(requested));
71
+ if (matches.length === 1)
72
+ return matches[0];
73
+ if (matches.length > 1) {
74
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_AMBIGUOUS', `The requested device name matches multiple connected targets: ${requested}.`, { operation: 'select-device' });
75
+ }
76
+ if (!options.emulator) {
77
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', `The requested Harmony device is not connected: ${requested}.`, { operation: 'select-device' });
78
+ }
79
+ }
80
+ else {
81
+ if (connected.length === 1)
82
+ return connected[0];
83
+ if (connected.length > 1) {
84
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_AMBIGUOUS', `Multiple Harmony devices are connected (${connected.map(target => target.id).join(', ')}); use --device.`, { operation: 'select-device' });
85
+ }
86
+ }
87
+ if (!options.emulator) {
88
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', 'No connected Harmony device was reported by HDC.', { operation: 'select-device' });
89
+ }
90
+ const instances = await (0, emulators_1.listEmulatorsAsync)(options.emulator, { cwd: options.cwd });
91
+ const running = instances.filter(instance => instance.running);
92
+ const candidates = requested
93
+ ? instances.filter(instance => instance.name === requested)
94
+ : running.length ? running : instances;
95
+ if (candidates.length === 0) {
96
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', requested
97
+ ? `No connected HDC target or local emulator matches ${requested}. Available emulators: ${instances.map(instance => instance.name).join(', ') || 'none'}.`
98
+ : 'No connected Harmony device or local emulator was found. Create an emulator in DevEco Studio Device Manager first.', { operation: 'select-device' });
99
+ }
100
+ if (candidates.length > 1) {
101
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_AMBIGUOUS', `Multiple Harmony emulators are available (${candidates.map(instance => JSON.stringify(instance.name)).join(', ')}); use --device <emulator-name>.`, { operation: 'select-device' });
102
+ }
103
+ const instance = candidates[0];
104
+ let launch;
105
+ if (!instance.running) {
106
+ if (!options.emulatorLogFile) {
107
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', 'An emulator log file is required to start a Harmony emulator.', { operation: 'start-emulator' });
108
+ }
109
+ options.onProgress?.(`Starting Harmony emulator ${instance.name}`);
110
+ launch = (0, emulators_1.startEmulator)(options.emulator, instance.name, options.emulatorLogFile, options.cwd);
111
+ }
112
+ options.onProgress?.(`Waiting for Harmony emulator ${instance.name} to connect`);
113
+ const deadline = Date.now() + (options.timeoutMs || 120_000);
114
+ while (Date.now() < deadline) {
115
+ launch?.assertRunning();
116
+ const devices = await listConnectedDevicesAsync(hdc, {
117
+ cwd: options.cwd,
118
+ timeoutMs: Math.max(1, Math.min(15_000, deadline - Date.now())),
119
+ });
120
+ for (const device of devices) {
121
+ if (Date.now() >= deadline)
122
+ break;
123
+ // DevEco may report hw.hdc.port as "notset" even while running. Query
124
+ // the guest's instance name instead of guessing from a newly seen ID.
125
+ if (!/^(?:127\.0\.0\.1|localhost|\[::1\]):\d+$/u.test(device.id))
126
+ continue;
127
+ const identity = await runHdcAsync(hdc, ['-t', device.id, 'shell', 'param', 'get', 'ohos.qemu.hvd.name'], {
128
+ allowFailure: true,
129
+ cwd: options.cwd,
130
+ operation: 'identify-emulator',
131
+ timeoutMs: Math.max(1, Math.min(5_000, deadline - Date.now())),
132
+ });
133
+ if (identity.code !== 0 || identity.timedOut || identity.stdout.trim() !== instance.name)
134
+ continue;
135
+ if (Date.now() >= deadline)
136
+ break;
137
+ const boot = await runHdcAsync(hdc, ['-t', device.id, 'shell', 'param', 'get', 'bootevent.boot.completed'], {
138
+ allowFailure: true,
139
+ cwd: options.cwd,
140
+ operation: 'wait-emulator-boot',
141
+ timeoutMs: Math.max(1, Math.min(5_000, deadline - Date.now())),
142
+ });
143
+ if (!hasCommandFailure(boot) && boot.stdout.trim() === 'true')
144
+ return device;
145
+ }
146
+ if (Date.now() >= deadline)
147
+ break;
148
+ await (0, promises_1.setTimeout)(Math.min(1_000, deadline - Date.now()));
149
+ }
150
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_EMULATOR_TIMEOUT', `Timed out waiting for Harmony emulator ${instance.name} to connect to HDC.${launch ? ` See ${options.emulatorLogFile}.` : ''} Try starting it in DevEco Studio to resolve any license or login requirements.`, { operation: 'start-emulator' });
151
+ }
152
+ async function installHapAsync(hdc, device, hap, options = {}) {
153
+ await runHdcAsync(hdc, ['-t', device.id, 'install', '-r', hap], {
154
+ code: 'ERR_HARMONY_INSTALL_FAILED',
155
+ cwd: options.cwd,
156
+ message: `Cannot install the Harmony HAP on ${device.id}`,
157
+ operation: 'install-hap',
158
+ timeoutMs: options.timeoutMs || 2 * 60_000,
159
+ });
160
+ }
161
+ async function configureMetroPortAsync(hdc, device, port, options = {}) {
162
+ const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
163
+ const hostEndpoint = `tcp:${port}`;
164
+ const forwards = await runHdcAsync(hdc, ['-t', device.id, 'fport', 'ls'], {
165
+ cwd: options.cwd,
166
+ operation: 'list-metro-ports',
167
+ timeoutMs: 15_000,
168
+ });
169
+ for (const line of forwards.stdout.split(/\r?\n/u)) {
170
+ const [target, remote, local, direction] = line.trim().split(/\s+/u);
171
+ if (target !== device.id || remote !== deviceEndpoint || direction !== '[Reverse]')
172
+ continue;
173
+ if (local === hostEndpoint)
174
+ return;
175
+ await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', remote, local], {
176
+ cwd: options.cwd,
177
+ operation: 'remove-metro-port',
178
+ timeoutMs: 15_000,
179
+ });
180
+ }
181
+ await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
182
+ code: 'ERR_HARMONY_METRO_FORWARD',
183
+ cwd: options.cwd,
184
+ message: `Cannot reverse Metro port ${port} for ${device.id}`,
185
+ operation: 'reverse-metro-port',
186
+ timeoutMs: 15_000,
187
+ });
188
+ }
189
+ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}) {
190
+ await runHdcAsync(hdc, [
191
+ '-t', device.id,
192
+ 'shell',
193
+ 'aa',
194
+ 'force-stop',
195
+ bundleName,
196
+ ], {
197
+ allowFailure: true,
198
+ cwd: options.cwd,
199
+ operation: 'force-stop-app',
200
+ timeoutMs: 15_000,
201
+ });
202
+ await runHdcAsync(hdc, [
203
+ '-t', device.id,
204
+ 'shell',
205
+ 'aa',
206
+ 'start',
207
+ '-a', abilityName,
208
+ '-b', bundleName,
209
+ ], {
210
+ code: 'ERR_HARMONY_LAUNCH_FAILED',
211
+ cwd: options.cwd,
212
+ message: `Cannot launch ${bundleName} on ${device.id}`,
213
+ operation: 'launch-app',
214
+ timeoutMs: 30_000,
215
+ });
216
+ }
@@ -0,0 +1,13 @@
1
+ import { type HarmonyTool } from '../native/toolchain';
2
+ interface HarmonyEmulator {
3
+ name: string;
4
+ running: boolean;
5
+ }
6
+ declare function listEmulatorsAsync(tool: HarmonyTool, options?: {
7
+ cwd?: string;
8
+ timeoutMs?: number;
9
+ }): Promise<HarmonyEmulator[]>;
10
+ declare function startEmulator(tool: HarmonyTool, name: string, logFile: string, cwd?: string): {
11
+ assertRunning(): void;
12
+ };
13
+ export { listEmulatorsAsync, startEmulator };
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.listEmulatorsAsync = listEmulatorsAsync;
7
+ exports.startEmulator = startEmulator;
8
+ const node_fs_1 = __importDefault(require("node:fs"));
9
+ const node_path_1 = __importDefault(require("node:path"));
10
+ const cross_spawn_1 = __importDefault(require("cross-spawn"));
11
+ const errors_1 = require("../errors");
12
+ const process_1 = require("../process");
13
+ async function listEmulatorsAsync(tool, options = {}) {
14
+ let result;
15
+ try {
16
+ result = await (0, process_1.spawnAsync)(tool.command, [...tool.args, '-list', '-details'], {
17
+ capture: true,
18
+ cwd: options.cwd,
19
+ operation: 'list-emulators',
20
+ timeoutMs: options.timeoutMs || 15_000,
21
+ });
22
+ }
23
+ catch (cause) {
24
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_EMULATOR_LIST', 'Cannot run Emulator. Install DevEco Studio 6.1.0 or newer, add tools/emulator to PATH, or set HARMONY_EMULATOR.', { cause, operation: 'list-emulators' });
25
+ }
26
+ if (result.code !== 0 || result.timedOut) {
27
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_EMULATOR_LIST', `Cannot list Harmony emulators: ${(0, process_1.formatDiagnostics)(result, 2_000) || 'Emulator timed out or exited unsuccessfully.'}`, { operation: 'list-emulators' });
28
+ }
29
+ try {
30
+ const instances = JSON.parse(result.stdout);
31
+ if (!Array.isArray(instances))
32
+ throw new Error('Expected an array.');
33
+ return instances.map((instance) => {
34
+ if (!instance || typeof instance.name !== 'string' || !instance.name.trim()
35
+ || /[\0\r\n]/u.test(instance.name)
36
+ || ![true, false, 'true', 'false'].includes(instance.isRunning)) {
37
+ throw new Error('Invalid emulator instance.');
38
+ }
39
+ return {
40
+ name: instance.name,
41
+ running: instance.isRunning === true || instance.isRunning === 'true',
42
+ };
43
+ });
44
+ }
45
+ catch (cause) {
46
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_EMULATOR_OUTPUT', 'Emulator returned an unsupported instance list. Use DevEco Studio 6.1.0 or newer with Emulator -list -details support.', { cause, operation: 'list-emulators' });
47
+ }
48
+ }
49
+ function startEmulator(tool, name, logFile, cwd) {
50
+ node_fs_1.default.mkdirSync(node_path_1.default.dirname(logFile), { recursive: true });
51
+ const log = node_fs_1.default.openSync(logFile, 'w', 0o600);
52
+ let failure = null;
53
+ try {
54
+ // A detached process with file output lets the emulator survive CLI exit.
55
+ const child = (0, cross_spawn_1.default)(tool.command, [...tool.args, '-start', name], {
56
+ cwd,
57
+ detached: true,
58
+ shell: false,
59
+ stdio: ['ignore', log, log],
60
+ windowsHide: false,
61
+ });
62
+ child.once('error', (cause) => {
63
+ failure = cause.message;
64
+ });
65
+ child.once('exit', (code, signal) => {
66
+ // Some launchers exit before the guest boots; HDC still has to confirm readiness.
67
+ if (code !== 0)
68
+ failure = signal ? `terminated by ${signal}` : `exited with code ${code}`;
69
+ });
70
+ child.unref();
71
+ }
72
+ finally {
73
+ node_fs_1.default.closeSync(log);
74
+ }
75
+ return {
76
+ assertRunning() {
77
+ if (failure) {
78
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_EMULATOR_START', `Cannot start Harmony emulator ${name}: ${failure}. See ${logFile}. Try starting it in DevEco Studio to resolve any license or login requirements.`, { operation: 'start-emulator' });
79
+ }
80
+ },
81
+ };
82
+ }
@@ -0,0 +1,2 @@
1
+ import { type Command } from '../command';
2
+ export declare const command: Command;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.command = void 0;
4
+ const command_1 = require("../command");
5
+ const options_1 = require("./options");
6
+ const command = async (argv, io) => {
7
+ const args = (0, command_1.parseCommandArgs)(options_1.parseRunArgs, argv, io);
8
+ if (!args)
9
+ return 0;
10
+ const { projectRoot: root, options } = args;
11
+ const { runHarmonySessionAsync } = await import('./run.js');
12
+ const session = await runHarmonySessionAsync(root, {
13
+ ...options,
14
+ interactiveBundler: true,
15
+ io,
16
+ });
17
+ const { result } = session;
18
+ io.log(`Launched ${result.bundleName} on ${result.device.id} (${result.variant}).`);
19
+ if (session.metro.owner === 'started') {
20
+ io.log('\n› Logs for your project will appear below. Press Ctrl+C to exit.');
21
+ await session.metro.waitAsync();
22
+ }
23
+ return 0;
24
+ };
25
+ exports.command = command;
@@ -0,0 +1,21 @@
1
+ import { readDevelopmentSessionAsync } from '../development/session';
2
+ import { startManagedProcess } from '../process';
3
+ interface MetroOptions {
4
+ interactive?: boolean;
5
+ host?: string;
6
+ port: number;
7
+ privateKeyPath?: string;
8
+ readyTimeoutMs?: number;
9
+ resetCache?: boolean;
10
+ }
11
+ export interface MetroSession {
12
+ owner: 'existing' | 'started';
13
+ development?: Awaited<ReturnType<typeof readDevelopmentSessionAsync>>;
14
+ port: number;
15
+ process?: ReturnType<typeof startManagedProcess>;
16
+ stop(): Promise<unknown>;
17
+ waitAsync(): Promise<void>;
18
+ }
19
+ declare function requireExistingMetroAsync(port: number): Promise<MetroSession>;
20
+ declare function startExpoMetroAsync(projectRoot: string, options: MetroOptions): Promise<MetroSession>;
21
+ export { requireExistingMetroAsync, startExpoMetroAsync, };