@expo-harmony/cli 55.0.26-harmony.1

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,237 @@
1
+ import http from 'node:http';
2
+
3
+ import { HarmonyCliError } from '../errors';
4
+ import { formatDiagnostics, startManagedProcess, type ProcessResult } from '../process';
5
+ import { resolveExpoCli } from '../expo';
6
+
7
+ type MetroStatus = 'free' | 'metro' | 'occupied';
8
+
9
+ interface MetroOptions {
10
+ interactive?: boolean;
11
+ port: number;
12
+ readyTimeoutMs?: number;
13
+ resetCache?: boolean;
14
+ }
15
+
16
+ export interface MetroSession {
17
+ owner: 'existing' | 'started';
18
+ port: number;
19
+ process?: ReturnType<typeof startManagedProcess>;
20
+ stop(): Promise<unknown>;
21
+ waitAsync(): Promise<void>;
22
+ }
23
+
24
+ function delay(milliseconds) {
25
+ return new Promise(resolve => setTimeout(resolve, milliseconds));
26
+ }
27
+
28
+ function probeMetroAsync(
29
+ port: number,
30
+ options: { host?: string; timeoutMs?: number } = {}
31
+ ): Promise<MetroStatus> {
32
+ const timeoutMs = options.timeoutMs || 750;
33
+
34
+ return new Promise<MetroStatus>((resolve) => {
35
+ let settled = false;
36
+
37
+ const finish = (value) => {
38
+ if (settled) return;
39
+
40
+ settled = true;
41
+ resolve(value);
42
+ };
43
+
44
+ const request = http.get({
45
+ headers: { Accept: 'text/plain' },
46
+ host: options.host || '127.0.0.1',
47
+ path: '/status',
48
+ port,
49
+ }, (response) => {
50
+ let body = '';
51
+
52
+ response.setEncoding('utf8');
53
+ response.on('data', (chunk) => {
54
+ if (body.length < 1_024) body += chunk;
55
+ });
56
+ response.on('end', () => finish(
57
+ response.statusCode === 200 && body.trim() === 'packager-status:running'
58
+ ? 'metro'
59
+ : 'occupied'
60
+ ));
61
+ });
62
+
63
+ request.setTimeout(timeoutMs, () => {
64
+ request.destroy();
65
+ finish('occupied');
66
+ });
67
+
68
+ request.on('error', (error: NodeJS.ErrnoException) => finish(
69
+ error.code === 'ECONNREFUSED' || error.code === 'EHOSTUNREACH'
70
+ ? 'free'
71
+ : 'occupied'
72
+ ));
73
+ });
74
+ }
75
+
76
+ async function requireExistingMetroAsync(port: number): Promise<MetroSession> {
77
+ const status = await probeMetroAsync(port);
78
+
79
+ if (status === 'metro') {
80
+ return {
81
+ owner: 'existing',
82
+ port,
83
+ stop: async () => {},
84
+ waitAsync: async () => {},
85
+ };
86
+ }
87
+
88
+ const code = status === 'free' ? 'ERR_HARMONY_METRO_UNAVAILABLE' : 'ERR_HARMONY_METRO_PORT_IN_USE';
89
+ const message = status === 'free'
90
+ ? `No Metro server is running on port ${port}; start Expo Metro before using --no-bundler.`
91
+ : `Port ${port} is occupied by a process that is not a compatible Metro server.`;
92
+
93
+ throw new HarmonyCliError(code, message, { operation: 'metro-probe' });
94
+ }
95
+
96
+ async function startExpoMetroAsync(
97
+ projectRoot: string,
98
+ options: MetroOptions
99
+ ): Promise<MetroSession> {
100
+ const before = await probeMetroAsync(options.port);
101
+
102
+ if (before === 'metro') {
103
+ return {
104
+ owner: 'existing',
105
+ port: options.port,
106
+ stop: async () => {},
107
+ waitAsync: async () => {},
108
+ };
109
+ }
110
+
111
+ if (before === 'occupied') {
112
+ throw new HarmonyCliError(
113
+ 'ERR_HARMONY_METRO_PORT_IN_USE',
114
+ `Port ${options.port} is occupied by a process that is not a compatible Metro server.`,
115
+ { operation: 'metro-probe' }
116
+ );
117
+ }
118
+
119
+ const expo = resolveExpoCli(projectRoot);
120
+ const managed = startManagedProcess(process.execPath, [
121
+ expo.cliPath,
122
+ 'start',
123
+ projectRoot,
124
+ '--dev-client',
125
+ '--port', String(options.port),
126
+ ...(options.resetCache ? ['--clear'] : []),
127
+ ], {
128
+ cwd: projectRoot,
129
+ env: {
130
+ ...process.env,
131
+ EXPO_METRO_TARGET: 'harmony',
132
+ },
133
+ operation: 'expo-metro',
134
+ outputLimit: 1024 * 1024,
135
+ stdio: options.interactive ? 'inherit' : 'pipe',
136
+ });
137
+ let exitResult: ProcessResult | null = null;
138
+ let exitError: unknown = null;
139
+
140
+ managed.completion.then(
141
+ (result) => {
142
+ exitResult = result;
143
+ },
144
+ (error) => {
145
+ exitError = error;
146
+ }
147
+ );
148
+
149
+ const startedAt = Date.now();
150
+ const timeoutMs = options.readyTimeoutMs || 60_000;
151
+
152
+ try {
153
+ while (Date.now() - startedAt < timeoutMs) {
154
+ if (exitError instanceof HarmonyCliError) {
155
+ throw new HarmonyCliError(exitError.code, exitError.message, {
156
+ cause: exitError,
157
+ exitCode: exitError.exitCode,
158
+ operation: exitError.operation,
159
+ });
160
+ }
161
+
162
+ if (exitError) {
163
+ const failure = exitError as {
164
+ code?: string;
165
+ exitCode?: number;
166
+ message?: string;
167
+ operation?: string;
168
+ };
169
+ throw new HarmonyCliError(
170
+ failure.code || 'ERR_HARMONY_METRO_EXITED',
171
+ failure.message || 'Expo Metro failed before becoming ready.',
172
+ { cause: exitError, exitCode: failure.exitCode, operation: failure.operation }
173
+ );
174
+ }
175
+
176
+ if (exitResult) {
177
+ const diagnostics = formatDiagnostics(exitResult);
178
+ throw new HarmonyCliError(
179
+ 'ERR_HARMONY_METRO_EXITED',
180
+ `Expo Metro exited before becoming ready with code ${exitResult.code}.${diagnostics ? `\n${diagnostics}` : ''}`,
181
+ { exitCode: exitResult.code || 1, operation: 'expo-metro' }
182
+ );
183
+ }
184
+
185
+ if (await probeMetroAsync(options.port) === 'metro') {
186
+ return {
187
+ owner: 'started',
188
+ port: options.port,
189
+ process: managed,
190
+ stop: () => managed.stop(),
191
+ async waitAsync() {
192
+ const result = await managed.completion;
193
+ if (managed.wasStopped()) return;
194
+ const interrupted = result.signal === 'SIGINT' || result.signal === 'SIGTERM';
195
+ if (result.code === 0 || interrupted) return;
196
+
197
+ const diagnostics = formatDiagnostics(result);
198
+ throw new HarmonyCliError(
199
+ 'ERR_HARMONY_METRO_EXITED',
200
+ `Expo Metro exited with code ${result.code}.${diagnostics ? `\n${diagnostics}` : ''}`,
201
+ { exitCode: result.code || 1, operation: 'expo-metro' }
202
+ );
203
+ },
204
+ };
205
+ }
206
+
207
+ await delay(150);
208
+ }
209
+
210
+ throw new HarmonyCliError(
211
+ 'ERR_HARMONY_METRO_TIMEOUT',
212
+ `Expo Metro did not become ready on port ${options.port} within ${timeoutMs}ms.`,
213
+ { operation: 'expo-metro' }
214
+ );
215
+ } catch (error) {
216
+ await managed.stop();
217
+
218
+ if (error instanceof HarmonyCliError) {
219
+ throw new HarmonyCliError(error.code, error.message, {
220
+ cause: error,
221
+ exitCode: error.exitCode,
222
+ operation: error.operation,
223
+ });
224
+ }
225
+
226
+ throw new HarmonyCliError(
227
+ error.code || 'ERR_HARMONY_METRO_EXITED',
228
+ error.message || 'Expo Metro failed while waiting for the server to become ready.',
229
+ { cause: error, exitCode: error.exitCode, operation: error.operation }
230
+ );
231
+ }
232
+ }
233
+
234
+ export {
235
+ requireExistingMetroAsync,
236
+ startExpoMetroAsync,
237
+ };
@@ -0,0 +1,71 @@
1
+ import { HarmonyCliError } from '../errors';
2
+ import { CommonOptions, parseArgs } from '../args';
3
+
4
+ const RunOptions = {
5
+ ...CommonOptions,
6
+ 'app-id': { type: 'string' },
7
+ 'device': { type: 'string' },
8
+ 'no-bundler': { type: 'boolean' },
9
+ 'no-install': { type: 'boolean' },
10
+ 'port': { type: 'string' },
11
+ 'reset-cache': { type: 'boolean' },
12
+ 'sync': { type: 'boolean' },
13
+ 'variant': { type: 'string' },
14
+ } as const;
15
+
16
+ function parseRunArgs(argv: string[]) {
17
+ const { positionals, values } = parseArgs(RunOptions, argv);
18
+
19
+ if (positionals.length > 1) {
20
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected run positional argument: ${positionals[1]}`, {
21
+ operation: 'parse-arguments',
22
+ });
23
+ }
24
+
25
+ const variant = values.variant || 'debug';
26
+
27
+ if (variant !== 'debug' && variant !== 'release') {
28
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--variant must be debug or release.', {
29
+ operation: 'parse-arguments',
30
+ });
31
+ }
32
+
33
+ const port = values.port === undefined ? 8081 : Number(values.port);
34
+
35
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
36
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--port must be an integer between 1 and 65535.', {
37
+ operation: 'parse-arguments',
38
+ });
39
+ }
40
+
41
+ const appId = values['app-id'];
42
+
43
+ if (appId !== undefined && (!appId || appId.trim() !== appId || appId.includes('\0'))) {
44
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--app-id must be a non-empty Harmony bundle name.', {
45
+ operation: 'parse-arguments',
46
+ });
47
+ }
48
+
49
+ const device = values.device;
50
+
51
+ if (device !== undefined && (!device || device.trim() !== device || /[\0\r\n]/u.test(device))) {
52
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--device must be a non-empty HDC id or exact emulator name.', {
53
+ operation: 'parse-arguments',
54
+ });
55
+ }
56
+
57
+ return {
58
+ appId,
59
+ device,
60
+ help: Boolean(values.help),
61
+ noBundler: Boolean(values['no-bundler']),
62
+ noInstall: Boolean(values['no-install']),
63
+ port,
64
+ project: positionals[0],
65
+ resetCache: Boolean(values['reset-cache']),
66
+ sync: Boolean(values.sync),
67
+ variant: variant as 'debug' | 'release',
68
+ };
69
+ }
70
+
71
+ export { parseRunArgs };
package/src/run/run.ts ADDED
@@ -0,0 +1,302 @@
1
+ import path from 'node:path';
2
+
3
+ import {
4
+ configureMetroPortAsync,
5
+ installHapAsync,
6
+ launchAppAsync,
7
+ selectDeviceAsync,
8
+ } from './devices';
9
+ import { HarmonyCliError } from '../errors';
10
+ import { exportEmbedAsync } from '../exportEmbed/export';
11
+ import type { HarmonyExportManifest } from '../exportEmbed/manifest';
12
+ import {
13
+ resolveHarmonyBuildPlanAsync,
14
+ resolveHarmonyEmulator,
15
+ resolveHarmonyToolchain,
16
+ type HarmonyBuildPlan,
17
+ } from '../tools';
18
+ import { installHarmonyDependenciesAsync } from './install';
19
+ import {
20
+ requireExistingMetroAsync,
21
+ startExpoMetroAsync,
22
+ type MetroSession,
23
+ } from './metro';
24
+ import {
25
+ commitHarmonyNativeBuildCacheAsync,
26
+ prepareHarmonyNativeBuildCacheAsync,
27
+ } from './cache';
28
+ import { toPosixPath } from '../path';
29
+ import { withHarmonyProjectLockAsync } from '../projectLock';
30
+ import {
31
+ ensureGeneratedProjectAsync,
32
+ isNonEmptyRegularFile,
33
+ progress,
34
+ runCheckedAsync,
35
+ timed,
36
+ } from '../buildHap/common';
37
+
38
+ export interface HarmonyRunOptions {
39
+ appId?: string;
40
+ device?: string;
41
+ io?: Pick<Console, 'error' | 'log' | 'warn'>;
42
+ noBundler?: boolean;
43
+ noInstall?: boolean;
44
+ port?: number;
45
+ resetCache?: boolean;
46
+ sync?: boolean;
47
+ variant?: 'debug' | 'release';
48
+ }
49
+
50
+ export interface HarmonyRunResult {
51
+ bundleName: string;
52
+ device: { id: string; transport: string };
53
+ export: null | { assetCount: number; bundleSha256: string; sourceMapSha256: string };
54
+ hapPath: string;
55
+ installed: boolean;
56
+ launched: true;
57
+ metro: { owner: 'disabled' | 'existing' | 'started'; port: number };
58
+ ok: true;
59
+ schemaVersion: 1;
60
+ steps: Record<string, number>;
61
+ variant: 'debug' | 'release';
62
+ }
63
+
64
+ interface HarmonyRunSessionOptions extends HarmonyRunOptions {
65
+ /** Give a CLI-started Metro process ownership of the current terminal. */
66
+ interactiveBundler?: boolean;
67
+ }
68
+
69
+ interface HarmonyRunSession {
70
+ metro: MetroSession | {
71
+ owner: 'disabled';
72
+ port: number;
73
+ stop(): Promise<void>;
74
+ waitAsync(): Promise<void>;
75
+ };
76
+ result: HarmonyRunResult;
77
+ }
78
+
79
+ type NormalizedRunOptions = Required<Omit<HarmonyRunSessionOptions, 'appId' | 'device'>>
80
+ & Pick<HarmonyRunSessionOptions, 'appId' | 'device'>;
81
+
82
+ const BundleName = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*){2,}$/u;
83
+
84
+ function resolveRunIdentity(plan: HarmonyBuildPlan, options: NormalizedRunOptions) {
85
+ if (options.appId && !BundleName.test(options.appId)) {
86
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--app-id must contain at least three valid dot-separated segments.', { operation: 'resolve-run' });
87
+ }
88
+
89
+ if (options.appId && !options.noInstall && options.appId !== plan.bundleName) {
90
+ throw new HarmonyCliError(
91
+ 'ERR_HARMONY_APP_ID_MISMATCH',
92
+ `--app-id can differ from the generated bundle name only with --no-install; expected ${plan.bundleName}.`,
93
+ { operation: 'resolve-run' }
94
+ );
95
+ }
96
+
97
+ return {
98
+ abilityName: plan.abilityName,
99
+ bundleName: options.appId || plan.bundleName,
100
+ };
101
+ }
102
+
103
+ async function runHarmonyUnlockedAsync(
104
+ projectRoot: string,
105
+ options: HarmonyRunSessionOptions = {}
106
+ ): Promise<HarmonyRunSession> {
107
+ const normalizedOptions: NormalizedRunOptions = {
108
+ appId: options.appId,
109
+ device: options.device,
110
+ interactiveBundler: Boolean(options.interactiveBundler),
111
+ io: options.io || console,
112
+ noBundler: Boolean(options.noBundler),
113
+ noInstall: Boolean(options.noInstall),
114
+ port: options.port || 8081,
115
+ resetCache: Boolean(options.resetCache),
116
+ sync: Boolean(options.sync),
117
+ variant: options.variant || 'debug',
118
+ };
119
+ const steps: Record<string, number> = {};
120
+
121
+ await ensureGeneratedProjectAsync(projectRoot, normalizedOptions, steps);
122
+
123
+ const plan = await timed(steps, 'buildPlan', () => resolveHarmonyBuildPlanAsync(
124
+ projectRoot,
125
+ { buildMode: normalizedOptions.variant }
126
+ ));
127
+ const identity = resolveRunIdentity(plan, normalizedOptions);
128
+ const toolchain = resolveHarmonyToolchain();
129
+
130
+ progress(normalizedOptions, 'Selecting a Harmony device or emulator');
131
+ const device = await timed(steps, 'device', () => selectDeviceAsync(
132
+ toolchain.hdc,
133
+ normalizedOptions.device,
134
+ {
135
+ cwd: plan.harmonyRoot,
136
+ emulator: resolveHarmonyEmulator(toolchain),
137
+ emulatorLogFile: path.join(projectRoot, '.expo', 'harmony', 'emulator.log'),
138
+ onProgress: message => progress(normalizedOptions, message),
139
+ }
140
+ ));
141
+
142
+ let exportManifest: HarmonyExportManifest | null = null;
143
+ if (normalizedOptions.variant === 'release') {
144
+ progress(normalizedOptions, 'Exporting the release Hermes bundle');
145
+ exportManifest = await timed(steps, 'export', () => exportEmbedAsync(
146
+ projectRoot,
147
+ { resetCache: normalizedOptions.resetCache, skipDoctor: true }
148
+ ));
149
+ } else {
150
+ steps.export = 0;
151
+ }
152
+
153
+ progress(normalizedOptions, 'Installing Harmony project dependencies');
154
+ await timed(steps, 'ohpm', () => installHarmonyDependenciesAsync(plan, toolchain));
155
+
156
+ let metro: HarmonyRunSession['metro'] = {
157
+ owner: 'disabled',
158
+ port: normalizedOptions.port,
159
+ stop: async () => {},
160
+ waitAsync: async () => {},
161
+ };
162
+ try {
163
+ progress(normalizedOptions, 'Checking Harmony native dependency cache');
164
+ const nativeBuildCache = await timed(steps, 'nativeCache', () => (
165
+ prepareHarmonyNativeBuildCacheAsync(projectRoot, plan)
166
+ ));
167
+
168
+ if (nativeBuildCache.changed) {
169
+ progress(normalizedOptions, 'Invalidated stale Harmony native build objects');
170
+ }
171
+
172
+ progress(normalizedOptions, `Building the ${normalizedOptions.variant} HAP`);
173
+ const buildEnv = {
174
+ ...process.env,
175
+ EXPO_HARMONY_NODE: process.env.EXPO_HARMONY_NODE || process.execPath,
176
+ EXPO_METRO_TARGET: 'harmony',
177
+ HERMES_V1_ENABLED: 'true',
178
+ ...(normalizedOptions.variant === 'release' ? { EXPO_HARMONY_BUNDLE_PREBUILT: '1' } : {}),
179
+ ...(toolchain.sdkHome && !process.env.DEVECO_SDK_HOME
180
+ ? { DEVECO_SDK_HOME: toolchain.sdkHome }
181
+ : {}),
182
+ };
183
+ await timed(steps, 'build', () => runCheckedAsync(toolchain.hvigor.command, [
184
+ ...toolchain.hvigor.args,
185
+ ...plan.hvigorArgs,
186
+ ], {
187
+ code: 'ERR_HARMONY_BUILD_FAILED',
188
+ cwd: plan.harmonyRoot,
189
+ env: buildEnv,
190
+ message: 'Hvigor build',
191
+ operation: 'hvigor-build',
192
+ timeoutMs: 15 * 60_000,
193
+ }));
194
+
195
+ if (!isNonEmptyRegularFile(plan.expectedHap)) {
196
+ throw new HarmonyCliError('ERR_HARMONY_HAP_MISSING', 'Hvigor completed without producing the expected non-empty regular HAP.', { operation: 'verify-hap' });
197
+ }
198
+
199
+ await timed(steps, 'nativeCacheCommit', () => commitHarmonyNativeBuildCacheAsync(nativeBuildCache));
200
+
201
+ if (normalizedOptions.variant === 'debug') {
202
+ progress(normalizedOptions, normalizedOptions.noBundler
203
+ ? 'Connecting to the existing Expo Metro server'
204
+ : 'Starting Expo Metro');
205
+ metro = await timed(steps, 'metro', () => normalizedOptions.noBundler
206
+ ? requireExistingMetroAsync(normalizedOptions.port)
207
+ : startExpoMetroAsync(projectRoot, {
208
+ interactive: normalizedOptions.interactiveBundler,
209
+ port: normalizedOptions.port,
210
+ resetCache: normalizedOptions.resetCache,
211
+ }));
212
+
213
+ await timed(steps, 'metroPort', () => configureMetroPortAsync(
214
+ toolchain.hdc,
215
+ device,
216
+ normalizedOptions.port,
217
+ { cwd: plan.harmonyRoot }
218
+ ));
219
+ } else {
220
+ steps.metro = 0;
221
+ steps.metroPort = 0;
222
+ }
223
+
224
+ if (normalizedOptions.noInstall) {
225
+ steps.install = 0;
226
+ } else {
227
+ progress(normalizedOptions, `Installing the HAP on ${device.id}`);
228
+ await timed(steps, 'install', () => installHapAsync(
229
+ toolchain.hdc,
230
+ device,
231
+ plan.expectedHap,
232
+ { cwd: plan.harmonyRoot }
233
+ ));
234
+ }
235
+
236
+ progress(normalizedOptions, `Launching ${identity.bundleName}`);
237
+ await timed(steps, 'launch', () => launchAppAsync(
238
+ toolchain.hdc,
239
+ device,
240
+ identity.bundleName,
241
+ identity.abilityName,
242
+ { cwd: plan.harmonyRoot }
243
+ ));
244
+
245
+ const result: HarmonyRunResult = {
246
+ bundleName: identity.bundleName,
247
+ device: {
248
+ id: device.id,
249
+ transport: device.transport,
250
+ },
251
+ export: exportManifest
252
+ ? {
253
+ assetCount: exportManifest.assets.length,
254
+ bundleSha256: exportManifest.bundle.sha256,
255
+ sourceMapSha256: exportManifest.sourceMap.sha256,
256
+ }
257
+ : null,
258
+ hapPath: toPosixPath(path.relative(projectRoot, plan.expectedHap)),
259
+ installed: !normalizedOptions.noInstall,
260
+ launched: true,
261
+ metro: {
262
+ owner: metro.owner,
263
+ port: metro.port,
264
+ },
265
+ ok: true,
266
+ schemaVersion: 1,
267
+ steps,
268
+ variant: normalizedOptions.variant,
269
+ };
270
+
271
+ return { metro, result };
272
+ } catch (error) {
273
+ await metro.stop();
274
+ throw error;
275
+ }
276
+ }
277
+
278
+ async function runHarmonySessionAsync(
279
+ projectRoot: string,
280
+ options: HarmonyRunSessionOptions = {}
281
+ ): Promise<HarmonyRunSession> {
282
+ return withHarmonyProjectLockAsync(
283
+ projectRoot,
284
+ 'run',
285
+ () => runHarmonyUnlockedAsync(projectRoot, options)
286
+ );
287
+ }
288
+
289
+ async function runHarmonyAsync(
290
+ projectRoot: string,
291
+ options: HarmonyRunOptions = {}
292
+ ): Promise<HarmonyRunResult> {
293
+ const session = await runHarmonySessionAsync(projectRoot, options);
294
+
295
+ try {
296
+ return session.result;
297
+ } finally {
298
+ await session.metro.stop();
299
+ }
300
+ }
301
+
302
+ export { runHarmonyAsync, runHarmonySessionAsync };
@@ -0,0 +1,36 @@
1
+ import { CommonOptions, parseArgs } from '../args';
2
+ import { HarmonyCliError } from '../errors';
3
+
4
+ const StartOptions = {
5
+ ...CommonOptions,
6
+ 'clear': { short: 'c', type: 'boolean' },
7
+ 'port': { type: 'string' },
8
+ 'reset-cache': { type: 'boolean' },
9
+ } as const;
10
+
11
+ function parseStartArgs(argv: string[]) {
12
+ const { positionals, values } = parseArgs(StartOptions, argv);
13
+
14
+ if (positionals.length > 1) {
15
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', `Unexpected start positional argument: ${positionals[1]}`, {
16
+ operation: 'parse-arguments',
17
+ });
18
+ }
19
+
20
+ const port = values.port === undefined ? 8081 : Number(values.port);
21
+
22
+ if (!Number.isInteger(port) || port < 1 || port > 65_535) {
23
+ throw new HarmonyCliError('ERR_HARMONY_CONFIG_INVALID', '--port must be an integer between 1 and 65535.', {
24
+ operation: 'parse-arguments',
25
+ });
26
+ }
27
+
28
+ return {
29
+ help: Boolean(values.help),
30
+ port,
31
+ project: positionals[0],
32
+ resetCache: Boolean(values['reset-cache'] || values.clear),
33
+ };
34
+ }
35
+
36
+ export { parseStartArgs };