@ethlete/cli 2.1.0-next.9 → 3.0.0-next.17

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 (117) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +30 -7
  3. package/design/define-call.ts +8 -8
  4. package/design/web/frame.ts +11 -5
  5. package/design/web/host.css +4 -4
  6. package/design/web/host.ts +68 -68
  7. package/package.json +3 -3
  8. package/src/index.d.ts +1 -0
  9. package/src/index.js +18 -4
  10. package/src/index.js.map +1 -1
  11. package/src/lib/api/clear.js +13 -3
  12. package/src/lib/api/clear.js.map +1 -1
  13. package/src/lib/api/compose.js +5 -1
  14. package/src/lib/api/compose.js.map +1 -1
  15. package/src/lib/api/definition.js +6 -2
  16. package/src/lib/api/definition.js.map +1 -1
  17. package/src/lib/api/git.d.ts +11 -4
  18. package/src/lib/api/git.js +35 -8
  19. package/src/lib/api/git.js.map +1 -1
  20. package/src/lib/api/help.js +18 -4
  21. package/src/lib/api/help.js.map +1 -1
  22. package/src/lib/api/index.d.ts +1 -0
  23. package/src/lib/api/index.js +1 -0
  24. package/src/lib/api/index.js.map +1 -1
  25. package/src/lib/api/load-definitions.js +10 -1
  26. package/src/lib/api/load-definitions.js.map +1 -1
  27. package/src/lib/api/ports.js +12 -3
  28. package/src/lib/api/ports.js.map +1 -1
  29. package/src/lib/api/run.js +81 -7
  30. package/src/lib/api/run.js.map +1 -1
  31. package/src/lib/api/seeds.d.ts +55 -0
  32. package/src/lib/api/seeds.js +215 -0
  33. package/src/lib/api/seeds.js.map +1 -0
  34. package/src/lib/api/setup.js +1 -1
  35. package/src/lib/api/setup.js.map +1 -1
  36. package/src/lib/api/state.js +7 -5
  37. package/src/lib/api/state.js.map +1 -1
  38. package/src/lib/auth/composer-auth.js +3 -2
  39. package/src/lib/auth/composer-auth.js.map +1 -1
  40. package/src/lib/auth/gitlab-token.js +1 -1
  41. package/src/lib/auth/gitlab-token.js.map +1 -1
  42. package/src/lib/config/diagnose.js +15 -9
  43. package/src/lib/config/diagnose.js.map +1 -1
  44. package/src/lib/config/local-config.d.ts +1 -1
  45. package/src/lib/design/check.js +27 -16
  46. package/src/lib/design/check.js.map +1 -1
  47. package/src/lib/design/serve.js +5 -5
  48. package/src/lib/design/serve.js.map +1 -1
  49. package/src/lib/design/vite-paths.d.ts +7 -0
  50. package/src/lib/design/vite-paths.js +15 -0
  51. package/src/lib/design/vite-paths.js.map +1 -0
  52. package/src/lib/doctor/doctor-command.js +18 -4
  53. package/src/lib/doctor/doctor-command.js.map +1 -1
  54. package/src/lib/release.d.ts +38 -1
  55. package/src/lib/release.js +150 -43
  56. package/src/lib/release.js.map +1 -1
  57. package/src/lib/update/agent-rules-sync.d.ts +37 -0
  58. package/src/lib/update/agent-rules-sync.js +40 -0
  59. package/src/lib/update/agent-rules-sync.js.map +1 -0
  60. package/src/lib/update/agent-setup.d.ts +33 -0
  61. package/src/lib/update/agent-setup.js +124 -0
  62. package/src/lib/update/agent-setup.js.map +1 -0
  63. package/src/lib/update/ai.d.ts +37 -8
  64. package/src/lib/update/ai.js +193 -24
  65. package/src/lib/update/ai.js.map +1 -1
  66. package/src/lib/update/args.d.ts +2 -0
  67. package/src/lib/update/args.js +22 -2
  68. package/src/lib/update/args.js.map +1 -1
  69. package/src/lib/update/available-migrations.d.ts +10 -0
  70. package/src/lib/update/available-migrations.js +23 -0
  71. package/src/lib/update/available-migrations.js.map +1 -0
  72. package/src/lib/update/commits.d.ts +63 -0
  73. package/src/lib/update/commits.js +146 -0
  74. package/src/lib/update/commits.js.map +1 -0
  75. package/src/lib/update/gitignore.d.ts +8 -0
  76. package/src/lib/update/gitignore.js +22 -0
  77. package/src/lib/update/gitignore.js.map +1 -0
  78. package/src/lib/update/index.d.ts +6 -0
  79. package/src/lib/update/index.js +6 -0
  80. package/src/lib/update/index.js.map +1 -1
  81. package/src/lib/update/migration-manifest.d.ts +10 -0
  82. package/src/lib/update/migration-manifest.js +15 -3
  83. package/src/lib/update/migration-manifest.js.map +1 -1
  84. package/src/lib/update/migration-record.d.ts +24 -0
  85. package/src/lib/update/migration-record.js +48 -0
  86. package/src/lib/update/migration-record.js.map +1 -0
  87. package/src/lib/update/migrations-command.d.ts +14 -0
  88. package/src/lib/update/migrations-command.js +212 -0
  89. package/src/lib/update/migrations-command.js.map +1 -0
  90. package/src/lib/update/package-manager.d.ts +20 -1
  91. package/src/lib/update/package-manager.js +34 -15
  92. package/src/lib/update/package-manager.js.map +1 -1
  93. package/src/lib/update/packages.js +31 -9
  94. package/src/lib/update/packages.js.map +1 -1
  95. package/src/lib/update/pending.d.ts +16 -0
  96. package/src/lib/update/pending.js +21 -5
  97. package/src/lib/update/pending.js.map +1 -1
  98. package/src/lib/update/plan.d.ts +28 -2
  99. package/src/lib/update/plan.js +54 -3
  100. package/src/lib/update/plan.js.map +1 -1
  101. package/src/lib/update/registry.d.ts +32 -4
  102. package/src/lib/update/registry.js +145 -8
  103. package/src/lib/update/registry.js.map +1 -1
  104. package/src/lib/update/run-migrations.d.ts +2 -0
  105. package/src/lib/update/run-migrations.js +20 -15
  106. package/src/lib/update/run-migrations.js.map +1 -1
  107. package/src/lib/update/scan.d.ts +14 -0
  108. package/src/lib/update/scan.js +57 -0
  109. package/src/lib/update/scan.js.map +1 -0
  110. package/src/lib/update/semver.js +2 -1
  111. package/src/lib/update/semver.js.map +1 -1
  112. package/src/lib/update/tasks.d.ts +18 -1
  113. package/src/lib/update/tasks.js +114 -43
  114. package/src/lib/update/tasks.js.map +1 -1
  115. package/src/lib/update/update-command.d.ts +7 -0
  116. package/src/lib/update/update-command.js +286 -64
  117. package/src/lib/update/update-command.js.map +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,89 @@
1
1
  # @ethlete/cli
2
2
 
3
+ ## 3.0.0-next.17
4
+
5
+ ### Minor Changes
6
+
7
+ - Add `et api seed` and `et api fixtures`, which copy `.ethlete/seeds/<api>/` into the API container and run the seeds its `seeds.json` names. A clone now gitignores only its own `/.ethlete/<api>/`.
8
+
9
+ ## 3.0.0-next.16
10
+
11
+ ### Major Changes
12
+
13
+ - Breaking: `release()` takes `{ args, root?, invocation? }` and resolves to an exit code instead of exiting. `et release` tags after the release commit and takes `--message` and `--help`; `et update` names the `.npmrc` whose token was refused.
14
+
15
+ ### Minor Changes
16
+
17
+ - New `et migrations` lists the recommended and optional migrations an app has not run, and `et migrations run <package>:<name>` runs one and records it in `.ethlete/migrations.json`.
18
+ - `et update` runs only `required` migrations and points at the recommended and optional ones it left.
19
+ - `et migrations` shows the number of affected files next to each migration whose manifest entry names a `scan` generator.
20
+ - Peer dependencies are ranges (`^22.1.0` for Angular, `^7.8.0` for RxJS, ...) instead of the workspace's exact versions. `et update` says when another dist tag holds a newer major, also when the repo is up to date.
21
+
22
+ ### Patch Changes
23
+
24
+ - `et release` and `et update` now reject values on boolean flags and empty `--tag=`/`--to=`, and version comparison ignores build metadata.
25
+ - `et update --from` rejects a version that is not one, such as `core@v5.0.0`, instead of running every migration of the package.
26
+ - The call title bar no longer covers the top of a drawing: the drawing starts below it, and it fades out while the drawing scrolls down and back in on scroll up.
27
+
28
+ ## 2.1.0-next.15
29
+
30
+ ### Minor Changes
31
+
32
+ - `et update --ai` ends an interactive agent session by itself once the agent writes the task's `.finished` file, then commits the task and starts the next one. `/exit` still works.
33
+
34
+ ## 2.1.0-next.14
35
+
36
+ ### Minor Changes
37
+
38
+ - `et update` commits each step by itself - the version bump, each codemod, the agent rules sync and each finished `--ai` task - and never a file that was dirty before the run; `--no-commit` turns this off.
39
+ - `et update --ai` without `updateAgentCommand` lists the agent CLIs on `PATH` and saves the one you pick, as an interactive session or a headless run.
40
+
41
+ ### Patch Changes
42
+
43
+ - The Claude Code choices in `et update --ai` start the agent in auto mode, so it can run its checks without asking.
44
+ - `et update --ai` tells you, for each task and in the agent's last message, to type `/exit` so the next task starts.
45
+
46
+ ## 2.1.0-next.13
47
+
48
+ ### Patch Changes
49
+
50
+ - `et update` hands the migrations to the newly installed `et` when the update moves `@ethlete/cli`, so steps a newer CLI added, such as the agent rules sync, run in that same update.
51
+ - `et update` follows the dist tag that holds the newest version of the installed prerelease line, so a package that changesets published to `latest`, such as `@ethlete/agent-rules`, no longer stays on the older `next` version.
52
+
53
+ ## 2.1.0-next.12
54
+
55
+ ### Patch Changes
56
+
57
+ - `et api clear` keeps a checkout git cannot read, and `et api up` and `setup` no longer fail on output over 1 MB.
58
+ - `et api clear` also keeps a checkout with stashes or detached-HEAD commits, `checkout` never reads a branch as a flag or path, `-h` shows help, and a port held on loopback or IPv6 counts as taken.
59
+ - `ethlete api` no longer leaves an `ethlete-api-*` directory in the temp folder after each podman run.
60
+ - GitLab token checks refuse redirects, `auth.json` is narrowed before the token is written, `release` matches its flags exactly, and `doctor` reports a broken legacy config file.
61
+ - `et design check` closes the browser when the page fails, and `et design serve` resolves calls on Windows.
62
+ - `et doctor` reports an `ethlete.apis.js` that throws while it loads instead of crashing.
63
+ - `release --force` stages only the files `changeset version` changed instead of running `git add .`, so unrelated uncommitted changes stay out of the "Release versions" commit.
64
+ - `et update` passes the agent prompt through environment variables, sends `.npmrc` registry tokens, keeps CRLF line endings, skips git-ignored manifests, and rejects migration names or instructions outside the package.
65
+ - `et update` prefers the lockfile over the calling `npx`, runs package managers on Windows, and points every run to `--continue` while an update is unfinished.
66
+
67
+ ## 2.1.0-next.11
68
+
69
+ ### Minor Changes
70
+
71
+ - `et update` runs `ethlete-agents sync` after the install when it moved `@ethlete/agent-rules` and the repo has an `ethlete-agents.config.json`, so the regenerated rules and skills match the tasks the update writes.
72
+
73
+ ### Patch Changes
74
+
75
+ - `et update --ai` stops before any change when `updateAgentCommand` is missing, hands the agent a real prompt, reports each task, and works on the tasks an earlier run left.
76
+ - `et update` gitignores its task list, so a later run no longer needs `--force`, and `--continue` keeps the `--from` versions and the codemods an earlier run applied.
77
+ - Fix `et update` for a real consumer: the `et` bin runs, `--continue` skips applied codemods, `--check` reports an unfinished run, lookups use the repo's registry, task links point at the right docs site, and failed syncs are reported.
78
+ - `yarn et update` on yarn 1 installs from the registry the repo's `.npmrc` or `.yarnrc` names, not the default registry yarn exports to scripts.
79
+ - `et update` drops deleted tasks from `tasks.md` even when nothing is pending, prints "none" for an empty dry-run migration list, and aligns the `--continue` plan.
80
+
81
+ ## 2.1.0-next.10
82
+
83
+ ### Patch Changes
84
+
85
+ - A design call now lists its drawings as `variants` in `variant-<key>.ts` files, and `et design check` takes `--variant`; the old `options` key and `--option` flag are gone.
86
+
3
87
  ## 2.1.0-next.9
4
88
 
5
89
  ### Minor Changes
package/README.md CHANGED
@@ -5,7 +5,7 @@ Repo tooling - installs the `et` binary.
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- yarn add --dev @ethlete/cli
8
+ yarn add --dev @ethlete/cli@next
9
9
  ```
10
10
 
11
11
  ## `et release`
@@ -14,7 +14,9 @@ yarn add --dev @ethlete/cli
14
14
  yarn et release
15
15
  ```
16
16
 
17
- Turns pending changesets into a tagged, pushed release commit (version → tag → commit → push).
17
+ Turns pending changesets into a tagged, pushed release commit (version → commit → tag → push). A
18
+ failed commit stops the run before any tag exists. Pass `--message "chore(release): Release versions"`
19
+ when a commit-msg hook rejects the default `Release versions`; `et release --help` lists the flags.
18
20
 
19
21
  ## `et auth`
20
22
 
@@ -47,23 +49,36 @@ yarn et update --tag latest # leave the prerelease line
47
49
  yarn et update core --to 5.0.0-next.55 # an exact version for the package you name
48
50
  yarn et update --continue # finish a run that stopped
49
51
  yarn et update --ai # hand every agent-assisted task to an agent
52
+ yarn et update --no-commit # leave every change uncommitted
50
53
  ```
51
54
 
52
55
  The target follows the dist tag the installed version is on, so a repo on a `-next` prerelease stays
53
- on `next`. The working tree must be clean, because the codemods rewrite files; `--force` skips that
56
+ on `next`, and a repo on `latest` is told per package when a newer major waits on `next`. The working
57
+ tree must be clean, because the codemods rewrite files; `--force` skips that
54
58
  check.
55
59
 
56
60
  The install runs before the migrations are read, because the migrations of a version ship inside that
57
61
  version. A run that stops leaves `.ethlete/update/pending.json` behind, and `--continue` picks it up -
58
62
  starting over would skip every migration the stopped run had not reached.
59
63
 
64
+ When the update moved `@ethlete/agent-rules`, it runs `ethlete-agents sync` before the codemods, so the
65
+ rules and skills match the tasks it writes. `--continue` skips the codemods that already applied, and
66
+ every other run exits 1 and points to `--continue` while a stopped run is unfinished.
67
+
60
68
  Everything a codemod cannot decide is written to `.ethlete/update`: `tasks.md` for you, `tasks.json`
61
69
  for an agent, and one file per task holding the instructions the package ships. A task is `manual` (it
62
70
  needs a decision), `assisted` (written as a prompt for an agent) or `unsupported` (a codemod this repo
63
71
  has no Nx to run, with the command to run it by hand).
64
72
 
65
- `--ai` hands each assisted task to the command in `updateAgentCommand` in
66
- `ethlete.config.local.json`, one run per task. No agent is detected automatically.
73
+ `--ai` hands each open assisted task to the command in `updateAgentCommand` in
74
+ `ethlete.config.local.json` (for example `claude --permission-mode auto`), one run per task,
75
+ and reports each run. It also works on the tasks an earlier run left. When the key is missing, `--ai`
76
+ lists the agent CLIs on `PATH` and saves the command you pick.
77
+
78
+ Each step is committed by itself: the version bump with the lockfile, each codemod that changed files,
79
+ the agent rules sync, and each agent task that ends done. A commit takes only the paths its step
80
+ changed, and a step that touched a file you had changed before the run is left uncommitted.
81
+ `--no-commit` commits nothing.
67
82
 
68
83
  A package declares its migrations in `migrations.json` at its own root, pointed at from `package.json`
69
84
  with `"ethlete": { "migrations": "./migrations.json" }`. The full format is on the docs site.
@@ -89,10 +104,18 @@ yarn et api clone hub # clone the API into .ethlete/hub
89
104
  yarn et api clear hub # remove that clone again
90
105
  yarn et api clear --all # remove every managed clone
91
106
  yarn et api setup hub # run the API's own setupCommand, which writes its .env
107
+ yarn et api seed hub # list the seeds in .ethlete/seeds/hub/seeds.json
108
+ yarn et api seed hub demo,shop # run two of them, in that order
109
+ yarn et api fixtures hub demo # load the fixtures, then run the demo seed
92
110
  ```
93
111
 
94
112
  Every command takes a comma-separated list of names, and acts on each API in turn.
95
113
 
114
+ `seed` and `fixtures` read `.ethlete/seeds/<name>/seeds.json`, copy that folder into the API
115
+ container at `$SEED_DIR`, and run the shell lines of each seed you name there. The folder is
116
+ committed, so gitignore each managed checkout as `/.ethlete/<name>/`, never `.ethlete/` as a whole.
117
+ An `exec` entry named `seed` or `fixtures` runs instead.
118
+
96
119
  `clone`, `checkout`, `pull`, `setup` and `clear` act on the checkout itself, so they work before the
97
120
  API has an `.env` and need no container engine. `pull` refuses to run on a checkout with uncommitted
98
121
  changes unless you pass `--force`. `--force` resets the branch to its remote and throws away local
@@ -174,5 +197,5 @@ fallback with a warning. Add `ethlete.config.local.json` to your `.gitignore`.
174
197
 
175
198
  Full command reference on the docs site:
176
199
 
177
- - [Overview & usage](https://ethlete-sdk-docs.web.app/cli/)
178
- - [Updating the SDK](https://ethlete-sdk-docs.web.app/cli/update)
200
+ - [Overview & usage](https://ethlete-sdk-docs-next.web.app/cli/)
201
+ - [Updating the SDK](https://ethlete-sdk-docs-next.web.app/cli/update)
@@ -37,9 +37,9 @@ export const drawing = (drawn: Drawing): Drawing => drawn;
37
37
 
38
38
  /**
39
39
  * One drawn answer to a call. `load` resolves the module whose default export is the
40
- * drawing that answers it, so a broken option breaks its own frame only.
40
+ * drawing that answers it, so a broken variant breaks its own frame only.
41
41
  */
42
- export type CallOption = {
42
+ export type CallVariant = {
43
43
  key: string;
44
44
  name: string;
45
45
  /** Left out by a view: a single drawing answers no question, so it argues nothing. */
@@ -53,8 +53,8 @@ export type CallOption = {
53
53
  };
54
54
 
55
55
  /**
56
- * One pass over a call: the options drawn together, and what came out of them. A round
57
- * whose options all carry a verdict is settled, and the page folds it down to its winner.
56
+ * One pass over a call: the variants drawn together, and what came out of them. A round
57
+ * whose variants all carry a verdict is settled, and the page folds it down to its winner.
58
58
  */
59
59
  export type CallRound = {
60
60
  key: string;
@@ -71,8 +71,8 @@ export type CallRound = {
71
71
  export type CallMode = 'wireframe' | 'design';
72
72
 
73
73
  /**
74
- * One open question of an exploration, with every option drawn at the same geometry.
75
- * A call with one option and no claim is a view: one reference picture, drawn full width.
74
+ * One open question of an exploration, with every variant drawn at the same geometry.
75
+ * A call with one variant and no claim is a view: one reference picture, drawn full width.
76
76
  */
77
77
  export type Call = {
78
78
  /** The feature this call belongs to, for example 'the hour strip'. Left out by a loose call. */
@@ -80,13 +80,13 @@ export type Call = {
80
80
  eyebrow: string;
81
81
  headline: string;
82
82
  intro: string;
83
- /** The width every option frame gets, in px. The geometry the thing ships in. */
83
+ /** The width every variant frame gets, in px. The geometry the thing ships in. */
84
84
  frameWidth: number;
85
85
  /** Left out by a call that draws the real thing, which is what `design` means. */
86
86
  mode?: CallMode;
87
87
  /** Left out by a short call. With rounds, the intro says only what the call is about. */
88
88
  rounds?: CallRound[];
89
- options: CallOption[];
89
+ variants: CallVariant[];
90
90
  };
91
91
 
92
92
  export const defineCall = (call: Call): Call => call;
@@ -3,7 +3,7 @@ import { loadEnv } from 'virtual:design-explore/env';
3
3
 
4
4
  const params = new URLSearchParams(location.search);
5
5
  const slug = params.get('call') ?? '';
6
- const key = params.get('option') ?? '';
6
+ const key = params.get('variant') ?? '';
7
7
 
8
8
  await loadEnv(slug.split('/')[0] ?? '');
9
9
 
@@ -17,10 +17,10 @@ const fail = (message: string) => {
17
17
  const call = calls[slug];
18
18
  if (!call) fail(`design-explore: no call "${slug}"`);
19
19
  else {
20
- const option = (await call()).default.options.find((o) => o.key === key);
21
- if (!option) fail(`design-explore: call "${slug}" has no option "${key}"`);
20
+ const variant = (await call()).default.variants.find((o) => o.key === key);
21
+ if (!variant) fail(`design-explore: call "${slug}" has no variant "${key}"`);
22
22
  else {
23
- const loaded = (await option.load()).default;
23
+ const loaded = (await variant.load()).default;
24
24
  const style = document.createElement('style');
25
25
 
26
26
  style.textContent = loaded.styles;
@@ -31,8 +31,14 @@ else {
31
31
 
32
32
  const report = () => {
33
33
  const height = Math.ceil(document.documentElement.getBoundingClientRect().height);
34
- parent.postMessage({ type: 'design-explore:height', option: key, height }, '*');
34
+ parent.postMessage({ type: 'design-explore:height', variant: key, height }, '*');
35
35
  };
36
36
 
37
37
  new ResizeObserver(report).observe(document.documentElement);
38
38
  report();
39
+
40
+ addEventListener(
41
+ 'scroll',
42
+ () => parent.postMessage({ type: 'design-explore:scroll', variant: key, y: Math.round(scrollY) }, '*'),
43
+ { passive: true },
44
+ );
@@ -203,7 +203,7 @@ main {
203
203
  color: var(--h-ink);
204
204
  }
205
205
 
206
- /* A folded option keeps its name on the page, so the thread of the round is still readable. */
206
+ /* A folded variant keeps its name on the page, so the thread of the round is still readable. */
207
207
  .folded {
208
208
  display: flex;
209
209
  max-width: 68ch;
@@ -270,7 +270,7 @@ header p {
270
270
  }
271
271
 
272
272
  /*
273
- * A track is as wide as the frame it holds, so the options wrap to the next band when the window
273
+ * A track is as wide as the frame it holds, so the variants wrap to the next band when the window
274
274
  * runs out of room. Row flow is what lets them wrap: each column takes four rows, so the next one
275
275
  * starts beside it and only a full band pushes to a new line.
276
276
  */
@@ -386,7 +386,7 @@ header p {
386
386
  transform-origin: top left;
387
387
  }
388
388
 
389
- /* The contact sheet draws every option at once, so one screen holds the whole call. */
389
+ /* The contact sheet draws every variant at once, so one screen holds the whole call. */
390
390
  .grid.sheet {
391
391
  --thumb-width: calc(var(--frame-width) * var(--de-scale));
392
392
 
@@ -441,7 +441,7 @@ header p {
441
441
  outline-offset: 4px;
442
442
  }
443
443
 
444
- /* A rejected option stays readable, and comes back to full strength under the pointer. */
444
+ /* A rejected variant stays readable, and comes back to full strength under the pointer. */
445
445
  .column[data-verdict='rejected'] {
446
446
  opacity: 0.45;
447
447
  transition: opacity 0.15s;
@@ -1,4 +1,4 @@
1
- import type { Call, CallOption, CallRound } from '@design-explore';
1
+ import type { Call, CallVariant, CallRound } from '@design-explore';
2
2
  import { calls, defaultCall } from 'virtual:design-explore';
3
3
  import './host.css';
4
4
 
@@ -33,21 +33,21 @@ const listLink = (name: string, list: string[], key: string) => {
33
33
  const toggleOpen = (key: string) => listLink('open', [...opened], key);
34
34
  const togglePick = (key: string) => listLink('pick', picked, key);
35
35
 
36
- type Band = { key: string | null; number: number; round: CallRound | null; options: CallOption[] };
36
+ type Band = { key: string | null; number: number; round: CallRound | null; variants: CallVariant[] };
37
37
 
38
38
  /**
39
- * The options are the call. A band exists because an option names it, never because `rounds`
40
- * declares it, so an option added without its round entry still gets a band, a heading and a
39
+ * The variants are the call. A band exists because a variant names it, never because `rounds`
40
+ * declares it, so a variant added without its round entry still gets a band, a heading and a
41
41
  * menu row - all three say the bare key until somebody writes the prose.
42
42
  */
43
43
  const bandsOf = (call: Call): Band[] => {
44
44
  const declared = new Map((call.rounds ?? []).map((round) => [round.key, round]));
45
45
  const bands = new Map<string, Band>();
46
46
 
47
- for (const option of call.options) {
48
- const key = option.round ?? '';
49
- const band = bands.get(key) ?? { key: key || null, number: 0, round: declared.get(key) ?? null, options: [] };
50
- band.options.push(option);
47
+ for (const variant of call.variants) {
48
+ const key = variant.round ?? '';
49
+ const band = bands.get(key) ?? { key: key || null, number: 0, round: declared.get(key) ?? null, variants: [] };
50
+ band.variants.push(variant);
51
51
  bands.set(key, band);
52
52
  }
53
53
 
@@ -57,7 +57,7 @@ const bandsOf = (call: Call): Band[] => {
57
57
  return [...bands.values()];
58
58
  };
59
59
 
60
- /** Prose that no option claims is stale prose, and it is the one thing the options cannot show. */
60
+ /** Prose that no variant claims is stale prose, and it is the one thing the variants cannot show. */
61
61
  const problemsOf = (call: Call, bands: Band[]) => {
62
62
  const keys = (call.rounds ?? []).map((round) => round.key);
63
63
  const drawn = new Set(bands.map((band) => band.key));
@@ -66,7 +66,7 @@ const problemsOf = (call: Call, bands: Band[]) => {
66
66
  ...[...new Set(keys.filter((key, index) => keys.indexOf(key) !== index))].map(
67
67
  (key) => `two rounds share the key "${key}"`,
68
68
  ),
69
- ...keys.filter((key) => !drawn.has(key)).map((key) => `round "${key}" has no options`),
69
+ ...keys.filter((key) => !drawn.has(key)).map((key) => `round "${key}" has no variants`),
70
70
  ];
71
71
  };
72
72
 
@@ -77,10 +77,10 @@ const problemsOf = (call: Call, bands: Band[]) => {
77
77
  */
78
78
  const chainOf = (bands: Band[]) => {
79
79
  const named = bands.filter((band) => band.key);
80
- const ruled = named.length > 1 && named.every((band) => band.options.every((option) => option.verdict));
80
+ const ruled = named.length > 1 && named.every((band) => band.variants.every((variant) => variant.verdict));
81
81
  const chain = named
82
- .map((band) => band.options.find((option) => option.verdict === 'chosen'))
83
- .filter((option): option is CallOption => !!option);
82
+ .map((band) => band.variants.find((variant) => variant.verdict === 'chosen'))
83
+ .filter((variant): variant is CallVariant => !!variant);
84
84
 
85
85
  return ruled && chain.length > 0 ? chain : [];
86
86
  };
@@ -133,54 +133,54 @@ if (!load) {
133
133
  const bands = bandsOf(call);
134
134
  const chain = chainOf(bands);
135
135
  const result = chain.at(-1);
136
- const isView = call.options.length === 1 && !call.options[0]?.claim;
136
+ const isView = call.variants.length === 1 && !call.variants[0]?.claim;
137
137
  const geometry = `--frame-width:${call.frameWidth}px`;
138
138
 
139
- const frame = (option: CallOption) => `
139
+ const frame = (variant: CallVariant) => `
140
140
  <iframe
141
- src="frame.html?call=${encodeURIComponent(slug)}&option=${encodeURIComponent(option.key)}"
142
- data-option="${esc(option.key)}"
143
- title="${esc(option.name)}"
141
+ src="frame.html?call=${encodeURIComponent(slug)}&variant=${encodeURIComponent(variant.key)}"
142
+ data-variant="${esc(variant.key)}"
143
+ title="${esc(variant.name)}"
144
144
  style="width:${call.frameWidth}px"
145
145
  ></iframe>`;
146
146
 
147
- const column = (option: CallOption) => `
148
- <div class="column" data-verdict="${option.verdict ?? 'open'}" ${picked.includes(option.key) ? 'data-picked' : ''}>
147
+ const column = (variant: CallVariant) => `
148
+ <div class="column" data-verdict="${variant.verdict ?? 'open'}" ${picked.includes(variant.key) ? 'data-picked' : ''}>
149
149
  ${
150
150
  isView
151
151
  ? ''
152
152
  : `<h3>
153
- <a href="${togglePick(option.key)}">${esc(option.name)}</a>
154
- <span class="tag">${option.verdict ?? 'open'}</span>
153
+ <a href="${togglePick(variant.key)}">${esc(variant.name)}</a>
154
+ <span class="tag">${variant.verdict ?? 'open'}</span>
155
155
  </h3>`
156
156
  }
157
- <div class="stage">${frame(option)}</div>
158
- ${option.claim ? `<p class="claim">${esc(option.claim)}</p>` : ''}
159
- ${option.cost ? `<p class="cost">${esc(option.cost)}</p>` : ''}
157
+ <div class="stage">${frame(variant)}</div>
158
+ ${variant.claim ? `<p class="claim">${esc(variant.claim)}</p>` : ''}
159
+ ${variant.cost ? `<p class="cost">${esc(variant.cost)}</p>` : ''}
160
160
  </div>`;
161
161
 
162
- const grid = (options: CallOption[]) =>
163
- options.length === 0
162
+ const grid = (variants: CallVariant[]) =>
163
+ variants.length === 0
164
164
  ? ''
165
- : `<div class="grid" style="${geometry}" ${isView ? 'data-view' : ''}>${options.map(column).join('')}</div>`;
165
+ : `<div class="grid" style="${geometry}" ${isView ? 'data-view' : ''}>${variants.map(column).join('')}</div>`;
166
166
 
167
- const rows = (options: CallOption[]) =>
168
- options.length === 0
167
+ const rows = (variants: CallVariant[]) =>
168
+ variants.length === 0
169
169
  ? ''
170
- : `<ul class="folded">${options
170
+ : `<ul class="folded">${variants
171
171
  .map(
172
- (option) =>
173
- `<li data-verdict="${option.verdict ?? 'open'}"><a href="${togglePick(option.key)}">${esc(
174
- option.name,
175
- )}</a><span class="tag">${option.verdict ?? 'open'}</span></li>`,
172
+ (variant) =>
173
+ `<li data-verdict="${variant.verdict ?? 'open'}"><a href="${togglePick(variant.key)}">${esc(
174
+ variant.name,
175
+ )}</a><span class="tag">${variant.verdict ?? 'open'}</span></li>`,
176
176
  )
177
177
  .join('')}</ul>`;
178
178
 
179
- const band = ({ key, number, round, options }: Band) => {
180
- const settled = options.every((option) => option.verdict);
179
+ const band = ({ key, number, round, variants }: Band) => {
180
+ const settled = variants.every((variant) => variant.verdict);
181
181
  const open = !key || !settled || opened.has(key);
182
- const drawn = open ? options : chain.length > 0 ? [] : options.filter((option) => option.verdict === 'chosen');
183
- const folded = options.filter((option) => !drawn.includes(option));
182
+ const drawn = open ? variants : chain.length > 0 ? [] : variants.filter((variant) => variant.verdict === 'chosen');
183
+ const folded = variants.filter((variant) => !drawn.includes(variant));
184
184
 
185
185
  const head = !key
186
186
  ? ''
@@ -191,7 +191,7 @@ if (!load) {
191
191
  ${
192
192
  settled
193
193
  ? `<a class="fold" href="${toggleOpen(key)}">${
194
- open ? `Fold ${options.length} options away` : `Show all ${options.length} options`
194
+ open ? `Fold ${variants.length} variants away` : `Show all ${variants.length} variants`
195
195
  }</a>`
196
196
  : ''
197
197
  }
@@ -208,11 +208,11 @@ if (!load) {
208
208
  /** Each step is the one before it plus a change, so the arrow is the order they were drawn in. */
209
209
  const trail = () =>
210
210
  chain
211
- .map((option) => {
212
- const band = bands.find((entry) => entry.options.includes(option));
213
- const target = band?.key ? `${toggleOpen(band.key)}#round-${band.key}` : togglePick(option.key);
214
- return `<a href="${esc(target)}" ${option === result ? 'aria-current="step"' : ''}>${esc(
215
- option.key.toUpperCase(),
211
+ .map((variant) => {
212
+ const band = bands.find((entry) => entry.variants.includes(variant));
213
+ const target = band?.key ? `${toggleOpen(band.key)}#round-${band.key}` : togglePick(variant.key);
214
+ return `<a href="${esc(target)}" ${variant === result ? 'aria-current="step"' : ''}>${esc(
215
+ variant.key.toUpperCase(),
216
216
  )}</a>`;
217
217
  })
218
218
  .join('<span class="arrow">→</span>');
@@ -236,21 +236,21 @@ if (!load) {
236
236
  </section>`;
237
237
 
238
238
  const tray = () => {
239
- const options = picked
240
- .map((key) => call.options.find((option) => option.key === key))
241
- .filter((option): option is CallOption => !!option);
242
- if (options.length === 0) return '';
239
+ const variants = picked
240
+ .map((key) => call.variants.find((variant) => variant.key === key))
241
+ .filter((variant): variant is CallVariant => !!variant);
242
+ if (variants.length === 0) return '';
243
243
 
244
- const two = options.length === 2;
244
+ const two = variants.length === 2;
245
245
 
246
246
  return `
247
247
  <section class="round tray">
248
248
  <div class="round-head">
249
- <span class="eyebrow">Compare · ${options.length}</span>
250
- <h2>${options.map((option) => esc(option.name)).join(' · ')}</h2>
249
+ <span class="eyebrow">Compare · ${variants.length}</span>
250
+ <h2>${variants.map((variant) => esc(variant.name)).join(' · ')}</h2>
251
251
  <div class="switch">
252
- ${options
253
- .map((option, index) => `<button data-step="${index}">${esc(option.key.toUpperCase())}</button>`)
252
+ ${variants
253
+ .map((variant, index) => `<button data-step="${index}">${esc(variant.key.toUpperCase())}</button>`)
254
254
  .join('')}
255
255
  <span class="hint">${
256
256
  two ? 'click or space to blink · arrows to wipe' : 'click or space for the next one'
@@ -259,7 +259,7 @@ if (!load) {
259
259
  <a class="fold" href="${link({ pick: null })}">Clear the comparison</a>
260
260
  </div>
261
261
  <div class="overlay" data-two="${two}" style="width:${call.frameWidth}px">
262
- ${options.map(frame).join('')}
262
+ ${variants.map(frame).join('')}
263
263
  <div class="seam"></div>
264
264
  </div>
265
265
  </section>`;
@@ -267,14 +267,14 @@ if (!load) {
267
267
 
268
268
  const sheet = () => `
269
269
  <div class="grid sheet" style="${geometry};--de-scale:${SHEET_SCALE}">
270
- ${call.options
270
+ ${call.variants
271
271
  .map(
272
- (option) => `
273
- <a class="thumb" href="${togglePick(option.key)}" data-verdict="${option.verdict ?? 'open'}" ${
274
- picked.includes(option.key) ? 'data-picked' : ''
272
+ (variant) => `
273
+ <a class="thumb" href="${togglePick(variant.key)}" data-verdict="${variant.verdict ?? 'open'}" ${
274
+ picked.includes(variant.key) ? 'data-picked' : ''
275
275
  }>
276
- <div class="thumb-frame">${frame(option)}</div>
277
- <span class="thumb-name">${esc(option.name)}</span>
276
+ <div class="thumb-frame">${frame(variant)}</div>
277
+ <span class="thumb-name">${esc(variant.name)}</span>
278
278
  </a>`,
279
279
  )
280
280
  .join('')}
@@ -286,7 +286,7 @@ if (!load) {
286
286
  : `<div class="views">
287
287
  <a href="${link({ view: null })}" ${view === 'rounds' ? 'aria-current="page"' : ''}>rounds</a>
288
288
  <a href="${link({ view: 'sheet' })}" ${view === 'sheet' ? 'aria-current="page"' : ''}>all ${
289
- call.options.length
289
+ call.variants.length
290
290
  }</a>
291
291
  </div>`;
292
292
 
@@ -345,8 +345,8 @@ const heights = new Map<string, number>();
345
345
 
346
346
  /** The overlay is as tall as its tallest pick, so no frame is cut and none of them move. */
347
347
  const size = () => {
348
- for (const frame of document.querySelectorAll<HTMLIFrameElement>('iframe[data-option]')) {
349
- const height = heights.get(frame.dataset.option ?? '');
348
+ for (const frame of document.querySelectorAll<HTMLIFrameElement>('iframe[data-variant]')) {
349
+ const height = heights.get(frame.dataset.variant ?? '');
350
350
  if (!height) continue;
351
351
 
352
352
  frame.style.height = `${height}px`;
@@ -359,17 +359,17 @@ const size = () => {
359
359
  if (!overlay) return;
360
360
 
361
361
  const tallest = [...overlay.querySelectorAll<HTMLIFrameElement>('iframe')]
362
- .map((frame) => heights.get(frame.dataset.option ?? '') ?? 0)
362
+ .map((frame) => heights.get(frame.dataset.variant ?? '') ?? 0)
363
363
  .reduce((a, b) => Math.max(a, b), 0);
364
364
 
365
365
  if (tallest > 0) overlay.style.height = `${tallest}px`;
366
366
  };
367
367
 
368
368
  addEventListener('message', (event) => {
369
- const data = event.data as { type?: string; option?: string; height?: number };
370
- if (data?.type !== 'design-explore:height' || !data.height || !data.option) return;
369
+ const data = event.data as { type?: string; variant?: string; height?: number };
370
+ if (data?.type !== 'design-explore:height' || !data.height || !data.variant) return;
371
371
 
372
- heights.set(data.option, data.height);
372
+ heights.set(data.variant, data.height);
373
373
  size();
374
374
 
375
375
  if (restoring) scrollTo(0, target);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ethlete/cli",
3
- "version": "2.1.0-next.9",
3
+ "version": "3.0.0-next.17",
4
4
  "license": "MIT",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -14,8 +14,8 @@
14
14
  "vite": "8.1.5"
15
15
  },
16
16
  "peerDependencies": {
17
- "playwright": "1.61.1",
18
- "tslib": "2.8.1"
17
+ "playwright": "^1.50.0",
18
+ "tslib": "^2.3.0"
19
19
  },
20
20
  "peerDependenciesMeta": {
21
21
  "playwright": {
package/src/index.d.ts CHANGED
@@ -1 +1,2 @@
1
+ #!/usr/bin/env node
1
2
  export * from './lib';
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
+ #!/usr/bin/env node
1
2
  "use strict";
2
3
  Object.defineProperty(exports, "__esModule", { value: true });
3
4
  const tslib_1 = require("tslib");
@@ -38,6 +39,11 @@ const USAGE_ROWS = [
38
39
  args: ' [packages...]',
39
40
  lines: ['Move the @ethlete/* packages to a newer version and run the', 'migrations those versions ship'],
40
41
  },
42
+ {
43
+ subcommand: 'migrations',
44
+ args: ' [run <package>:<name>]',
45
+ lines: ['List the recommended and optional migrations this repo has', 'not run yet, or run one of them'],
46
+ },
41
47
  ];
42
48
  const usage = (root) => {
43
49
  const rows = USAGE_ROWS.map((row) => (Object.assign(Object.assign({}, row), { command: `${(0, lib_1.repoInvocation)({ root, subcommand: row.subcommand })}${row.args}` })));
@@ -52,10 +58,11 @@ const usage = (root) => {
52
58
  const cli = (args) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
53
59
  switch (args[0]) {
54
60
  case 'release':
55
- // `release` reads its flags from a list that was split on "=" before this switch existed.
56
- // Keep that shape here so its own parsing is untouched.
57
- yield (0, lib_1.release)(args.join('=').split('='));
58
- return 0;
61
+ return (0, lib_1.release)({
62
+ root: process.cwd(),
63
+ args: args.slice(1),
64
+ invocation: (0, lib_1.repoInvocation)({ root: process.cwd(), subcommand: 'release' }),
65
+ });
59
66
  case 'api':
60
67
  return (0, lib_1.apiCommand)({
61
68
  root: process.cwd(),
@@ -80,6 +87,13 @@ const cli = (args) => tslib_1.__awaiter(void 0, void 0, void 0, function* () {
80
87
  argv: args.slice(1),
81
88
  invocation: (0, lib_1.repoInvocation)({ root: process.cwd(), subcommand: 'update' }),
82
89
  });
90
+ case 'migrations':
91
+ return (0, lib_1.migrationsCommand)({
92
+ root: process.cwd(),
93
+ argv: args.slice(1),
94
+ invocation: (0, lib_1.repoInvocation)({ root: process.cwd(), subcommand: 'migrations' }),
95
+ updateInvocation: (0, lib_1.repoInvocation)({ root: process.cwd(), subcommand: 'update' }),
96
+ });
83
97
  case 'doctor':
84
98
  // Every fix it names is an `et api` command, so it needs that script rather than its own.
85
99
  return (0, lib_1.doctorCommand)({