maestrostack 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,205 @@
1
+ # MaestroStack
2
+
3
+ MaestroStack is a config-driven CLI for running [Maestro](https://maestro.mobile.dev/)
4
+ mobile tests on [BrowserStack App Automate](https://www.browserstack.com/app-automate).
5
+
6
+ It packages Maestro flow folders, uploads app and test artifacts, and starts
7
+ BrowserStack Maestro builds across one or more real devices from a single command:
8
+
9
+ ```bash
10
+ maestrostack run
11
+ ```
12
+
13
+ Under the hood BrowserStack's Maestro workflow is several manual REST calls: upload
14
+ the app, upload a zipped suite, copy the returned URLs, hand-build a JSON payload
15
+ with the right devices and `execute` paths, and POST to the correct Android or iOS
16
+ build endpoint. MaestroStack turns that into one repeatable command you can run
17
+ locally or in CI.
18
+
19
+ ## Install
20
+
21
+ ```bash
22
+ npm install -g maestrostack
23
+ ```
24
+
25
+ Requires Node.js 18+. Works on macOS, Linux and Windows.
26
+
27
+ ## Quick start
28
+
29
+ ```bash
30
+ # 1. Scaffold a config
31
+ maestrostack init --android
32
+
33
+ # 2. Provide BrowserStack credentials (see Authentication below)
34
+ export BROWSERSTACK_USERNAME=...
35
+ export BROWSERSTACK_ACCESS_KEY=...
36
+
37
+ # 3. Check everything before touching the network
38
+ maestrostack validate
39
+ maestrostack run --dry-run
40
+
41
+ # 4. Run on BrowserStack
42
+ maestrostack run
43
+ ```
44
+
45
+ ## Project layout
46
+
47
+ MaestroStack encourages this structure:
48
+
49
+ ```text
50
+ mobile-tests/
51
+ ├── maestrostack.yml # tool config (NOT a Maestro flow)
52
+ ├── smoke/
53
+ │ ├── login.yml # Maestro flow
54
+ │ └── signup.yml
55
+ ├── regression/
56
+ │ └── checkout.yml
57
+ └── apps/
58
+ └── app-release.apk
59
+ ```
60
+
61
+ > **Root-level YAML files are treated as config files, not Maestro flows.**
62
+ > Keep your Maestro flows inside subfolders. This avoids confusing
63
+ > `maestrostack.yml` (tool config) with `smoke/login.yml` (a test flow).
64
+
65
+ ## Configuration
66
+
67
+ `maestrostack init` writes a starter `maestrostack.yml`. A full example:
68
+
69
+ ```yaml
70
+ version: 1
71
+
72
+ auth:
73
+ username: ${BROWSERSTACK_USERNAME}
74
+ accessKey: ${BROWSERSTACK_ACCESS_KEY}
75
+
76
+ platform: android # android | ios
77
+
78
+ app:
79
+ source: upload # upload | app_url
80
+ path: ./apps/app-release.apk
81
+ customId: SampleApp
82
+
83
+ suite:
84
+ root: .
85
+ packageName: Flows.zip
86
+ customId: SampleTest
87
+ include:
88
+ - smoke/**/*.yml
89
+ - regression/**/*.yml
90
+ exclude:
91
+ - apps/**
92
+
93
+ run:
94
+ project: Maestro_Test
95
+ devices:
96
+ - Samsung Galaxy S20-10.0
97
+ - Google Pixel 7-13.0
98
+ executeMode: explicit # explicit | main
99
+ execute:
100
+ - smoke/login.yml
101
+ - smoke/signup.yml
102
+ - regression/checkout.yml
103
+ options:
104
+ networkLogs: true
105
+ deviceLogs: true
106
+ ```
107
+
108
+ ### App source
109
+
110
+ | `app.source` | Required fields | Notes |
111
+ |---|---|---|
112
+ | `upload` | `path` | Uploads a local `.apk` (Android) / `.ipa` (iOS). |
113
+ | `app_url` | `appUrl` | Reuses an existing `bs://` app reference. |
114
+
115
+ ### Execute modes
116
+
117
+ - **`explicit`** (recommended): list flow files in `run.execute`. MaestroStack
118
+ sends them as the BrowserStack `execute` array - no root `main.yaml` needed.
119
+ - **`main`**: omit `run.execute` and provide a `main.yaml` at the suite root.
120
+ BrowserStack runs it as the entrypoint.
121
+
122
+ ### Authentication
123
+
124
+ Credentials are read from environment variables referenced as `${VAR}` in the
125
+ config. MaestroStack loads a `.env` file from the working directory if present:
126
+
127
+ ```env
128
+ BROWSERSTACK_USERNAME=my_username
129
+ BROWSERSTACK_ACCESS_KEY=my_access_key
130
+ ```
131
+
132
+ If a referenced variable is missing, MaestroStack fails *before* any upload.
133
+ Secrets are never printed.
134
+
135
+ ## Commands
136
+
137
+ | Command | Description |
138
+ |---|---|
139
+ | `maestrostack init` | Create a starter config (`--android`, `--ios`, `--force`). |
140
+ | `maestrostack validate` | Validate config, suite structure, app and devices. |
141
+ | `maestrostack package` | Discover flows and build the suite zip (no upload). |
142
+ | `maestrostack upload-app` | Upload only the app; print its `app_url`. |
143
+ | `maestrostack upload-suite` | Package and upload only the suite; print its `test_suite_url`. |
144
+ | `maestrostack run` | Package, upload and trigger a build. |
145
+
146
+ ### Global options
147
+
148
+ - `-c, --config <path>` - use a specific config file (e.g. `maestrostack.staging.yml`).
149
+ - `--debug` - verbose logging.
150
+
151
+ ### `run` options
152
+
153
+ - `--dry-run` - validate, show the files that would be zipped and the exact
154
+ BrowserStack payload + endpoint, without making any API calls.
155
+ - `--device <name>` - override `run.devices` (repeatable).
156
+ - `--execute <path>` - override `run.execute` (repeatable; forces explicit mode).
157
+
158
+ ```bash
159
+ maestrostack run -c maestrostack.android.smoke.yml
160
+ maestrostack run --device "Google Pixel 7-13.0" --device "Samsung Galaxy S20-10.0"
161
+ maestrostack run --execute smoke/login.yml --dry-run
162
+ ```
163
+
164
+ ## How it works
165
+
166
+ `maestrostack run`:
167
+
168
+ 1. Loads `.env` and the config, substitutes `${VAR}` tokens, validates everything.
169
+ 2. Discovers flow files (ignoring root-level YAML, `node_modules`, `.git`).
170
+ 3. Zips the suite under a `Flows/` prefix preserving folder structure.
171
+ 4. Resolves the app (uploads the local binary or uses an existing `bs://` URL).
172
+ 5. Uploads the suite zip.
173
+ 6. POSTs the build to `.../maestro/v2/{android|ios}/build` and prints the build id.
174
+
175
+ Example output:
176
+
177
+ ```text
178
+ MaestroStack run started
179
+
180
+ Project: Maestro_Test
181
+ Platform: android
182
+ Devices:
183
+ - Samsung Galaxy S20-10.0
184
+ - Google Pixel 7-13.0
185
+
186
+ App: bs://...
187
+ Test suite: bs://...
188
+ Build ID: 5c5ab4338cec13aeb78f7a6977344556ac00bccd6
189
+ ```
190
+
191
+ ## Development
192
+
193
+ ```bash
194
+ npm install
195
+ npm run build # bundle with tsup -> dist/cli.js
196
+ npm run typecheck # tsc --noEmit
197
+ npm test # vitest
198
+ ```
199
+
200
+ The BrowserStack integration is covered by tests with mocked HTTP, so the full
201
+ package to payload path runs without a live account.
202
+
203
+ ## License
204
+
205
+ MIT
package/dist/cli.js ADDED
@@ -0,0 +1,801 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { Command } from "commander";
5
+
6
+ // src/commands/init.ts
7
+ import { writeFile } from "fs/promises";
8
+ import path2 from "path";
9
+
10
+ // src/utils/errors.ts
11
+ var MaestroStackError = class extends Error {
12
+ /** Optional extra lines shown beneath the main message (e.g. hints). */
13
+ details;
14
+ constructor(message, details) {
15
+ super(message);
16
+ this.name = "MaestroStackError";
17
+ this.details = details;
18
+ }
19
+ };
20
+ function isMaestroStackError(err) {
21
+ return err instanceof MaestroStackError;
22
+ }
23
+
24
+ // src/utils/fs.ts
25
+ import { constants } from "fs";
26
+ import fs from "fs/promises";
27
+ import path from "path";
28
+ async function pathExists(p) {
29
+ try {
30
+ await fs.access(p, constants.F_OK);
31
+ return true;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+ async function isFile(p) {
37
+ try {
38
+ const stat = await fs.stat(p);
39
+ return stat.isFile();
40
+ } catch {
41
+ return false;
42
+ }
43
+ }
44
+ async function isDirectory(p) {
45
+ try {
46
+ const stat = await fs.stat(p);
47
+ return stat.isDirectory();
48
+ } catch {
49
+ return false;
50
+ }
51
+ }
52
+ async function ensureDir(dir) {
53
+ await fs.mkdir(dir, { recursive: true });
54
+ }
55
+ function extname(p) {
56
+ return path.extname(p).toLowerCase();
57
+ }
58
+
59
+ // src/utils/logger.ts
60
+ var debugEnabled = false;
61
+ function setDebug(enabled) {
62
+ debugEnabled = enabled;
63
+ }
64
+ var logger = {
65
+ /** Primary user-facing output. */
66
+ info(message = "") {
67
+ console.log(message);
68
+ },
69
+ warn(message) {
70
+ console.warn(`warning: ${message}`);
71
+ },
72
+ error(message) {
73
+ console.error(message);
74
+ },
75
+ /** Only printed when --debug is set. Prefixed so it is visually distinct. */
76
+ debug(message) {
77
+ if (debugEnabled) {
78
+ console.error(`[debug] ${message}`);
79
+ }
80
+ }
81
+ };
82
+
83
+ // src/commands/init.ts
84
+ var CONFIG_NAME = "maestrostack.yml";
85
+ async function initCommand(opts) {
86
+ if (opts.android && opts.ios) {
87
+ throw new MaestroStackError("Pass only one of --android or --ios.");
88
+ }
89
+ const platform = opts.ios ? "ios" : "android";
90
+ const cwd = process.cwd();
91
+ const target = path2.join(cwd, CONFIG_NAME);
92
+ if (await pathExists(target) && !opts.force) {
93
+ throw new MaestroStackError(`${CONFIG_NAME} already exists.`, [
94
+ "Use --force to overwrite it."
95
+ ]);
96
+ }
97
+ await writeFile(target, template(platform), "utf8");
98
+ logger.info(`Created ${CONFIG_NAME} (platform: ${platform}).`);
99
+ logger.info("Next: set BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY, then run `maestrostack validate`.");
100
+ }
101
+ function template(platform) {
102
+ const appPath = platform === "ios" ? "./apps/app-release.ipa" : "./apps/app-release.apk";
103
+ const devices = platform === "ios" ? ["iPhone 15-17.0"] : ["Samsung Galaxy S20-10.0", "Google Pixel 7-13.0"];
104
+ const deviceLines = devices.map((d) => ` - ${d}`).join("\n");
105
+ return [
106
+ "version: 1",
107
+ "",
108
+ "auth:",
109
+ " username: ${BROWSERSTACK_USERNAME}",
110
+ " accessKey: ${BROWSERSTACK_ACCESS_KEY}",
111
+ "",
112
+ `platform: ${platform}`,
113
+ "",
114
+ "app:",
115
+ " source: upload",
116
+ ` path: ${appPath}`,
117
+ " customId: SampleApp",
118
+ "",
119
+ "suite:",
120
+ " root: .",
121
+ " packageName: Flows.zip",
122
+ " customId: SampleTest",
123
+ " include:",
124
+ " - smoke/**/*.yml",
125
+ " - regression/**/*.yml",
126
+ " exclude:",
127
+ " - apps/**",
128
+ "",
129
+ "run:",
130
+ " project: Maestro_Test",
131
+ " devices:",
132
+ deviceLines,
133
+ " executeMode: explicit",
134
+ " execute:",
135
+ " - smoke/login.yml",
136
+ " options:",
137
+ " networkLogs: true",
138
+ " deviceLogs: true",
139
+ ""
140
+ ].join("\n");
141
+ }
142
+
143
+ // src/commands/package.ts
144
+ import path9 from "path";
145
+
146
+ // src/config/loadConfig.ts
147
+ import { readFile } from "fs/promises";
148
+ import path3 from "path";
149
+ import dotenv from "dotenv";
150
+ import { parse as parseYaml } from "yaml";
151
+
152
+ // src/config/resolveEnv.ts
153
+ var ENV_TOKEN = /\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
154
+ function resolveEnv(raw, env = process.env) {
155
+ const missing = /* @__PURE__ */ new Set();
156
+ const resolved = raw.replace(ENV_TOKEN, (_match, name) => {
157
+ const value = env[name];
158
+ if (value === void 0 || value === "") {
159
+ missing.add(name);
160
+ return "";
161
+ }
162
+ return value;
163
+ });
164
+ if (missing.size > 0) {
165
+ const names = [...missing].sort();
166
+ throw new MaestroStackError(
167
+ `Missing required environment variable(s): ${names.join(", ")}`,
168
+ ["Set them in your shell or a .env file in the working directory."]
169
+ );
170
+ }
171
+ return resolved;
172
+ }
173
+
174
+ // src/config/schema.ts
175
+ import { z } from "zod";
176
+ var nonEmpty = (label) => z.string({ required_error: `${label} is required` }).trim().min(1, `${label} must not be empty`);
177
+ var authSchema = z.object({
178
+ username: nonEmpty("BrowserStack username (auth.username)"),
179
+ accessKey: nonEmpty("BrowserStack access key (auth.accessKey)")
180
+ });
181
+ var appUploadSchema = z.object({
182
+ source: z.literal("upload"),
183
+ path: nonEmpty("app.path"),
184
+ customId: z.string().trim().min(1).optional()
185
+ });
186
+ var appUrlSchema = z.object({
187
+ source: z.literal("app_url"),
188
+ appUrl: nonEmpty("app.appUrl").refine((v) => v.startsWith("bs://"), {
189
+ message: "app.appUrl must start with bs://"
190
+ }),
191
+ customId: z.string().trim().min(1).optional()
192
+ });
193
+ var appCustomIdSchema = z.object({
194
+ // Present in the schema so configs validate, but resolution is deferred (see validate.ts).
195
+ source: z.literal("custom_id"),
196
+ customId: nonEmpty("app.customId")
197
+ });
198
+ var appSchema = z.discriminatedUnion("source", [
199
+ appUploadSchema,
200
+ appUrlSchema,
201
+ appCustomIdSchema
202
+ ]);
203
+ var suiteSchema = z.object({
204
+ root: z.string().trim().min(1).default("."),
205
+ packageName: z.string().trim().min(1).default("Flows.zip"),
206
+ customId: z.string().trim().min(1).optional(),
207
+ include: z.array(nonEmpty("suite.include entry")).default(["**/*.yml", "**/*.yaml"]),
208
+ exclude: z.array(z.string()).default([])
209
+ });
210
+ var optionsSchema = z.object({
211
+ networkLogs: z.boolean().optional(),
212
+ deviceLogs: z.boolean().optional()
213
+ }).default({});
214
+ var runSchema = z.object({
215
+ project: nonEmpty("run.project"),
216
+ devices: z.array(nonEmpty("device")).min(1, "run.devices must contain at least one device"),
217
+ executeMode: z.enum(["explicit", "main"]).default("explicit"),
218
+ execute: z.array(nonEmpty("run.execute entry")).optional(),
219
+ options: optionsSchema
220
+ });
221
+ var configSchema = z.object({
222
+ version: z.literal(1, {
223
+ errorMap: () => ({ message: "Unsupported config version. Expected version: 1" })
224
+ }),
225
+ auth: authSchema,
226
+ platform: z.enum(["android", "ios"], {
227
+ errorMap: () => ({ message: "Unsupported platform. Expected android or ios" })
228
+ }),
229
+ app: appSchema,
230
+ suite: suiteSchema.default({}),
231
+ run: runSchema
232
+ });
233
+
234
+ // src/config/loadConfig.ts
235
+ var DEFAULT_CONFIG_NAMES = ["maestrostack.yml", "maestrostack.yaml"];
236
+ async function resolveConfigPath(explicit, cwd = process.cwd()) {
237
+ if (explicit) {
238
+ const resolved = path3.resolve(cwd, explicit);
239
+ if (!await isFile(resolved)) {
240
+ throw new MaestroStackError(`Config file not found: ${explicit}`);
241
+ }
242
+ return resolved;
243
+ }
244
+ for (const name of DEFAULT_CONFIG_NAMES) {
245
+ const candidate = path3.resolve(cwd, name);
246
+ if (await isFile(candidate)) {
247
+ return candidate;
248
+ }
249
+ }
250
+ throw new MaestroStackError("maestrostack.yml not found.", [
251
+ "Run `maestrostack init` to create one, or pass --config <path>."
252
+ ]);
253
+ }
254
+ async function loadConfig(explicit, cwd = process.cwd()) {
255
+ dotenv.config({ path: path3.resolve(cwd, ".env") });
256
+ const configPath = await resolveConfigPath(explicit, cwd);
257
+ logger.debug(`Using config: ${configPath}`);
258
+ const raw = await readFile(configPath, "utf8");
259
+ const substituted = resolveEnv(raw);
260
+ let parsed;
261
+ try {
262
+ parsed = parseYaml(substituted);
263
+ } catch (err) {
264
+ const reason = err instanceof Error ? err.message : String(err);
265
+ throw new MaestroStackError(`Invalid YAML in ${path3.basename(configPath)}.`, [reason]);
266
+ }
267
+ const result = configSchema.safeParse(parsed);
268
+ if (!result.success) {
269
+ const details = result.error.issues.map((issue) => {
270
+ const where = issue.path.length > 0 ? `${issue.path.join(".")}: ` : "";
271
+ return `- ${where}${issue.message}`;
272
+ });
273
+ throw new MaestroStackError("Invalid configuration:", details);
274
+ }
275
+ return { config: result.data, configPath };
276
+ }
277
+
278
+ // src/suite/createZip.ts
279
+ import { createWriteStream } from "fs";
280
+ import path5 from "path";
281
+ import archiver from "archiver";
282
+
283
+ // src/utils/paths.ts
284
+ import path4 from "path";
285
+ var WORK_DIR = ".maestrostack";
286
+ function distDir(cwd = process.cwd()) {
287
+ return path4.join(cwd, WORK_DIR, "dist");
288
+ }
289
+ function toPosix(p) {
290
+ return p.split(/[\\/]+/).join("/");
291
+ }
292
+
293
+ // src/suite/createZip.ts
294
+ var SUITE_PREFIX = "Flows";
295
+ async function createZip(options) {
296
+ const cwd = options.cwd ?? process.cwd();
297
+ const root = path5.resolve(cwd, options.suiteRoot);
298
+ const outDir = distDir(cwd);
299
+ await ensureDir(outDir);
300
+ const zipPath = path5.join(outDir, options.packageName);
301
+ await new Promise((resolve, reject) => {
302
+ const output = createWriteStream(zipPath);
303
+ const archive = archiver("zip", { zlib: { level: 9 } });
304
+ output.on("close", () => resolve());
305
+ output.on("error", reject);
306
+ archive.on("error", reject);
307
+ archive.on("warning", (err) => {
308
+ if (err.code === "ENOENT") return;
309
+ reject(err);
310
+ });
311
+ archive.pipe(output);
312
+ for (const rel of options.files) {
313
+ const absolute = path5.join(root, ...rel.split("/"));
314
+ const entryName = `${SUITE_PREFIX}/${rel}`;
315
+ archive.file(absolute, { name: entryName });
316
+ }
317
+ void archive.finalize();
318
+ });
319
+ return { zipPath, files: options.files };
320
+ }
321
+
322
+ // src/commands/validate.ts
323
+ import path8 from "path";
324
+
325
+ // src/suite/discoverFlows.ts
326
+ import path6 from "path";
327
+ import fg from "fast-glob";
328
+ var BUILT_IN_IGNORES = [
329
+ "*.yml",
330
+ "*.yaml",
331
+ ".git/**",
332
+ "node_modules/**",
333
+ ".maestrostack/**"
334
+ ];
335
+ async function discoverFlows(suite, opts = {}) {
336
+ const cwd = opts.cwd ?? process.cwd();
337
+ const root = path6.resolve(cwd, suite.root);
338
+ if (!await isDirectory(root)) {
339
+ throw new MaestroStackError(`suite.root does not exist: ${suite.root}`);
340
+ }
341
+ const ignore = [...BUILT_IN_IGNORES, ...suite.exclude];
342
+ if (opts.configBasename) {
343
+ ignore.push(opts.configBasename);
344
+ }
345
+ const matches = await fg(suite.include, {
346
+ cwd: root,
347
+ ignore,
348
+ onlyFiles: true,
349
+ dot: false,
350
+ followSymbolicLinks: false
351
+ });
352
+ if (matches.length === 0) {
353
+ throw new MaestroStackError("No Maestro flow files found.", [
354
+ "Maestro flow files should live inside subfolders (root-level YAML files are ignored).",
355
+ `Checked suite.root="${suite.root}" with include=${JSON.stringify(suite.include)}.`
356
+ ]);
357
+ }
358
+ return matches.sort();
359
+ }
360
+
361
+ // src/suite/validateSuite.ts
362
+ import path7 from "path";
363
+ var MAIN_FLOW = "main.yaml";
364
+ async function validateSuite(config, discovered, cwd = process.cwd()) {
365
+ const { run, suite } = config;
366
+ if (run.executeMode === "main") {
367
+ const mainPath = path7.resolve(cwd, suite.root, MAIN_FLOW);
368
+ if (!await isFile(mainPath)) {
369
+ throw new MaestroStackError(
370
+ `executeMode is "main" but ${MAIN_FLOW} was not found in suite.root (${suite.root}).`,
371
+ [`Add a ${MAIN_FLOW} or switch to executeMode: explicit with a run.execute list.`]
372
+ );
373
+ }
374
+ return;
375
+ }
376
+ if (run.execute && run.execute.length > 0) {
377
+ const available = new Set(discovered.map(toPosix));
378
+ const missing = run.execute.map(toPosix).filter((p) => !available.has(p));
379
+ if (missing.length > 0) {
380
+ throw new MaestroStackError(
381
+ "Configured execute file(s) do not exist in the packaged suite:",
382
+ missing.map((p) => `- ${p}`)
383
+ );
384
+ }
385
+ }
386
+ }
387
+
388
+ // src/commands/validate.ts
389
+ var PLATFORM_EXT = {
390
+ android: ".apk",
391
+ ios: ".ipa"
392
+ };
393
+ async function validateApp(config, cwd) {
394
+ const { app, platform } = config;
395
+ if (app.source === "custom_id") {
396
+ throw new MaestroStackError(
397
+ 'app.source "custom_id" is not supported in the MVP.',
398
+ ["Use source: upload (with a path) or source: app_url (with a bs:// url)."]
399
+ );
400
+ }
401
+ if (app.source === "upload") {
402
+ const appPath = path8.resolve(cwd, app.path);
403
+ if (!await isFile(appPath)) {
404
+ throw new MaestroStackError(`app.path does not exist: ${app.path}`);
405
+ }
406
+ const expected = PLATFORM_EXT[platform];
407
+ if (extname(appPath) !== expected) {
408
+ throw new MaestroStackError(
409
+ `Platform "${platform}" requires a ${expected} file, but app.path is "${app.path}".`
410
+ );
411
+ }
412
+ }
413
+ }
414
+ async function validateAll(loaded, cwd) {
415
+ const { config, configPath } = loaded;
416
+ await validateApp(config, cwd);
417
+ const flows = await discoverFlows(config.suite, {
418
+ cwd,
419
+ configBasename: path8.basename(configPath)
420
+ });
421
+ await validateSuite(config, flows, cwd);
422
+ return { flows };
423
+ }
424
+ async function validateCommand(opts) {
425
+ const cwd = process.cwd();
426
+ const loaded = await loadConfig(opts.config, cwd);
427
+ const { flows } = await validateAll(loaded, cwd);
428
+ const { config } = loaded;
429
+ logger.info("Configuration is valid.");
430
+ logger.info("");
431
+ logger.info(`Config: ${path8.relative(cwd, loaded.configPath) || loaded.configPath}`);
432
+ logger.info(`Platform: ${config.platform}`);
433
+ logger.info(`Project: ${config.run.project}`);
434
+ logger.info(`App: ${describeApp(config)}`);
435
+ logger.info(`Devices: ${config.run.devices.length}`);
436
+ for (const device of config.run.devices) {
437
+ logger.info(` - ${device}`);
438
+ }
439
+ logger.info(`Flows: ${flows.length}`);
440
+ for (const flow of flows) {
441
+ logger.info(` - ${flow}`);
442
+ }
443
+ logger.info(`Execute: ${config.run.executeMode}`);
444
+ }
445
+ function describeApp(config) {
446
+ const { app } = config;
447
+ switch (app.source) {
448
+ case "upload":
449
+ return `upload ${app.path}`;
450
+ case "app_url":
451
+ return `app_url ${app.appUrl}`;
452
+ default:
453
+ return app.source;
454
+ }
455
+ }
456
+
457
+ // src/commands/package.ts
458
+ async function packageCommand(opts) {
459
+ const cwd = process.cwd();
460
+ const loaded = await loadConfig(opts.config, cwd);
461
+ const { config } = loaded;
462
+ const { flows } = await validateAll(loaded, cwd);
463
+ const { zipPath } = await createZip({
464
+ files: flows,
465
+ suiteRoot: config.suite.root,
466
+ packageName: config.suite.packageName,
467
+ cwd
468
+ });
469
+ logger.info(`Created suite zip: ${path9.relative(cwd, zipPath) || zipPath}`);
470
+ logger.info("Included flows:");
471
+ for (const flow of flows) {
472
+ logger.info(`- ${flow}`);
473
+ }
474
+ }
475
+
476
+ // src/commands/run.ts
477
+ import path12 from "path";
478
+
479
+ // src/browserstack/uploadApp.ts
480
+ import { openAsBlob } from "fs";
481
+ import path10 from "path";
482
+ import { FormData } from "undici";
483
+
484
+ // src/browserstack/client.ts
485
+ import { fetch } from "undici";
486
+ var BASE_URL = "https://api-cloud.browserstack.com";
487
+ function authHeader(auth) {
488
+ const token = Buffer.from(`${auth.username}:${auth.accessKey}`).toString("base64");
489
+ return `Basic ${token}`;
490
+ }
491
+ async function bsRequest(options) {
492
+ const url = `${BASE_URL}${options.path}`;
493
+ const headers = {
494
+ authorization: authHeader(options.auth)
495
+ };
496
+ let body;
497
+ if (options.jsonBody !== void 0) {
498
+ headers["content-type"] = "application/json";
499
+ body = JSON.stringify(options.jsonBody);
500
+ } else if (options.rawBody !== void 0) {
501
+ body = options.rawBody;
502
+ }
503
+ logger.debug(`${options.method} ${url}`);
504
+ let response;
505
+ try {
506
+ response = await fetch(url, { method: options.method, headers, body });
507
+ } catch (err) {
508
+ const reason = err instanceof Error ? err.message : String(err);
509
+ throw new MaestroStackError(`Failed to reach BrowserStack: ${reason}`);
510
+ }
511
+ const text = await response.text();
512
+ let data = {};
513
+ if (text) {
514
+ try {
515
+ data = JSON.parse(text);
516
+ } catch {
517
+ data = { raw: text };
518
+ }
519
+ }
520
+ if (!response.ok) {
521
+ const message = extractErrorMessage(data) ?? response.statusText;
522
+ throw new MaestroStackError(`BrowserStack API error (${response.status}): ${message}`);
523
+ }
524
+ return data;
525
+ }
526
+ function extractErrorMessage(data) {
527
+ if (data && typeof data === "object") {
528
+ const obj = data;
529
+ for (const key of ["message", "error", "errors"]) {
530
+ const value = obj[key];
531
+ if (typeof value === "string" && value.length > 0) return value;
532
+ }
533
+ }
534
+ return void 0;
535
+ }
536
+
537
+ // src/browserstack/uploadApp.ts
538
+ async function uploadApp(options) {
539
+ if (!await isFile(options.filePath)) {
540
+ throw new MaestroStackError(`app.path does not exist: ${options.filePath}`);
541
+ }
542
+ const form = new FormData();
543
+ const blob = await openAsBlob(options.filePath);
544
+ form.set("file", blob, path10.basename(options.filePath));
545
+ if (options.customId) {
546
+ form.set("custom_id", options.customId);
547
+ }
548
+ return bsRequest({
549
+ path: "/app-automate/maestro/v2/app",
550
+ method: "POST",
551
+ auth: options.auth,
552
+ rawBody: form
553
+ });
554
+ }
555
+
556
+ // src/browserstack/uploadSuite.ts
557
+ import { openAsBlob as openAsBlob2 } from "fs";
558
+ import path11 from "path";
559
+ import { FormData as FormData2 } from "undici";
560
+ async function uploadSuite(options) {
561
+ if (!await isFile(options.zipPath)) {
562
+ throw new MaestroStackError(`Suite zip does not exist: ${options.zipPath}`);
563
+ }
564
+ const form = new FormData2();
565
+ const blob = await openAsBlob2(options.zipPath);
566
+ form.set("file", blob, path11.basename(options.zipPath));
567
+ if (options.customId) {
568
+ form.set("custom_id", options.customId);
569
+ }
570
+ return bsRequest({
571
+ path: "/app-automate/maestro/v2/test-suite",
572
+ method: "POST",
573
+ auth: options.auth,
574
+ rawBody: form
575
+ });
576
+ }
577
+
578
+ // src/browserstack/startBuild.ts
579
+ function buildPath(platform) {
580
+ return `/app-automate/maestro/v2/${platform}/build`;
581
+ }
582
+ function buildPayload(config, resolved) {
583
+ const { run } = config;
584
+ const payload = {
585
+ app: resolved.app,
586
+ testSuite: resolved.testSuite,
587
+ project: run.project,
588
+ devices: run.devices
589
+ };
590
+ if (run.executeMode === "explicit" && run.execute && run.execute.length > 0) {
591
+ payload.execute = run.execute;
592
+ }
593
+ if (run.options.networkLogs !== void 0) {
594
+ payload.networkLogs = run.options.networkLogs;
595
+ }
596
+ if (run.options.deviceLogs !== void 0) {
597
+ payload.deviceLogs = run.options.deviceLogs;
598
+ }
599
+ return payload;
600
+ }
601
+ async function startBuild(options) {
602
+ return bsRequest({
603
+ path: buildPath(options.platform),
604
+ method: "POST",
605
+ auth: options.auth,
606
+ jsonBody: options.payload
607
+ });
608
+ }
609
+
610
+ // src/commands/run.ts
611
+ var APP_PLACEHOLDER = "<resolved after upload>";
612
+ var SUITE_PLACEHOLDER = "<resolved after upload>";
613
+ function applyOverrides(config, opts) {
614
+ let run = config.run;
615
+ if (opts.device && opts.device.length > 0) {
616
+ run = { ...run, devices: opts.device };
617
+ }
618
+ if (opts.execute && opts.execute.length > 0) {
619
+ run = { ...run, execute: opts.execute.map(toPosix), executeMode: "explicit" };
620
+ }
621
+ return { ...config, run };
622
+ }
623
+ async function runCommand(opts) {
624
+ const cwd = process.cwd();
625
+ const loaded = await loadConfig(opts.config, cwd);
626
+ const config = applyOverrides(loaded.config, opts);
627
+ const { flows } = await validateAll({ ...loaded, config }, cwd);
628
+ if (opts.dryRun) {
629
+ printDryRun(config, flows, cwd);
630
+ return;
631
+ }
632
+ const { zipPath } = await createZip({
633
+ files: flows,
634
+ suiteRoot: config.suite.root,
635
+ packageName: config.suite.packageName,
636
+ cwd
637
+ });
638
+ logger.debug(`Created suite zip: ${zipPath}`);
639
+ const appUrl = await resolveApp(config, cwd);
640
+ logger.debug(`Resolved app: ${appUrl}`);
641
+ logger.info(`Uploading test suite (${flows.length} flow(s)) ...`);
642
+ const suiteResult = await uploadSuite({
643
+ auth: config.auth,
644
+ zipPath,
645
+ customId: config.suite.customId
646
+ });
647
+ const payload = buildPayload(config, {
648
+ app: appUrl,
649
+ testSuite: suiteResult.test_suite_url
650
+ });
651
+ logger.info(`Starting ${config.platform} build ...`);
652
+ const build = await startBuild({ auth: config.auth, platform: config.platform, payload });
653
+ printSummary(config, appUrl, suiteResult.test_suite_url, build.build_id);
654
+ }
655
+ async function resolveApp(config, cwd) {
656
+ const { app } = config;
657
+ if (app.source === "app_url") {
658
+ return app.appUrl;
659
+ }
660
+ if (app.source === "upload") {
661
+ logger.info(`Uploading app: ${app.path} ...`);
662
+ const result = await uploadApp({
663
+ auth: config.auth,
664
+ filePath: path12.resolve(cwd, app.path),
665
+ customId: app.customId
666
+ });
667
+ return result.app_url;
668
+ }
669
+ throw new MaestroStackError(`Unsupported app.source: ${app.source}`);
670
+ }
671
+ function previewApp(config) {
672
+ const { app } = config;
673
+ return app.source === "app_url" ? app.appUrl : APP_PLACEHOLDER;
674
+ }
675
+ function printDryRun(config, flows, cwd) {
676
+ logger.info("Dry run only. No API calls made.");
677
+ logger.info("");
678
+ logger.info("Would package:");
679
+ for (const flow of flows) {
680
+ logger.info(`- ${flow}`);
681
+ }
682
+ logger.info("");
683
+ const { app } = config;
684
+ if (app.source === "upload") {
685
+ logger.info(`Would upload app:
686
+ ${app.path}`);
687
+ } else if (app.source === "app_url") {
688
+ logger.info(`Would use app:
689
+ ${app.appUrl}`);
690
+ }
691
+ logger.info("");
692
+ logger.info(`Would call:
693
+ POST ${buildPath(config.platform)}`);
694
+ logger.info("");
695
+ const payload = buildPayload(config, {
696
+ app: previewApp(config),
697
+ testSuite: SUITE_PLACEHOLDER
698
+ });
699
+ logger.info("Payload:");
700
+ logger.info(JSON.stringify(payload, null, 2));
701
+ }
702
+ function printSummary(config, appUrl, testSuiteUrl, buildId) {
703
+ logger.info("");
704
+ logger.info("MaestroStack run started");
705
+ logger.info("");
706
+ logger.info(`Project: ${config.run.project}`);
707
+ logger.info(`Platform: ${config.platform}`);
708
+ logger.info("Devices:");
709
+ for (const device of config.run.devices) {
710
+ logger.info(`- ${device}`);
711
+ }
712
+ logger.info("");
713
+ logger.info(`App: ${appUrl}`);
714
+ logger.info(`Test suite: ${testSuiteUrl}`);
715
+ logger.info(`Build ID: ${buildId}`);
716
+ }
717
+
718
+ // src/commands/uploadApp.ts
719
+ import path13 from "path";
720
+ async function uploadAppCommand(opts) {
721
+ const cwd = process.cwd();
722
+ const { config } = await loadConfig(opts.config, cwd);
723
+ await validateApp(config, cwd);
724
+ if (config.app.source !== "upload") {
725
+ throw new MaestroStackError(
726
+ `upload-app requires app.source "upload" (current: "${config.app.source}").`
727
+ );
728
+ }
729
+ const filePath = path13.resolve(cwd, config.app.path);
730
+ logger.info(`Uploading app: ${config.app.path} ...`);
731
+ const result = await uploadApp({
732
+ auth: config.auth,
733
+ filePath,
734
+ customId: config.app.customId
735
+ });
736
+ logger.info("Uploaded app:");
737
+ logger.info(`app_url: ${result.app_url}`);
738
+ if (result.custom_id) logger.info(`custom_id: ${result.custom_id}`);
739
+ if (result.expiry) logger.info(`expires: ${result.expiry}`);
740
+ }
741
+
742
+ // src/commands/uploadSuite.ts
743
+ async function uploadSuiteCommand(opts) {
744
+ const cwd = process.cwd();
745
+ const loaded = await loadConfig(opts.config, cwd);
746
+ const { config } = loaded;
747
+ const { flows } = await validateAll(loaded, cwd);
748
+ const { zipPath } = await createZip({
749
+ files: flows,
750
+ suiteRoot: config.suite.root,
751
+ packageName: config.suite.packageName,
752
+ cwd
753
+ });
754
+ logger.info(`Uploading test suite (${flows.length} flow(s)) ...`);
755
+ const result = await uploadSuite({
756
+ auth: config.auth,
757
+ zipPath,
758
+ customId: config.suite.customId
759
+ });
760
+ logger.info("Uploaded test suite:");
761
+ logger.info(`test_suite_url: ${result.test_suite_url}`);
762
+ if (result.custom_id) logger.info(`custom_id: ${result.custom_id}`);
763
+ if (result.expiry) logger.info(`expires: ${result.expiry}`);
764
+ }
765
+
766
+ // src/cli.ts
767
+ var program = new Command();
768
+ function collect(value, previous) {
769
+ return [...previous, value];
770
+ }
771
+ function action(fn) {
772
+ return async (rawOpts) => {
773
+ const opts = { ...program.opts(), ...rawOpts };
774
+ if (opts.debug) setDebug(true);
775
+ try {
776
+ await fn(opts);
777
+ } catch (err) {
778
+ if (isMaestroStackError(err)) {
779
+ logger.error(err.message);
780
+ for (const line of err.details ?? []) {
781
+ logger.error(` ${line}`);
782
+ }
783
+ } else {
784
+ logger.error(err instanceof Error ? err.stack ?? err.message : String(err));
785
+ }
786
+ process.exitCode = 1;
787
+ }
788
+ };
789
+ }
790
+ program.name("maestrostack").description("Config-driven CLI for running Maestro tests on BrowserStack App Automate.").version("0.1.0").option("-c, --config <path>", "path to the config file").option("--debug", "enable debug logging");
791
+ program.command("init").description("Create a starter maestrostack.yml").option("--android", "scaffold an Android config").option("--ios", "scaffold an iOS config").option("--force", "overwrite an existing config").action(action(initCommand));
792
+ program.command("validate").description("Validate the config, suite structure and devices").action(action(validateCommand));
793
+ program.command("package").description("Discover flows and create the suite zip without uploading").action(action(packageCommand));
794
+ program.command("upload-app").description("Upload only the app and print its BrowserStack app_url").action(action(uploadAppCommand));
795
+ program.command("upload-suite").description("Package and upload only the Maestro suite").action(action(uploadSuiteCommand));
796
+ program.command("run").description("Package, upload and trigger a BrowserStack Maestro build").option("--dry-run", "validate and print the payload without making API calls").option("--device <name>", "override run.devices (repeatable)", collect, []).option("--execute <path>", "override run.execute (repeatable)", collect, []).action(action(runCommand));
797
+ program.parseAsync(process.argv).catch((err) => {
798
+ logger.error(err instanceof Error ? err.message : String(err));
799
+ process.exitCode = 1;
800
+ });
801
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts","../src/commands/init.ts","../src/utils/errors.ts","../src/utils/fs.ts","../src/utils/logger.ts","../src/commands/package.ts","../src/config/loadConfig.ts","../src/config/resolveEnv.ts","../src/config/schema.ts","../src/suite/createZip.ts","../src/utils/paths.ts","../src/commands/validate.ts","../src/suite/discoverFlows.ts","../src/suite/validateSuite.ts","../src/commands/run.ts","../src/browserstack/uploadApp.ts","../src/browserstack/client.ts","../src/browserstack/uploadSuite.ts","../src/browserstack/startBuild.ts","../src/commands/uploadApp.ts","../src/commands/uploadSuite.ts"],"sourcesContent":["import { Command } from \"commander\";\nimport { initCommand } from \"./commands/init.js\";\nimport { packageCommand } from \"./commands/package.js\";\nimport { runCommand } from \"./commands/run.js\";\nimport { uploadAppCommand } from \"./commands/uploadApp.js\";\nimport { uploadSuiteCommand } from \"./commands/uploadSuite.js\";\nimport { validateCommand } from \"./commands/validate.js\";\nimport { isMaestroStackError } from \"./utils/errors.js\";\nimport { logger, setDebug } from \"./utils/logger.js\";\n\nconst program = new Command();\n\n/** Accumulate repeated CLI options (e.g. --device a --device b) into an array. */\nfunction collect(value: string, previous: string[]): string[] {\n return [...previous, value];\n}\n\n/**\n * Wrap a command action: merge program-level (global) options, apply debug mode,\n * and turn expected errors into a clean message with exit code 1 instead of an\n * unhandled rejection / stack dump.\n */\nfunction action<T extends object>(\n fn: (opts: T & { config?: string; debug?: boolean }) => Promise<void>,\n): (opts: T) => Promise<void> {\n return async (rawOpts: T) => {\n const opts = { ...program.opts(), ...rawOpts };\n if (opts.debug) setDebug(true);\n try {\n await fn(opts);\n } catch (err) {\n if (isMaestroStackError(err)) {\n logger.error(err.message);\n for (const line of err.details ?? []) {\n logger.error(` ${line}`);\n }\n } else {\n logger.error(err instanceof Error ? (err.stack ?? err.message) : String(err));\n }\n process.exitCode = 1;\n }\n };\n}\n\nprogram\n .name(\"maestrostack\")\n .description(\"Config-driven CLI for running Maestro tests on BrowserStack App Automate.\")\n .version(\"0.1.0\")\n .option(\"-c, --config <path>\", \"path to the config file\")\n .option(\"--debug\", \"enable debug logging\");\n\nprogram\n .command(\"init\")\n .description(\"Create a starter maestrostack.yml\")\n .option(\"--android\", \"scaffold an Android config\")\n .option(\"--ios\", \"scaffold an iOS config\")\n .option(\"--force\", \"overwrite an existing config\")\n .action(action(initCommand));\n\nprogram\n .command(\"validate\")\n .description(\"Validate the config, suite structure and devices\")\n .action(action(validateCommand));\n\nprogram\n .command(\"package\")\n .description(\"Discover flows and create the suite zip without uploading\")\n .action(action(packageCommand));\n\nprogram\n .command(\"upload-app\")\n .description(\"Upload only the app and print its BrowserStack app_url\")\n .action(action(uploadAppCommand));\n\nprogram\n .command(\"upload-suite\")\n .description(\"Package and upload only the Maestro suite\")\n .action(action(uploadSuiteCommand));\n\nprogram\n .command(\"run\")\n .description(\"Package, upload and trigger a BrowserStack Maestro build\")\n .option(\"--dry-run\", \"validate and print the payload without making API calls\")\n .option(\"--device <name>\", \"override run.devices (repeatable)\", collect, [])\n .option(\"--execute <path>\", \"override run.execute (repeatable)\", collect, [])\n .action(action(runCommand));\n\nprogram.parseAsync(process.argv).catch((err) => {\n logger.error(err instanceof Error ? err.message : String(err));\n process.exitCode = 1;\n});\n","import { writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport type { Platform } from \"../config/schema.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { pathExists } from \"../utils/fs.js\";\nimport { logger } from \"../utils/logger.js\";\n\nexport interface InitOptions {\n android?: boolean;\n ios?: boolean;\n force?: boolean;\n}\n\nconst CONFIG_NAME = \"maestrostack.yml\";\n\n/** Create a starter maestrostack.yml. */\nexport async function initCommand(opts: InitOptions): Promise<void> {\n if (opts.android && opts.ios) {\n throw new MaestroStackError(\"Pass only one of --android or --ios.\");\n }\n const platform: Platform = opts.ios ? \"ios\" : \"android\";\n\n const cwd = process.cwd();\n const target = path.join(cwd, CONFIG_NAME);\n\n if ((await pathExists(target)) && !opts.force) {\n throw new MaestroStackError(`${CONFIG_NAME} already exists.`, [\n \"Use --force to overwrite it.\",\n ]);\n }\n\n await writeFile(target, template(platform), \"utf8\");\n logger.info(`Created ${CONFIG_NAME} (platform: ${platform}).`);\n logger.info(\"Next: set BROWSERSTACK_USERNAME / BROWSERSTACK_ACCESS_KEY, then run `maestrostack validate`.\");\n}\n\nfunction template(platform: Platform): string {\n const appPath = platform === \"ios\" ? \"./apps/app-release.ipa\" : \"./apps/app-release.apk\";\n const devices =\n platform === \"ios\"\n ? [\"iPhone 15-17.0\"]\n : [\"Samsung Galaxy S20-10.0\", \"Google Pixel 7-13.0\"];\n\n const deviceLines = devices.map((d) => ` - ${d}`).join(\"\\n\");\n\n // Always \\n line endings so the file is identical across platforms.\n return [\n \"version: 1\",\n \"\",\n \"auth:\",\n \" username: ${BROWSERSTACK_USERNAME}\",\n \" accessKey: ${BROWSERSTACK_ACCESS_KEY}\",\n \"\",\n `platform: ${platform}`,\n \"\",\n \"app:\",\n \" source: upload\",\n ` path: ${appPath}`,\n \" customId: SampleApp\",\n \"\",\n \"suite:\",\n \" root: .\",\n \" packageName: Flows.zip\",\n \" customId: SampleTest\",\n \" include:\",\n \" - smoke/**/*.yml\",\n \" - regression/**/*.yml\",\n \" exclude:\",\n \" - apps/**\",\n \"\",\n \"run:\",\n \" project: Maestro_Test\",\n \" devices:\",\n deviceLines,\n \" executeMode: explicit\",\n \" execute:\",\n \" - smoke/login.yml\",\n \" options:\",\n \" networkLogs: true\",\n \" deviceLogs: true\",\n \"\",\n ].join(\"\\n\");\n}\n","/**\n * Error type for all expected, user-facing failures (bad config, missing files,\n * API errors, etc). The CLI entrypoint catches these and prints a clean message\n * with exit code 1, rather than dumping a stack trace.\n */\nexport class MaestroStackError extends Error {\n /** Optional extra lines shown beneath the main message (e.g. hints). */\n readonly details?: string[];\n\n constructor(message: string, details?: string[]) {\n super(message);\n this.name = \"MaestroStackError\";\n this.details = details;\n }\n}\n\n/** Type guard for {@link MaestroStackError}. */\nexport function isMaestroStackError(err: unknown): err is MaestroStackError {\n return err instanceof MaestroStackError;\n}\n","import { constants } from \"node:fs\";\nimport fs from \"node:fs/promises\";\nimport path from \"node:path\";\n\n/** Returns true if a file or directory exists at `p`. */\nexport async function pathExists(p: string): Promise<boolean> {\n try {\n await fs.access(p, constants.F_OK);\n return true;\n } catch {\n return false;\n }\n}\n\n/** Returns true if `p` exists and is a regular file. */\nexport async function isFile(p: string): Promise<boolean> {\n try {\n const stat = await fs.stat(p);\n return stat.isFile();\n } catch {\n return false;\n }\n}\n\n/** Returns true if `p` exists and is a directory. */\nexport async function isDirectory(p: string): Promise<boolean> {\n try {\n const stat = await fs.stat(p);\n return stat.isDirectory();\n } catch {\n return false;\n }\n}\n\n/** Creates a directory (and parents) if it does not already exist. */\nexport async function ensureDir(dir: string): Promise<void> {\n await fs.mkdir(dir, { recursive: true });\n}\n\n/** Removes a directory tree if present; no-op if missing. */\nexport async function removeDir(dir: string): Promise<void> {\n await fs.rm(dir, { recursive: true, force: true });\n}\n\n/** Lower-cased file extension including the dot (e.g. \".apk\"). */\nexport function extname(p: string): string {\n return path.extname(p).toLowerCase();\n}\n","/* eslint-disable no-console */\n\nlet debugEnabled = false;\n\n/** Enable or disable debug-level output (wired to the global --debug flag). */\nexport function setDebug(enabled: boolean): void {\n debugEnabled = enabled;\n}\n\nexport function isDebugEnabled(): boolean {\n return debugEnabled;\n}\n\nexport const logger = {\n /** Primary user-facing output. */\n info(message = \"\"): void {\n console.log(message);\n },\n warn(message: string): void {\n console.warn(`warning: ${message}`);\n },\n error(message: string): void {\n console.error(message);\n },\n /** Only printed when --debug is set. Prefixed so it is visually distinct. */\n debug(message: string): void {\n if (debugEnabled) {\n console.error(`[debug] ${message}`);\n }\n },\n};\n\n/**\n * Mask a secret for display, keeping only the last few characters so logs are\n * still useful for debugging without exposing the value. Empty/short values are\n * fully masked.\n */\nexport function redact(secret: string | undefined): string {\n if (!secret) return \"\";\n if (secret.length <= 4) return \"****\";\n return `****${secret.slice(-4)}`;\n}\n","import path from \"node:path\";\nimport { loadConfig } from \"../config/loadConfig.js\";\nimport { createZip } from \"../suite/createZip.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { validateAll, type GlobalOptions } from \"./validate.js\";\n\n/**\n * Discover flows and create the suite zip without uploading. Useful for\n * debugging the resulting archive structure.\n */\nexport async function packageCommand(opts: GlobalOptions): Promise<void> {\n const cwd = process.cwd();\n const loaded = await loadConfig(opts.config, cwd);\n const { config } = loaded;\n const { flows } = await validateAll(loaded, cwd);\n\n const { zipPath } = await createZip({\n files: flows,\n suiteRoot: config.suite.root,\n packageName: config.suite.packageName,\n cwd,\n });\n\n logger.info(`Created suite zip: ${path.relative(cwd, zipPath) || zipPath}`);\n logger.info(\"Included flows:\");\n for (const flow of flows) {\n logger.info(`- ${flow}`);\n }\n}\n","import { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport dotenv from \"dotenv\";\nimport { parse as parseYaml } from \"yaml\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { isFile } from \"../utils/fs.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { resolveEnv } from \"./resolveEnv.js\";\nimport { configSchema, type Config } from \"./schema.js\";\n\nconst DEFAULT_CONFIG_NAMES = [\"maestrostack.yml\", \"maestrostack.yaml\"];\n\nexport interface LoadedConfig {\n config: Config;\n /** Absolute path to the resolved config file. */\n configPath: string;\n}\n\n/**\n * Resolve which config file to use. An explicit `-c` path must exist; otherwise\n * we look for maestrostack.yml then maestrostack.yaml in the working directory.\n */\nexport async function resolveConfigPath(\n explicit: string | undefined,\n cwd: string = process.cwd(),\n): Promise<string> {\n if (explicit) {\n const resolved = path.resolve(cwd, explicit);\n if (!(await isFile(resolved))) {\n throw new MaestroStackError(`Config file not found: ${explicit}`);\n }\n return resolved;\n }\n\n for (const name of DEFAULT_CONFIG_NAMES) {\n const candidate = path.resolve(cwd, name);\n if (await isFile(candidate)) {\n return candidate;\n }\n }\n\n throw new MaestroStackError(\"maestrostack.yml not found.\", [\n \"Run `maestrostack init` to create one, or pass --config <path>.\",\n ]);\n}\n\n/**\n * Load, env-substitute, parse and validate a config file. The `.env` in the\n * working directory is loaded first so `${VAR}` tokens can resolve against it.\n */\nexport async function loadConfig(\n explicit: string | undefined,\n cwd: string = process.cwd(),\n): Promise<LoadedConfig> {\n // Load .env (does not override already-set process.env values).\n dotenv.config({ path: path.resolve(cwd, \".env\") });\n\n const configPath = await resolveConfigPath(explicit, cwd);\n logger.debug(`Using config: ${configPath}`);\n\n const raw = await readFile(configPath, \"utf8\");\n const substituted = resolveEnv(raw);\n\n let parsed: unknown;\n try {\n parsed = parseYaml(substituted);\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new MaestroStackError(`Invalid YAML in ${path.basename(configPath)}.`, [reason]);\n }\n\n const result = configSchema.safeParse(parsed);\n if (!result.success) {\n const details = result.error.issues.map((issue) => {\n const where = issue.path.length > 0 ? `${issue.path.join(\".\")}: ` : \"\";\n return `- ${where}${issue.message}`;\n });\n throw new MaestroStackError(\"Invalid configuration:\", details);\n }\n\n return { config: result.data, configPath };\n}\n","import { MaestroStackError } from \"../utils/errors.js\";\n\n/** Matches ${VAR_NAME} tokens. Variable names follow shell-ish conventions. */\nconst ENV_TOKEN = /\\$\\{([A-Za-z_][A-Za-z0-9_]*)\\}/g;\n\n/**\n * Substitute `${VAR}` tokens in the raw config text using `env` (defaults to\n * process.env). All missing variables are collected and reported together so the\n * user fixes them in one pass - and crucially this happens before any upload.\n */\nexport function resolveEnv(\n raw: string,\n env: NodeJS.ProcessEnv = process.env,\n): string {\n const missing = new Set<string>();\n\n const resolved = raw.replace(ENV_TOKEN, (_match, name: string) => {\n const value = env[name];\n if (value === undefined || value === \"\") {\n missing.add(name);\n return \"\";\n }\n return value;\n });\n\n if (missing.size > 0) {\n const names = [...missing].sort();\n throw new MaestroStackError(\n `Missing required environment variable(s): ${names.join(\", \")}`,\n [\"Set them in your shell or a .env file in the working directory.\"],\n );\n }\n\n return resolved;\n}\n","import { z } from \"zod\";\n\n/**\n * Zod schema for maestrostack.yml. Parsing yields a fully-typed {@link Config}.\n * Validation messages here are intentionally human-readable since they surface\n * directly in the CLI.\n */\n\nconst nonEmpty = (label: string) =>\n z.string({ required_error: `${label} is required` }).trim().min(1, `${label} must not be empty`);\n\nconst authSchema = z.object({\n username: nonEmpty(\"BrowserStack username (auth.username)\"),\n accessKey: nonEmpty(\"BrowserStack access key (auth.accessKey)\"),\n});\n\nconst appUploadSchema = z.object({\n source: z.literal(\"upload\"),\n path: nonEmpty(\"app.path\"),\n customId: z.string().trim().min(1).optional(),\n});\n\nconst appUrlSchema = z.object({\n source: z.literal(\"app_url\"),\n appUrl: nonEmpty(\"app.appUrl\").refine((v) => v.startsWith(\"bs://\"), {\n message: \"app.appUrl must start with bs://\",\n }),\n customId: z.string().trim().min(1).optional(),\n});\n\nconst appCustomIdSchema = z.object({\n // Present in the schema so configs validate, but resolution is deferred (see validate.ts).\n source: z.literal(\"custom_id\"),\n customId: nonEmpty(\"app.customId\"),\n});\n\nconst appSchema = z.discriminatedUnion(\"source\", [\n appUploadSchema,\n appUrlSchema,\n appCustomIdSchema,\n]);\n\nconst suiteSchema = z.object({\n root: z.string().trim().min(1).default(\".\"),\n packageName: z.string().trim().min(1).default(\"Flows.zip\"),\n customId: z.string().trim().min(1).optional(),\n include: z.array(nonEmpty(\"suite.include entry\")).default([\"**/*.yml\", \"**/*.yaml\"]),\n exclude: z.array(z.string()).default([]),\n});\n\nconst optionsSchema = z\n .object({\n networkLogs: z.boolean().optional(),\n deviceLogs: z.boolean().optional(),\n })\n .default({});\n\nconst runSchema = z.object({\n project: nonEmpty(\"run.project\"),\n devices: z\n .array(nonEmpty(\"device\"))\n .min(1, \"run.devices must contain at least one device\"),\n executeMode: z.enum([\"explicit\", \"main\"]).default(\"explicit\"),\n execute: z.array(nonEmpty(\"run.execute entry\")).optional(),\n options: optionsSchema,\n});\n\nexport const configSchema = z.object({\n version: z.literal(1, {\n errorMap: () => ({ message: \"Unsupported config version. Expected version: 1\" }),\n }),\n auth: authSchema,\n platform: z.enum([\"android\", \"ios\"], {\n errorMap: () => ({ message: \"Unsupported platform. Expected android or ios\" }),\n }),\n app: appSchema,\n suite: suiteSchema.default({}),\n run: runSchema,\n});\n\nexport type Config = z.infer<typeof configSchema>;\nexport type AppConfig = Config[\"app\"];\nexport type Platform = Config[\"platform\"];\n","import { createWriteStream } from \"node:fs\";\nimport path from \"node:path\";\nimport archiver from \"archiver\";\nimport { ensureDir } from \"../utils/fs.js\";\nimport { distDir } from \"../utils/paths.js\";\n\nexport interface ZipResult {\n /** Absolute path to the written zip file. */\n zipPath: string;\n /** Forward-slash flow paths that were included (relative to suite root). */\n files: string[];\n}\n\n/** Top-level folder inside the archive. BrowserStack locates flows under this. */\nexport const SUITE_PREFIX = \"Flows\";\n\n/**\n * Create the suite zip from the given flow files. Each entry is stored under\n * `Flows/<relativePath>` using forward slashes (required by the ZIP spec and so\n * BrowserStack can locate flows regardless of the host OS that built the zip).\n */\nexport async function createZip(options: {\n files: string[];\n suiteRoot: string;\n packageName: string;\n cwd?: string;\n}): Promise<ZipResult> {\n const cwd = options.cwd ?? process.cwd();\n const root = path.resolve(cwd, options.suiteRoot);\n const outDir = distDir(cwd);\n await ensureDir(outDir);\n const zipPath = path.join(outDir, options.packageName);\n\n await new Promise<void>((resolve, reject) => {\n const output = createWriteStream(zipPath);\n const archive = archiver(\"zip\", { zlib: { level: 9 } });\n\n output.on(\"close\", () => resolve());\n output.on(\"error\", reject);\n archive.on(\"error\", reject);\n archive.on(\"warning\", (err) => {\n if (err.code === \"ENOENT\") return; // non-fatal\n reject(err);\n });\n\n archive.pipe(output);\n\n for (const rel of options.files) {\n // `rel` is already forward-slash (from discoverFlows); resolve to the OS\n // path for reading, but name the entry with forward slashes.\n const absolute = path.join(root, ...rel.split(\"/\"));\n const entryName = `${SUITE_PREFIX}/${rel}`;\n archive.file(absolute, { name: entryName });\n }\n\n void archive.finalize();\n });\n\n return { zipPath, files: options.files };\n}\n","import path from \"node:path\";\n\n/**\n * Internal working directory MaestroStack writes to. Relative to the current\n * working directory so it lands next to the user's project. Should be gitignored.\n */\nexport const WORK_DIR = \".maestrostack\";\n\n/** Directory for build artifacts (the generated suite zip). */\nexport function distDir(cwd: string = process.cwd()): string {\n return path.join(cwd, WORK_DIR, \"dist\");\n}\n\n/** Directory for transient files. */\nexport function tmpDir(cwd: string = process.cwd()): string {\n return path.join(cwd, WORK_DIR, \"tmp\");\n}\n\n/**\n * Convert a path into the forward-slash form used internally and by BrowserStack\n * (zip entries, execute paths). Both separators are normalized so a config or\n * `--execute` value authored on Windows (`smoke\\login.yml`) matches discovered\n * flows regardless of the OS MaestroStack runs on.\n */\nexport function toPosix(p: string): string {\n return p.split(/[\\\\/]+/).join(\"/\");\n}\n","import path from \"node:path\";\nimport type { Config } from \"../config/schema.js\";\nimport { loadConfig, type LoadedConfig } from \"../config/loadConfig.js\";\nimport { discoverFlows } from \"../suite/discoverFlows.js\";\nimport { validateSuite } from \"../suite/validateSuite.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { extname, isFile } from \"../utils/fs.js\";\nimport { logger } from \"../utils/logger.js\";\n\nexport interface GlobalOptions {\n config?: string;\n debug?: boolean;\n}\n\nconst PLATFORM_EXT: Record<Config[\"platform\"], string> = {\n android: \".apk\",\n ios: \".ipa\",\n};\n\n/**\n * Semantic validation of the app block beyond the Zod schema: deferred sources,\n * file existence and platform/extension agreement.\n */\nexport async function validateApp(config: Config, cwd: string): Promise<void> {\n const { app, platform } = config;\n\n if (app.source === \"custom_id\") {\n throw new MaestroStackError(\n 'app.source \"custom_id\" is not supported in the MVP.',\n [\"Use source: upload (with a path) or source: app_url (with a bs:// url).\"],\n );\n }\n\n if (app.source === \"upload\") {\n const appPath = path.resolve(cwd, app.path);\n if (!(await isFile(appPath))) {\n throw new MaestroStackError(`app.path does not exist: ${app.path}`);\n }\n const expected = PLATFORM_EXT[platform];\n if (extname(appPath) !== expected) {\n throw new MaestroStackError(\n `Platform \"${platform}\" requires a ${expected} file, but app.path is \"${app.path}\".`,\n );\n }\n }\n}\n\n/**\n * Full validation pipeline shared by `validate` and `run`: app block, flow\n * discovery and suite/execute checks. Returns the discovered flow paths.\n */\nexport async function validateAll(\n loaded: LoadedConfig,\n cwd: string,\n): Promise<{ flows: string[] }> {\n const { config, configPath } = loaded;\n await validateApp(config, cwd);\n\n const flows = await discoverFlows(config.suite, {\n cwd,\n configBasename: path.basename(configPath),\n });\n\n await validateSuite(config, flows, cwd);\n\n return { flows };\n}\n\nexport async function validateCommand(opts: GlobalOptions): Promise<void> {\n const cwd = process.cwd();\n const loaded = await loadConfig(opts.config, cwd);\n const { flows } = await validateAll(loaded, cwd);\n const { config } = loaded;\n\n logger.info(\"Configuration is valid.\");\n logger.info(\"\");\n logger.info(`Config: ${path.relative(cwd, loaded.configPath) || loaded.configPath}`);\n logger.info(`Platform: ${config.platform}`);\n logger.info(`Project: ${config.run.project}`);\n logger.info(`App: ${describeApp(config)}`);\n logger.info(`Devices: ${config.run.devices.length}`);\n for (const device of config.run.devices) {\n logger.info(` - ${device}`);\n }\n logger.info(`Flows: ${flows.length}`);\n for (const flow of flows) {\n logger.info(` - ${flow}`);\n }\n logger.info(`Execute: ${config.run.executeMode}`);\n}\n\nfunction describeApp(config: Config): string {\n const { app } = config;\n switch (app.source) {\n case \"upload\":\n return `upload ${app.path}`;\n case \"app_url\":\n return `app_url ${app.appUrl}`;\n default:\n return app.source;\n }\n}\n","import path from \"node:path\";\nimport fg from \"fast-glob\";\nimport type { Config } from \"../config/schema.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { isDirectory } from \"../utils/fs.js\";\n\nexport interface DiscoverOptions {\n /** Working directory the suite root is resolved against. */\n cwd?: string;\n /** Basename of the active config file, always excluded from the suite. */\n configBasename?: string;\n}\n\n/**\n * Always-on ignore patterns. Root-level YAML files are treated as config files,\n * not Maestro flows (top-level only - `*.yml` matches `staging.yml` but not\n * `smoke/login.yml`). Build/VCS/dependency dirs are never packaged.\n */\nconst BUILT_IN_IGNORES = [\n \"*.yml\",\n \"*.yaml\",\n \".git/**\",\n \"node_modules/**\",\n \".maestrostack/**\",\n];\n\n/**\n * Discover Maestro flow files under `suite.root` using the configured include /\n * exclude globs. Returns sorted, forward-slash relative paths (fast-glob emits\n * POSIX separators on every OS, keeping output identical across platforms).\n */\nexport async function discoverFlows(\n suite: Config[\"suite\"],\n opts: DiscoverOptions = {},\n): Promise<string[]> {\n const cwd = opts.cwd ?? process.cwd();\n const root = path.resolve(cwd, suite.root);\n\n if (!(await isDirectory(root))) {\n throw new MaestroStackError(`suite.root does not exist: ${suite.root}`);\n }\n\n const ignore = [...BUILT_IN_IGNORES, ...suite.exclude];\n if (opts.configBasename) {\n ignore.push(opts.configBasename);\n }\n\n const matches = await fg(suite.include, {\n cwd: root,\n ignore,\n onlyFiles: true,\n dot: false,\n followSymbolicLinks: false,\n });\n\n if (matches.length === 0) {\n throw new MaestroStackError(\"No Maestro flow files found.\", [\n \"Maestro flow files should live inside subfolders (root-level YAML files are ignored).\",\n `Checked suite.root=\"${suite.root}\" with include=${JSON.stringify(suite.include)}.`,\n ]);\n }\n\n return matches.sort();\n}\n","import path from \"node:path\";\nimport type { Config } from \"../config/schema.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { isFile } from \"../utils/fs.js\";\nimport { toPosix } from \"../utils/paths.js\";\n\nexport const MAIN_FLOW = \"main.yaml\";\n\n/**\n * Validate the suite against the discovered flow set and the configured execute\n * mode.\n *\n * - explicit mode: every `run.execute` path must exist in the discovered flows.\n * - main mode: a `main.yaml` must exist at the suite root.\n *\n * `discovered` paths are forward-slash relative to the suite root; execute paths\n * supplied by users are normalized so Windows-style separators still match.\n */\nexport async function validateSuite(\n config: Config,\n discovered: string[],\n cwd: string = process.cwd(),\n): Promise<void> {\n const { run, suite } = config;\n\n if (run.executeMode === \"main\") {\n const mainPath = path.resolve(cwd, suite.root, MAIN_FLOW);\n if (!(await isFile(mainPath))) {\n throw new MaestroStackError(\n `executeMode is \"main\" but ${MAIN_FLOW} was not found in suite.root (${suite.root}).`,\n [`Add a ${MAIN_FLOW} or switch to executeMode: explicit with a run.execute list.`],\n );\n }\n return;\n }\n\n // explicit mode\n if (run.execute && run.execute.length > 0) {\n const available = new Set(discovered.map(toPosix));\n const missing = run.execute\n .map(toPosix)\n .filter((p) => !available.has(p));\n\n if (missing.length > 0) {\n throw new MaestroStackError(\n \"Configured execute file(s) do not exist in the packaged suite:\",\n missing.map((p) => `- ${p}`),\n );\n }\n }\n}\n","import path from \"node:path\";\nimport { loadConfig } from \"../config/loadConfig.js\";\nimport type { Config } from \"../config/schema.js\";\nimport { uploadApp } from \"../browserstack/uploadApp.js\";\nimport { uploadSuite } from \"../browserstack/uploadSuite.js\";\nimport { buildPath, buildPayload, startBuild } from \"../browserstack/startBuild.js\";\nimport { createZip } from \"../suite/createZip.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { toPosix } from \"../utils/paths.js\";\nimport { validateAll, type GlobalOptions } from \"./validate.js\";\n\nexport interface RunOptions extends GlobalOptions {\n dryRun?: boolean;\n /** Temporary device override(s); replaces run.devices. */\n device?: string[];\n /** Temporary execute override(s); replaces run.execute (forces explicit mode). */\n execute?: string[];\n}\n\nconst APP_PLACEHOLDER = \"<resolved after upload>\";\nconst SUITE_PLACEHOLDER = \"<resolved after upload>\";\n\n/** Apply CLI overrides for devices / execute onto a loaded config. */\nexport function applyOverrides(config: Config, opts: RunOptions): Config {\n let run = config.run;\n if (opts.device && opts.device.length > 0) {\n run = { ...run, devices: opts.device };\n }\n if (opts.execute && opts.execute.length > 0) {\n run = { ...run, execute: opts.execute.map(toPosix), executeMode: \"explicit\" };\n }\n return { ...config, run };\n}\n\nexport async function runCommand(opts: RunOptions): Promise<void> {\n const cwd = process.cwd();\n const loaded = await loadConfig(opts.config, cwd);\n const config = applyOverrides(loaded.config, opts);\n\n // Validate with overrides applied.\n const { flows } = await validateAll({ ...loaded, config }, cwd);\n\n if (opts.dryRun) {\n printDryRun(config, flows, cwd);\n return;\n }\n\n // 1. Package the suite.\n const { zipPath } = await createZip({\n files: flows,\n suiteRoot: config.suite.root,\n packageName: config.suite.packageName,\n cwd,\n });\n logger.debug(`Created suite zip: ${zipPath}`);\n\n // 2. Resolve the app (upload local binary or use an existing bs:// url).\n const appUrl = await resolveApp(config, cwd);\n logger.debug(`Resolved app: ${appUrl}`);\n\n // 3. Upload the suite.\n logger.info(`Uploading test suite (${flows.length} flow(s)) ...`);\n const suiteResult = await uploadSuite({\n auth: config.auth,\n zipPath,\n customId: config.suite.customId,\n });\n\n // 4. Trigger the build.\n const payload = buildPayload(config, {\n app: appUrl,\n testSuite: suiteResult.test_suite_url,\n });\n logger.info(`Starting ${config.platform} build ...`);\n const build = await startBuild({ auth: config.auth, platform: config.platform, payload });\n\n printSummary(config, appUrl, suiteResult.test_suite_url, build.build_id);\n}\n\nasync function resolveApp(config: Config, cwd: string): Promise<string> {\n const { app } = config;\n if (app.source === \"app_url\") {\n return app.appUrl;\n }\n if (app.source === \"upload\") {\n logger.info(`Uploading app: ${app.path} ...`);\n const result = await uploadApp({\n auth: config.auth,\n filePath: path.resolve(cwd, app.path),\n customId: app.customId,\n });\n return result.app_url;\n }\n throw new MaestroStackError(`Unsupported app.source: ${app.source}`);\n}\n\nfunction previewApp(config: Config): string {\n const { app } = config;\n return app.source === \"app_url\" ? app.appUrl : APP_PLACEHOLDER;\n}\n\nfunction printDryRun(config: Config, flows: string[], cwd: string): void {\n logger.info(\"Dry run only. No API calls made.\");\n logger.info(\"\");\n logger.info(\"Would package:\");\n for (const flow of flows) {\n logger.info(`- ${flow}`);\n }\n logger.info(\"\");\n\n const { app } = config;\n if (app.source === \"upload\") {\n logger.info(`Would upload app:\\n${app.path}`);\n } else if (app.source === \"app_url\") {\n logger.info(`Would use app:\\n${app.appUrl}`);\n }\n logger.info(\"\");\n\n logger.info(`Would call:\\nPOST ${buildPath(config.platform)}`);\n logger.info(\"\");\n\n const payload = buildPayload(config, {\n app: previewApp(config),\n testSuite: SUITE_PLACEHOLDER,\n });\n logger.info(\"Payload:\");\n logger.info(JSON.stringify(payload, null, 2));\n}\n\nfunction printSummary(\n config: Config,\n appUrl: string,\n testSuiteUrl: string,\n buildId: string,\n): void {\n logger.info(\"\");\n logger.info(\"MaestroStack run started\");\n logger.info(\"\");\n logger.info(`Project: ${config.run.project}`);\n logger.info(`Platform: ${config.platform}`);\n logger.info(\"Devices:\");\n for (const device of config.run.devices) {\n logger.info(`- ${device}`);\n }\n logger.info(\"\");\n logger.info(`App: ${appUrl}`);\n logger.info(`Test suite: ${testSuiteUrl}`);\n logger.info(`Build ID: ${buildId}`);\n}\n","import { openAsBlob } from \"node:fs\";\nimport path from \"node:path\";\nimport { FormData } from \"undici\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { isFile } from \"../utils/fs.js\";\nimport { bsRequest, type BsAuth } from \"./client.js\";\n\nexport interface UploadAppResult {\n app_url: string;\n custom_id?: string;\n expiry?: string;\n app_name?: string;\n}\n\n/**\n * Upload a local app binary (.apk / .ipa) to BrowserStack.\n * POST /app-automate/maestro/v2/app (multipart: file, optional custom_id)\n */\nexport async function uploadApp(options: {\n auth: BsAuth;\n filePath: string;\n customId?: string;\n}): Promise<UploadAppResult> {\n if (!(await isFile(options.filePath))) {\n throw new MaestroStackError(`app.path does not exist: ${options.filePath}`);\n }\n\n const form = new FormData();\n const blob = await openAsBlob(options.filePath);\n form.set(\"file\", blob, path.basename(options.filePath));\n if (options.customId) {\n form.set(\"custom_id\", options.customId);\n }\n\n return bsRequest<UploadAppResult>({\n path: \"/app-automate/maestro/v2/app\",\n method: \"POST\",\n auth: options.auth,\n rawBody: form,\n });\n}\n","import { fetch, type BodyInit } from \"undici\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { logger } from \"../utils/logger.js\";\n\nexport const BASE_URL = \"https://api-cloud.browserstack.com\";\n\nexport interface BsAuth {\n username: string;\n accessKey: string;\n}\n\n/** Build an HTTP Basic auth header from BrowserStack credentials. */\nexport function authHeader(auth: BsAuth): string {\n const token = Buffer.from(`${auth.username}:${auth.accessKey}`).toString(\"base64\");\n return `Basic ${token}`;\n}\n\ninterface RequestOptions {\n path: string;\n method: \"GET\" | \"POST\";\n auth: BsAuth;\n /** JSON body - serialized and sent with application/json. */\n jsonBody?: unknown;\n /** Raw body (e.g. multipart FormData). undici sets the content-type. */\n rawBody?: BodyInit;\n}\n\n/**\n * Perform a request against the BrowserStack API with Basic auth, returning the\n * parsed JSON. Non-2xx responses become a {@link MaestroStackError} carrying the\n * status and any server message - credentials are never logged.\n */\nexport async function bsRequest<T>(options: RequestOptions): Promise<T> {\n const url = `${BASE_URL}${options.path}`;\n const headers: Record<string, string> = {\n authorization: authHeader(options.auth),\n };\n\n let body: BodyInit | undefined;\n if (options.jsonBody !== undefined) {\n headers[\"content-type\"] = \"application/json\";\n body = JSON.stringify(options.jsonBody);\n } else if (options.rawBody !== undefined) {\n body = options.rawBody;\n }\n\n logger.debug(`${options.method} ${url}`);\n\n let response: Awaited<ReturnType<typeof fetch>>;\n try {\n response = await fetch(url, { method: options.method, headers, body });\n } catch (err) {\n const reason = err instanceof Error ? err.message : String(err);\n throw new MaestroStackError(`Failed to reach BrowserStack: ${reason}`);\n }\n\n const text = await response.text();\n let data: unknown = {};\n if (text) {\n try {\n data = JSON.parse(text);\n } catch {\n data = { raw: text };\n }\n }\n\n if (!response.ok) {\n const message = extractErrorMessage(data) ?? response.statusText;\n throw new MaestroStackError(`BrowserStack API error (${response.status}): ${message}`);\n }\n\n return data as T;\n}\n\nfunction extractErrorMessage(data: unknown): string | undefined {\n if (data && typeof data === \"object\") {\n const obj = data as Record<string, unknown>;\n for (const key of [\"message\", \"error\", \"errors\"]) {\n const value = obj[key];\n if (typeof value === \"string\" && value.length > 0) return value;\n }\n }\n return undefined;\n}\n","import { openAsBlob } from \"node:fs\";\nimport path from \"node:path\";\nimport { FormData } from \"undici\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { isFile } from \"../utils/fs.js\";\nimport { bsRequest, type BsAuth } from \"./client.js\";\n\nexport interface UploadSuiteResult {\n test_suite_url: string;\n custom_id?: string;\n expiry?: string;\n test_suite_name?: string;\n framework?: string;\n}\n\n/**\n * Upload a zipped Maestro test suite to BrowserStack.\n * POST /app-automate/maestro/v2/test-suite (multipart: file, optional custom_id)\n */\nexport async function uploadSuite(options: {\n auth: BsAuth;\n zipPath: string;\n customId?: string;\n}): Promise<UploadSuiteResult> {\n if (!(await isFile(options.zipPath))) {\n throw new MaestroStackError(`Suite zip does not exist: ${options.zipPath}`);\n }\n\n const form = new FormData();\n const blob = await openAsBlob(options.zipPath);\n form.set(\"file\", blob, path.basename(options.zipPath));\n if (options.customId) {\n form.set(\"custom_id\", options.customId);\n }\n\n return bsRequest<UploadSuiteResult>({\n path: \"/app-automate/maestro/v2/test-suite\",\n method: \"POST\",\n auth: options.auth,\n rawBody: form,\n });\n}\n","import type { Config, Platform } from \"../config/schema.js\";\nimport { bsRequest, type BsAuth } from \"./client.js\";\n\nexport interface BuildPayload {\n app: string;\n testSuite: string;\n project: string;\n devices: string[];\n execute?: string[];\n networkLogs?: boolean;\n deviceLogs?: boolean;\n}\n\nexport interface StartBuildResult {\n message?: string;\n build_id: string;\n}\n\n/** Build endpoint path for the given platform. */\nexport function buildPath(platform: Platform): string {\n return `/app-automate/maestro/v2/${platform}/build`;\n}\n\n/**\n * Construct the BrowserStack build payload from config and the resolved app /\n * test-suite references. Pure and side-effect free so it can be unit-tested and\n * reused for the `--dry-run` preview.\n *\n * - `execute` is included only in explicit mode (in main mode BrowserStack runs\n * the suite's main.yaml).\n * - `networkLogs` / `deviceLogs` are JSON booleans, included only when set.\n */\nexport function buildPayload(\n config: Config,\n resolved: { app: string; testSuite: string },\n): BuildPayload {\n const { run } = config;\n const payload: BuildPayload = {\n app: resolved.app,\n testSuite: resolved.testSuite,\n project: run.project,\n devices: run.devices,\n };\n\n if (run.executeMode === \"explicit\" && run.execute && run.execute.length > 0) {\n payload.execute = run.execute;\n }\n\n if (run.options.networkLogs !== undefined) {\n payload.networkLogs = run.options.networkLogs;\n }\n if (run.options.deviceLogs !== undefined) {\n payload.deviceLogs = run.options.deviceLogs;\n }\n\n return payload;\n}\n\n/** Trigger a Maestro build and return the BrowserStack build id. */\nexport async function startBuild(options: {\n auth: BsAuth;\n platform: Platform;\n payload: BuildPayload;\n}): Promise<StartBuildResult> {\n return bsRequest<StartBuildResult>({\n path: buildPath(options.platform),\n method: \"POST\",\n auth: options.auth,\n jsonBody: options.payload,\n });\n}\n","import path from \"node:path\";\nimport { loadConfig } from \"../config/loadConfig.js\";\nimport { uploadApp } from \"../browserstack/uploadApp.js\";\nimport { MaestroStackError } from \"../utils/errors.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { validateApp, type GlobalOptions } from \"./validate.js\";\n\n/**\n * Upload only the app and print the resulting BrowserStack app_url. Only valid\n * for app.source: upload.\n */\nexport async function uploadAppCommand(opts: GlobalOptions): Promise<void> {\n const cwd = process.cwd();\n const { config } = await loadConfig(opts.config, cwd);\n await validateApp(config, cwd);\n\n if (config.app.source !== \"upload\") {\n throw new MaestroStackError(\n `upload-app requires app.source \"upload\" (current: \"${config.app.source}\").`,\n );\n }\n\n const filePath = path.resolve(cwd, config.app.path);\n logger.info(`Uploading app: ${config.app.path} ...`);\n\n const result = await uploadApp({\n auth: config.auth,\n filePath,\n customId: config.app.customId,\n });\n\n logger.info(\"Uploaded app:\");\n logger.info(`app_url: ${result.app_url}`);\n if (result.custom_id) logger.info(`custom_id: ${result.custom_id}`);\n if (result.expiry) logger.info(`expires: ${result.expiry}`);\n}\n","import { loadConfig } from \"../config/loadConfig.js\";\nimport { uploadSuite } from \"../browserstack/uploadSuite.js\";\nimport { createZip } from \"../suite/createZip.js\";\nimport { logger } from \"../utils/logger.js\";\nimport { validateAll, type GlobalOptions } from \"./validate.js\";\n\n/** Package and upload only the Maestro suite, printing the test_suite_url. */\nexport async function uploadSuiteCommand(opts: GlobalOptions): Promise<void> {\n const cwd = process.cwd();\n const loaded = await loadConfig(opts.config, cwd);\n const { config } = loaded;\n const { flows } = await validateAll(loaded, cwd);\n\n const { zipPath } = await createZip({\n files: flows,\n suiteRoot: config.suite.root,\n packageName: config.suite.packageName,\n cwd,\n });\n\n logger.info(`Uploading test suite (${flows.length} flow(s)) ...`);\n\n const result = await uploadSuite({\n auth: config.auth,\n zipPath,\n customId: config.suite.customId,\n });\n\n logger.info(\"Uploaded test suite:\");\n logger.info(`test_suite_url: ${result.test_suite_url}`);\n if (result.custom_id) logger.info(`custom_id: ${result.custom_id}`);\n if (result.expiry) logger.info(`expires: ${result.expiry}`);\n}\n"],"mappings":";;;AAAA,SAAS,eAAe;;;ACAxB,SAAS,iBAAiB;AAC1B,OAAOA,WAAU;;;ACIV,IAAM,oBAAN,cAAgC,MAAM;AAAA;AAAA,EAElC;AAAA,EAET,YAAY,SAAiB,SAAoB;AAC/C,UAAM,OAAO;AACb,SAAK,OAAO;AACZ,SAAK,UAAU;AAAA,EACjB;AACF;AAGO,SAAS,oBAAoB,KAAwC;AAC1E,SAAO,eAAe;AACxB;;;ACnBA,SAAS,iBAAiB;AAC1B,OAAO,QAAQ;AACf,OAAO,UAAU;AAGjB,eAAsB,WAAW,GAA6B;AAC5D,MAAI;AACF,UAAM,GAAG,OAAO,GAAG,UAAU,IAAI;AACjC,WAAO;AAAA,EACT,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,OAAO,GAA6B;AACxD,MAAI;AACF,UAAM,OAAO,MAAM,GAAG,KAAK,CAAC;AAC5B,WAAO,KAAK,OAAO;AAAA,EACrB,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,YAAY,GAA6B;AAC7D,MAAI;AACF,UAAM,OAAO,MAAM,GAAG,KAAK,CAAC;AAC5B,WAAO,KAAK,YAAY;AAAA,EAC1B,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAGA,eAAsB,UAAU,KAA4B;AAC1D,QAAM,GAAG,MAAM,KAAK,EAAE,WAAW,KAAK,CAAC;AACzC;AAQO,SAAS,QAAQ,GAAmB;AACzC,SAAO,KAAK,QAAQ,CAAC,EAAE,YAAY;AACrC;;;AC7CA,IAAI,eAAe;AAGZ,SAAS,SAAS,SAAwB;AAC/C,iBAAe;AACjB;AAMO,IAAM,SAAS;AAAA;AAAA,EAEpB,KAAK,UAAU,IAAU;AACvB,YAAQ,IAAI,OAAO;AAAA,EACrB;AAAA,EACA,KAAK,SAAuB;AAC1B,YAAQ,KAAK,YAAY,OAAO,EAAE;AAAA,EACpC;AAAA,EACA,MAAM,SAAuB;AAC3B,YAAQ,MAAM,OAAO;AAAA,EACvB;AAAA;AAAA,EAEA,MAAM,SAAuB;AAC3B,QAAI,cAAc;AAChB,cAAQ,MAAM,WAAW,OAAO,EAAE;AAAA,IACpC;AAAA,EACF;AACF;;;AHjBA,IAAM,cAAc;AAGpB,eAAsB,YAAY,MAAkC;AAClE,MAAI,KAAK,WAAW,KAAK,KAAK;AAC5B,UAAM,IAAI,kBAAkB,sCAAsC;AAAA,EACpE;AACA,QAAM,WAAqB,KAAK,MAAM,QAAQ;AAE9C,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAASC,MAAK,KAAK,KAAK,WAAW;AAEzC,MAAK,MAAM,WAAW,MAAM,KAAM,CAAC,KAAK,OAAO;AAC7C,UAAM,IAAI,kBAAkB,GAAG,WAAW,oBAAoB;AAAA,MAC5D;AAAA,IACF,CAAC;AAAA,EACH;AAEA,QAAM,UAAU,QAAQ,SAAS,QAAQ,GAAG,MAAM;AAClD,SAAO,KAAK,WAAW,WAAW,eAAe,QAAQ,IAAI;AAC7D,SAAO,KAAK,8FAA8F;AAC5G;AAEA,SAAS,SAAS,UAA4B;AAC5C,QAAM,UAAU,aAAa,QAAQ,2BAA2B;AAChE,QAAM,UACJ,aAAa,QACT,CAAC,gBAAgB,IACjB,CAAC,2BAA2B,qBAAqB;AAEvD,QAAM,cAAc,QAAQ,IAAI,CAAC,MAAM,SAAS,CAAC,EAAE,EAAE,KAAK,IAAI;AAG9D,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,aAAa,QAAQ;AAAA,IACrB;AAAA,IACA;AAAA,IACA;AAAA,IACA,WAAW,OAAO;AAAA,IAClB;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EACF,EAAE,KAAK,IAAI;AACb;;;AIlFA,OAAOC,WAAU;;;ACAjB,SAAS,gBAAgB;AACzB,OAAOC,WAAU;AACjB,OAAO,YAAY;AACnB,SAAS,SAAS,iBAAiB;;;ACAnC,IAAM,YAAY;AAOX,SAAS,WACd,KACA,MAAyB,QAAQ,KACzB;AACR,QAAM,UAAU,oBAAI,IAAY;AAEhC,QAAM,WAAW,IAAI,QAAQ,WAAW,CAAC,QAAQ,SAAiB;AAChE,UAAM,QAAQ,IAAI,IAAI;AACtB,QAAI,UAAU,UAAa,UAAU,IAAI;AACvC,cAAQ,IAAI,IAAI;AAChB,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT,CAAC;AAED,MAAI,QAAQ,OAAO,GAAG;AACpB,UAAM,QAAQ,CAAC,GAAG,OAAO,EAAE,KAAK;AAChC,UAAM,IAAI;AAAA,MACR,6CAA6C,MAAM,KAAK,IAAI,CAAC;AAAA,MAC7D,CAAC,iEAAiE;AAAA,IACpE;AAAA,EACF;AAEA,SAAO;AACT;;;AClCA,SAAS,SAAS;AAQlB,IAAM,WAAW,CAAC,UAChB,EAAE,OAAO,EAAE,gBAAgB,GAAG,KAAK,eAAe,CAAC,EAAE,KAAK,EAAE,IAAI,GAAG,GAAG,KAAK,oBAAoB;AAEjG,IAAM,aAAa,EAAE,OAAO;AAAA,EAC1B,UAAU,SAAS,uCAAuC;AAAA,EAC1D,WAAW,SAAS,0CAA0C;AAChE,CAAC;AAED,IAAM,kBAAkB,EAAE,OAAO;AAAA,EAC/B,QAAQ,EAAE,QAAQ,QAAQ;AAAA,EAC1B,MAAM,SAAS,UAAU;AAAA,EACzB,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AAC9C,CAAC;AAED,IAAM,eAAe,EAAE,OAAO;AAAA,EAC5B,QAAQ,EAAE,QAAQ,SAAS;AAAA,EAC3B,QAAQ,SAAS,YAAY,EAAE,OAAO,CAAC,MAAM,EAAE,WAAW,OAAO,GAAG;AAAA,IAClE,SAAS;AAAA,EACX,CAAC;AAAA,EACD,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AAC9C,CAAC;AAED,IAAM,oBAAoB,EAAE,OAAO;AAAA;AAAA,EAEjC,QAAQ,EAAE,QAAQ,WAAW;AAAA,EAC7B,UAAU,SAAS,cAAc;AACnC,CAAC;AAED,IAAM,YAAY,EAAE,mBAAmB,UAAU;AAAA,EAC/C;AAAA,EACA;AAAA,EACA;AACF,CAAC;AAED,IAAM,cAAc,EAAE,OAAO;AAAA,EAC3B,MAAM,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,QAAQ,GAAG;AAAA,EAC1C,aAAa,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,QAAQ,WAAW;AAAA,EACzD,UAAU,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,EAAE,SAAS;AAAA,EAC5C,SAAS,EAAE,MAAM,SAAS,qBAAqB,CAAC,EAAE,QAAQ,CAAC,YAAY,WAAW,CAAC;AAAA,EACnF,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,QAAQ,CAAC,CAAC;AACzC,CAAC;AAED,IAAM,gBAAgB,EACnB,OAAO;AAAA,EACN,aAAa,EAAE,QAAQ,EAAE,SAAS;AAAA,EAClC,YAAY,EAAE,QAAQ,EAAE,SAAS;AACnC,CAAC,EACA,QAAQ,CAAC,CAAC;AAEb,IAAM,YAAY,EAAE,OAAO;AAAA,EACzB,SAAS,SAAS,aAAa;AAAA,EAC/B,SAAS,EACN,MAAM,SAAS,QAAQ,CAAC,EACxB,IAAI,GAAG,8CAA8C;AAAA,EACxD,aAAa,EAAE,KAAK,CAAC,YAAY,MAAM,CAAC,EAAE,QAAQ,UAAU;AAAA,EAC5D,SAAS,EAAE,MAAM,SAAS,mBAAmB,CAAC,EAAE,SAAS;AAAA,EACzD,SAAS;AACX,CAAC;AAEM,IAAM,eAAe,EAAE,OAAO;AAAA,EACnC,SAAS,EAAE,QAAQ,GAAG;AAAA,IACpB,UAAU,OAAO,EAAE,SAAS,kDAAkD;AAAA,EAChF,CAAC;AAAA,EACD,MAAM;AAAA,EACN,UAAU,EAAE,KAAK,CAAC,WAAW,KAAK,GAAG;AAAA,IACnC,UAAU,OAAO,EAAE,SAAS,gDAAgD;AAAA,EAC9E,CAAC;AAAA,EACD,KAAK;AAAA,EACL,OAAO,YAAY,QAAQ,CAAC,CAAC;AAAA,EAC7B,KAAK;AACP,CAAC;;;AFpED,IAAM,uBAAuB,CAAC,oBAAoB,mBAAmB;AAYrE,eAAsB,kBACpB,UACA,MAAc,QAAQ,IAAI,GACT;AACjB,MAAI,UAAU;AACZ,UAAM,WAAWC,MAAK,QAAQ,KAAK,QAAQ;AAC3C,QAAI,CAAE,MAAM,OAAO,QAAQ,GAAI;AAC7B,YAAM,IAAI,kBAAkB,0BAA0B,QAAQ,EAAE;AAAA,IAClE;AACA,WAAO;AAAA,EACT;AAEA,aAAW,QAAQ,sBAAsB;AACvC,UAAM,YAAYA,MAAK,QAAQ,KAAK,IAAI;AACxC,QAAI,MAAM,OAAO,SAAS,GAAG;AAC3B,aAAO;AAAA,IACT;AAAA,EACF;AAEA,QAAM,IAAI,kBAAkB,+BAA+B;AAAA,IACzD;AAAA,EACF,CAAC;AACH;AAMA,eAAsB,WACpB,UACA,MAAc,QAAQ,IAAI,GACH;AAEvB,SAAO,OAAO,EAAE,MAAMA,MAAK,QAAQ,KAAK,MAAM,EAAE,CAAC;AAEjD,QAAM,aAAa,MAAM,kBAAkB,UAAU,GAAG;AACxD,SAAO,MAAM,iBAAiB,UAAU,EAAE;AAE1C,QAAM,MAAM,MAAM,SAAS,YAAY,MAAM;AAC7C,QAAM,cAAc,WAAW,GAAG;AAElC,MAAI;AACJ,MAAI;AACF,aAAS,UAAU,WAAW;AAAA,EAChC,SAAS,KAAK;AACZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,UAAM,IAAI,kBAAkB,mBAAmBA,MAAK,SAAS,UAAU,CAAC,KAAK,CAAC,MAAM,CAAC;AAAA,EACvF;AAEA,QAAM,SAAS,aAAa,UAAU,MAAM;AAC5C,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,UAAU,OAAO,MAAM,OAAO,IAAI,CAAC,UAAU;AACjD,YAAM,QAAQ,MAAM,KAAK,SAAS,IAAI,GAAG,MAAM,KAAK,KAAK,GAAG,CAAC,OAAO;AACpE,aAAO,KAAK,KAAK,GAAG,MAAM,OAAO;AAAA,IACnC,CAAC;AACD,UAAM,IAAI,kBAAkB,0BAA0B,OAAO;AAAA,EAC/D;AAEA,SAAO,EAAE,QAAQ,OAAO,MAAM,WAAW;AAC3C;;;AGjFA,SAAS,yBAAyB;AAClC,OAAOC,WAAU;AACjB,OAAO,cAAc;;;ACFrB,OAAOC,WAAU;AAMV,IAAM,WAAW;AAGjB,SAAS,QAAQ,MAAc,QAAQ,IAAI,GAAW;AAC3D,SAAOA,MAAK,KAAK,KAAK,UAAU,MAAM;AACxC;AAaO,SAAS,QAAQ,GAAmB;AACzC,SAAO,EAAE,MAAM,QAAQ,EAAE,KAAK,GAAG;AACnC;;;ADZO,IAAM,eAAe;AAO5B,eAAsB,UAAU,SAKT;AACrB,QAAM,MAAM,QAAQ,OAAO,QAAQ,IAAI;AACvC,QAAM,OAAOC,MAAK,QAAQ,KAAK,QAAQ,SAAS;AAChD,QAAM,SAAS,QAAQ,GAAG;AAC1B,QAAM,UAAU,MAAM;AACtB,QAAM,UAAUA,MAAK,KAAK,QAAQ,QAAQ,WAAW;AAErD,QAAM,IAAI,QAAc,CAAC,SAAS,WAAW;AAC3C,UAAM,SAAS,kBAAkB,OAAO;AACxC,UAAM,UAAU,SAAS,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,EAAE,CAAC;AAEtD,WAAO,GAAG,SAAS,MAAM,QAAQ,CAAC;AAClC,WAAO,GAAG,SAAS,MAAM;AACzB,YAAQ,GAAG,SAAS,MAAM;AAC1B,YAAQ,GAAG,WAAW,CAAC,QAAQ;AAC7B,UAAI,IAAI,SAAS,SAAU;AAC3B,aAAO,GAAG;AAAA,IACZ,CAAC;AAED,YAAQ,KAAK,MAAM;AAEnB,eAAW,OAAO,QAAQ,OAAO;AAG/B,YAAM,WAAWA,MAAK,KAAK,MAAM,GAAG,IAAI,MAAM,GAAG,CAAC;AAClD,YAAM,YAAY,GAAG,YAAY,IAAI,GAAG;AACxC,cAAQ,KAAK,UAAU,EAAE,MAAM,UAAU,CAAC;AAAA,IAC5C;AAEA,SAAK,QAAQ,SAAS;AAAA,EACxB,CAAC;AAED,SAAO,EAAE,SAAS,OAAO,QAAQ,MAAM;AACzC;;;AE3DA,OAAOC,WAAU;;;ACAjB,OAAOC,WAAU;AACjB,OAAO,QAAQ;AAiBf,IAAM,mBAAmB;AAAA,EACvB;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAOA,eAAsB,cACpB,OACA,OAAwB,CAAC,GACN;AACnB,QAAM,MAAM,KAAK,OAAO,QAAQ,IAAI;AACpC,QAAM,OAAOC,MAAK,QAAQ,KAAK,MAAM,IAAI;AAEzC,MAAI,CAAE,MAAM,YAAY,IAAI,GAAI;AAC9B,UAAM,IAAI,kBAAkB,8BAA8B,MAAM,IAAI,EAAE;AAAA,EACxE;AAEA,QAAM,SAAS,CAAC,GAAG,kBAAkB,GAAG,MAAM,OAAO;AACrD,MAAI,KAAK,gBAAgB;AACvB,WAAO,KAAK,KAAK,cAAc;AAAA,EACjC;AAEA,QAAM,UAAU,MAAM,GAAG,MAAM,SAAS;AAAA,IACtC,KAAK;AAAA,IACL;AAAA,IACA,WAAW;AAAA,IACX,KAAK;AAAA,IACL,qBAAqB;AAAA,EACvB,CAAC;AAED,MAAI,QAAQ,WAAW,GAAG;AACxB,UAAM,IAAI,kBAAkB,gCAAgC;AAAA,MAC1D;AAAA,MACA,uBAAuB,MAAM,IAAI,kBAAkB,KAAK,UAAU,MAAM,OAAO,CAAC;AAAA,IAClF,CAAC;AAAA,EACH;AAEA,SAAO,QAAQ,KAAK;AACtB;;;AC/DA,OAAOC,WAAU;AAMV,IAAM,YAAY;AAYzB,eAAsB,cACpB,QACA,YACA,MAAc,QAAQ,IAAI,GACX;AACf,QAAM,EAAE,KAAK,MAAM,IAAI;AAEvB,MAAI,IAAI,gBAAgB,QAAQ;AAC9B,UAAM,WAAWC,MAAK,QAAQ,KAAK,MAAM,MAAM,SAAS;AACxD,QAAI,CAAE,MAAM,OAAO,QAAQ,GAAI;AAC7B,YAAM,IAAI;AAAA,QACR,6BAA6B,SAAS,iCAAiC,MAAM,IAAI;AAAA,QACjF,CAAC,SAAS,SAAS,8DAA8D;AAAA,MACnF;AAAA,IACF;AACA;AAAA,EACF;AAGA,MAAI,IAAI,WAAW,IAAI,QAAQ,SAAS,GAAG;AACzC,UAAM,YAAY,IAAI,IAAI,WAAW,IAAI,OAAO,CAAC;AACjD,UAAM,UAAU,IAAI,QACjB,IAAI,OAAO,EACX,OAAO,CAAC,MAAM,CAAC,UAAU,IAAI,CAAC,CAAC;AAElC,QAAI,QAAQ,SAAS,GAAG;AACtB,YAAM,IAAI;AAAA,QACR;AAAA,QACA,QAAQ,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE;AAAA,MAC7B;AAAA,IACF;AAAA,EACF;AACF;;;AFpCA,IAAM,eAAmD;AAAA,EACvD,SAAS;AAAA,EACT,KAAK;AACP;AAMA,eAAsB,YAAY,QAAgB,KAA4B;AAC5E,QAAM,EAAE,KAAK,SAAS,IAAI;AAE1B,MAAI,IAAI,WAAW,aAAa;AAC9B,UAAM,IAAI;AAAA,MACR;AAAA,MACA,CAAC,yEAAyE;AAAA,IAC5E;AAAA,EACF;AAEA,MAAI,IAAI,WAAW,UAAU;AAC3B,UAAM,UAAUC,MAAK,QAAQ,KAAK,IAAI,IAAI;AAC1C,QAAI,CAAE,MAAM,OAAO,OAAO,GAAI;AAC5B,YAAM,IAAI,kBAAkB,4BAA4B,IAAI,IAAI,EAAE;AAAA,IACpE;AACA,UAAM,WAAW,aAAa,QAAQ;AACtC,QAAI,QAAQ,OAAO,MAAM,UAAU;AACjC,YAAM,IAAI;AAAA,QACR,aAAa,QAAQ,gBAAgB,QAAQ,2BAA2B,IAAI,IAAI;AAAA,MAClF;AAAA,IACF;AAAA,EACF;AACF;AAMA,eAAsB,YACpB,QACA,KAC8B;AAC9B,QAAM,EAAE,QAAQ,WAAW,IAAI;AAC/B,QAAM,YAAY,QAAQ,GAAG;AAE7B,QAAM,QAAQ,MAAM,cAAc,OAAO,OAAO;AAAA,IAC9C;AAAA,IACA,gBAAgBA,MAAK,SAAS,UAAU;AAAA,EAC1C,CAAC;AAED,QAAM,cAAc,QAAQ,OAAO,GAAG;AAEtC,SAAO,EAAE,MAAM;AACjB;AAEA,eAAsB,gBAAgB,MAAoC;AACxE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,WAAW,KAAK,QAAQ,GAAG;AAChD,QAAM,EAAE,MAAM,IAAI,MAAM,YAAY,QAAQ,GAAG;AAC/C,QAAM,EAAE,OAAO,IAAI;AAEnB,SAAO,KAAK,yBAAyB;AACrC,SAAO,KAAK,EAAE;AACd,SAAO,KAAK,cAAcA,MAAK,SAAS,KAAK,OAAO,UAAU,KAAK,OAAO,UAAU,EAAE;AACtF,SAAO,KAAK,cAAc,OAAO,QAAQ,EAAE;AAC3C,SAAO,KAAK,cAAc,OAAO,IAAI,OAAO,EAAE;AAC9C,SAAO,KAAK,cAAc,YAAY,MAAM,CAAC,EAAE;AAC/C,SAAO,KAAK,cAAc,OAAO,IAAI,QAAQ,MAAM,EAAE;AACrD,aAAW,UAAU,OAAO,IAAI,SAAS;AACvC,WAAO,KAAK,OAAO,MAAM,EAAE;AAAA,EAC7B;AACA,SAAO,KAAK,cAAc,MAAM,MAAM,EAAE;AACxC,aAAW,QAAQ,OAAO;AACxB,WAAO,KAAK,OAAO,IAAI,EAAE;AAAA,EAC3B;AACA,SAAO,KAAK,cAAc,OAAO,IAAI,WAAW,EAAE;AACpD;AAEA,SAAS,YAAY,QAAwB;AAC3C,QAAM,EAAE,IAAI,IAAI;AAChB,UAAQ,IAAI,QAAQ;AAAA,IAClB,KAAK;AACH,aAAO,UAAU,IAAI,IAAI;AAAA,IAC3B,KAAK;AACH,aAAO,WAAW,IAAI,MAAM;AAAA,IAC9B;AACE,aAAO,IAAI;AAAA,EACf;AACF;;;AN3FA,eAAsB,eAAe,MAAoC;AACvE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,WAAW,KAAK,QAAQ,GAAG;AAChD,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,EAAE,MAAM,IAAI,MAAM,YAAY,QAAQ,GAAG;AAE/C,QAAM,EAAE,QAAQ,IAAI,MAAM,UAAU;AAAA,IAClC,OAAO;AAAA,IACP,WAAW,OAAO,MAAM;AAAA,IACxB,aAAa,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO,KAAK,sBAAsBC,MAAK,SAAS,KAAK,OAAO,KAAK,OAAO,EAAE;AAC1E,SAAO,KAAK,iBAAiB;AAC7B,aAAW,QAAQ,OAAO;AACxB,WAAO,KAAK,KAAK,IAAI,EAAE;AAAA,EACzB;AACF;;;AS5BA,OAAOC,YAAU;;;ACAjB,SAAS,kBAAkB;AAC3B,OAAOC,YAAU;AACjB,SAAS,gBAAgB;;;ACFzB,SAAS,aAA4B;AAI9B,IAAM,WAAW;AAQjB,SAAS,WAAW,MAAsB;AAC/C,QAAM,QAAQ,OAAO,KAAK,GAAG,KAAK,QAAQ,IAAI,KAAK,SAAS,EAAE,EAAE,SAAS,QAAQ;AACjF,SAAO,SAAS,KAAK;AACvB;AAiBA,eAAsB,UAAa,SAAqC;AACtE,QAAM,MAAM,GAAG,QAAQ,GAAG,QAAQ,IAAI;AACtC,QAAM,UAAkC;AAAA,IACtC,eAAe,WAAW,QAAQ,IAAI;AAAA,EACxC;AAEA,MAAI;AACJ,MAAI,QAAQ,aAAa,QAAW;AAClC,YAAQ,cAAc,IAAI;AAC1B,WAAO,KAAK,UAAU,QAAQ,QAAQ;AAAA,EACxC,WAAW,QAAQ,YAAY,QAAW;AACxC,WAAO,QAAQ;AAAA,EACjB;AAEA,SAAO,MAAM,GAAG,QAAQ,MAAM,IAAI,GAAG,EAAE;AAEvC,MAAI;AACJ,MAAI;AACF,eAAW,MAAM,MAAM,KAAK,EAAE,QAAQ,QAAQ,QAAQ,SAAS,KAAK,CAAC;AAAA,EACvE,SAAS,KAAK;AACZ,UAAM,SAAS,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG;AAC9D,UAAM,IAAI,kBAAkB,iCAAiC,MAAM,EAAE;AAAA,EACvE;AAEA,QAAM,OAAO,MAAM,SAAS,KAAK;AACjC,MAAI,OAAgB,CAAC;AACrB,MAAI,MAAM;AACR,QAAI;AACF,aAAO,KAAK,MAAM,IAAI;AAAA,IACxB,QAAQ;AACN,aAAO,EAAE,KAAK,KAAK;AAAA,IACrB;AAAA,EACF;AAEA,MAAI,CAAC,SAAS,IAAI;AAChB,UAAM,UAAU,oBAAoB,IAAI,KAAK,SAAS;AACtD,UAAM,IAAI,kBAAkB,2BAA2B,SAAS,MAAM,MAAM,OAAO,EAAE;AAAA,EACvF;AAEA,SAAO;AACT;AAEA,SAAS,oBAAoB,MAAmC;AAC9D,MAAI,QAAQ,OAAO,SAAS,UAAU;AACpC,UAAM,MAAM;AACZ,eAAW,OAAO,CAAC,WAAW,SAAS,QAAQ,GAAG;AAChD,YAAM,QAAQ,IAAI,GAAG;AACrB,UAAI,OAAO,UAAU,YAAY,MAAM,SAAS,EAAG,QAAO;AAAA,IAC5D;AAAA,EACF;AACA,SAAO;AACT;;;ADjEA,eAAsB,UAAU,SAIH;AAC3B,MAAI,CAAE,MAAM,OAAO,QAAQ,QAAQ,GAAI;AACrC,UAAM,IAAI,kBAAkB,4BAA4B,QAAQ,QAAQ,EAAE;AAAA,EAC5E;AAEA,QAAM,OAAO,IAAI,SAAS;AAC1B,QAAM,OAAO,MAAM,WAAW,QAAQ,QAAQ;AAC9C,OAAK,IAAI,QAAQ,MAAMC,OAAK,SAAS,QAAQ,QAAQ,CAAC;AACtD,MAAI,QAAQ,UAAU;AACpB,SAAK,IAAI,aAAa,QAAQ,QAAQ;AAAA,EACxC;AAEA,SAAO,UAA2B;AAAA,IAChC,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,MAAM,QAAQ;AAAA,IACd,SAAS;AAAA,EACX,CAAC;AACH;;;AExCA,SAAS,cAAAC,mBAAkB;AAC3B,OAAOC,YAAU;AACjB,SAAS,YAAAC,iBAAgB;AAiBzB,eAAsB,YAAY,SAIH;AAC7B,MAAI,CAAE,MAAM,OAAO,QAAQ,OAAO,GAAI;AACpC,UAAM,IAAI,kBAAkB,6BAA6B,QAAQ,OAAO,EAAE;AAAA,EAC5E;AAEA,QAAM,OAAO,IAAIC,UAAS;AAC1B,QAAM,OAAO,MAAMC,YAAW,QAAQ,OAAO;AAC7C,OAAK,IAAI,QAAQ,MAAMC,OAAK,SAAS,QAAQ,OAAO,CAAC;AACrD,MAAI,QAAQ,UAAU;AACpB,SAAK,IAAI,aAAa,QAAQ,QAAQ;AAAA,EACxC;AAEA,SAAO,UAA6B;AAAA,IAClC,MAAM;AAAA,IACN,QAAQ;AAAA,IACR,MAAM,QAAQ;AAAA,IACd,SAAS;AAAA,EACX,CAAC;AACH;;;ACtBO,SAAS,UAAU,UAA4B;AACpD,SAAO,4BAA4B,QAAQ;AAC7C;AAWO,SAAS,aACd,QACA,UACc;AACd,QAAM,EAAE,IAAI,IAAI;AAChB,QAAM,UAAwB;AAAA,IAC5B,KAAK,SAAS;AAAA,IACd,WAAW,SAAS;AAAA,IACpB,SAAS,IAAI;AAAA,IACb,SAAS,IAAI;AAAA,EACf;AAEA,MAAI,IAAI,gBAAgB,cAAc,IAAI,WAAW,IAAI,QAAQ,SAAS,GAAG;AAC3E,YAAQ,UAAU,IAAI;AAAA,EACxB;AAEA,MAAI,IAAI,QAAQ,gBAAgB,QAAW;AACzC,YAAQ,cAAc,IAAI,QAAQ;AAAA,EACpC;AACA,MAAI,IAAI,QAAQ,eAAe,QAAW;AACxC,YAAQ,aAAa,IAAI,QAAQ;AAAA,EACnC;AAEA,SAAO;AACT;AAGA,eAAsB,WAAW,SAIH;AAC5B,SAAO,UAA4B;AAAA,IACjC,MAAM,UAAU,QAAQ,QAAQ;AAAA,IAChC,QAAQ;AAAA,IACR,MAAM,QAAQ;AAAA,IACd,UAAU,QAAQ;AAAA,EACpB,CAAC;AACH;;;AJlDA,IAAM,kBAAkB;AACxB,IAAM,oBAAoB;AAGnB,SAAS,eAAe,QAAgB,MAA0B;AACvE,MAAI,MAAM,OAAO;AACjB,MAAI,KAAK,UAAU,KAAK,OAAO,SAAS,GAAG;AACzC,UAAM,EAAE,GAAG,KAAK,SAAS,KAAK,OAAO;AAAA,EACvC;AACA,MAAI,KAAK,WAAW,KAAK,QAAQ,SAAS,GAAG;AAC3C,UAAM,EAAE,GAAG,KAAK,SAAS,KAAK,QAAQ,IAAI,OAAO,GAAG,aAAa,WAAW;AAAA,EAC9E;AACA,SAAO,EAAE,GAAG,QAAQ,IAAI;AAC1B;AAEA,eAAsB,WAAW,MAAiC;AAChE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,WAAW,KAAK,QAAQ,GAAG;AAChD,QAAM,SAAS,eAAe,OAAO,QAAQ,IAAI;AAGjD,QAAM,EAAE,MAAM,IAAI,MAAM,YAAY,EAAE,GAAG,QAAQ,OAAO,GAAG,GAAG;AAE9D,MAAI,KAAK,QAAQ;AACf,gBAAY,QAAQ,OAAO,GAAG;AAC9B;AAAA,EACF;AAGA,QAAM,EAAE,QAAQ,IAAI,MAAM,UAAU;AAAA,IAClC,OAAO;AAAA,IACP,WAAW,OAAO,MAAM;AAAA,IACxB,aAAa,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF,CAAC;AACD,SAAO,MAAM,sBAAsB,OAAO,EAAE;AAG5C,QAAM,SAAS,MAAM,WAAW,QAAQ,GAAG;AAC3C,SAAO,MAAM,iBAAiB,MAAM,EAAE;AAGtC,SAAO,KAAK,yBAAyB,MAAM,MAAM,eAAe;AAChE,QAAM,cAAc,MAAM,YAAY;AAAA,IACpC,MAAM,OAAO;AAAA,IACb;AAAA,IACA,UAAU,OAAO,MAAM;AAAA,EACzB,CAAC;AAGD,QAAM,UAAU,aAAa,QAAQ;AAAA,IACnC,KAAK;AAAA,IACL,WAAW,YAAY;AAAA,EACzB,CAAC;AACD,SAAO,KAAK,YAAY,OAAO,QAAQ,YAAY;AACnD,QAAM,QAAQ,MAAM,WAAW,EAAE,MAAM,OAAO,MAAM,UAAU,OAAO,UAAU,QAAQ,CAAC;AAExF,eAAa,QAAQ,QAAQ,YAAY,gBAAgB,MAAM,QAAQ;AACzE;AAEA,eAAe,WAAW,QAAgB,KAA8B;AACtE,QAAM,EAAE,IAAI,IAAI;AAChB,MAAI,IAAI,WAAW,WAAW;AAC5B,WAAO,IAAI;AAAA,EACb;AACA,MAAI,IAAI,WAAW,UAAU;AAC3B,WAAO,KAAK,kBAAkB,IAAI,IAAI,MAAM;AAC5C,UAAM,SAAS,MAAM,UAAU;AAAA,MAC7B,MAAM,OAAO;AAAA,MACb,UAAUC,OAAK,QAAQ,KAAK,IAAI,IAAI;AAAA,MACpC,UAAU,IAAI;AAAA,IAChB,CAAC;AACD,WAAO,OAAO;AAAA,EAChB;AACA,QAAM,IAAI,kBAAkB,2BAA2B,IAAI,MAAM,EAAE;AACrE;AAEA,SAAS,WAAW,QAAwB;AAC1C,QAAM,EAAE,IAAI,IAAI;AAChB,SAAO,IAAI,WAAW,YAAY,IAAI,SAAS;AACjD;AAEA,SAAS,YAAY,QAAgB,OAAiB,KAAmB;AACvE,SAAO,KAAK,kCAAkC;AAC9C,SAAO,KAAK,EAAE;AACd,SAAO,KAAK,gBAAgB;AAC5B,aAAW,QAAQ,OAAO;AACxB,WAAO,KAAK,KAAK,IAAI,EAAE;AAAA,EACzB;AACA,SAAO,KAAK,EAAE;AAEd,QAAM,EAAE,IAAI,IAAI;AAChB,MAAI,IAAI,WAAW,UAAU;AAC3B,WAAO,KAAK;AAAA,EAAsB,IAAI,IAAI,EAAE;AAAA,EAC9C,WAAW,IAAI,WAAW,WAAW;AACnC,WAAO,KAAK;AAAA,EAAmB,IAAI,MAAM,EAAE;AAAA,EAC7C;AACA,SAAO,KAAK,EAAE;AAEd,SAAO,KAAK;AAAA,OAAqB,UAAU,OAAO,QAAQ,CAAC,EAAE;AAC7D,SAAO,KAAK,EAAE;AAEd,QAAM,UAAU,aAAa,QAAQ;AAAA,IACnC,KAAK,WAAW,MAAM;AAAA,IACtB,WAAW;AAAA,EACb,CAAC;AACD,SAAO,KAAK,UAAU;AACtB,SAAO,KAAK,KAAK,UAAU,SAAS,MAAM,CAAC,CAAC;AAC9C;AAEA,SAAS,aACP,QACA,QACA,cACA,SACM;AACN,SAAO,KAAK,EAAE;AACd,SAAO,KAAK,0BAA0B;AACtC,SAAO,KAAK,EAAE;AACd,SAAO,KAAK,YAAY,OAAO,IAAI,OAAO,EAAE;AAC5C,SAAO,KAAK,aAAa,OAAO,QAAQ,EAAE;AAC1C,SAAO,KAAK,UAAU;AACtB,aAAW,UAAU,OAAO,IAAI,SAAS;AACvC,WAAO,KAAK,KAAK,MAAM,EAAE;AAAA,EAC3B;AACA,SAAO,KAAK,EAAE;AACd,SAAO,KAAK,QAAQ,MAAM,EAAE;AAC5B,SAAO,KAAK,eAAe,YAAY,EAAE;AACzC,SAAO,KAAK,aAAa,OAAO,EAAE;AACpC;;;AKrJA,OAAOC,YAAU;AAWjB,eAAsB,iBAAiB,MAAoC;AACzE,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,EAAE,OAAO,IAAI,MAAM,WAAW,KAAK,QAAQ,GAAG;AACpD,QAAM,YAAY,QAAQ,GAAG;AAE7B,MAAI,OAAO,IAAI,WAAW,UAAU;AAClC,UAAM,IAAI;AAAA,MACR,sDAAsD,OAAO,IAAI,MAAM;AAAA,IACzE;AAAA,EACF;AAEA,QAAM,WAAWC,OAAK,QAAQ,KAAK,OAAO,IAAI,IAAI;AAClD,SAAO,KAAK,kBAAkB,OAAO,IAAI,IAAI,MAAM;AAEnD,QAAM,SAAS,MAAM,UAAU;AAAA,IAC7B,MAAM,OAAO;AAAA,IACb;AAAA,IACA,UAAU,OAAO,IAAI;AAAA,EACvB,CAAC;AAED,SAAO,KAAK,eAAe;AAC3B,SAAO,KAAK,YAAY,OAAO,OAAO,EAAE;AACxC,MAAI,OAAO,UAAW,QAAO,KAAK,cAAc,OAAO,SAAS,EAAE;AAClE,MAAI,OAAO,OAAQ,QAAO,KAAK,YAAY,OAAO,MAAM,EAAE;AAC5D;;;AC5BA,eAAsB,mBAAmB,MAAoC;AAC3E,QAAM,MAAM,QAAQ,IAAI;AACxB,QAAM,SAAS,MAAM,WAAW,KAAK,QAAQ,GAAG;AAChD,QAAM,EAAE,OAAO,IAAI;AACnB,QAAM,EAAE,MAAM,IAAI,MAAM,YAAY,QAAQ,GAAG;AAE/C,QAAM,EAAE,QAAQ,IAAI,MAAM,UAAU;AAAA,IAClC,OAAO;AAAA,IACP,WAAW,OAAO,MAAM;AAAA,IACxB,aAAa,OAAO,MAAM;AAAA,IAC1B;AAAA,EACF,CAAC;AAED,SAAO,KAAK,yBAAyB,MAAM,MAAM,eAAe;AAEhE,QAAM,SAAS,MAAM,YAAY;AAAA,IAC/B,MAAM,OAAO;AAAA,IACb;AAAA,IACA,UAAU,OAAO,MAAM;AAAA,EACzB,CAAC;AAED,SAAO,KAAK,sBAAsB;AAClC,SAAO,KAAK,mBAAmB,OAAO,cAAc,EAAE;AACtD,MAAI,OAAO,UAAW,QAAO,KAAK,cAAc,OAAO,SAAS,EAAE;AAClE,MAAI,OAAO,OAAQ,QAAO,KAAK,YAAY,OAAO,MAAM,EAAE;AAC5D;;;ApBtBA,IAAM,UAAU,IAAI,QAAQ;AAG5B,SAAS,QAAQ,OAAe,UAA8B;AAC5D,SAAO,CAAC,GAAG,UAAU,KAAK;AAC5B;AAOA,SAAS,OACP,IAC4B;AAC5B,SAAO,OAAO,YAAe;AAC3B,UAAM,OAAO,EAAE,GAAG,QAAQ,KAAK,GAAG,GAAG,QAAQ;AAC7C,QAAI,KAAK,MAAO,UAAS,IAAI;AAC7B,QAAI;AACF,YAAM,GAAG,IAAI;AAAA,IACf,SAAS,KAAK;AACZ,UAAI,oBAAoB,GAAG,GAAG;AAC5B,eAAO,MAAM,IAAI,OAAO;AACxB,mBAAW,QAAQ,IAAI,WAAW,CAAC,GAAG;AACpC,iBAAO,MAAM,KAAK,IAAI,EAAE;AAAA,QAC1B;AAAA,MACF,OAAO;AACL,eAAO,MAAM,eAAe,QAAS,IAAI,SAAS,IAAI,UAAW,OAAO,GAAG,CAAC;AAAA,MAC9E;AACA,cAAQ,WAAW;AAAA,IACrB;AAAA,EACF;AACF;AAEA,QACG,KAAK,cAAc,EACnB,YAAY,2EAA2E,EACvF,QAAQ,OAAO,EACf,OAAO,uBAAuB,yBAAyB,EACvD,OAAO,WAAW,sBAAsB;AAE3C,QACG,QAAQ,MAAM,EACd,YAAY,mCAAmC,EAC/C,OAAO,aAAa,4BAA4B,EAChD,OAAO,SAAS,wBAAwB,EACxC,OAAO,WAAW,8BAA8B,EAChD,OAAO,OAAO,WAAW,CAAC;AAE7B,QACG,QAAQ,UAAU,EAClB,YAAY,kDAAkD,EAC9D,OAAO,OAAO,eAAe,CAAC;AAEjC,QACG,QAAQ,SAAS,EACjB,YAAY,2DAA2D,EACvE,OAAO,OAAO,cAAc,CAAC;AAEhC,QACG,QAAQ,YAAY,EACpB,YAAY,wDAAwD,EACpE,OAAO,OAAO,gBAAgB,CAAC;AAElC,QACG,QAAQ,cAAc,EACtB,YAAY,2CAA2C,EACvD,OAAO,OAAO,kBAAkB,CAAC;AAEpC,QACG,QAAQ,KAAK,EACb,YAAY,0DAA0D,EACtE,OAAO,aAAa,yDAAyD,EAC7E,OAAO,mBAAmB,qCAAqC,SAAS,CAAC,CAAC,EAC1E,OAAO,oBAAoB,qCAAqC,SAAS,CAAC,CAAC,EAC3E,OAAO,OAAO,UAAU,CAAC;AAE5B,QAAQ,WAAW,QAAQ,IAAI,EAAE,MAAM,CAAC,QAAQ;AAC9C,SAAO,MAAM,eAAe,QAAQ,IAAI,UAAU,OAAO,GAAG,CAAC;AAC7D,UAAQ,WAAW;AACrB,CAAC;","names":["path","path","path","path","path","path","path","path","path","path","path","path","path","path","path","path","path","path","openAsBlob","path","FormData","FormData","openAsBlob","path","path","path","path"]}
package/package.json ADDED
@@ -0,0 +1,51 @@
1
+ {
2
+ "name": "maestrostack",
3
+ "version": "0.1.0",
4
+ "description": "Config-driven CLI for running Maestro mobile tests on BrowserStack App Automate.",
5
+ "keywords": [
6
+ "maestro",
7
+ "browserstack",
8
+ "app-automate",
9
+ "mobile-testing",
10
+ "cli",
11
+ "android",
12
+ "ios"
13
+ ],
14
+ "license": "MIT",
15
+ "type": "module",
16
+ "bin": {
17
+ "maestrostack": "dist/cli.js"
18
+ },
19
+ "files": [
20
+ "dist",
21
+ "README.md"
22
+ ],
23
+ "engines": {
24
+ "node": ">=18"
25
+ },
26
+ "scripts": {
27
+ "build": "tsup",
28
+ "dev": "tsup --watch",
29
+ "typecheck": "tsc --noEmit",
30
+ "test": "vitest run",
31
+ "test:watch": "vitest",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "archiver": "^7.0.1",
36
+ "commander": "^12.1.0",
37
+ "dotenv": "^16.4.5",
38
+ "fast-glob": "^3.3.2",
39
+ "undici": "^6.21.0",
40
+ "yaml": "^2.6.1",
41
+ "zod": "^3.24.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/archiver": "^6.0.3",
45
+ "@types/node": "^22.10.5",
46
+ "tsup": "^8.3.5",
47
+ "typescript": "^5.7.3",
48
+ "unzipper": "^0.12.3",
49
+ "vitest": "^2.1.8"
50
+ }
51
+ }