@percy/cli-app 1.32.2-beta.0 → 1.32.3-beta.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/dist/exec.js CHANGED
@@ -1,3 +1,6 @@
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import path from 'path';
1
4
  import command from '@percy/cli-command';
2
5
  import * as ExecPlugin from '@percy/cli-exec';
3
6
  export const ping = ExecPlugin.ping;
@@ -11,6 +14,158 @@ export const start = command('start', {
11
14
  skipDiscovery: true
12
15
  }
13
16
  }, ExecPlugin.start.callback);
17
+
18
+ // Locate the `test` subcommand in argv. Maestro accepts global parent
19
+ // flags before the subcommand, e.g.:
20
+ // maestro test flow.yaml
21
+ // maestro --udid <serial> test flow.yaml
22
+ // maestro --platform=android test flow.yaml
23
+ // maestro --verbose --no-ansi test flow.yaml
24
+ // We must find `test` by scanning rather than checking args[1] === 'test',
25
+ // or our injects silently no-op when the customer pins a device.
26
+ //
27
+ // Returns the index of the `test` literal, or -1 if not present. Skips
28
+ // over the value of known value-taking parent flags so a literal `test`
29
+ // supplied as a flag value (e.g. `--udid test`) isn't mistaken for the
30
+ // subcommand. Equals-form (`--flag=value`) doesn't need a skip — the
31
+ // value is part of the same argv token.
32
+ const MAESTRO_PARENT_VALUE_FLAGS = new Set(['--udid', '--device', '-p', '--platform', '--host', '--port', '--driver-host-port']);
33
+ function findTestSubcommandIdx(args) {
34
+ for (let i = 1; i < args.length; i++) {
35
+ const tok = args[i];
36
+ if (tok === 'test') return i;
37
+ if (typeof tok === 'string' && MAESTRO_PARENT_VALUE_FLAGS.has(tok)) {
38
+ i++; // skip the value of this flag
39
+ }
40
+ }
41
+ return -1;
42
+ }
43
+ function hasExistingPercyServerFlag(args, testIdx) {
44
+ for (let i = testIdx + 1; i < args.length - 1; i++) {
45
+ if (args[i] === '-e' && /^PERCY_SERVER=/.test(args[i + 1])) return true;
46
+ }
47
+ return false;
48
+ }
49
+
50
+ // Returns the customer-supplied value of `--test-output-dir` (whether the
51
+ // space-form `--test-output-dir <path>` or the equals-form
52
+ // `--test-output-dir=<path>` — both are valid picocli syntax), or null if
53
+ // absent. Returning the value (not just an index) lets the screenshot-dir
54
+ // helper align PERCY_MAESTRO_SCREENSHOT_DIR with the customer's value in
55
+ // either form without re-deriving it.
56
+ //
57
+ // An empty value (space-form value === '' or equals-form `--test-output-dir=`
58
+ // with nothing after the `=`) is treated as ABSENT — returning the empty
59
+ // string would tell the caller "customer set this" and bypass the
60
+ // auto-resolve fallback, leaving Maestro to default its output location
61
+ // while PERCY_MAESTRO_SCREENSHOT_DIR stays unset. That mismatch silently
62
+ // produces all-404 snapshots, so fall through to the auto-resolve path
63
+ // instead.
64
+ const TEST_OUTPUT_DIR_EQ_PREFIX = '--test-output-dir=';
65
+ function findTestOutputDirValue(args, testIdx) {
66
+ for (let i = testIdx + 1; i < args.length; i++) {
67
+ const tok = args[i];
68
+ if (tok === '--test-output-dir' && i + 1 < args.length) {
69
+ const val = args[i + 1];
70
+ if (typeof val === 'string' && val.length > 0) return val;
71
+ } else if (typeof tok === 'string' && tok.startsWith(TEST_OUTPUT_DIR_EQ_PREFIX)) {
72
+ const val = tok.slice(TEST_OUTPUT_DIR_EQ_PREFIX.length);
73
+ if (val.length > 0) return val;
74
+ }
75
+ }
76
+ return null;
77
+ }
78
+
79
+ // Maestro's GraalJS sandbox does NOT inherit the parent process's env,
80
+ // so `PERCY_SERVER_ADDRESS` exported by app:exec is invisible to the
81
+ // SDK. When wrapping `maestro test`, surface the CLI address through
82
+ // Maestro's only env channel — `-e KEY=VALUE` flags — so the SDK
83
+ // healthcheck can find the local CLI without the customer having to
84
+ // pair ports manually. No-op when the customer already supplied their
85
+ // own `-e PERCY_SERVER=...`.
86
+ //
87
+ // When percy?.address() is falsy (percy disabled, start failed), emit a
88
+ // WARN so the customer is not surprised by a silent zero-snapshot build.
89
+ // The customer-override skip case (their own `-e PERCY_SERVER=...` is in
90
+ // argv) does NOT warn — that's intentional flow control, not a problem.
91
+ export function maybeInjectMaestroServer(ctx, log) {
92
+ var _ctx$percy;
93
+ const args = ctx === null || ctx === void 0 ? void 0 : ctx.argv;
94
+ if (!Array.isArray(args) || args.length < 2) return;
95
+ if (path.basename(args[0]) !== 'maestro') return;
96
+ const testIdx = findTestSubcommandIdx(args);
97
+ if (testIdx < 0) return;
98
+ if (hasExistingPercyServerFlag(args, testIdx)) return;
99
+ const addr = (_ctx$percy = ctx.percy) === null || _ctx$percy === void 0 ? void 0 : _ctx$percy.address();
100
+ if (!addr) {
101
+ log === null || log === void 0 || log.warn('app:exec did not start the Percy CLI server (percy disabled or start ' + 'failed); -e PERCY_SERVER not injected into maestro test. Snapshots will ' + 'NOT be uploaded. Set PERCY_TOKEN and re-run, or check the percy log above.');
102
+ return;
103
+ }
104
+ // Inject after `test` so `-e KEY=VAL` is bound to the `test` subcommand
105
+ // (the only Maestro subcommand that accepts `-e`).
106
+ args.splice(testIdx + 1, 0, '-e', `PERCY_SERVER=${addr}`);
107
+ }
108
+
109
+ // Auto-resolve the Maestro screenshot output directory so customers don't
110
+ // have to pair `export PERCY_MAESTRO_SCREENSHOT_DIR=...` with a matching
111
+ // `--test-output-dir <same>` in their maestro test command.
112
+ //
113
+ // Resolution order:
114
+ // 1. Customer set BOTH process.env.PERCY_MAESTRO_SCREENSHOT_DIR and
115
+ // --test-output-dir in argv → trust them, do nothing.
116
+ // 2. Customer set PERCY_MAESTRO_SCREENSHOT_DIR only → use it, inject
117
+ // `--test-output-dir <env value>` into argv.
118
+ // 3. Customer set --test-output-dir only → use that value, mirror it
119
+ // into process.env.PERCY_MAESTRO_SCREENSHOT_DIR (so the SDK +
120
+ // CLI relay see the same path).
121
+ // 4. Neither set → pick `${process.cwd()}/.percy-out`. On any mkdir
122
+ // failure (read-only CWD, EACCES, EEXIST as a file), fall back to
123
+ // `${os.tmpdir()}/percy-maestro-<pid>` with a WARN log.
124
+ //
125
+ // The env-var update and argv splice always keep both sources of truth
126
+ // (SDK reads env var; Maestro reads the flag) aligned to the same path.
127
+ export function maybeInjectScreenshotDir(ctx, log) {
128
+ const args = ctx === null || ctx === void 0 ? void 0 : ctx.argv;
129
+ if (!Array.isArray(args) || args.length < 2) return;
130
+ if (path.basename(args[0]) !== 'maestro') return;
131
+ const testIdx = findTestSubcommandIdx(args);
132
+ if (testIdx < 0) return;
133
+ const envSet = !!process.env.PERCY_MAESTRO_SCREENSHOT_DIR;
134
+ const existingFlagValue = findTestOutputDirValue(args, testIdx);
135
+ const flagSet = existingFlagValue !== null;
136
+
137
+ // Fully customer-controlled — nothing to do.
138
+ if (envSet && flagSet) return;
139
+ let resolved;
140
+ if (envSet) {
141
+ resolved = process.env.PERCY_MAESTRO_SCREENSHOT_DIR;
142
+ } else if (flagSet) {
143
+ resolved = existingFlagValue;
144
+ } else {
145
+ const preferred = path.join(process.cwd(), '.percy-out');
146
+ try {
147
+ fs.mkdirSync(preferred, {
148
+ recursive: true
149
+ });
150
+ resolved = preferred;
151
+ } catch (err) {
152
+ const fallback = path.join(os.tmpdir(), `percy-maestro-${process.pid}`);
153
+ try {
154
+ fs.mkdirSync(fallback, {
155
+ recursive: true
156
+ });
157
+ } catch (_) {
158
+ // tmpdir mkdir failure is exceedingly rare; fall through and let
159
+ // downstream code surface a clearer error than this helper can.
160
+ }
161
+ resolved = fallback;
162
+ log === null || log === void 0 || log.warn(`Could not create ${preferred} (${err.code || err.message}); ` + `falling back to ${fallback}. Set PERCY_MAESTRO_SCREENSHOT_DIR to ` + 'pick a specific location.');
163
+ }
164
+ }
165
+ if (!envSet) process.env.PERCY_MAESTRO_SCREENSHOT_DIR = resolved;
166
+ // Inject right after `test` (the subcommand that owns `--test-output-dir`).
167
+ if (!flagSet) args.splice(testIdx + 1, 0, '--test-output-dir', resolved);
168
+ }
14
169
  export const exec = command('exec', {
15
170
  description: 'Start and stop Percy around a supplied command for native apps',
16
171
  usage: '[options] -- <command>',
@@ -23,5 +178,17 @@ export const exec = command('exec', {
23
178
  projectType: 'app',
24
179
  skipDiscovery: true
25
180
  }
26
- }, ExecPlugin.default.callback);
181
+ }, async function* (ctx) {
182
+ // The two helpers splice their flag groups at argv index 2 (between `test`
183
+ // and the flow file) because `-e` and `--test-output-dir` are
184
+ // `test`-subcommand options. Resulting argv for `maestro test flow.yaml`:
185
+ // maestro test --test-output-dir <dir> -e PERCY_SERVER=<url> flow.yaml
186
+ // iOS driver port: not prescribed from this side — the @percy/core relay
187
+ // reads `PERCY_IOS_DRIVER_HOST_PORT` (BS-host-injected on production
188
+ // hosts) and probes the documented Maestro 2.4.0 single-simulator default
189
+ // (`127.0.0.1:7001`) when it isn't set. See `packages/core/src/maestro-hierarchy.js`.
190
+ maybeInjectMaestroServer(ctx, ctx.log);
191
+ maybeInjectScreenshotDir(ctx, ctx.log);
192
+ yield* ExecPlugin.default.callback(ctx);
193
+ });
27
194
  export default exec;
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  export { default, app } from './app.js';
2
- export { exec, start, stop, ping } from './exec.js';
2
+ export { exec, start, stop, ping, maybeInjectMaestroServer, maybeInjectScreenshotDir } from './exec.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@percy/cli-app",
3
- "version": "1.32.2-beta.0",
3
+ "version": "1.32.3-beta.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -33,8 +33,8 @@
33
33
  ]
34
34
  },
35
35
  "dependencies": {
36
- "@percy/cli-command": "1.32.2-beta.0",
37
- "@percy/cli-exec": "1.32.2-beta.0"
36
+ "@percy/cli-command": "1.32.3-beta.0",
37
+ "@percy/cli-exec": "1.32.3-beta.0"
38
38
  },
39
- "gitHead": "b9f4f5ccfa31695047c2b9b1a4549403af075f7a"
39
+ "gitHead": "e8af175695b4afdb78cdd48b67037710f5c8b1c9"
40
40
  }