@codyswann/lisa 3.20.0 → 3.22.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.
Files changed (56) hide show
  1. package/dist/core/upstream-evidence-manifest.d.ts.map +1 -1
  2. package/dist/core/upstream-evidence-manifest.js +7 -0
  3. package/dist/core/upstream-evidence-manifest.js.map +1 -1
  4. package/expo/create-only/eas.json +111 -0
  5. package/package.json +1 -1
  6. package/plugins/lisa/.claude-plugin/plugin.json +1 -1
  7. package/plugins/lisa/.codex-plugin/plugin.json +1 -1
  8. package/plugins/lisa-agy/plugin.json +1 -1
  9. package/plugins/lisa-cdk/.claude-plugin/plugin.json +1 -1
  10. package/plugins/lisa-cdk/.codex-plugin/plugin.json +1 -1
  11. package/plugins/lisa-cdk-agy/plugin.json +1 -1
  12. package/plugins/lisa-cdk-copilot/.claude-plugin/plugin.json +1 -1
  13. package/plugins/lisa-cdk-cursor/.claude-plugin/plugin.json +1 -1
  14. package/plugins/lisa-copilot/.claude-plugin/plugin.json +1 -1
  15. package/plugins/lisa-cursor/.claude-plugin/plugin.json +1 -1
  16. package/plugins/lisa-expo/.claude-plugin/plugin.json +1 -1
  17. package/plugins/lisa-expo/.codex-plugin/plugin.json +1 -1
  18. package/plugins/lisa-expo-agy/plugin.json +1 -1
  19. package/plugins/lisa-expo-copilot/.claude-plugin/plugin.json +1 -1
  20. package/plugins/lisa-expo-cursor/.claude-plugin/plugin.json +1 -1
  21. package/plugins/lisa-harper-fabric/.claude-plugin/plugin.json +1 -1
  22. package/plugins/lisa-harper-fabric/.codex-plugin/plugin.json +1 -1
  23. package/plugins/lisa-harper-fabric-agy/plugin.json +1 -1
  24. package/plugins/lisa-harper-fabric-copilot/.claude-plugin/plugin.json +1 -1
  25. package/plugins/lisa-harper-fabric-cursor/.claude-plugin/plugin.json +1 -1
  26. package/plugins/lisa-nestjs/.claude-plugin/plugin.json +1 -1
  27. package/plugins/lisa-nestjs/.codex-plugin/plugin.json +1 -1
  28. package/plugins/lisa-nestjs-agy/plugin.json +1 -1
  29. package/plugins/lisa-nestjs-copilot/.claude-plugin/plugin.json +1 -1
  30. package/plugins/lisa-nestjs-cursor/.claude-plugin/plugin.json +1 -1
  31. package/plugins/lisa-openclaw/.claude-plugin/plugin.json +1 -1
  32. package/plugins/lisa-openclaw/.codex-plugin/plugin.json +1 -1
  33. package/plugins/lisa-openclaw-agy/plugin.json +1 -1
  34. package/plugins/lisa-openclaw-copilot/.claude-plugin/plugin.json +1 -1
  35. package/plugins/lisa-openclaw-cursor/.claude-plugin/plugin.json +1 -1
  36. package/plugins/lisa-phaser/.claude-plugin/plugin.json +1 -1
  37. package/plugins/lisa-phaser/.codex-plugin/plugin.json +1 -1
  38. package/plugins/lisa-phaser-agy/plugin.json +1 -1
  39. package/plugins/lisa-phaser-copilot/.claude-plugin/plugin.json +1 -1
  40. package/plugins/lisa-phaser-cursor/.claude-plugin/plugin.json +1 -1
  41. package/plugins/lisa-rails/.claude-plugin/plugin.json +1 -1
  42. package/plugins/lisa-rails/.codex-plugin/plugin.json +1 -1
  43. package/plugins/lisa-rails-agy/plugin.json +1 -1
  44. package/plugins/lisa-rails-copilot/.claude-plugin/plugin.json +1 -1
  45. package/plugins/lisa-rails-cursor/.claude-plugin/plugin.json +1 -1
  46. package/plugins/lisa-typescript/.claude-plugin/plugin.json +1 -1
  47. package/plugins/lisa-typescript/.codex-plugin/plugin.json +1 -1
  48. package/plugins/lisa-typescript-agy/plugin.json +1 -1
  49. package/plugins/lisa-typescript-copilot/.claude-plugin/plugin.json +1 -1
  50. package/plugins/lisa-typescript-cursor/.claude-plugin/plugin.json +1 -1
  51. package/plugins/lisa-wiki/.claude-plugin/plugin.json +1 -1
  52. package/plugins/lisa-wiki/.codex-plugin/plugin.json +1 -1
  53. package/plugins/lisa-wiki-agy/plugin.json +1 -1
  54. package/plugins/lisa-wiki-copilot/.claude-plugin/plugin.json +1 -1
  55. package/plugins/lisa-wiki-cursor/.claude-plugin/plugin.json +1 -1
  56. package/scripts/lisa-assert-eas-profile.mjs +210 -0
@@ -0,0 +1,210 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Refuse an EAS build profile that cannot produce a testable E2E binary.
4
+ *
5
+ * Two profile mistakes waste a full native build and then fail the suite in a
6
+ * way that reads as a product bug rather than a configuration one:
7
+ *
8
+ * 1. `developmentClient: true` builds a **dev client**, which is a shell that
9
+ * loads JavaScript from a Metro server at runtime. CI has no Metro server,
10
+ * so the app opens its "development servers" launcher instead of the
11
+ * product. Every flow then fails on a screen that appears nowhere in the
12
+ * app, and the failure looks like a broken selector.
13
+ * 2. A profile with no `channel` takes the default one, so the binary can
14
+ * receive an over-the-air update published for unrelated work while the
15
+ * suite is running. The build under test stops being the build that was
16
+ * made, and the run is no longer reproducible.
17
+ *
18
+ * Both are cheap to detect and expensive to discover late, so this runs before
19
+ * the build rather than after it.
20
+ *
21
+ * ## Why this does not use `JSON.parse`, and does not read one profile
22
+ *
23
+ * `eas.json` is not required to be strict JSON — trailing commas are common
24
+ * and EAS accepts them. A real consumer file (`geminisportsai/frontend-v2`)
25
+ * has one on line 16, so a strict parse would crash this guard on the very
26
+ * repository the standard was extracted from.
27
+ *
28
+ * And a profile's effective settings are inherited: `dev-e2e` extends
29
+ * `dev-base` extends `base`, and `developmentClient` may be set on any of
30
+ * them. Reading only the named profile would pass a profile that inherits
31
+ * `developmentClient: true` from a parent.
32
+ * @module scripts/lisa-assert-eas-profile
33
+ */
34
+
35
+ import { existsSync, readFileSync } from "node:fs";
36
+
37
+ import { invokedAsScript } from "./lib/invoked-as-script.mjs";
38
+
39
+ /** Channel values that mean "no dedicated channel was chosen". */
40
+ const DEFAULT_CHANNELS = new Set(["default"]);
41
+
42
+ /** How deep an `extends` chain may go before it is treated as a cycle. */
43
+ const MAX_DEPTH = 20;
44
+
45
+ /**
46
+ * Parse `eas.json` the way EAS does rather than the way `JSON.parse` does.
47
+ *
48
+ * Strips `//` and block comments, then trailing commas. Deliberately tolerant:
49
+ * the guard's job is to judge the profile, and refusing to read a file EAS
50
+ * itself accepts would fail builds over punctuation.
51
+ * @param {string} text Raw file contents.
52
+ * @returns {Record<string, any>} The parsed document.
53
+ */
54
+ export function parseEasJson(text) {
55
+ const withoutComments = text
56
+ .replace(/(^|[^:])\/\/.*$/gm, "$1")
57
+ .replace(/\/\*[\s\S]*?\*\//g, "");
58
+ const withoutTrailingCommas = withoutComments.replace(/,(\s*[}\]])/g, "$1");
59
+ return JSON.parse(withoutTrailingCommas);
60
+ }
61
+
62
+ /**
63
+ * Flatten a build profile through its `extends` chain.
64
+ *
65
+ * Ancestors are applied first so the named profile wins, which is EAS's own
66
+ * precedence. `env` is merged key-by-key for the same reason.
67
+ * @param {Record<string, any>} profiles The `build` block.
68
+ * @param {string} name Profile to resolve.
69
+ * @returns {Record<string, any>} The effective profile.
70
+ */
71
+ export function resolveProfile(profiles, name) {
72
+ const chain = [];
73
+ const seen = new Set();
74
+ let current = name;
75
+
76
+ while (current) {
77
+ if (seen.has(current)) {
78
+ throw new Error(
79
+ `The build profile "${name}" extends itself through "${current}", so its settings cannot be worked out. Break the loop in eas.json.`
80
+ );
81
+ }
82
+ if (chain.length >= MAX_DEPTH) {
83
+ throw new Error(
84
+ `The build profile "${name}" extends more than ${MAX_DEPTH} profiles deep, which almost certainly means a loop in eas.json.`
85
+ );
86
+ }
87
+ const profile = profiles?.[current];
88
+ if (!profile) {
89
+ const known = Object.keys(profiles ?? {});
90
+ throw new Error(
91
+ `eas.json has no build profile called "${current}"` +
92
+ (current === name ? "" : ` (reached by extending from "${name}")`) +
93
+ `. It defines: ${known.length ? known.join(", ") : "none"}.`
94
+ );
95
+ }
96
+ seen.add(current);
97
+ chain.unshift(profile);
98
+ current = profile.extends;
99
+ }
100
+
101
+ const effective = {};
102
+ for (const link of chain) {
103
+ Object.assign(effective, link, {
104
+ env: { ...(effective.env ?? {}), ...(link.env ?? {}) },
105
+ });
106
+ }
107
+ delete effective.extends;
108
+ return effective;
109
+ }
110
+
111
+ /**
112
+ * Judge a resolved profile, returning every problem rather than the first.
113
+ * @param {Record<string, any>} profile Effective profile.
114
+ * @param {string} name Its name, for the messages.
115
+ * @returns {string[]} Operator-readable problems; empty means usable.
116
+ */
117
+ export function problemsWith(profile, name) {
118
+ const problems = [];
119
+
120
+ if (profile.developmentClient === true) {
121
+ problems.push(
122
+ `"${name}" builds a development client (developmentClient: true). A development client loads the app's JavaScript from a Metro server at runtime, and there is no Metro server in CI, so the app opens its "development servers" screen instead of the product and every flow fails on a screen that is not part of the app. Set developmentClient: false on the profile used for end-to-end runs.`
123
+ );
124
+ }
125
+
126
+ const channel = profile.channel;
127
+ if (!channel) {
128
+ problems.push(
129
+ `"${name}" sets no channel, so the build takes the default one and can receive an over-the-air update published for unrelated work while the suite is running — meaning the build being tested is no longer the build that was made. Give the profile its own channel, conventionally "e2e".`
130
+ );
131
+ } else if (DEFAULT_CHANNELS.has(channel)) {
132
+ problems.push(
133
+ `"${name}" uses the "${channel}" channel, which is shared with everything else that does not choose one, so an unrelated over-the-air update can replace the build mid-suite. Give the profile its own channel, conventionally "e2e".`
134
+ );
135
+ }
136
+
137
+ return problems;
138
+ }
139
+
140
+ /**
141
+ * CLI entry point.
142
+ * @param {string[]} argv Arguments after the script path.
143
+ * @returns {number} Process exit code.
144
+ */
145
+ export function main(argv) {
146
+ const flag = name => {
147
+ const hit = argv.find(arg => arg.startsWith(`--${name}=`));
148
+ return hit ? hit.slice(name.length + 3) : null;
149
+ };
150
+ const file = flag("file") ?? "eas.json";
151
+ const name = flag("profile");
152
+
153
+ if (!name) {
154
+ console.error(
155
+ "usage: lisa-assert-eas-profile.mjs --profile=<name> [--file=eas.json]"
156
+ );
157
+ return 1;
158
+ }
159
+
160
+ if (!existsSync(file)) {
161
+ // Not every project builds with EAS. One that does not has no profile to
162
+ // judge, and inventing a failure here would block suites that never had
163
+ // this class of problem.
164
+ console.log(
165
+ `No ${file} in this project, so there is no EAS build profile to check.`
166
+ );
167
+ return 0;
168
+ }
169
+
170
+ let document;
171
+ try {
172
+ document = parseEasJson(readFileSync(file, "utf8"));
173
+ } catch (error) {
174
+ console.error(
175
+ `::error title=Unreadable eas.json::${file} could not be read: ${error.message}`
176
+ );
177
+ return 1;
178
+ }
179
+
180
+ let profile;
181
+ try {
182
+ profile = resolveProfile(document.build, name);
183
+ } catch (error) {
184
+ console.error(`::error title=EAS profile not usable::${error.message}`);
185
+ return 1;
186
+ }
187
+
188
+ const problems = problemsWith(profile, name);
189
+ for (const problem of problems) {
190
+ console.error(`::error title=EAS profile not usable for E2E::${problem}`);
191
+ }
192
+ if (problems.length) {
193
+ console.error(
194
+ `\nThe "${name}" profile would build an app the suite cannot drive, so the build was stopped before it started rather than after ninety minutes.`
195
+ );
196
+ return 1;
197
+ }
198
+
199
+ console.log(
200
+ `EAS profile "${name}" is usable for end-to-end runs: developmentClient=${profile.developmentClient === true}, channel=${profile.channel}.`
201
+ );
202
+ return 0;
203
+ }
204
+
205
+ // Realpath both sides rather than comparing URL strings: agents in this repo
206
+ // run from git worktrees and /tmp on macOS, both symlinked, where a raw
207
+ // comparison silently answers "no" and the CLI body never runs.
208
+ if (invokedAsScript(import.meta.url)) {
209
+ process.exit(main(process.argv.slice(2)));
210
+ }