@ultimat3/cli 19.4.0 → 20.0.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/CLAUDE.md CHANGED
@@ -259,6 +259,7 @@ change, and CI does not install one.
259
259
  |---|---|---|
260
260
  | `x shot <route>` | `x dev` on a scratch port, plus the app's own `puppeteer-core` through `@ultimat3/scraping` — launching Chrome here, or **attaching** to one over `--cdp-url` / `SCRAPE_CDP_URL`, which is what every stealth provider sells and what `remoteBrowser()` has called its primary path since it shipped | the static build — `--target static` prerenders `site/` only, so an `app/` route would photograph the landing page |
261
261
  | `x shot --island <name>` | the same server and the same browser, plus the app's own `*.island.states.ts` | a second command — photographing a route and photographing a component are one job with two subjects, and `--island` with a route positional is refused by name |
262
+ | `x shot --all-islands` | every `*.island.states.ts` in the app, one boot, one browser per declared VIEWPORT across all of them | a second capture loop — one island and every island are `runIslandSweep` with one argument, because two loops are two answers to what a run produced |
262
263
  | `x pr review\|resolve\|reply` | `gh api graphql`, through the injected `Runner` | `gh pr view --comments`, which shows *issue* comments and not the line-anchored threads that carry the findings |
263
264
  | `x ci` | `gh run view --log-failed`, one call | a per-job log fetch — the run and all its jobs come back together |
264
265
 
@@ -277,10 +278,12 @@ ones a running app will not produce on request. `--island` takes them, one addre
277
278
  | `island-harness.ts` | the document that mounts ONE island over `data-x-entry` / `data-x-props` |
278
279
  | `island-harness-script.ts` | what runs before the chunk does: the sealed network, the pinned clock, the readiness watch |
279
280
  | `island-harness-route.ts` | `GET /_x/island`, mounted by `x dev` |
280
- | `island-shot.ts` | the capture loop, the assertions before each shutter, the missing-shot gate |
281
+ | `island-shot.ts` | the RUN: which islands, in which order, which artifacts — and the missing-shot gate |
282
+ | `island-capture.ts` | ONE picture: the assertions before the shutter, the crop rectangle, the one session |
283
+ | `island-shot-index.ts` | `index.md` as a PURE function of (manifests, targets, verdicts) — no I/O in the rule |
281
284
  | `shot-browser.ts` | which browser a run gets — launch one here, or attach over `--cdp-url` / `SCRAPE_CDP_URL` — as three rules over plain inputs |
282
285
  | `island-verdict.ts` | the per-state verdict — a PNG cannot say the component threw or logged |
283
- | `cmd-shot-island.ts` | the flags, and the one browser per declared viewport |
286
+ | `cmd-shot-island.ts` | the flags, the refusals, and the one browser per declared viewport |
284
287
 
285
288
  The vocabulary is **`@ultimat3/testing`'s**, not this package's: `defineIslandStates`,
286
289
  `islandShotTargets`, `islandAddress` / `parseIslandAddress`, `findIslandStates`,
@@ -312,6 +315,43 @@ mount that never finished, a page that never went quiet, a zero-sized box, a box
312
315
  and no text. Then a byte floor as a backstop. Every one of them otherwise comes out as a plausible
313
316
  image of the wrong thing.
314
317
 
318
+ **`--all-islands` is a spelling, never `--island` with no value.** The parser refuses a bare
319
+ `--island` ("expects a value") and `--island=` is an empty NAME, so "every island" had no form a
320
+ reader could type that could not be read as a mistyped one. A boolean cannot be confused with a
321
+ name. Every contradictory pair is refused BY NAME and before a value is read — beside `--island`,
322
+ beside a route positional, and beside `--state`, which belongs to ONE manifest's vocabulary
323
+ (`empty` in two manifests is two unrelated states). An app declaring no states at all is refused
324
+ too, rather than answered with an empty gallery: "produced nothing and exited 0" is the one outcome
325
+ a reader cannot tell from success. Its `fix:` names `x g island`, and deliberately not a states
326
+ generator — a `fix:` may only cite a command this build ships (`fix-command.ts`), and there is none.
327
+
328
+ **A sweep never aborts on a failure.** Every state the app CAN photograph is captured, every
329
+ per-island `verdict.json` and the index are written, and only then does the missing-picture gate
330
+ turn the reasons into a non-zero exit. One island that will not mount must not cost a reader the
331
+ other nineteen — which is the same rule the per-state loop already followed, one level up.
332
+
333
+ **`.x/shot/island/index.md` is the file an agent opens**, and it is written for a SINGLE-island run
334
+ too: the file that says what a picture IS cannot be a property of how many islands were asked for.
335
+ `island-shot-index.ts` is a pure renderer over (manifests, targets, verdicts) — no disk, no browser
336
+ — and the caller does the writing, which is the split every check in this repo uses. It carries the
337
+ counts, the re-run commands, the verdict's own `blind` list (handed in, never reworded: two
338
+ wordings drift), and per state the `id`, the `title`, the `note` — the *"you cannot reach this by
339
+ clicking, because …"* line, which is the whole reason a reviewer knows what they are looking at.
340
+
341
+ **A console WARNING and an overflowing box are recorded and gate NOTHING.** `stateShotOk` reads
342
+ neither, on purpose: a signal that fails a run is a signal an author switches off. The warnings were
343
+ already in `page.console()` and already in `--json`; what was missing is that nobody counted them.
344
+ The overflow is new and is the readiness probe's own answer — `scrollWidth > clientWidth` on the
345
+ crop target, measured in the same round trip, because a fact measured after the shutter is a fact
346
+ about a different page.
347
+
348
+ **The clip carries a margin, clamped to the document.** It was the readiness box EXACTLY, and a
349
+ pixel-tight rectangle shaves off everything a component paints outside its border box — a
350
+ `box-shadow`, an outline, a focus ring, a hairline border on a subpixel — so a reviewer reads a
351
+ component with no elevation as flat. `ISLAND_CROP_MARGIN_PX` is the one constant; the clamp reads
352
+ the document's own extent off the probe (`page`), never the viewport, and it may only ever make the
353
+ frame BIGGER than the component's box — a clamp that could shrink it would crop the subject.
354
+
315
355
  **One session per picture, and that is not an optimisation to collapse.** `page.console()` and
316
356
  `page.pageErrors()` are bounded rings over the whole SESSION, so a shared one files state A's
317
357
  console errors under state B — and per-state attribution is the half of the artifact that gates.
@@ -1487,7 +1527,20 @@ refuses). A guard returning `[1n]` is `X_GUARD_FINDING_INVALID`, per candidate,
1487
1527
  entry costs its own line and not the real findings beside it. The mechanism whose job is producing
1488
1528
  structured failures handing back a stack trace is the one outcome it exists to prevent.
1489
1529
 
1490
- **`x new` ships four guards, `As of 2026-08-22`.** The scaffolded `AGENTS.md` states nine
1530
+ **`x new` ships NINE guards, `As of 2026-09-08`** — the four below plus five interface rules
1531
+ (`semantic-interactive`, `focus-visible`, `image-dimensions`, `animated-layout-property`,
1532
+ `island-without-states`), each statically decidable and each carrying a legitimate-lookalike test
1533
+ that must NOT be reported. **Two of the original four were broken the whole time and nobody could
1534
+ see it**, because no guard had ever run against a real app: `raw-colour` reported all 87 uses of
1535
+ `rgb(var(--color-…))` — which IS the token form `_colors.scss` emits, so its own cause line ("a
1536
+ value no theme can restate") was false of every one — and `untranslated-string`'s JSX mask
1537
+ `/\{[^{}]*\}/g` does not nest, so `{t('k', { org: x })}` lost its inner brace group first and the
1538
+ remnant read as prose, flagging every `t()` call with an interpolation object or a template-literal
1539
+ key. Both are fixed and both now report zero against `examples/dummy`, which is the first tracked
1540
+ app to carry a `guards/` directory at all. Read the count, never this sentence:
1541
+ `bun -e "import {scaffoldGuardFiles} from './packages/cli/src/templates/scaffold-guards'; console.log(scaffoldGuardFiles().length)"`.
1542
+
1543
+ The scaffolded `AGENTS.md` states nine
1491
1544
  non-negotiables, and five of them used to be prose — each proven green on `x verify`: a hardcoded
1492
1545
  JSX string beside a `t()` call, `color: #ff0000` in a stylesheet whose own scaffolded header called
1493
1546
  it "a lint failure", `toLocaleDateString('en-US')` with no `timeZone`, `t.number` money, and a bare
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ultimat3/cli",
3
- "version": "19.4.0",
3
+ "version": "20.0.0",
4
4
  "description": "The `x` binary: new, dev, build, verify, generate, db, mcp, doctor, deploy",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -37,34 +37,34 @@
37
37
  },
38
38
  "dependencies": {
39
39
  "@babel/core": "^7.28.4",
40
- "@ultimat3/action": "19.4.0",
41
- "@ultimat3/admin": "19.4.0",
42
- "@ultimat3/ai": "19.4.0",
43
- "@ultimat3/auth": "19.4.0",
44
- "@ultimat3/cache": "19.4.0",
45
- "@ultimat3/core": "19.4.0",
46
- "@ultimat3/db": "19.4.0",
47
- "@ultimat3/entity": "19.4.0",
48
- "@ultimat3/flags": "19.4.0",
49
- "@ultimat3/http": "19.4.0",
50
- "@ultimat3/i18n": "19.4.0",
51
- "@ultimat3/jobs": "19.4.0",
52
- "@ultimat3/mail": "19.4.0",
53
- "@ultimat3/manifest": "19.4.0",
54
- "@ultimat3/mcp": "19.4.0",
55
- "@ultimat3/money": "19.4.0",
56
- "@ultimat3/notify": "19.4.0",
57
- "@ultimat3/policy": "19.4.0",
58
- "@ultimat3/pwa": "19.4.0",
59
- "@ultimat3/query": "19.4.0",
60
- "@ultimat3/realtime": "19.4.0",
61
- "@ultimat3/render": "19.4.0",
62
- "@ultimat3/schema": "19.4.0",
63
- "@ultimat3/scraping": "19.4.0",
64
- "@ultimat3/seo": "19.4.0",
65
- "@ultimat3/storage": "19.4.0",
66
- "@ultimat3/testing": "19.4.0",
67
- "@ultimat3/time": "19.4.0",
40
+ "@ultimat3/action": "20.0.0",
41
+ "@ultimat3/admin": "20.0.0",
42
+ "@ultimat3/ai": "20.0.0",
43
+ "@ultimat3/auth": "20.0.0",
44
+ "@ultimat3/cache": "20.0.0",
45
+ "@ultimat3/core": "20.0.0",
46
+ "@ultimat3/db": "20.0.0",
47
+ "@ultimat3/entity": "20.0.0",
48
+ "@ultimat3/flags": "20.0.0",
49
+ "@ultimat3/http": "20.0.0",
50
+ "@ultimat3/i18n": "20.0.0",
51
+ "@ultimat3/jobs": "20.0.0",
52
+ "@ultimat3/mail": "20.0.0",
53
+ "@ultimat3/manifest": "20.0.0",
54
+ "@ultimat3/mcp": "20.0.0",
55
+ "@ultimat3/money": "20.0.0",
56
+ "@ultimat3/notify": "20.0.0",
57
+ "@ultimat3/policy": "20.0.0",
58
+ "@ultimat3/pwa": "20.0.0",
59
+ "@ultimat3/query": "20.0.0",
60
+ "@ultimat3/realtime": "20.0.0",
61
+ "@ultimat3/render": "20.0.0",
62
+ "@ultimat3/schema": "20.0.0",
63
+ "@ultimat3/scraping": "20.0.0",
64
+ "@ultimat3/seo": "20.0.0",
65
+ "@ultimat3/storage": "20.0.0",
66
+ "@ultimat3/testing": "20.0.0",
67
+ "@ultimat3/time": "20.0.0",
68
68
  "babel-preset-solid": "^1.9.15"
69
69
  }
70
70
  }
@@ -1,7 +1,11 @@
1
- // `--island`'s half of `x shot`: read the flags, resolve the manifest, refuse the combinations
2
- // that cannot mean anything, and hand `runIslandShot` values. Split from `cmd-shot.ts` so neither
3
- // file has to hold both a route capture and a component capture — the two share a boot, a browser
4
- // and an output tree, and nothing else.
1
+ // `--island` and `--all-islands`: read the flags, resolve the manifests, refuse the combinations
2
+ // that cannot mean anything, and hand the run its values. Split from `cmd-shot.ts` so neither file
3
+ // has to hold both a route capture and a component capture — the two share a boot, a browser and
4
+ // an output tree, and nothing else.
5
+ //
6
+ // One island and every island are one code path with one argument (`runIslandSweep`), never two
7
+ // loops: two would be two answers to what a run produced, and the sweep is the form an agent
8
+ // reaches for first.
5
9
 
6
10
  // why: no Bun native joins or resolves a path; `--out` is resolved against the app root.
7
11
  import { join, resolve } from 'node:path';
@@ -11,10 +15,16 @@ import { findIslandStates } from '@ultimat3/testing';
11
15
  import { appBrowser } from './browser-launcher';
12
16
  import { BadFlagError } from './errors';
13
17
  import type { IslandBrowser } from './island-shot';
14
- import { ISLAND_SHOT_DIR, runIslandShot } from './island-shot';
18
+ import { ISLAND_SHOT_DIR, runIslandShot, runIslandSweep } from './island-shot';
15
19
  import { loadIslandStates } from './island-states-load';
16
- import type { IslandArtifacts } from './island-verdict';
17
- import { islandShotLines, islandShotSummary, islandVerdictJson } from './island-verdict';
20
+ import type { IslandArtifacts, IslandSweepArtifacts } from './island-verdict';
21
+ import {
22
+ islandShotLines,
23
+ islandShotSummary,
24
+ islandSweepLines,
25
+ islandSweepSummary,
26
+ islandVerdictJson,
27
+ } from './island-verdict';
18
28
  import type { CommandResult } from './output';
19
29
  import type { ShotServer } from './shot-server';
20
30
  import { SHOT_DIR } from './shot-server';
@@ -79,6 +89,62 @@ export function refuseRouteWithIsland(route: string | undefined, island: string)
79
89
  });
80
90
  }
81
91
 
92
+ /**
93
+ * An app with no `*.island.states.ts` anywhere. Refused by NAME rather than answered with an empty
94
+ * gallery: a run that produces no picture and exits 0 is the one outcome a reader cannot tell from
95
+ * success, and it is what this whole command exists to make impossible.
96
+ *
97
+ * The `fix:` is the generator that exists. It deliberately does not name a states generator: a
98
+ * `fix:` may only cite a command this build ships (`fix-command.ts`), and there is none.
99
+ */
100
+ export function refuseNoIslandStates(flag: 'island' | 'all-islands'): never {
101
+ throw new BadFlagError({
102
+ flag,
103
+ command: 'shot',
104
+ reason: 'this app declares no island states at all, so there is nothing to photograph',
105
+ fix: "x g island settings --at apps/web/app/settings # then declare its states beside it with defineIslandStates({ island: '…', states: [...] })",
106
+ });
107
+ }
108
+
109
+ /**
110
+ * `--all-islands` is the WHOLE app and `--island <name>` is one component, so naming both is two
111
+ * subjects and one command. Refused by name for `refuseRouteWithIsland`'s reason: a reader who
112
+ * typed both has a belief about which one runs, and half of them would be wrong.
113
+ */
114
+ export function refuseSweepWithIsland(island: string): never {
115
+ throw new BadFlagError({
116
+ flag: 'all-islands',
117
+ command: 'shot',
118
+ reason: `--all-islands photographs every island and --island ${island} photographs one; x shot takes one subject`,
119
+ fix: 'x shot --all-islands --json',
120
+ });
121
+ }
122
+
123
+ /** A route and every island are two subjects too, and the same rule decides it. */
124
+ export function refuseSweepWithRoute(route: string): never {
125
+ throw new BadFlagError({
126
+ flag: 'all-islands',
127
+ command: 'shot',
128
+ reason: `--all-islands photographs every island and "${route}" is a route; x shot takes one subject`,
129
+ fix: 'x shot --all-islands --json',
130
+ });
131
+ }
132
+
133
+ /**
134
+ * A state id belongs to ONE island's vocabulary — `empty` in two manifests is two unrelated
135
+ * states — so a filter across every island either means nothing or silently means whichever
136
+ * islands happen to share the word. The fix names the one-island form, which is where a state id
137
+ * can be resolved at all.
138
+ */
139
+ export function refuseSweepWithState(state: string): never {
140
+ throw new BadFlagError({
141
+ flag: 'state',
142
+ command: 'shot',
143
+ reason: `--state ${state} names one island's declared state, and --all-islands spans every island`,
144
+ fix: 'x shot --island <name> --state <id> --json',
145
+ });
146
+ }
147
+
82
148
  export interface IslandShotInput {
83
149
  readonly root: string;
84
150
  readonly island: string;
@@ -96,6 +162,9 @@ export interface IslandShotInput {
96
162
  readonly minBytes?: number | undefined;
97
163
  }
98
164
 
165
+ /** Every field the one-island form takes, minus the two that name one island and one state. */
166
+ export type IslandSweepInput = Omit<IslandShotInput, 'island' | 'state'>;
167
+
99
168
  /**
100
169
  * The states are loaded, expanded and the name resolved BEFORE a browser or a dev server exists —
101
170
  * a typo must not cost an embedded Postgres to report, and the expected picture list has to be
@@ -103,14 +172,7 @@ export interface IslandShotInput {
103
172
  */
104
173
  export async function islandShot(input: IslandShotInput): Promise<IslandArtifacts> {
105
174
  const all = await loadIslandStates(input.root);
106
- if (all.length === 0) {
107
- throw new BadFlagError({
108
- flag: 'island',
109
- command: 'shot',
110
- reason: 'this app declares no island states at all, so there is nothing to photograph',
111
- fix: "x g island settings --at apps/web/app/settings # then declare its states beside it with defineIslandStates({ island: '…', states: [...] })",
112
- });
113
- }
175
+ if (all.length === 0) refuseNoIslandStates('island');
114
176
  // `findIslandStates` is loose on the way in — `Settings`, `settings`, `settings.island.tsx` and
115
177
  // the full path are one name — and refuses an unresolved one by listing every valid name, which
116
178
  // is what tells a typo apart from an island whose states were never declared.
@@ -138,6 +200,48 @@ export async function islandShot(input: IslandShotInput): Promise<IslandArtifact
138
200
  });
139
201
  }
140
202
 
203
+ /**
204
+ * Every island in the app, one boot, one browser per declared viewport across ALL of them — the
205
+ * memo lives in the `islandBrowser` closure built here, so a sweep of forty states at one size is
206
+ * still one launch. The manifests are loaded before a browser or a dev server exists, for
207
+ * `islandShot`'s reason: the complete expected picture list has to be knowable without either.
208
+ */
209
+ export async function islandSweep(input: IslandSweepInput): Promise<IslandSweepArtifacts> {
210
+ const all = await loadIslandStates(input.root);
211
+ if (all.length === 0) refuseNoIslandStates('all-islands');
212
+ return runIslandSweep({
213
+ manifests: all,
214
+ outDir:
215
+ input.out === undefined
216
+ ? join(input.root, SHOT_DIR, ISLAND_SHOT_DIR)
217
+ : resolve(input.root, input.out),
218
+ driver:
219
+ input.driver ??
220
+ islandBrowser({
221
+ root: input.root,
222
+ ...(input.executablePath === undefined ? {} : { executablePath: input.executablePath }),
223
+ ...(input.cdpUrl === undefined ? {} : { cdpUrl: input.cdpUrl }),
224
+ }),
225
+ boot: input.boot,
226
+ settleMs: input.settleMs,
227
+ timeoutMs: input.timeoutMs,
228
+ ...(input.extraHosts === undefined ? {} : { extraHosts: input.extraHosts }),
229
+ ...(input.minBytes === undefined ? {} : { minBytes: input.minBytes }),
230
+ });
231
+ }
232
+
233
+ export const islandSweepResult = (artifacts: IslandSweepArtifacts): CommandResult => ({
234
+ ok: artifacts.ok,
235
+ command: 'shot',
236
+ summary: islandSweepSummary(artifacts),
237
+ lines: islandSweepLines(artifacts),
238
+ data: {
239
+ dir: artifacts.dir,
240
+ indexFile: artifacts.indexFile,
241
+ islands: artifacts.verdicts.map(islandVerdictJson),
242
+ },
243
+ });
244
+
141
245
  export const islandShotResult = (artifacts: IslandArtifacts): CommandResult => ({
142
246
  ok: artifacts.verdict.ok,
143
247
  command: 'shot',
@@ -146,6 +250,7 @@ export const islandShotResult = (artifacts: IslandArtifacts): CommandResult => (
146
250
  data: {
147
251
  dir: artifacts.dir,
148
252
  verdictFile: artifacts.verdictFile,
253
+ indexFile: artifacts.indexFile,
149
254
  verdict: islandVerdictJson(artifacts.verdict),
150
255
  },
151
256
  });
package/src/cmd-shot.ts CHANGED
@@ -12,7 +12,16 @@ import type { ScrapeDriver, ScrapeSession } from '@ultimat3/scraping';
12
12
  import { DEFAULT_PAGE_TIMEOUT_MS, systemScrapeClock } from '@ultimat3/scraping';
13
13
  import { requireAppRoot } from './app-root';
14
14
  import { appBrowser } from './browser-launcher';
15
- import { islandShot, islandShotResult, refuseRouteWithIsland } from './cmd-shot-island';
15
+ import {
16
+ islandShot,
17
+ islandShotResult,
18
+ islandSweep,
19
+ islandSweepResult,
20
+ refuseRouteWithIsland,
21
+ refuseSweepWithIsland,
22
+ refuseSweepWithRoute,
23
+ refuseSweepWithState,
24
+ } from './cmd-shot-island';
16
25
  import type { CliCommand, CommandContext } from './command';
17
26
  import { BadFlagError, MissingPositionalError } from './errors';
18
27
  import { intFlagOr, PORT_RANGE } from './flag-number';
@@ -275,9 +284,9 @@ export const shotResult = (artifacts: ShotArtifacts): CommandResult => ({
275
284
  export const shotCommand: CliCommand = {
276
285
  spec: {
277
286
  name: 'shot',
278
- summary: 'photograph one route, or one island in a state it declares, from a real browser',
287
+ summary: 'photograph one route, one island in a state it declares, or every island in the app',
279
288
  usage:
280
- 'x shot <route> | --island <name> [--state <id>] [--port 0] [--out <dir>] [--settle 2000] [--json]',
289
+ 'x shot <route> | --island <name> [--state <id>] | --all-islands [--port 0] [--out <dir>] [--settle 2000] [--json]',
281
290
  requiresApp: true,
282
291
  flags: [
283
292
  { name: 'port', type: 'string', summary: 'dev port (0 lets the kernel pick a free one)' },
@@ -305,6 +314,15 @@ export const shotCommand: CliCommand = {
305
314
  type: 'string',
306
315
  summary: 'one declared state of that island, not all of them',
307
316
  },
317
+ // Its own SPELLING and never `--island` with no value: the parser refuses a bare `--island`
318
+ // ("expects a value") and `--island=` is an empty name, so "every island" had no form a
319
+ // reader could type that could not be read as a mistyped one. A boolean cannot be confused
320
+ // with a name, and `x shot --all-islands` says what it does beside `x shot --island <name>`.
321
+ {
322
+ name: 'all-islands',
323
+ type: 'boolean',
324
+ summary: 'every island in the app, in every state it declares, plus an index.md',
325
+ },
308
326
  ],
309
327
  },
310
328
  async run(ctx: CommandContext): Promise<CommandResult> {
@@ -312,11 +330,20 @@ export const shotCommand: CliCommand = {
312
330
  // Every value read before anything boots: a typo must not cost a browser and a dev server to
313
331
  // report, which is the rule `x routes` and `x mcp` already follow.
314
332
  const island = flagString(ctx.args, 'island');
333
+ const state = flagString(ctx.args, 'state');
315
334
  const positional = ctx.args.positionals[0];
335
+ const sweep = flagBool(ctx.args, 'all-islands');
336
+ // Every ambiguous pair refused BY NAME, before a value is read: a reader who typed two
337
+ // subjects has a belief about which one runs, and half of them would be wrong.
338
+ if (sweep && island !== undefined && island !== '') refuseSweepWithIsland(island);
339
+ if (sweep && positional !== undefined) refuseSweepWithRoute(positional);
340
+ // A state id is one manifest's vocabulary, so it cannot mean anything across every island.
341
+ if (sweep && state !== undefined && state !== '') refuseSweepWithState(state);
316
342
  if (island !== undefined && island !== '' && positional !== undefined) {
317
343
  refuseRouteWithIsland(positional, island);
318
344
  }
319
- const route = island === undefined || island === '' ? readRoute(positional) : '';
345
+ const component = sweep || (island !== undefined && island !== '');
346
+ const route = component ? '' : readRoute(positional);
320
347
  const port = intFlag(ctx.args, 'port', PORT_RANGE.min, DEFAULT_PORT, PORT_RANGE.max);
321
348
  const settleMs = intFlag(ctx.args, 'settle', 0, DEFAULT_SETTLE_MS);
322
349
  const timeoutMs = intFlag(ctx.args, 'timeout', 1, DEFAULT_PAGE_TIMEOUT_MS);
@@ -332,24 +359,22 @@ export const shotCommand: CliCommand = {
332
359
  });
333
360
  const out = flagString(ctx.args, 'out');
334
361
  const boot = (): Promise<ShotServer> => devServerFor(root, ctx.env, port);
362
+ const shared = {
363
+ root,
364
+ ...(out === undefined ? {} : { out }),
365
+ settleMs,
366
+ timeoutMs,
367
+ ...(executablePath === undefined ? {} : { executablePath }),
368
+ ...(cdpUrl === undefined ? {} : { cdpUrl }),
369
+ ...(flagString(ctx.args, 'allow-hosts') === undefined
370
+ ? {}
371
+ : { extraHosts: flagString(ctx.args, 'allow-hosts') }),
372
+ boot,
373
+ };
374
+ if (sweep) return islandSweepResult(await islandSweep(shared));
335
375
  if (island !== undefined && island !== '') {
336
376
  return islandShotResult(
337
- await islandShot({
338
- root,
339
- island,
340
- ...(flagString(ctx.args, 'state') === undefined
341
- ? {}
342
- : { state: flagString(ctx.args, 'state') }),
343
- ...(out === undefined ? {} : { out }),
344
- settleMs,
345
- timeoutMs,
346
- ...(executablePath === undefined ? {} : { executablePath }),
347
- ...(cdpUrl === undefined ? {} : { cdpUrl }),
348
- ...(flagString(ctx.args, 'allow-hosts') === undefined
349
- ? {}
350
- : { extraHosts: flagString(ctx.args, 'allow-hosts') }),
351
- boot,
352
- }),
377
+ await islandShot({ ...shared, island, ...(state === undefined ? {} : { state }) }),
353
378
  );
354
379
  }
355
380
  // Resolved before the boot for the same reason: an app with no browser installed must not pay