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

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 (73) hide show
  1. package/build/args.d.ts +19 -0
  2. package/build/args.js +27 -0
  3. package/build/bin/expo-harmony.d.ts +2 -0
  4. package/build/buildHap/build.d.ts +25 -0
  5. package/build/buildHap/build.js +96 -0
  6. package/build/buildHap/common.d.ts +24 -0
  7. package/build/buildHap/common.js +92 -0
  8. package/build/buildHap/options.d.ts +7 -0
  9. package/build/buildHap/options.js +30 -0
  10. package/build/cli.d.ts +3 -0
  11. package/build/cli.js +202 -0
  12. package/build/doctor/doctor.d.ts +20 -0
  13. package/build/doctor/doctor.js +217 -0
  14. package/build/doctor/options.d.ts +5 -0
  15. package/build/doctor/options.js +15 -0
  16. package/build/entry.d.ts +2 -0
  17. package/build/entry.js +47 -0
  18. package/build/errors.d.ts +12 -0
  19. package/build/errors.js +16 -0
  20. package/build/expo.d.ts +19 -0
  21. package/build/expo.js +53 -0
  22. package/build/exportEmbed/export.d.ts +16 -0
  23. package/build/exportEmbed/export.js +125 -0
  24. package/build/exportEmbed/manifest.d.ts +44 -0
  25. package/build/exportEmbed/manifest.js +171 -0
  26. package/build/exportEmbed/options.d.ts +7 -0
  27. package/build/exportEmbed/options.js +22 -0
  28. package/build/file.d.ts +9 -0
  29. package/build/file.js +100 -0
  30. package/build/index.d.ts +12 -0
  31. package/build/modules/modules.d.ts +42 -0
  32. package/build/modules/modules.js +84 -0
  33. package/build/modules/options.d.ts +10 -0
  34. package/build/modules/options.js +45 -0
  35. package/build/path.d.ts +4 -0
  36. package/build/path.js +27 -0
  37. package/build/prebuild/check.d.ts +6 -0
  38. package/build/prebuild/check.js +139 -0
  39. package/build/prebuild/clean.d.ts +2 -0
  40. package/build/prebuild/clean.js +50 -0
  41. package/build/prebuild/options.d.ts +10 -0
  42. package/build/prebuild/options.js +50 -0
  43. package/build/prebuild/prebuild.d.ts +6 -0
  44. package/build/prebuild/prebuild.js +62 -0
  45. package/build/prebuild/template.d.ts +8 -0
  46. package/build/prebuild/template.js +86 -0
  47. package/build/process.d.ts +31 -0
  48. package/build/process.js +236 -0
  49. package/build/project.d.ts +2 -0
  50. package/build/project.js +23 -0
  51. package/build/projectLock.d.ts +2 -0
  52. package/build/projectLock.js +180 -0
  53. package/build/run/cache.d.ts +11 -0
  54. package/build/run/cache.js +73 -0
  55. package/build/run/devices.d.ts +29 -0
  56. package/build/run/devices.js +211 -0
  57. package/build/run/emulators.d.ts +13 -0
  58. package/build/run/emulators.js +85 -0
  59. package/build/run/install.d.ts +7 -0
  60. package/build/run/install.js +26 -0
  61. package/build/run/metro.d.ts +17 -0
  62. package/build/run/metro.js +156 -0
  63. package/build/run/options.d.ts +13 -0
  64. package/build/run/options.js +60 -0
  65. package/build/run/run.d.ts +51 -0
  66. package/build/run/run.js +174 -0
  67. package/build/start/options.d.ts +7 -0
  68. package/build/start/options.js +31 -0
  69. package/build/tools.d.ts +39 -0
  70. package/build/tools.js +226 -0
  71. package/build/upstream.d.ts +9 -0
  72. package/build/upstream.js +17 -0
  73. package/package.json +4 -4
@@ -0,0 +1,211 @@
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
+ // Only the first HDC column is a selectable target name. The remaining
22
+ // verbose columns describe transport, state, location and connect tool.
23
+ aliases: [fields[0]],
24
+ connectTool: fields[4] || null,
25
+ id: fields[0],
26
+ location: fields[3] || null,
27
+ state: fields[2],
28
+ transport: fields[1],
29
+ };
30
+ });
31
+ }
32
+ function hasCommandFailure(result) {
33
+ const output = `${result.stdout || ''}\n${result.stderr || ''}`;
34
+ return result.code !== 0 || result.timedOut
35
+ // HDC occasionally reports transport and package-manager failures on
36
+ // stdout while still returning exit code 0 (for example,
37
+ // "Connect server failed"). Treat its documented failure vocabulary as
38
+ // authoritative regardless of where it appears on the line.
39
+ || /\[(?:Fail|Error)\]|\b(?:Failure|failed)\b|(?:失败|错误)/iu.test(output);
40
+ }
41
+ async function runHdcAsync(hdc, args, options = {}) {
42
+ const result = await (0, process_1.spawnAsync)(hdc.command, [
43
+ ...hdc.args,
44
+ ...args,
45
+ ], {
46
+ capture: true,
47
+ cwd: options.cwd,
48
+ operation: options.operation || 'hdc',
49
+ outputLimit: options.outputLimit || 256 * 1024,
50
+ timeoutMs: options.timeoutMs || 60_000,
51
+ });
52
+ if (!options.allowFailure && hasCommandFailure(result)) {
53
+ const diagnostics = (0, process_1.formatDiagnostics)(result, 2_000);
54
+ 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' });
55
+ }
56
+ return result;
57
+ }
58
+ async function listConnectedDevicesAsync(hdc, options) {
59
+ const result = await runHdcAsync(hdc, ['list', 'targets', '-v'], {
60
+ code: 'ERR_HARMONY_DEVICE_LIST',
61
+ cwd: options.cwd,
62
+ message: 'Cannot list Harmony devices',
63
+ operation: 'list-devices',
64
+ timeoutMs: options.timeoutMs || 15_000,
65
+ });
66
+ const targets = parseHdcTargets(result.stdout || result.stderr);
67
+ return targets.filter(target => target.state.toLowerCase() === 'connected');
68
+ }
69
+ async function selectDeviceAsync(hdc, requested, options = {}) {
70
+ if (requested !== undefined && (!requested || /[\0\r\n]/u.test(requested) || requested.trim() !== requested)) {
71
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--device must be a non-empty HDC id or exact emulator name.', { operation: 'select-device' });
72
+ }
73
+ const connected = await listConnectedDevicesAsync(hdc, { cwd: options.cwd });
74
+ if (requested) {
75
+ const matches = connected.filter(target => target.id === requested || target.aliases.includes(requested));
76
+ if (matches.length === 1)
77
+ return matches[0];
78
+ if (matches.length > 1) {
79
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_AMBIGUOUS', `The requested device name matches multiple connected targets: ${requested}.`, { operation: 'select-device' });
80
+ }
81
+ if (!options.emulator) {
82
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', `The requested Harmony device is not connected: ${requested}.`, { operation: 'select-device' });
83
+ }
84
+ }
85
+ else {
86
+ if (connected.length === 1)
87
+ return connected[0];
88
+ if (connected.length > 1) {
89
+ 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' });
90
+ }
91
+ }
92
+ if (!options.emulator) {
93
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', 'No connected Harmony device was reported by HDC.', { operation: 'select-device' });
94
+ }
95
+ const instances = await (0, emulators_1.listEmulatorsAsync)(options.emulator, { cwd: options.cwd });
96
+ // Prefer an instance that is already booting when HDC is not connected yet.
97
+ const running = instances.filter(instance => instance.running);
98
+ const candidates = requested
99
+ ? instances.filter(instance => instance.name === requested)
100
+ : running.length ? running : instances;
101
+ if (candidates.length === 0) {
102
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_DEVICE_NOT_FOUND', requested
103
+ ? `No connected HDC target or local emulator matches ${requested}. Available emulators: ${instances.map(instance => instance.name).join(', ') || 'none'}.`
104
+ : 'No connected Harmony device or local emulator was found. Create an emulator in DevEco Studio Device Manager first.', { operation: 'select-device' });
105
+ }
106
+ if (candidates.length > 1) {
107
+ 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' });
108
+ }
109
+ const instance = candidates[0];
110
+ let launch;
111
+ if (!instance.running) {
112
+ if (!options.emulatorLogFile) {
113
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', 'An emulator log file is required to start a Harmony emulator.', { operation: 'start-emulator' });
114
+ }
115
+ options.onProgress?.(`Starting Harmony emulator ${instance.name}`);
116
+ launch = (0, emulators_1.startEmulator)(options.emulator, instance.name, options.emulatorLogFile, options.cwd);
117
+ }
118
+ options.onProgress?.(`Waiting for Harmony emulator ${instance.name} to connect`);
119
+ const deadline = Date.now() + (options.timeoutMs || 120_000);
120
+ while (Date.now() < deadline) {
121
+ launch?.assertRunning();
122
+ const devices = await listConnectedDevicesAsync(hdc, {
123
+ cwd: options.cwd,
124
+ timeoutMs: Math.max(1, Math.min(15_000, deadline - Date.now())),
125
+ });
126
+ for (const device of devices) {
127
+ if (Date.now() >= deadline)
128
+ break;
129
+ // DevEco may report hw.hdc.port as "notset" even while running. Query
130
+ // the guest's instance name instead of guessing from a newly seen ID.
131
+ if (!/^(?:127\.0\.0\.1|localhost|\[::1\]):\d+$/u.test(device.id))
132
+ continue;
133
+ const identity = await runHdcAsync(hdc, ['-t', device.id, 'shell', 'param', 'get', 'ohos.qemu.hvd.name'], {
134
+ allowFailure: true,
135
+ cwd: options.cwd,
136
+ operation: 'identify-emulator',
137
+ timeoutMs: Math.max(1, Math.min(5_000, deadline - Date.now())),
138
+ });
139
+ if (identity.code !== 0 || identity.timedOut || identity.stdout.trim() !== instance.name)
140
+ continue;
141
+ if (Date.now() >= deadline)
142
+ break;
143
+ const boot = await runHdcAsync(hdc, ['-t', device.id, 'shell', 'param', 'get', 'bootevent.boot.completed'], {
144
+ allowFailure: true,
145
+ cwd: options.cwd,
146
+ operation: 'wait-emulator-boot',
147
+ timeoutMs: Math.max(1, Math.min(5_000, deadline - Date.now())),
148
+ });
149
+ if (!hasCommandFailure(boot) && boot.stdout.trim() === 'true')
150
+ return device;
151
+ }
152
+ if (Date.now() >= deadline)
153
+ break;
154
+ await (0, promises_1.setTimeout)(Math.min(1_000, deadline - Date.now()));
155
+ }
156
+ 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' });
157
+ }
158
+ async function installHapAsync(hdc, device, hap, options = {}) {
159
+ await runHdcAsync(hdc, ['-t', device.id, 'install', '-r', hap], {
160
+ code: 'ERR_HARMONY_INSTALL_FAILED',
161
+ cwd: options.cwd,
162
+ message: `Cannot install the Harmony HAP on ${device.id}`,
163
+ operation: 'install-hap',
164
+ timeoutMs: options.timeoutMs || 2 * 60_000,
165
+ });
166
+ }
167
+ async function configureMetroPortAsync(hdc, device, port, options = {}) {
168
+ const deviceEndpoint = `tcp:${options.devicePort || 8081}`;
169
+ const hostEndpoint = `tcp:${port}`;
170
+ await runHdcAsync(hdc, ['-t', device.id, 'fport', 'rm', deviceEndpoint, hostEndpoint], {
171
+ allowFailure: true,
172
+ cwd: options.cwd,
173
+ operation: 'remove-metro-port',
174
+ timeoutMs: 15_000,
175
+ });
176
+ await runHdcAsync(hdc, ['-t', device.id, 'rport', deviceEndpoint, hostEndpoint], {
177
+ code: 'ERR_HARMONY_METRO_FORWARD',
178
+ cwd: options.cwd,
179
+ message: `Cannot reverse Metro port ${port} for ${device.id}`,
180
+ operation: 'reverse-metro-port',
181
+ timeoutMs: 15_000,
182
+ });
183
+ }
184
+ async function launchAppAsync(hdc, device, bundleName, abilityName, options = {}) {
185
+ await runHdcAsync(hdc, [
186
+ '-t', device.id,
187
+ 'shell',
188
+ 'aa',
189
+ 'force-stop',
190
+ bundleName,
191
+ ], {
192
+ allowFailure: true,
193
+ cwd: options.cwd,
194
+ operation: 'force-stop-app',
195
+ timeoutMs: 15_000,
196
+ });
197
+ await runHdcAsync(hdc, [
198
+ '-t', device.id,
199
+ 'shell',
200
+ 'aa',
201
+ 'start',
202
+ '-a', abilityName,
203
+ '-b', bundleName,
204
+ ], {
205
+ code: 'ERR_HARMONY_LAUNCH_FAILED',
206
+ cwd: options.cwd,
207
+ message: `Cannot launch ${bundleName} on ${device.id}`,
208
+ operation: 'launch-app',
209
+ timeoutMs: 30_000,
210
+ });
211
+ }
@@ -0,0 +1,13 @@
1
+ import type { HarmonyTool } from '../tools';
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,85 @@
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
+ // Emulator may remain alive for the whole GUI session. Give it its own
55
+ // process group and file-backed output so it survives the CLI/Metro exit.
56
+ // Default instance/image paths match the ones used by -list -details.
57
+ const child = (0, cross_spawn_1.default)(tool.command, [...tool.args, '-start', name], {
58
+ cwd,
59
+ detached: true,
60
+ shell: false,
61
+ stdio: ['ignore', log, log],
62
+ windowsHide: false,
63
+ });
64
+ child.once('error', (cause) => {
65
+ failure = cause.message;
66
+ });
67
+ child.once('exit', (code, signal) => {
68
+ // Some versions use a short-lived launcher. A zero exit alone does not
69
+ // prove readiness; the caller still checks the guest's name and boot state.
70
+ if (code !== 0)
71
+ failure = signal ? `terminated by ${signal}` : `exited with code ${code}`;
72
+ });
73
+ child.unref();
74
+ }
75
+ finally {
76
+ node_fs_1.default.closeSync(log);
77
+ }
78
+ return {
79
+ assertRunning() {
80
+ if (failure) {
81
+ 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' });
82
+ }
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,7 @@
1
+ import type { HarmonyBuildPlan, HarmonyToolchain } from '../tools';
2
+ interface InstallOptions {
3
+ timeoutMs?: number;
4
+ }
5
+ /** Runs OHPM against the generated project without rewriting its manifest. */
6
+ declare function installHarmonyDependenciesAsync(plan: HarmonyBuildPlan, toolchain: HarmonyToolchain, options?: InstallOptions): Promise<void>;
7
+ export { installHarmonyDependenciesAsync };
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.installHarmonyDependenciesAsync = installHarmonyDependenciesAsync;
4
+ const errors_1 = require("../errors");
5
+ const process_1 = require("../process");
6
+ function installMessage(result) {
7
+ const diagnostics = (0, process_1.formatDiagnostics)(result);
8
+ return `OHPM install exited with code ${result.code}${result.timedOut ? ' after timing out' : ''}.`
9
+ + `${diagnostics ? `\n${diagnostics}` : ''}`;
10
+ }
11
+ /** Runs OHPM against the generated project without rewriting its manifest. */
12
+ async function installHarmonyDependenciesAsync(plan, toolchain, options = {}) {
13
+ const result = await (0, process_1.spawnAsync)(toolchain.ohpm.command, [...toolchain.ohpm.args, 'install', '--all'], {
14
+ capture: true,
15
+ cwd: plan.harmonyRoot,
16
+ operation: 'ohpm-install',
17
+ outputLimit: 2 * 1024 * 1024,
18
+ timeoutMs: options.timeoutMs || 5 * 60_000,
19
+ });
20
+ if (result.code !== 0 || result.timedOut) {
21
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_OHPM_FAILED', installMessage(result), {
22
+ exitCode: result.code || 1,
23
+ operation: 'ohpm-install',
24
+ });
25
+ }
26
+ }
@@ -0,0 +1,17 @@
1
+ import { startManagedProcess } from '../process';
2
+ interface MetroOptions {
3
+ interactive?: boolean;
4
+ port: number;
5
+ readyTimeoutMs?: number;
6
+ resetCache?: boolean;
7
+ }
8
+ export interface MetroSession {
9
+ owner: 'existing' | 'started';
10
+ port: number;
11
+ process?: ReturnType<typeof startManagedProcess>;
12
+ stop(): Promise<unknown>;
13
+ waitAsync(): Promise<void>;
14
+ }
15
+ declare function requireExistingMetroAsync(port: number): Promise<MetroSession>;
16
+ declare function startExpoMetroAsync(projectRoot: string, options: MetroOptions): Promise<MetroSession>;
17
+ export { requireExistingMetroAsync, startExpoMetroAsync, };
@@ -0,0 +1,156 @@
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.requireExistingMetroAsync = requireExistingMetroAsync;
7
+ exports.startExpoMetroAsync = startExpoMetroAsync;
8
+ const node_http_1 = __importDefault(require("node:http"));
9
+ const errors_1 = require("../errors");
10
+ const process_1 = require("../process");
11
+ const expo_1 = require("../expo");
12
+ function delay(milliseconds) {
13
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
14
+ }
15
+ function probeMetroAsync(port, options = {}) {
16
+ const timeoutMs = options.timeoutMs || 750;
17
+ return new Promise((resolve) => {
18
+ let settled = false;
19
+ const finish = (value) => {
20
+ if (settled)
21
+ return;
22
+ settled = true;
23
+ resolve(value);
24
+ };
25
+ const request = node_http_1.default.get({
26
+ headers: { Accept: 'text/plain' },
27
+ host: options.host || '127.0.0.1',
28
+ path: '/status',
29
+ port,
30
+ }, (response) => {
31
+ let body = '';
32
+ response.setEncoding('utf8');
33
+ response.on('data', (chunk) => {
34
+ if (body.length < 1_024)
35
+ body += chunk;
36
+ });
37
+ response.on('end', () => finish(response.statusCode === 200 && body.trim() === 'packager-status:running'
38
+ ? 'metro'
39
+ : 'occupied'));
40
+ });
41
+ request.setTimeout(timeoutMs, () => {
42
+ request.destroy();
43
+ finish('occupied');
44
+ });
45
+ request.on('error', (error) => finish(error.code === 'ECONNREFUSED' || error.code === 'EHOSTUNREACH'
46
+ ? 'free'
47
+ : 'occupied'));
48
+ });
49
+ }
50
+ async function requireExistingMetroAsync(port) {
51
+ const status = await probeMetroAsync(port);
52
+ if (status === 'metro') {
53
+ return {
54
+ owner: 'existing',
55
+ port,
56
+ stop: async () => { },
57
+ waitAsync: async () => { },
58
+ };
59
+ }
60
+ const code = status === 'free' ? 'ERR_HARMONY_METRO_UNAVAILABLE' : 'ERR_HARMONY_METRO_PORT_IN_USE';
61
+ const message = status === 'free'
62
+ ? `No Metro server is running on port ${port}; start Expo Metro before using --no-bundler.`
63
+ : `Port ${port} is occupied by a process that is not a compatible Metro server.`;
64
+ throw new errors_1.HarmonyCliError(code, message, { operation: 'metro-probe' });
65
+ }
66
+ async function startExpoMetroAsync(projectRoot, options) {
67
+ const before = await probeMetroAsync(options.port);
68
+ if (before === 'metro') {
69
+ return {
70
+ owner: 'existing',
71
+ port: options.port,
72
+ stop: async () => { },
73
+ waitAsync: async () => { },
74
+ };
75
+ }
76
+ if (before === 'occupied') {
77
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_METRO_PORT_IN_USE', `Port ${options.port} is occupied by a process that is not a compatible Metro server.`, { operation: 'metro-probe' });
78
+ }
79
+ const expo = (0, expo_1.resolveExpoCli)(projectRoot);
80
+ const managed = (0, process_1.startManagedProcess)(process.execPath, [
81
+ expo.cliPath,
82
+ 'start',
83
+ projectRoot,
84
+ '--dev-client',
85
+ '--port', String(options.port),
86
+ ...(options.resetCache ? ['--clear'] : []),
87
+ ], {
88
+ cwd: projectRoot,
89
+ env: {
90
+ ...process.env,
91
+ EXPO_METRO_TARGET: 'harmony',
92
+ },
93
+ operation: 'expo-metro',
94
+ outputLimit: 1024 * 1024,
95
+ stdio: options.interactive ? 'inherit' : 'pipe',
96
+ });
97
+ let exitResult = null;
98
+ let exitError = null;
99
+ managed.completion.then((result) => {
100
+ exitResult = result;
101
+ }, (error) => {
102
+ exitError = error;
103
+ });
104
+ const startedAt = Date.now();
105
+ const timeoutMs = options.readyTimeoutMs || 60_000;
106
+ try {
107
+ while (Date.now() - startedAt < timeoutMs) {
108
+ if (exitError instanceof errors_1.HarmonyCliError) {
109
+ throw new errors_1.HarmonyCliError(exitError.code, exitError.message, {
110
+ cause: exitError,
111
+ exitCode: exitError.exitCode,
112
+ operation: exitError.operation,
113
+ });
114
+ }
115
+ if (exitError) {
116
+ const failure = exitError;
117
+ throw new errors_1.HarmonyCliError(failure.code || 'ERR_HARMONY_METRO_EXITED', failure.message || 'Expo Metro failed before becoming ready.', { cause: exitError, exitCode: failure.exitCode, operation: failure.operation });
118
+ }
119
+ if (exitResult) {
120
+ const diagnostics = (0, process_1.formatDiagnostics)(exitResult);
121
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_METRO_EXITED', `Expo Metro exited before becoming ready with code ${exitResult.code}.${diagnostics ? `\n${diagnostics}` : ''}`, { exitCode: exitResult.code || 1, operation: 'expo-metro' });
122
+ }
123
+ if (await probeMetroAsync(options.port) === 'metro') {
124
+ return {
125
+ owner: 'started',
126
+ port: options.port,
127
+ process: managed,
128
+ stop: () => managed.stop(),
129
+ async waitAsync() {
130
+ const result = await managed.completion;
131
+ if (managed.wasStopped())
132
+ return;
133
+ const interrupted = result.signal === 'SIGINT' || result.signal === 'SIGTERM';
134
+ if (result.code === 0 || interrupted)
135
+ return;
136
+ const diagnostics = (0, process_1.formatDiagnostics)(result);
137
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_METRO_EXITED', `Expo Metro exited with code ${result.code}.${diagnostics ? `\n${diagnostics}` : ''}`, { exitCode: result.code || 1, operation: 'expo-metro' });
138
+ },
139
+ };
140
+ }
141
+ await delay(150);
142
+ }
143
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_METRO_TIMEOUT', `Expo Metro did not become ready on port ${options.port} within ${timeoutMs}ms.`, { operation: 'expo-metro' });
144
+ }
145
+ catch (error) {
146
+ await managed.stop();
147
+ if (error instanceof errors_1.HarmonyCliError) {
148
+ throw new errors_1.HarmonyCliError(error.code, error.message, {
149
+ cause: error,
150
+ exitCode: error.exitCode,
151
+ operation: error.operation,
152
+ });
153
+ }
154
+ throw new errors_1.HarmonyCliError(error.code || 'ERR_HARMONY_METRO_EXITED', error.message || 'Expo Metro failed while waiting for the server to become ready.', { cause: error, exitCode: error.exitCode, operation: error.operation });
155
+ }
156
+ }
@@ -0,0 +1,13 @@
1
+ declare function parseRunArgs(argv: string[]): {
2
+ appId: string;
3
+ device: string;
4
+ help: boolean;
5
+ noBundler: boolean;
6
+ noInstall: boolean;
7
+ port: number;
8
+ project: string;
9
+ resetCache: boolean;
10
+ sync: boolean;
11
+ variant: "debug" | "release";
12
+ };
13
+ export { parseRunArgs };
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseRunArgs = parseRunArgs;
4
+ const errors_1 = require("../errors");
5
+ const args_1 = require("../args");
6
+ const RunOptions = {
7
+ ...args_1.CommonOptions,
8
+ 'app-id': { type: 'string' },
9
+ 'device': { type: 'string' },
10
+ 'no-bundler': { type: 'boolean' },
11
+ 'no-install': { type: 'boolean' },
12
+ 'port': { type: 'string' },
13
+ 'reset-cache': { type: 'boolean' },
14
+ 'sync': { type: 'boolean' },
15
+ 'variant': { type: 'string' },
16
+ };
17
+ function parseRunArgs(argv) {
18
+ const { positionals, values } = (0, args_1.parseArgs)(RunOptions, argv);
19
+ if (positionals.length > 1) {
20
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected run positional argument: ${positionals[1]}`, {
21
+ operation: 'parse-arguments',
22
+ });
23
+ }
24
+ const variant = values.variant || 'debug';
25
+ if (variant !== 'debug' && variant !== 'release') {
26
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--variant must be debug or release.', {
27
+ operation: 'parse-arguments',
28
+ });
29
+ }
30
+ const port = values.port === undefined ? 8081 : Number(values.port);
31
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
32
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--port must be an integer between 1 and 65535.', {
33
+ operation: 'parse-arguments',
34
+ });
35
+ }
36
+ const appId = values['app-id'];
37
+ if (appId !== undefined && (!appId || appId.trim() !== appId || appId.includes('\0'))) {
38
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--app-id must be a non-empty Harmony bundle name.', {
39
+ operation: 'parse-arguments',
40
+ });
41
+ }
42
+ const device = values.device;
43
+ if (device !== undefined && (!device || device.trim() !== device || /[\0\r\n]/u.test(device))) {
44
+ throw new errors_1.HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--device must be a non-empty HDC id or exact emulator name.', {
45
+ operation: 'parse-arguments',
46
+ });
47
+ }
48
+ return {
49
+ appId,
50
+ device,
51
+ help: Boolean(values.help),
52
+ noBundler: Boolean(values['no-bundler']),
53
+ noInstall: Boolean(values['no-install']),
54
+ port,
55
+ project: positionals[0],
56
+ resetCache: Boolean(values['reset-cache']),
57
+ sync: Boolean(values.sync),
58
+ variant: variant,
59
+ };
60
+ }
@@ -0,0 +1,51 @@
1
+ import { type MetroSession } from './metro';
2
+ export interface HarmonyRunOptions {
3
+ appId?: string;
4
+ device?: string;
5
+ io?: Pick<Console, 'error' | 'log' | 'warn'>;
6
+ noBundler?: boolean;
7
+ noInstall?: boolean;
8
+ port?: number;
9
+ resetCache?: boolean;
10
+ sync?: boolean;
11
+ variant?: 'debug' | 'release';
12
+ }
13
+ export interface HarmonyRunResult {
14
+ bundleName: string;
15
+ device: {
16
+ id: string;
17
+ transport: string;
18
+ };
19
+ export: null | {
20
+ assetCount: number;
21
+ bundleSha256: string;
22
+ sourceMapSha256: string;
23
+ };
24
+ hapPath: string;
25
+ installed: boolean;
26
+ launched: true;
27
+ metro: {
28
+ owner: 'disabled' | 'existing' | 'started';
29
+ port: number;
30
+ };
31
+ ok: true;
32
+ schemaVersion: 1;
33
+ steps: Record<string, number>;
34
+ variant: 'debug' | 'release';
35
+ }
36
+ interface HarmonyRunSessionOptions extends HarmonyRunOptions {
37
+ /** Give a CLI-started Metro process ownership of the current terminal. */
38
+ interactiveBundler?: boolean;
39
+ }
40
+ interface HarmonyRunSession {
41
+ metro: MetroSession | {
42
+ owner: 'disabled';
43
+ port: number;
44
+ stop(): Promise<void>;
45
+ waitAsync(): Promise<void>;
46
+ };
47
+ result: HarmonyRunResult;
48
+ }
49
+ declare function runHarmonySessionAsync(projectRoot: string, options?: HarmonyRunSessionOptions): Promise<HarmonyRunSession>;
50
+ declare function runHarmonyAsync(projectRoot: string, options?: HarmonyRunOptions): Promise<HarmonyRunResult>;
51
+ export { runHarmonyAsync, runHarmonySessionAsync };