kaloko 0.3.0 → 0.4.1
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 +29 -11
- package/package.json +1 -1
- package/schemas/scenario.v1.json +14 -4
- package/skills/kaloko/SKILL.md +10 -4
- package/skills/kaloko/reference/acceptance.md +21 -0
- package/skills/kaloko/reference/collaboration.md +2 -2
- package/skills/kaloko/reference/design.md +31 -1
- package/skills/kaloko/reference/evaluation.md +2 -3
- package/skills/kaloko/reference/portable-html.md +92 -0
- package/src/cli.mjs +52 -4
- package/src/lib/access.mjs +7 -0
- package/src/lib/build.mjs +4 -1
- package/src/lib/capture.mjs +52 -0
- package/src/lib/config.mjs +8 -2
- package/src/lib/design-export.mjs +162 -0
- package/src/lib/design-lint.mjs +189 -0
- package/src/lib/design.mjs +7 -0
- package/src/lib/evaluate.mjs +1 -1
- package/src/lib/linkcheck.mjs +149 -0
- package/src/lib/recheck.mjs +1 -1
- package/src/lib/run.mjs +1 -1
- package/src/lib/zip.mjs +58 -0
- package/viewer/index.html +146 -20
package/README.md
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
# Kaloko
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
See what your agent built, on one canvas, and sign it off.
|
|
4
4
|
|
|
5
|
-
Your coding agent (Claude Code, Codex or any LLM with a shell)
|
|
6
|
-
|
|
7
|
-
e-mail of the flow, checks the criteria and
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
Your coding agent (Claude Code, Codex or any LLM with a shell) walks your
|
|
6
|
+
acceptance plan with the `kaloko` skill. Kaloko captures every screen and
|
|
7
|
+
e-mail of the flow, checks the criteria and lays the whole flow out on a
|
|
8
|
+
canvas: screens, branches, locales and evidence. People review and approve it
|
|
9
|
+
there, and the newest captures double as documentation.
|
|
10
10
|
|
|
11
11
|
- Product and guides: <https://kaloko.app>
|
|
12
12
|
- Service (organizations, shared canvases, reviews): <https://kaloko.app>
|
|
@@ -32,9 +32,9 @@ after `npm update kaloko` to refresh the skill.
|
|
|
32
32
|
|
|
33
33
|
## Then ask your agent
|
|
34
34
|
|
|
35
|
-
With the skill installed
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
With the skill installed, the agent types the commands below, not you. It
|
|
36
|
+
writes the acceptance plan and the scenario from the task, walks the screens,
|
|
37
|
+
evaluates, shares the canvas and later works through what reviewers said.
|
|
38
38
|
For example:
|
|
39
39
|
|
|
40
40
|
> Read the task in docs/tasks/TASK-123, write its acceptance plan and a Kaloko
|
|
@@ -66,8 +66,8 @@ token, environment URL, evaluator key), `npx kaloko doctor` says what to fix.
|
|
|
66
66
|
|
|
67
67
|
## Design (Kaloko Design)
|
|
68
68
|
|
|
69
|
-
Design the screens of a flow as live HTML with your own agent and skills, in the same scenario development
|
|
70
|
-
QA use. A design environment (`kind: design`, a folder of steps) is walked and synced into a shared draft the
|
|
69
|
+
Design the screens of a flow as live HTML with your own agent and skills, in the same scenario that development
|
|
70
|
+
and QA use. A design environment (`kind: design`, a folder of steps) is walked and synced into a shared draft the
|
|
71
71
|
team sees live; `kaloko share` there publishes a version of the flow (unchanged steps are stored once). An
|
|
72
72
|
accepted version becomes the design reference, and `packs: [design]` checks every implementation run against
|
|
73
73
|
it (pixels, structure, tokens, contrast). See `skills/kaloko/reference/design.md`, `docs/GLOSSARY.md` and
|
|
@@ -75,11 +75,29 @@ it (pixels, structure, tokens, contrast). See `skills/kaloko/reference/design.md
|
|
|
75
75
|
|
|
76
76
|
```bash
|
|
77
77
|
npx kaloko start --scenario qa/flows/checkout.yml --env design
|
|
78
|
+
npx kaloko design lint # portable HTML: works on the canvas, from disk and in other tools
|
|
78
79
|
npx kaloko walk && npx kaloko sync # only changed steps render and travel
|
|
79
80
|
npx kaloko share --name "Checkout v2" # a version of the whole flow
|
|
80
81
|
npx kaloko reference --design --version 2 # after the team accepted it
|
|
81
82
|
```
|
|
82
83
|
|
|
84
|
+
On the canvas, *Prototype* (or `P`) presents the steps full screen as live pages: click through links between
|
|
85
|
+
steps, switch the device, walk with the arrow keys, and share a link that opens straight into a step. The rules
|
|
86
|
+
for pages that work everywhere are in `skills/kaloko/reference/portable-html.md`.
|
|
87
|
+
|
|
88
|
+
## Your data is yours: formats
|
|
89
|
+
|
|
90
|
+
Nothing in Kaloko is stored in a format only Kaloko can read. Every export opens from disk without it:
|
|
91
|
+
|
|
92
|
+
| Export | Command | Formats |
|
|
93
|
+
|---|---|---|
|
|
94
|
+
| Design version | `npx kaloko export --design [--version n] --out v2.zip` | HTML, CSS, JS, fonts and images in the design folder layout; `tokens.json` (W3C DTCG); `scenario.yml` (`qawalk.scenario.v1`); screenshots (WebP); `index.html` start page |
|
|
95
|
+
| Live files (handoff) | `npx kaloko export --live [--version n] --out design/` | the design folder as written |
|
|
96
|
+
| Shared run | `npx kaloko export --run <id>` | zip with `run.json` (`qawalk.run.v1`), screenshots and HTML snapshots, live HTML of design steps, comments and verdicts, and `index.html`: the canvas, offline |
|
|
97
|
+
| Documentation captures | `npx kaloko export --scenario <id>` | newest image and HTML of every step, `index.json` |
|
|
98
|
+
|
|
99
|
+
The schemas of these formats ship with the package (`schemas/`).
|
|
100
|
+
|
|
83
101
|
## Configuration and secrets
|
|
84
102
|
|
|
85
103
|
Project configuration is `kaloko.config.yml` in the repository root:
|
package/package.json
CHANGED
package/schemas/scenario.v1.json
CHANGED
|
@@ -487,6 +487,8 @@
|
|
|
487
487
|
"link_matches",
|
|
488
488
|
"http_status",
|
|
489
489
|
"link_home",
|
|
490
|
+
"links_ok",
|
|
491
|
+
"hreflang_pairs",
|
|
490
492
|
"app_touch_targets",
|
|
491
493
|
"app_no_truncation",
|
|
492
494
|
"app_no_crash",
|
|
@@ -508,10 +510,12 @@
|
|
|
508
510
|
]
|
|
509
511
|
},
|
|
510
512
|
"selector": {
|
|
511
|
-
"type": "string"
|
|
513
|
+
"type": "string",
|
|
514
|
+
"description": "CSS selector; links_ok: the links to check (default a[href])."
|
|
512
515
|
},
|
|
513
516
|
"pattern": {
|
|
514
|
-
"type": "string"
|
|
517
|
+
"type": "string",
|
|
518
|
+
"description": "Regex; links_ok: link paths to skip."
|
|
515
519
|
},
|
|
516
520
|
"attr": {
|
|
517
521
|
"type": "string"
|
|
@@ -526,17 +530,23 @@
|
|
|
526
530
|
},
|
|
527
531
|
"min": {
|
|
528
532
|
"type": "integer",
|
|
529
|
-
"minimum": 0
|
|
533
|
+
"minimum": 0,
|
|
534
|
+
"description": "count: at least; http_status: lowest status; hreflang_pairs: languages required (default 2)."
|
|
530
535
|
},
|
|
531
536
|
"max": {
|
|
532
537
|
"type": "integer",
|
|
533
|
-
"minimum": 0
|
|
538
|
+
"minimum": 0,
|
|
539
|
+
"description": "count: at most; http_status: highest status; perf_*: the limit; links_ok: links checked per page (default 100)."
|
|
534
540
|
},
|
|
535
541
|
"ratio": {
|
|
536
542
|
"type": "number",
|
|
537
543
|
"minimum": 0,
|
|
538
544
|
"description": "design_pixels: maximum share of differing pixels (default 0.03)."
|
|
539
545
|
},
|
|
546
|
+
"follow": {
|
|
547
|
+
"type": "boolean",
|
|
548
|
+
"description": "links_ok: follow links even when neither the environment nor the scenario is readonly (GET from an isolated copy of the session; links that look like actions are never followed)."
|
|
549
|
+
},
|
|
540
550
|
"flags": {
|
|
541
551
|
"type": "string",
|
|
542
552
|
"pattern": "^[imsu]{0,4}$",
|
package/skills/kaloko/SKILL.md
CHANGED
|
@@ -31,9 +31,10 @@ the canvas, storage and collaboration are the service.
|
|
|
31
31
|
the work belongs to, and you read `kaloko feedback` and `kaloko inbox`
|
|
32
32
|
before continuing on that task.
|
|
33
33
|
- Changes to intent are written back to `ACCEPTANCE.md` (change log row).
|
|
34
|
-
- On a design environment: the folder follows the bundle rules
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
- On a design environment: the folder follows the bundle rules and
|
|
35
|
+
`kaloko design lint` shows no error, `kaloko walk` and `kaloko sync` ran,
|
|
36
|
+
`kaloko status` shows no conflict, and you read the draft's feedback; a
|
|
37
|
+
version is published only when the person asked.
|
|
37
38
|
|
|
38
39
|
## You may do without asking
|
|
39
40
|
|
|
@@ -84,7 +85,12 @@ they can do; never write secrets into the config, a scenario or a script.
|
|
|
84
85
|
[reference/documentation.md](reference/documentation.md)
|
|
85
86
|
- Designing in a design environment (bundle, sync and publish, comments,
|
|
86
87
|
references, handoff, tokens, the design pack, canvas edits, branches, HTML
|
|
87
|
-
view, bringing your own design skill):
|
|
88
|
+
view, presenting the prototype, exports, bringing your own design skill):
|
|
89
|
+
[reference/design.md](reference/design.md)
|
|
90
|
+
- Writing design pages that work on the live step, from disk and in other
|
|
91
|
+
tools (relative URLs, no CDNs, state in the URL, deterministic content,
|
|
92
|
+
links between steps; what `kaloko design lint` checks):
|
|
93
|
+
[reference/portable-html.md](reference/portable-html.md)
|
|
88
94
|
- Command reference: `kaloko help`; when something does not work (Chrome,
|
|
89
95
|
token, environment URL, evaluator key), run `kaloko doctor` first and fix
|
|
90
96
|
what it marks ✕.
|
|
@@ -187,6 +187,27 @@ button styled uppercase reads `VYTVOŘIT`. Add `flags: i` to ignore case
|
|
|
187
187
|
white-label check for `vyvolej` also matches the Czech verb "Vyvolej si";
|
|
188
188
|
anchor it (`vyvolej\.to`).
|
|
189
189
|
|
|
190
|
+
Two checks need the network and run next to the capture, once per page:
|
|
191
|
+
|
|
192
|
+
- `links_ok`: every same-origin link (`selector`, default `a[href]`) answers
|
|
193
|
+
below 400 after redirects. `max` caps the links per page (default 100),
|
|
194
|
+
`pattern` skips matching paths. Redirects are listed in the evidence.
|
|
195
|
+
Links are followed only on a readonly environment or scenario; elsewhere
|
|
196
|
+
the criterion is not evaluated unless the check sets `follow: true`. Links
|
|
197
|
+
that look like actions (log out, unsubscribe, delete, remove, tokens,
|
|
198
|
+
switching organization) are never requested.
|
|
199
|
+
- `hreflang_pairs`: at least `min` languages (default 2); the page's own
|
|
200
|
+
language points to the page itself; every other version answers 200
|
|
201
|
+
without a redirect and links back. This is what search engines require
|
|
202
|
+
of hreflang; a count of `link[hreflang]` does not prove it.
|
|
203
|
+
|
|
204
|
+
Both are GETs from a separate request context that starts with a copy of
|
|
205
|
+
the walk's cookies and the environment's gate and never writes back (a
|
|
206
|
+
`?lang=` link cannot switch the language of the next step). They are cached
|
|
207
|
+
per environment, locale and sign-in state, so a stack of hundreds of pages
|
|
208
|
+
fetches its shared navigation once. A recheck
|
|
209
|
+
from stored HTML keeps their last result; capture again to refresh them.
|
|
210
|
+
|
|
190
211
|
Describe every relevant variant as nodes and `alt` edges, even when a given
|
|
191
212
|
run covers only part of it. Decisions are `kind: decision` nodes.
|
|
192
213
|
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Collaboration: share, feedback, inbox
|
|
2
2
|
|
|
3
|
-
A run
|
|
4
|
-
|
|
3
|
+
A run is only worth something once the people who decide have seen it. The
|
|
4
|
+
loop teams use:
|
|
5
5
|
|
|
6
6
|
1. **Share the run** (`kaloko share`, tester token, after asking). The
|
|
7
7
|
command prints the canvas URL.
|
|
@@ -37,9 +37,14 @@ design/
|
|
|
37
37
|
images, fonts, module imports, `fetch('./fixtures/x.json')`). Links to other steps are not files it needs.
|
|
38
38
|
- Live steps run sandboxed on their own origin with `connect-src 'self'`: no real APIs. Use JSON fixtures in
|
|
39
39
|
the bundle.
|
|
40
|
-
-
|
|
40
|
+
- Links between steps are plain relative links to the page: `<a href="../payment/index.html">`. The canvas
|
|
41
|
+
follows them; `data-kaloko-go="payment"` (or `data-qawalk-go`) moves to that step whatever the `href` says.
|
|
41
42
|
- Reference token values as CSS custom properties (`var(--color-accent)`), so the inspector and the token
|
|
42
43
|
check see where values come from.
|
|
44
|
+
- Write pages by the portable HTML rules ([portable-html.md](portable-html.md)): relative URLs only, bundled
|
|
45
|
+
fonts, no CDNs, state in the URL instead of storage, deterministic content, viewport meta and `lang`.
|
|
46
|
+
`kaloko design lint` checks the folder; `kaloko sync` runs it first and stops on errors (things that break on
|
|
47
|
+
the live step), warnings are printed.
|
|
43
48
|
|
|
44
49
|
## Sync and publish
|
|
45
50
|
|
|
@@ -62,6 +67,19 @@ design/
|
|
|
62
67
|
4. Change what the comments ask, sync again, then answer in the comment thread (MCP `add_draft_comment`).
|
|
63
68
|
5. When the person is happy, `kaloko share --name "…"` publishes a version.
|
|
64
69
|
|
|
70
|
+
## Present the prototype
|
|
71
|
+
|
|
72
|
+
The canvas plays design steps full screen: *Prototype* in the top bar or the step detail, or `P`. Each step runs as
|
|
73
|
+
its live page in a device frame (phone for mobile viewports, a window for desktop; switchable), clicks on links and
|
|
74
|
+
`data-kaloko-go` move between steps, `←` `→` walk the flow, `Esc` leaves. The address (`#present=<step>&vp=mobile`)
|
|
75
|
+
opens straight into the presentation of that step: share it with the person who reviews.
|
|
76
|
+
|
|
77
|
+
- Live pages need the user-content origin of the service; where it is not available (a self-hosted instance
|
|
78
|
+
without it, a step never rendered live) the presentation shows the screenshots.
|
|
79
|
+
- From the local preview and an offline export, the steps open from the run's merged folder (`live/site/`), so plain
|
|
80
|
+
relative links click through there too.
|
|
81
|
+
- A link's query and hash travel to the next step (`../payment/index.html?plan=pro#card`): keep prototype state there.
|
|
82
|
+
|
|
65
83
|
## References
|
|
66
84
|
|
|
67
85
|
- The design reference is an accepted version: `kaloko reference --design --version 4` (a person accepts the
|
|
@@ -78,6 +96,18 @@ Development builds from the design reference:
|
|
|
78
96
|
page structure and open comments; `list_step_changes` names the steps that changed since the last reference.
|
|
79
97
|
- Translate to the project's stack with the project's own skills; keep the step ids and the token names.
|
|
80
98
|
|
|
99
|
+
## Your data is yours
|
|
100
|
+
|
|
101
|
+
Everything exports in open formats, readable without Kaloko:
|
|
102
|
+
|
|
103
|
+
| What | Command | Contents |
|
|
104
|
+
|---|---|---|
|
|
105
|
+
| a design version (or `--draft`, default the design reference) | `kaloko export --design [--version n] --out dir/` or `--out v4.zip` | the live files in the folder layout, `tokens.json` (DTCG), `scenario.yml` (`qawalk.scenario.v1`), `screenshots/<step>/<locale>/<viewport>.webp`, `kaloko.json` (what is where) and `index.html`, a start page that opens from disk |
|
|
106
|
+
| the live files only (the handoff) | `kaloko export --live [--version n] --out design/` | the design folder as the designer wrote it |
|
|
107
|
+
| a shared run | `kaloko export --run <id>` | `run.json`, every capture, the live HTML of design steps (per revision and merged in `live/site/`), comments and verdicts, and `index.html`: the canvas, offline from `file://` |
|
|
108
|
+
|
|
109
|
+
When someone asks for the design or the evidence outside Kaloko, export; do not screenshot the canvas.
|
|
110
|
+
|
|
81
111
|
## Tokens
|
|
82
112
|
|
|
83
113
|
- The set is W3C DTCG. Both value styles are read: strings (`"#2E6BFF"`, `"16px"`) and the current format's
|
|
@@ -22,9 +22,8 @@ evaluating the same capture again costs nothing. `evaluators.jev.hosted: false`
|
|
|
22
22
|
When neither is available, or the allowance is used up, semantic criteria stay `not_evaluated`; say so in the
|
|
23
23
|
report with the reason `kaloko evaluate` gives.
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
which is why those are deterministic.
|
|
25
|
+
The default evaluator is text-only and most accurate in English. It is weak
|
|
26
|
+
at numbers, which is why those are checked deterministically.
|
|
28
27
|
|
|
29
28
|
When a criterion comes back `needs_review`, read the reduced state the
|
|
30
29
|
evaluator saw (`steps/<node>/<locale>/<viewport>.capture.json`, key `state`)
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# Portable interactive HTML
|
|
2
|
+
|
|
3
|
+
Design steps are live pages. The same files have to work in three places:
|
|
4
|
+
|
|
5
|
+
- **on the live origin** of the canvas (`<revision>.kaloko-usercontent.com`): a sandboxed frame with an opaque origin and
|
|
6
|
+
`Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; connect-src 'self'; frame-src 'none';
|
|
7
|
+
worker-src 'none'; form-action 'none'` (no popups, no modals, no service workers);
|
|
8
|
+
- **from disk** (`file://`): the local preview, `kaloko export --design`, and anyone who unzips an export;
|
|
9
|
+
- **in another tool**: the export is the handoff and the way out of Kaloko.
|
|
10
|
+
|
|
11
|
+
`kaloko design lint` checks a design folder against the rules below; `kaloko sync` runs it first and stops on errors
|
|
12
|
+
(`--no-lint` skips it, `kaloko walk` prints a summary). Errors break for certain on the live step; warnings make the
|
|
13
|
+
prototype fragile or the captures unstable. `kaloko design lint --all` also lists information, `--strict` fails on
|
|
14
|
+
warnings, `--json` gives every finding with file, line, rule and fix.
|
|
15
|
+
|
|
16
|
+
## Files
|
|
17
|
+
|
|
18
|
+
- Everything a page needs is in the design folder, referenced with **relative paths** (`../_shared/app.css`). No
|
|
19
|
+
`https://`, no protocol-relative `//cdn…`, no paths starting with `/` (on the live origin `/` is the origin root, from
|
|
20
|
+
disk it is the disk root). Errors: `external-url`, `root-relative`, `outside-folder`, `missing-file`.
|
|
21
|
+
- **No CDNs.** Download libraries into `_shared/` and link them relatively. Fonts are bundled as **woff2**, subset to
|
|
22
|
+
the characters used, with `@font-face` in the shared CSS; Google Fonts links do not load.
|
|
23
|
+
- Only bundle file types (html, css, js, mjs, json, svg, woff2, woff, png, jpg, webp, avif, gif, mp4, webm, txt).
|
|
24
|
+
Convert TTF/OTF fonts and other formats (`file-type`).
|
|
25
|
+
- **Size budget:** a file over 1 MB or a step over 5 MB (with the shared files) warns (`large-file`); 10 MB per file
|
|
26
|
+
and 40 MB per step are the limits (`too-large`). Images as webp/avif at the size shown.
|
|
27
|
+
|
|
28
|
+
## Links between steps
|
|
29
|
+
|
|
30
|
+
- Link steps with **plain relative links to the page itself**: `<a href="../payment/index.html">`, from a locale page
|
|
31
|
+
`../../payment/cs/index.html`. A link to a folder (`../payment/`) opens nothing from disk or a zip
|
|
32
|
+
(`directory-link`); a link to a page that does not exist warns (`broken-link`).
|
|
33
|
+
- The canvas follows such links in the presentation and the Live view, and `data-kaloko-go="payment"` (or the older
|
|
34
|
+
`data-qawalk-go`) moves to a step whatever the `href` says. Keep a real `href` next to it so the page also works
|
|
35
|
+
from disk.
|
|
36
|
+
- No `target="_blank"`, `window.open`, `top.location` (`new-window`, `top-navigation`): the frame may not open
|
|
37
|
+
windows or navigate the canvas.
|
|
38
|
+
- Forms do not submit (`form-action 'none'`): handle `submit` in a script (`event.preventDefault()`) and go on with a
|
|
39
|
+
link or `data-kaloko-go` (`form-action`).
|
|
40
|
+
|
|
41
|
+
## State and data
|
|
42
|
+
|
|
43
|
+
- **State lives in the URL**, not in storage: `localStorage`, `sessionStorage`, IndexedDB and cookies throw in the
|
|
44
|
+
sandboxed frame and are empty for every other viewer (`storage`). Use `location.hash` or the query
|
|
45
|
+
(`?plan=pro#card`): a link's query and hash travel to the next step in the presentation, and work from disk.
|
|
46
|
+
Wrap any storage access you keep in `try/catch` with a fallback.
|
|
47
|
+
- **No network:** real APIs, `XMLHttpRequest`, `WebSocket`, `EventSource` do not connect (`websocket`). Put data in
|
|
48
|
+
JSON fixtures of the bundle and `fetch('./fixtures/cart.json')`.
|
|
49
|
+
- **No code from strings:** `eval`, `new Function`, `setTimeout('…')` are blocked (`eval`). No service workers
|
|
50
|
+
(`service-worker`), no iframes, `object` or `embed` (`embedded-frame`). `alert`, `confirm` and `prompt` are blocked
|
|
51
|
+
(`modal`): use an in-page `<dialog>`.
|
|
52
|
+
|
|
53
|
+
## Deterministic content
|
|
54
|
+
|
|
55
|
+
Captures are cached by content and compared in pixels. A page that changes on every render makes unchanged steps look
|
|
56
|
+
changed (`nondeterministic`):
|
|
57
|
+
|
|
58
|
+
- fixed dates (`new Date('2026-01-15T10:00')`), no `Date.now()` or `new Date()` for visible text;
|
|
59
|
+
- fake data from a seeded generator, not `Math.random()` or `crypto.randomUUID()`;
|
|
60
|
+
- animations that end, or none at the moment of capture; content that does not depend on load timing.
|
|
61
|
+
|
|
62
|
+
## Document basics
|
|
63
|
+
|
|
64
|
+
Every page starts like this (`doctype`, `charset`, `viewport-meta`, `lang`, `title`):
|
|
65
|
+
|
|
66
|
+
```html
|
|
67
|
+
<!doctype html>
|
|
68
|
+
<html lang="en">
|
|
69
|
+
<head>
|
|
70
|
+
<meta charset="utf-8">
|
|
71
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
72
|
+
<title>Payment · Checkout</title>
|
|
73
|
+
<link rel="stylesheet" href="../_shared/app.css">
|
|
74
|
+
</head>
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
- Images have `alt` (`alt=""` for decoration) (`img-alt`); real buttons, labels for fields, one `<h1>`, landmarks.
|
|
78
|
+
- Respect `prefers-reduced-motion` when anything animates (`reduced-motion`):
|
|
79
|
+
`@media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation: none !important; transition: none !important } }`
|
|
80
|
+
- The mobile viewports render at their real width: the viewport meta and fluid layout matter.
|
|
81
|
+
|
|
82
|
+
## Tokens
|
|
83
|
+
|
|
84
|
+
- `tokens.json` (W3C DTCG) in the folder root; values as **CSS custom properties** in the shared CSS
|
|
85
|
+
(`--color-accent: #2e6bff`) and used with `var(--color-accent)`, so the inspector names them and `kaloko tokens check`
|
|
86
|
+
sees raw values (`tokens-unused`, `no-tokens`).
|
|
87
|
+
- Name custom properties after the token path with dots as dashes (`color.accent` → `--color-accent`).
|
|
88
|
+
|
|
89
|
+
## Done means
|
|
90
|
+
|
|
91
|
+
`kaloko design lint` shows no error and no warning you cannot explain, the pages open from the folder by
|
|
92
|
+
double-click and click through, and `kaloko walk` renders the same in every viewport.
|
package/src/cli.mjs
CHANGED
|
@@ -12,7 +12,8 @@ import { startBrowser, stopBrowser, connectBrowser, currentPage, launchEphemeral
|
|
|
12
12
|
import { captureStep, captureCrop, captureOptions, acceptDialogs } from './lib/capture.mjs'
|
|
13
13
|
import { observeConsole } from './lib/console.mjs'
|
|
14
14
|
import { mailClient, waitForMessage, recipient, readMessageFile } from './lib/mail.mjs'
|
|
15
|
-
import { newContext, accountFor, gateHeaders, gateOrigins, originMatcher } from './lib/access.mjs'
|
|
15
|
+
import { newContext, accountFor, gateHeaders, gateOrigins, originMatcher, gateForRequests } from './lib/access.mjs'
|
|
16
|
+
import { configureGate } from './lib/linkcheck.mjs'
|
|
16
17
|
import { rememberEnvironment } from './lib/secrets.mjs'
|
|
17
18
|
import { evaluateRun, semanticPrefetch } from './lib/evaluate.mjs'
|
|
18
19
|
import { buildManifest, writePreview } from './lib/build.mjs'
|
|
@@ -32,6 +33,8 @@ import { htmlViewAllowed } from './lib/htmlview.mjs'
|
|
|
32
33
|
import { service } from './lib/service.mjs'
|
|
33
34
|
import { walkDesign, syncDesign, publishVersion, historyCmd, referenceCmd, tokensCmd, exportLive, localRevisions, branchCmd, uploadBlobs } from './lib/design-cli.mjs'
|
|
34
35
|
import { designDir, readBase, headKey } from './lib/design.mjs'
|
|
36
|
+
import { lintDesign, formatLint } from './lib/design-lint.mjs'
|
|
37
|
+
import { exportDesign } from './lib/design-export.mjs'
|
|
35
38
|
import { driverFor, isApp, pngSize, ocr as ocrImage } from './lib/apps/drivers.mjs'
|
|
36
39
|
import { captureApp } from './lib/apps/capture.mjs'
|
|
37
40
|
import { readTree, flatten } from './lib/apps/tree.mjs'
|
|
@@ -121,7 +124,8 @@ Results
|
|
|
121
124
|
Semantic evaluator vs human labels: precision/recall per criterion at the
|
|
122
125
|
configured thresholds and suggested thresholds (labels from a golden file or
|
|
123
126
|
from reviewers' criterion verdicts in the service)
|
|
124
|
-
export [--run id] [--out file.zip] Download a shared run
|
|
127
|
+
export [--run id] [--out file.zip] Download a shared run as a zip before it expires: run.json, every file, the live
|
|
128
|
+
HTML of design steps and index.html, the canvas that opens offline from disk
|
|
125
129
|
export --scenario <id> [--accepted] [--out dir] [--locales l] [--viewports v]
|
|
126
130
|
Newest capture of every step as steps/<step>/<locale>/<viewport>.<ext> plus
|
|
127
131
|
index.json (documentation layer, Business plan); unchanged files are skipped
|
|
@@ -134,8 +138,12 @@ Design (environments with kind: design; live HTML steps in a folder)
|
|
|
134
138
|
start --scenario <file> --env <design env> A run of the design folder (same scenario as development and QA)
|
|
135
139
|
walk [--steps a,b] [--no-cache] Render the steps; unchanged revisions reuse their captures
|
|
136
140
|
status Per step: synced, changed here, changed in the draft, conflict; step status
|
|
137
|
-
sync [--pull|--push] [--branch b] [--mine a,b|--theirs a,b]
|
|
141
|
+
sync [--pull|--push] [--branch b] [--mine a,b|--theirs a,b] [--no-lint]
|
|
138
142
|
Exchange revisions with the shared draft; only changed steps and missing files travel
|
|
143
|
+
Lints the folder first: errors (things that break on the live step) stop the push
|
|
144
|
+
design lint [--env e | --dir design/] [--scenario file] [--all] [--strict] [--json]
|
|
145
|
+
Check the folder against the portable HTML rules (relative URLs, no CDNs, no storage,
|
|
146
|
+
deterministic content, viewport, lang, links that also work from disk); --strict fails on warnings
|
|
139
147
|
share [--name <text>] [--branch b] Publish the draft as a version of the flow (a run): "v4 · 3 changed, 17 unchanged"
|
|
140
148
|
history [--scenario id] [--step id] [--locale l] [--json]
|
|
141
149
|
Versions of the flow; revisions of a step with who (person or agent) and when
|
|
@@ -149,6 +157,9 @@ Design (environments with kind: design; live HTML steps in a folder)
|
|
|
149
157
|
Live files of a version (default: the design reference) into a folder: the handoff
|
|
150
158
|
export --live --views --run <id> [--viewport desktop] [--out design/]
|
|
151
159
|
Fork into a draft: the HTML views of an app run become design steps (experimental)
|
|
160
|
+
export --design [--version n|--draft|--run id] [--out dir|file.zip]
|
|
161
|
+
A design version in open formats: HTML + assets, tokens.json (DTCG), scenario.yml,
|
|
162
|
+
screenshots and an index.html that opens from disk (default: the design reference)
|
|
152
163
|
|
|
153
164
|
Common options: --run <id|dir> (or KALOKO_RUN) --config <file> KALOKO_AGENT=<name> labels writes by an agent
|
|
154
165
|
`
|
|
@@ -654,6 +665,7 @@ const commands = {
|
|
|
654
665
|
} finally { await enc?.close().catch(() => {}) }
|
|
655
666
|
return
|
|
656
667
|
}
|
|
668
|
+
configureGate(await gateForRequests(env, { root }))
|
|
657
669
|
let conn = await connectBrowser({ root, config, env })
|
|
658
670
|
let ephemeral = null
|
|
659
671
|
let page
|
|
@@ -709,12 +721,15 @@ const commands = {
|
|
|
709
721
|
const { config, root, run, scenario, env } = await runContext(args)
|
|
710
722
|
if (env.kind === 'design') {
|
|
711
723
|
await walkDesign({ config, root, run, scenario, env, pick: list(args.steps), locales: list(args.locales) || run.meta.scope.locales, viewports: scopeViewports(run, list(args.viewports)), say, noCache: !!args['no-cache'] })
|
|
724
|
+
const lint = await lintDesign({ dir: designDir(env, root), scenario })
|
|
725
|
+
if (lint.errors || lint.warnings) say(`\nPortable HTML: ${lint.errors} error(s), ${lint.warnings} warning(s); kaloko design lint lists them${lint.errors ? ' (errors stop kaloko sync: they break on the live step)' : ''}.`)
|
|
712
726
|
return
|
|
713
727
|
}
|
|
714
728
|
if (isApp(scenario.platform)) return walkApp({ config, root, run, scenario, env, args })
|
|
715
729
|
const pick = list(args.steps)
|
|
716
730
|
const locales = list(args.locales) || run.meta.scope.locales
|
|
717
731
|
// --ephemeral: an own browser (own cookies and tabs) even when the shared one runs, so runs can walk in parallel
|
|
732
|
+
configureGate(await gateForRequests(env, { root }))
|
|
718
733
|
const conn = args.ephemeral ? null : await connectBrowser({ root, config, env })
|
|
719
734
|
const releaseLock = conn ? await acquireWalkLock({ root, config, env, run: run.meta.id }).catch(async (e) => { await conn.browser.close(); throw e }) : null
|
|
720
735
|
const ephemeral = conn ? null : await launchEphemeral(config)
|
|
@@ -1026,9 +1041,33 @@ const commands = {
|
|
|
1026
1041
|
async sync (args) {
|
|
1027
1042
|
const { config, root, run, scenario, env } = await runContext(args)
|
|
1028
1043
|
const client = service(config)
|
|
1044
|
+
if (env.kind === 'design' && !args.pull && !args['no-lint']) {
|
|
1045
|
+
// what renders locally may still break on the live step (CSP, sandbox): say so before anything is pushed
|
|
1046
|
+
const lint = await lintDesign({ dir: designDir(env, root), scenario })
|
|
1047
|
+
if (lint.errors || lint.warnings) say(formatLint(lint, { max: 15 }) + '\n')
|
|
1048
|
+
if (lint.errors) throw new UserError(`${lint.errors} error(s) above break the design on the live step; fix them, or sync with --no-lint to push anyway.`)
|
|
1049
|
+
}
|
|
1029
1050
|
await syncDesign({ config, root, run, scenario, env, client, args, say, push: !args.pull, pull: !args.push, render: (steps) => walkDesign({ config, root, run, scenario, env, pick: steps, locales: run.meta.scope.locales, viewports: run.meta.scope.viewports, say }).then(() => evaluateRun({ run, scenario, config, root })) })
|
|
1030
1051
|
},
|
|
1031
1052
|
|
|
1053
|
+
async design (args) {
|
|
1054
|
+
if (args._[0] !== 'lint') throw new UserError('Usage: kaloko design lint [--env <design env> | --dir design/] [--scenario file] [--all] [--strict] [--json]')
|
|
1055
|
+
const { config, root } = await context(args)
|
|
1056
|
+
let run = null
|
|
1057
|
+
try { run = await resolveRun({ root, output: config.output, run: args.run }) } catch {}
|
|
1058
|
+
let dir = typeof args.dir === 'string' ? resolve(args.dir) : null
|
|
1059
|
+
if (!dir) {
|
|
1060
|
+
const runEnv = run ? environment(config, run.meta.environment.name) : null
|
|
1061
|
+
const env = typeof args.env === 'string' ? environment(config, args.env) : runEnv?.kind === 'design' ? runEnv : Object.keys(config.environments).map((n) => environment(config, n)).find((e) => e.kind === 'design')
|
|
1062
|
+
if (!env) throw new UserError('No design environment: pass --dir <folder> or --env <design env>.')
|
|
1063
|
+
dir = designDir(env, root)
|
|
1064
|
+
}
|
|
1065
|
+
const scenario = typeof args.scenario === 'string' && /\.ya?ml$/.test(args.scenario) ? await loadScenario(resolve(args.scenario)) : run ? await loadScenario(resolve(root, run.meta.scenario_file)).catch(() => null) : null
|
|
1066
|
+
const report = await lintDesign({ dir, scenario })
|
|
1067
|
+
say(args.json ? JSON.stringify(report, null, 2) : formatLint(report, { all: !!args.all, max: args.all ? 500 : 60 }))
|
|
1068
|
+
if (report.errors || (args.strict && report.warnings)) process.exitCode = 1
|
|
1069
|
+
},
|
|
1070
|
+
|
|
1032
1071
|
async history (args) {
|
|
1033
1072
|
const { config, root } = await context(args)
|
|
1034
1073
|
const scenarioId = await scenarioIdOf(args, config, root)
|
|
@@ -1234,7 +1273,7 @@ const commands = {
|
|
|
1234
1273
|
const d = compareRuns(run, base)
|
|
1235
1274
|
if (args.json) { say(JSON.stringify(d, null, 2)); return }
|
|
1236
1275
|
say(formatComparison(d))
|
|
1237
|
-
if (config.share?.url && run.org) say(`\nCanvas: ${config
|
|
1276
|
+
if (config.share?.url && run.org) { const { serviceUrl } = await import('./lib/service.mjs'); say(`\nCanvas: ${serviceUrl(config)}/${run.org}/runs/${run.id}/#cmp=${base.id}`) }
|
|
1238
1277
|
},
|
|
1239
1278
|
|
|
1240
1279
|
async draft (args) {
|
|
@@ -1290,6 +1329,15 @@ const commands = {
|
|
|
1290
1329
|
async export (args) {
|
|
1291
1330
|
const { config, root } = await context(args)
|
|
1292
1331
|
const client = service(config)
|
|
1332
|
+
if (args.design) {
|
|
1333
|
+
// open formats only: the version as a folder (or zip) that opens from disk without Kaloko
|
|
1334
|
+
const scenarioId = await scenarioIdOf(args, config, root)
|
|
1335
|
+
const v = typeof args.version === 'string' ? args.version : typeof args.design === 'string' ? args.design : null
|
|
1336
|
+
const ref = args.draft ? 'draft' : v ? `v${v.replace(/^v/, '')}` : typeof args.run === 'string' ? args.run : 'design'
|
|
1337
|
+
const out = typeof args.out === 'string' ? resolve(args.out) : resolve(root, config.output, 'design-export', `${scenarioId}-${ref}`)
|
|
1338
|
+
await exportDesign({ config, scenarioId, client, ref, branch: typeof args.branch === 'string' ? args.branch : 'main', out, say })
|
|
1339
|
+
return
|
|
1340
|
+
}
|
|
1293
1341
|
if (args.live) {
|
|
1294
1342
|
const scenarioId = await scenarioIdOf(args, config, root)
|
|
1295
1343
|
let out = typeof args.out === 'string' ? resolve(args.out) : null
|
package/src/lib/access.mjs
CHANGED
|
@@ -119,6 +119,13 @@ export async function contextOptions (env, { root, config, account } = {}) {
|
|
|
119
119
|
return options
|
|
120
120
|
}
|
|
121
121
|
|
|
122
|
+
/** The gate for requests made outside the browser (link checks): headers for the gate's origins only, Basic, mTLS. */
|
|
123
|
+
export async function gateForRequests (env, { root } = {}) {
|
|
124
|
+
const headers = gateHeaders(env)
|
|
125
|
+
const options = await contextOptions(env, { root })
|
|
126
|
+
return { headers, toGate: Object.keys(headers).length ? originMatcher(gateOrigins(env)) : () => false, httpCredentials: options.httpCredentials || null, clientCertificates: options.clientCertificates || [] }
|
|
127
|
+
}
|
|
128
|
+
|
|
122
129
|
/** A new browser context with the environment's gate, policy and (optionally) an account's session. */
|
|
123
130
|
export async function newContext (browser, env, opts = {}) {
|
|
124
131
|
const context = await browser.newContext(await contextOptions(env, opts))
|
package/src/lib/build.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { parsePlan, coverage, flaky } from './coverage.mjs'
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises'
|
|
3
|
+
import { existsSync } from 'node:fs'
|
|
3
4
|
import { join, resolve } from 'node:path'
|
|
4
5
|
import { readJson, writeJson } from './util.mjs'
|
|
5
6
|
import { readCaptures, runPaths, listRuns } from './run.mjs'
|
|
@@ -140,7 +141,9 @@ export async function writePreview ({ run, manifest }) {
|
|
|
140
141
|
for (const [l, vps] of Object.entries(n.captures)) { captures[l] = {}; for (const [vp, c] of Object.entries(vps)) captures[l][vp] = c?.tree ? { ...c, tree_inline: await readJson(join(run.dir, c.tree), null) } : c }
|
|
141
142
|
return { ...n, captures }
|
|
142
143
|
}
|
|
143
|
-
|
|
144
|
+
// design runs: live steps open from the merged tree, where links between steps resolve (design.mjs copyLive)
|
|
145
|
+
const site = existsSync(join(run.dir, 'live', 'site')) ? { live_site: 'live/site/' } : {}
|
|
146
|
+
const preview = { ...manifest, ...site, nodes: await Promise.all(manifest.nodes.map(async (n) => (n.stack ? { ...n, stack: { ...n.stack, items_inline: (await readJson(join(run.dir, n.stack.items), { items: [] })).items } } : withTrees(n)))) }
|
|
144
147
|
const json = JSON.stringify(preview).replace(/</g, '\\u003c')
|
|
145
148
|
const html = template.replace('/*__QAWALK_RUN__*/null', () => json)
|
|
146
149
|
await writeJson(runPaths(run.dir).manifest, manifest)
|
package/src/lib/capture.mjs
CHANGED
|
@@ -55,6 +55,7 @@ import { criteriaHash } from './evaluate.mjs'
|
|
|
55
55
|
import { consoleEntriesFor } from './console.mjs'
|
|
56
56
|
import { redact, redactDeep } from './secrets.mjs'
|
|
57
57
|
import { readFields } from './stack.mjs'
|
|
58
|
+
import { checkLinks, checkHreflang, requester, walkCache, NETWORK_ASSERTS, isolatedRequest, cacheScope } from './linkcheck.mjs'
|
|
58
59
|
|
|
59
60
|
/** Probe arguments derived from the node's criteria. */
|
|
60
61
|
export function probeArgs (node, maxChars, design = false) {
|
|
@@ -101,6 +102,7 @@ export async function captureStep ({ page, node, locale, viewports, run, console
|
|
|
101
102
|
const paths = runPaths(run.dir)
|
|
102
103
|
const original = page.viewportSize()
|
|
103
104
|
const results = []
|
|
105
|
+
let netResults = null
|
|
104
106
|
for (const [viewport, [width, height]] of Object.entries(viewports)) {
|
|
105
107
|
const now = page.viewportSize()
|
|
106
108
|
if (!now || now.width !== width || now.height !== height) await page.setViewportSize({ width, height })
|
|
@@ -153,6 +155,12 @@ export async function captureStep ({ page, node, locale, viewports, run, console
|
|
|
153
155
|
const ownStatus = (e) => result.http_status >= 400 && /^Failed to load resource: the server responded with a status of \d+/.test(e.text) && e.src && sameDocument(e.src, url)
|
|
154
156
|
const errors = consoleEntries.filter((e) => e.type !== 'blocked' && sameDocument(e.url, url) && !e.text.includes('ERR_BLOCKED_BY_CLIENT') && !ownStatus(e)).map((e) => e.text)
|
|
155
157
|
const blocked = consoleEntries.filter((e) => e.type === 'blocked').map((e) => `${e.text}`)
|
|
158
|
+
// network checks (internal links, hreflang pairs) run once per page, outside the page, with a walk-wide cache
|
|
159
|
+
const net = (node.criteria || []).filter((c) => c.check.type === 'deterministic' && NETWORK_ASSERTS.has(c.check.assert))
|
|
160
|
+
if (net.length) {
|
|
161
|
+
netResults ||= await networkChecks(page, net, url, { run, locale })
|
|
162
|
+
Object.assign(result.checks, netResults)
|
|
163
|
+
}
|
|
156
164
|
for (const c of node.criteria || []) {
|
|
157
165
|
if (c.check.type === 'deterministic' && c.check.assert === 'no_console_errors') {
|
|
158
166
|
result.checks[c.id] = collected === null
|
|
@@ -214,6 +222,50 @@ export async function captureCrop ({ page, node, locale, selector, title, run, p
|
|
|
214
222
|
return entry
|
|
215
223
|
}
|
|
216
224
|
|
|
225
|
+
/**
|
|
226
|
+
* links_ok and hreflang_pairs for the current page: { [criterion id]: { ok, evidence } }. The requests go through an
|
|
227
|
+
* isolated request context (linkcheck.mjs), so the walker's cookies, language and session stay as they were.
|
|
228
|
+
* links_ok follows links only on a readonly environment or scenario, or when the check says `follow: true`.
|
|
229
|
+
*/
|
|
230
|
+
export async function networkChecks (page, criteria, url, { run = null, locale = '', factory = null } = {}) {
|
|
231
|
+
const out = {}
|
|
232
|
+
const readonly = !!(run?.meta?.environment?.readonly || run?.meta?.scenario?.readonly)
|
|
233
|
+
const follows = (c) => c.check.assert !== 'links_ok' || readonly || c.check.follow === true
|
|
234
|
+
const todo = criteria.filter((c) => {
|
|
235
|
+
if (follows(c)) return true
|
|
236
|
+
out[c.id] = { ok: null, evidence: 'links not followed: the environment and scenario are not readonly (set follow: true on the check to opt in)' }
|
|
237
|
+
return false
|
|
238
|
+
})
|
|
239
|
+
if (!todo.length) return out
|
|
240
|
+
let request
|
|
241
|
+
let get
|
|
242
|
+
try {
|
|
243
|
+
request = await isolatedRequest(page.context(), { factory })
|
|
244
|
+
const { cookies = [] } = await page.context().storageState().catch(() => ({}))
|
|
245
|
+
get = requester(request, walkCache, cacheScope({ environment: run?.meta?.environment?.name || '', locale, cookies }))
|
|
246
|
+
} catch (error) {
|
|
247
|
+
for (const c of todo) out[c.id] = { ok: null, evidence: `network checks unavailable: ${error.message}` }
|
|
248
|
+
await request?.dispose().catch(() => {})
|
|
249
|
+
return out
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
const doc = await page.evaluate((sels) => ({
|
|
253
|
+
lang: document.documentElement.getAttribute('lang') || '',
|
|
254
|
+
alternates: [...document.querySelectorAll("link[rel~='alternate'][hreflang][href]")].map((l) => ({ hreflang: l.getAttribute('hreflang').toLowerCase(), href: l.href })),
|
|
255
|
+
links: Object.fromEntries(sels.map((sel) => [sel, (() => { try { return [...document.querySelectorAll(sel)].map((a) => a.href).filter(Boolean) } catch { return [] } })()]))
|
|
256
|
+
}), [...new Set(todo.filter((c) => c.check.assert === 'links_ok').map((c) => c.check.selector || 'a[href]'))]).catch(() => null)
|
|
257
|
+
for (const c of todo) {
|
|
258
|
+
if (!doc) { out[c.id] = { ok: null, evidence: 'page could not be read for network checks' }; continue }
|
|
259
|
+
try {
|
|
260
|
+
out[c.id] = c.check.assert === 'links_ok'
|
|
261
|
+
? await checkLinks({ links: doc.links[c.check.selector || 'a[href]'] || [], pageUrl: url, get, max: c.check.max ?? undefined, exclude: c.check.pattern || null })
|
|
262
|
+
: await checkHreflang({ alternates: doc.alternates, pageUrl: url, lang: doc.lang, get, min: c.check.min ?? 2 })
|
|
263
|
+
} catch (error) { out[c.id] = { ok: false, evidence: `check error: ${error.message}` } }
|
|
264
|
+
}
|
|
265
|
+
} finally { await request.dispose().catch(() => {}) }
|
|
266
|
+
return out
|
|
267
|
+
}
|
|
268
|
+
|
|
217
269
|
function sameDocument (a, b) {
|
|
218
270
|
try {
|
|
219
271
|
const x = new URL(a); const y = new URL(b)
|
package/src/lib/config.mjs
CHANGED
|
@@ -9,9 +9,13 @@ export const CONFIG_FILE = 'kaloko.config.yml'
|
|
|
9
9
|
/** The file name before the rename to Kaloko; still found, so existing projects keep working. */
|
|
10
10
|
export const LEGACY_CONFIG_FILE = 'qawalk.config.yml'
|
|
11
11
|
|
|
12
|
-
/** Environment variables were QAWALK_
|
|
12
|
+
/** Environment variables were QAWALK_*: either name fills the other when that one is not set, so new .env files work
|
|
13
|
+
* with configs that still name QAWALK_TOKEN (share.token_env) and old .env files work with the new defaults. */
|
|
13
14
|
export function aliasLegacyEnv (vars = process.env) {
|
|
14
|
-
for (const [k, v] of Object.entries(vars))
|
|
15
|
+
for (const [k, v] of Object.entries(vars)) {
|
|
16
|
+
const m = k.match(/^(QAWALK|KALOKO)_(.+)$/)
|
|
17
|
+
if (m && vars[`${m[1] === 'QAWALK' ? 'KALOKO' : 'QAWALK'}_${m[2]}`] == null) vars[`${m[1] === 'QAWALK' ? 'KALOKO' : 'QAWALK'}_${m[2]}`] = v
|
|
18
|
+
}
|
|
15
19
|
}
|
|
16
20
|
aliasLegacyEnv()
|
|
17
21
|
|
|
@@ -113,5 +117,7 @@ export async function loadEnvFiles (files) {
|
|
|
113
117
|
if (!m || process.env[m[1]] !== undefined) continue
|
|
114
118
|
process.env[m[1]] = m[2].replace(/^(['"])(.*)\1$/, '$2')
|
|
115
119
|
}
|
|
120
|
+
// per file, so a project .env with a former name still wins over a user-level file with the new one
|
|
121
|
+
aliasLegacyEnv()
|
|
116
122
|
}
|
|
117
123
|
}
|