haltija 1.12.1 → 1.12.5
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/CHANGELOG.md +221 -0
- package/apps/desktop/artifacts.js +5 -5
- package/apps/desktop/isolation.js +5 -5
- package/apps/desktop/main.js +56 -5
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/renderer/isolation.js +5 -5
- package/apps/desktop/resources/component.js +10 -14
- package/bin/cli-commands.mjs +4 -4
- package/bin/cli-subcommand.mjs +45 -4
- package/bin/format-test.mjs +8 -0
- package/bin/hj.mjs +27 -0
- package/bin/mcp-setup.mjs +2 -2
- package/bin/private-state.mjs +29 -0
- package/bin/project-origins.mjs +3 -3
- package/bin/semver.mjs +3 -3
- package/bin/server-list.mjs +4 -4
- package/bin/tosijs-dev.mjs +39 -1
- package/bin/version.mjs +1 -1
- package/bin/window-state.mjs +5 -5
- package/dist/api-schema.d.ts +60 -60
- package/dist/client.js +2 -2
- package/dist/codemirror-raw.js +19 -19
- package/dist/codemirror.js +19 -19
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +10 -14
- package/dist/component.js +10 -14
- package/dist/drag.d.ts +43 -0
- package/dist/hj.js +56 -6
- package/dist/index.js +438 -127
- package/dist/naming-probe.d.ts +97 -0
- package/dist/private-state.d.ts +34 -0
- package/dist/server.js +441 -130
- package/dist/test-actions.d.ts +214 -0
- package/dist/test.js +10 -4
- package/dist/types.d.ts +41 -2
- package/dist/version.d.ts +1 -1
- package/docs/CI-INTEGRATION.md +29 -18
- package/llms.txt +19 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,226 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.12.5
|
|
4
|
+
|
|
5
|
+
**Installing this delivers 1.12.3 and 1.12.4 as well** — both were tagged but never published, so
|
|
6
|
+
npm went straight from 1.12.2 to here. Their entries below still describe what they contained.
|
|
7
|
+
|
|
8
|
+
### Embedding on an HTTPS dev server no longer fails silently ([#33](https://github.com/tonioloewald/haltija/issues/33))
|
|
9
|
+
|
|
10
|
+
An `https://` page cannot load an `http://` script: the browser blocks it as **mixed content** and
|
|
11
|
+
reports nothing that names the cause. You get no widget, `hj where` says `0 tabs`, and the natural
|
|
12
|
+
conclusion is that your own setup is wrong. The reporter worked it out from `--help` and source.
|
|
13
|
+
|
|
14
|
+
This is not an edge case — the tosijs-ui doc-system dev server is HTTPS by default (`bun run tls`),
|
|
15
|
+
so the default project setup hits it on the first attempt.
|
|
16
|
+
|
|
17
|
+
A static `src` cannot branch on the page's scheme, so the canonical embed snippet is now a
|
|
18
|
+
three-line loader that picks the matching transport, with the plain tag kept below as the equivalent
|
|
19
|
+
for HTTP pages. Serving an HTTPS page also needs `bunx haltija --server --both` and accepting the
|
|
20
|
+
self-signed cert once at `https://localhost:8701`.
|
|
21
|
+
|
|
22
|
+
Same root cause as [#32](https://github.com/tonioloewald/haltija/issues/32) from the other end:
|
|
23
|
+
there, a shared HTTP-only server silently denied another project's HTTPS pages; here, an HTTPS page
|
|
24
|
+
silently cannot reach an HTTP server. **Opening both transports by default would remove the class
|
|
25
|
+
rather than documenting it**, which is why that remains the top of the queue rather than something
|
|
26
|
+
more prose can fix.
|
|
27
|
+
|
|
28
|
+
## 1.12.4
|
|
29
|
+
|
|
30
|
+
Backlog work done while releases were paused. **Includes everything in 1.12.3**, which was tagged
|
|
31
|
+
but never published — installing 1.12.4 gets both.
|
|
32
|
+
|
|
33
|
+
### Three bugs found by closing a coverage hole
|
|
34
|
+
|
|
35
|
+
**Ten of seventeen step actions had no executable coverage in any lane** — including the
|
|
36
|
+
deprecated-alias branch and the `select-text` dispatch that shipped in 1.12.3. Nothing was red; the
|
|
37
|
+
lanes simply never ran those paths, and the failure mode there is a *silent success*. There is now a
|
|
38
|
+
blocking suite that asserts an **observable effect** per action (a checkbox actually checked, a
|
|
39
|
+
keydown that actually arrived), and it found:
|
|
40
|
+
|
|
41
|
+
- **`hj drag` does nothing to a native `<input type=range>` and reported success.** Measured: a 60px
|
|
42
|
+
drag leaves a native range at 0 while moving a custom div thumb 0 → 60px. Browsers drive native
|
|
43
|
+
controls from *trusted* input only; a custom implementation listens for `mousemove` on `document`,
|
|
44
|
+
which is why MUI sliders, resize handles and reorder lists are unaffected. `/drag` now returns a
|
|
45
|
+
warning naming the cause and the way round it.
|
|
46
|
+
- **`hj test validate` accepted an `assert` step with no `assertion` object** — the shape our own
|
|
47
|
+
`CLAUDE.md` documented. Such a step never checks anything: a guard that cannot fail.
|
|
48
|
+
- **`tests/haltija.test.ts` reported 11 passes for work it never did.** With no server it early-returned
|
|
49
|
+
from each test body, and bun records that as PASSED. Now 0 pass / 11 skip.
|
|
50
|
+
|
|
51
|
+
### The MCP server has been shipping 43 of 63 endpoints since January
|
|
52
|
+
|
|
53
|
+
`apps/mcp/build/endpoints.json` — what the committed MCP server imports at runtime — had drifted
|
|
54
|
+
seven months behind. Anyone on the MCP path had no `/map`, `/find`, `/wait`, `/key`, `/call`,
|
|
55
|
+
`/form`, `/fetch`, `/select`, `/recording`, `/test/suite`, or the `/network`, `/video` and `/dialog`
|
|
56
|
+
families. Twenty endpoints.
|
|
57
|
+
|
|
58
|
+
`docs-drift` could not catch it, and that is the lesson: the gate asserts a build leaves the tree
|
|
59
|
+
clean, and **nothing in the build wrote that file**. A drift gate only covers what the build
|
|
60
|
+
produces. Now it writes it, with a test asserting the runtime copy matches the generated one.
|
|
61
|
+
|
|
62
|
+
Also fixed: `hj --setup-mcp` pointed at `node_modules/tosijs-dev/…` in two of three lookups, a
|
|
63
|
+
package name that changed long ago. (`apps/mcp` is still not in the npm `files` list — that is a
|
|
64
|
+
packaging decision, deliberately left rather than guessed at.)
|
|
65
|
+
|
|
66
|
+
### `hj where` says which transports are open — and why one is not ([#32](https://github.com/tonioloewald/haltija/issues/32))
|
|
67
|
+
|
|
68
|
+
```
|
|
69
|
+
transports: http 8700 ✓ https 8701 ✗ (no certs in <path>)
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The channel is shared across projects, so **one project's transport choice is paid for by another**:
|
|
73
|
+
an instance that came up HTTP-only leaves every `https` page with nothing to import, because mixed
|
|
74
|
+
content blocks the fallback. The reporter's doc site had no haltija at all while `hj where` said
|
|
75
|
+
"haltija 1.12.2, 1 tab" and everything looked healthy. A half-open channel and a full one were
|
|
76
|
+
indistinguishable from outside; now the reason is named (`not requested`, `no certs at <path>`, or
|
|
77
|
+
`port N is held`). A `--private` instance reports transports but not the cross-project warning — it
|
|
78
|
+
is isolated by construction and denies nobody.
|
|
79
|
+
|
|
80
|
+
Opening both transports by default (the other half of that issue) needs a release to exercise and is
|
|
81
|
+
queued rather than rushed.
|
|
82
|
+
|
|
83
|
+
### Also
|
|
84
|
+
|
|
85
|
+
- Four stale copies folded in, including a **fourth** hand-maintained step list in `api-schema.ts`
|
|
86
|
+
that had drifted to 8 of 17 and propagated to `API.md` and the MCP definitions.
|
|
87
|
+
- `CLAUDE.md`'s test-JSON example used the flat `assert` shape the runner ignores.
|
|
88
|
+
|
|
89
|
+
## 1.12.3
|
|
90
|
+
|
|
91
|
+
Documentation-drift machinery, a measured rename, and the six blockers a nine-lens review raised
|
|
92
|
+
against all of it. **Known open:** [#26](https://github.com/tonioloewald/haltija/issues/26) (a tab
|
|
93
|
+
becoming permanently undrivable — observed, not reproducible on any version) and
|
|
94
|
+
[#16](https://github.com/tonioloewald/haltija/issues/16) (native tosiAgent bridge, on hold).
|
|
95
|
+
|
|
96
|
+
**Recorded suites now use `select-text`, which servers before 1.12.3 reject.** If you record on an
|
|
97
|
+
updated machine and run against a pinned older haltija, that step fails with `unknown step action`.
|
|
98
|
+
The paved CI path (`bunx haltija@latest`) is unaffected.
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
### `select` is now `select-text` — and `select` is being freed
|
|
102
|
+
|
|
103
|
+
Measured, not argued. Shown the bare step vocabulary with no descriptions, **3/3 agents reached for
|
|
104
|
+
`select` to choose an option from a dropdown, and 3/3 read it cold as "choose an `<option>`".
|
|
105
|
+
Nobody read it as text selection** — which is what it does. A name that unanimously means something
|
|
106
|
+
else to competent readers is not a documentation problem.
|
|
107
|
+
|
|
108
|
+
`select-text` says what it does. **`select` still works** as a deprecated alias, so existing suites
|
|
109
|
+
keep running; steps using it now carry a `warning` naming the replacement, rendered in the test
|
|
110
|
+
report rather than only present in `--json`.
|
|
111
|
+
|
|
112
|
+
The recorder also stopped emitting two illegal steps it had emitted since long before this release:
|
|
113
|
+
`set` (not a step action at all — recorded suites died with "Unsupported step action: set") and
|
|
114
|
+
`select` for **dropdowns**, which resolved to `select-text` and dispatched a text-selection event at
|
|
115
|
+
the `<select>`, passing while choosing nothing. Both now record an explicit `eval` that works, and a
|
|
116
|
+
test asserts every action the recorder can emit is legal and non-deprecated.
|
|
117
|
+
|
|
118
|
+
The point of the deprecation is the reuse: once the alias can be dropped, `select` is free to mean
|
|
119
|
+
what everyone already expects — pick an option from a `<select>` — which haltija cannot do at all
|
|
120
|
+
today. A test enforces that the two meanings can never overlap (`no alias may shadow a live
|
|
121
|
+
action`), because a word meaning two things depending on vintage is exactly the silent-wrong-action
|
|
122
|
+
trap the rename exists to remove.
|
|
123
|
+
|
|
124
|
+
**Correction, and it matters more than the rename.** An earlier draft of these notes acquitted
|
|
125
|
+
`check`, `verify` and `tabs-focus` as "measured and fine". That was wrong: the harness spawned each
|
|
126
|
+
agent **in this repo**, so every "first impression" had our own `CLAUDE.md` — which documents the
|
|
127
|
+
vocabulary under test — in context. Re-run from a neutral directory, `check` is cold-read as
|
|
128
|
+
*"asserts that some condition is true"* by **3/3** (the exact confusion it was cleared of), and
|
|
129
|
+
`verify` slips to 2/3. What survives is narrower and honest: with the full vocabulary in view agents
|
|
130
|
+
still pick `assert` for assertions and `check` for checkboxes, so `check` is safe **in context** and
|
|
131
|
+
misleading **in isolation**. `tabs-focus` holds at 3/3.
|
|
132
|
+
|
|
133
|
+
The `select` verdict is unaffected — contamination ran in its favour and it still lost 3/3, and the
|
|
134
|
+
clean re-run agrees. The probe now runs each sample in an empty temp directory, and its vocabularies
|
|
135
|
+
are derived from the registries rather than hand-copied (the old copy still listed `select`, stale
|
|
136
|
+
as of the commit it motivated). Harness: `tools/naming-probe.mjs`, run with bun.
|
|
137
|
+
|
|
138
|
+
## 1.12.2
|
|
139
|
+
|
|
140
|
+
Two fixes, both from an agent driving real apps. Each is a case where haltija reported success — or
|
|
141
|
+
green health — while quietly producing something you couldn't trust, which is the same thread the
|
|
142
|
+
1.12.x line has been pulling on throughout.
|
|
143
|
+
|
|
144
|
+
**[#26](https://github.com/tonioloewald/haltija/issues/26) remains open and is NOT a 1.12.0
|
|
145
|
+
regression.** The reporter's own control run settled it: the known-bad version survived the original
|
|
146
|
+
failing surface through seven hard navigations plus an HMR rebuild. A tab becoming permanently
|
|
147
|
+
undrivable was really observed, so the issue stays open as a standing record — but neither of us can
|
|
148
|
+
currently reproduce it on any version, and nothing here claims to fix it.
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
### `--private` instances get their own Electron profile — [#31](https://github.com/tonioloewald/haltija/issues/31)
|
|
152
|
+
|
|
153
|
+
Private mode isolated ports, the registry, retirement and teardown — but **not the Electron
|
|
154
|
+
profile**, so every instance ran with the same `--user-data-dir`. Chromium's single-instance locking
|
|
155
|
+
means the second one to launch can't take the profile lock and falls back to caches it cannot
|
|
156
|
+
persist, losing the HTTP cache and the V8 code cache. A large app bundle is then fully re-parsed on
|
|
157
|
+
every navigation: **roughly 10x slower page boots**.
|
|
158
|
+
|
|
159
|
+
The damage isn't the slowness, it's that it **lies in the one workflow private mode exists for**.
|
|
160
|
+
Comparing two versions side by side, the failure followed **launch order, not version** — 22.1s vs
|
|
161
|
+
2.04s, the same version passing or failing depending only on which started first, and each fine
|
|
162
|
+
alone. The reporter nearly wrote up a version regression that did not exist. Every health signal
|
|
163
|
+
read green throughout, including `hj doctor` and a measured 120fps rAF cadence, because nothing was
|
|
164
|
+
broken — it was just slow.
|
|
165
|
+
|
|
166
|
+
Each private instance now gets `<tmpdir>/haltija-private-<pid>` as its `userData` and `sessionData`,
|
|
167
|
+
set before `app.whenReady()` and before anything reads `preferences.json`.
|
|
168
|
+
|
|
169
|
+
Two related leaks closed at the same time, both cases of "private" having meant *private ports*
|
|
170
|
+
rather than *touches nothing of yours*:
|
|
171
|
+
|
|
172
|
+
- **A private run no longer writes `~/.haltija/last-quit`.** That marker tells `hj`'s auto-launch the
|
|
173
|
+
user deliberately quit, so an automated run ending was suppressing auto-launch for the interactive
|
|
174
|
+
app a developer was using.
|
|
175
|
+
- **Stale private scratch is swept** at the start of the next private run — profiles and the
|
|
176
|
+
port-files the launcher writes (175 had accumulated on one machine). Swept at startup rather than
|
|
177
|
+
on exit because Chromium flushes its caches *after* `will-quit`, so deleting the profile there
|
|
178
|
+
just gets it recreated. The sweep keys on whether the owning pid is still alive and **never
|
|
179
|
+
touches a live peer** — `EPERM` from `kill(pid, 0)` counts as alive, since the process exists and
|
|
180
|
+
merely belongs to someone else. That decision is a tested function (`src/private-state.ts`), not a
|
|
181
|
+
loop in the launcher, because deleting a running instance's profile would be far worse than the
|
|
182
|
+
litter it tidies.
|
|
183
|
+
|
|
184
|
+
### The test-suite runner gains `drag`, and a `wait` can no longer pass without waiting — [#30](https://github.com/tonioloewald/haltija/issues/30)
|
|
185
|
+
|
|
186
|
+
**`drag` is now a step action.** `hj drag` and `POST /drag` had shipped for releases; the runner's
|
|
187
|
+
dispatcher simply had no case, so a perfectly reasonable suite failed with `Unsupported step action:
|
|
188
|
+
drag`. Sliders, resize handles and drag-reorder lists are exactly the interactions you cannot cover
|
|
189
|
+
another way — a synthetic keydown on a slider thumb is not a faithful substitute.
|
|
190
|
+
|
|
191
|
+
The routine now lives in `src/drag.ts` and both `/drag` and the runner call it. Dragging is not one
|
|
192
|
+
message to the widget (scroll into view → measure → mouseenter/over/move → mousedown → N
|
|
193
|
+
interpolated mousemoves → mouseup), and a second copy in the runner's switch would have been the
|
|
194
|
+
fifth instance this cycle of one idea with two implementations.
|
|
195
|
+
|
|
196
|
+
**A `wait` step with nothing to wait for was reported as PASSING.** `{"action": "wait",
|
|
197
|
+
"forElement": "tbody tr", "timeout": 10000}` fell out of the runner's chain to `break`, and since a
|
|
198
|
+
step passes by default it looked green — so a guard that had never waited for anything let every
|
|
199
|
+
assertion after it race the page. Two fixes: `forElement` is now accepted as an alias of `selector`
|
|
200
|
+
(the name `/wait` uses, and the name **our own SKILL.md example used**, which means the documented
|
|
201
|
+
example never waited), and a `wait` carrying none of `duration`/`ms`/`selector`/`forElement`/
|
|
202
|
+
`forWindow`/`url` is now an **error**.
|
|
203
|
+
|
|
204
|
+
This is the same defect as the CLI's `hj wait --hidden`, fixed in 1.12.0 — that fix landed in the
|
|
205
|
+
CLI and never reached the runner.
|
|
206
|
+
|
|
207
|
+
**`hj test validate` now rejects illegal steps before the suite runs**, with a "did you mean":
|
|
208
|
+
|
|
209
|
+
```
|
|
210
|
+
step 0: unknown step action "drg" — did you mean "drag"?. Legal actions: navigate, click, …
|
|
211
|
+
step 1: wait step has nothing to wait for — give it `duration` (ms), `selector` (or `forElement`) …
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
Validation previously checked only that selectors resolved, so `{"action": "drag"}` validated clean
|
|
215
|
+
and then died in CI.
|
|
216
|
+
|
|
217
|
+
**And the list is published and enforced.** There was nowhere to look up the legal actions: `hj api`
|
|
218
|
+
documents the HTTP endpoints, which reads as though the same verbs work as steps. The canonical list
|
|
219
|
+
is now `TEST_STEP_ACTIONS` in `src/test-actions.ts`, printed in `SKILL.md`, `CLAUDE.md` and
|
|
220
|
+
`docs/CI-INTEGRATION.md` — and a test asserts it matches the runner's `switch (step.action)` in
|
|
221
|
+
**both** directions. Writing that guard immediately found `screenshot` documented in `SKILL.md` as a
|
|
222
|
+
step action when the runner has never had one.
|
|
223
|
+
|
|
3
224
|
## 1.12.1
|
|
4
225
|
|
|
5
226
|
A patch of fixes reported by an agent driving a real React + web-components admin app against
|
|
@@ -40,12 +40,12 @@ var __export = (target, all) => {
|
|
|
40
40
|
// src/artifacts.ts
|
|
41
41
|
var exports_artifacts = {};
|
|
42
42
|
__export(exports_artifacts, {
|
|
43
|
-
|
|
44
|
-
pruneKind: () => pruneKind,
|
|
45
|
-
pruneArtifacts: () => pruneArtifacts,
|
|
46
|
-
parseDataUrl: () => parseDataUrl,
|
|
43
|
+
RETENTION: () => RETENTION,
|
|
47
44
|
artifactDir: () => artifactDir,
|
|
48
|
-
|
|
45
|
+
parseDataUrl: () => parseDataUrl,
|
|
46
|
+
pruneArtifacts: () => pruneArtifacts,
|
|
47
|
+
pruneKind: () => pruneKind,
|
|
48
|
+
saveDataUrl: () => saveDataUrl
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(exports_artifacts);
|
|
51
51
|
var import_promises = require("fs/promises");
|
|
@@ -40,12 +40,12 @@ var __export = (target, all) => {
|
|
|
40
40
|
// src/desktop-isolation.ts
|
|
41
41
|
var exports_desktop_isolation = {};
|
|
42
42
|
__export(exports_desktop_isolation, {
|
|
43
|
-
|
|
44
|
-
resolvePublicUrl: () => resolvePublicUrl,
|
|
45
|
-
resolveInternalPort: () => resolveInternalPort,
|
|
46
|
-
isPrivateInstance: () => isPrivateInstance,
|
|
43
|
+
SHARED_INTERNAL_PORT: () => SHARED_INTERNAL_PORT,
|
|
47
44
|
SHARED_PUBLIC_URL: () => SHARED_PUBLIC_URL,
|
|
48
|
-
|
|
45
|
+
isPrivateInstance: () => isPrivateInstance,
|
|
46
|
+
resolveInternalPort: () => resolveInternalPort,
|
|
47
|
+
resolvePublicUrl: () => resolvePublicUrl,
|
|
48
|
+
resolveServerUrl: () => resolveServerUrl
|
|
49
49
|
});
|
|
50
50
|
module.exports = __toCommonJS(exports_desktop_isolation);
|
|
51
51
|
var SHARED_PUBLIC_URL = "http://localhost:8700";
|
package/apps/desktop/main.js
CHANGED
|
@@ -44,6 +44,44 @@ const IS_PRIVATE = process.env.HALTIJA_PRIVATE === '1'
|
|
|
44
44
|
// consumer (e.g. a dev-server test lane) can drive this instance.
|
|
45
45
|
const CALLER_PORT_FILE = IS_PRIVATE ? (process.env.HALTIJA_PORT_FILE || null) : null
|
|
46
46
|
|
|
47
|
+
// A PRIVATE INSTANCE GETS ITS OWN ELECTRON PROFILE.
|
|
48
|
+
//
|
|
49
|
+
// Private mode isolated the ports, the registry, retirement and teardown — but not the profile, so
|
|
50
|
+
// every instance ran with the same `--user-data-dir`. Chromium single-instance locking means the
|
|
51
|
+
// second one to launch cannot take the profile lock and falls back to caches it cannot persist,
|
|
52
|
+
// losing the HTTP cache and the V8 code cache. A large app bundle is then fully re-parsed on every
|
|
53
|
+
// navigation: measured at roughly 10x slower page boots (issue #31).
|
|
54
|
+
//
|
|
55
|
+
// The damage is not just slowness, it is a LIE in the one workflow private mode exists for. Running
|
|
56
|
+
// two versions side by side, the failure followed LAUNCH ORDER rather than version — 22.1s vs 2.04s,
|
|
57
|
+
// and the same version passed or failed depending only on which started first. The reporter nearly
|
|
58
|
+
// wrote up a version regression that did not exist. Every health signal read green throughout,
|
|
59
|
+
// including `hj doctor` and a 120fps rAF cadence, because nothing was broken; it was just slow.
|
|
60
|
+
//
|
|
61
|
+
// Keyed on pid, not port: the private ports are ephemeral and not known yet at this point, and pid
|
|
62
|
+
// is unique across concurrent runs by construction. Set BEFORE `app.whenReady()` and before
|
|
63
|
+
// anything reads `app.getPath('userData')` (preferences.json does), or the isolation is partial —
|
|
64
|
+
// which is how this bug existed at all.
|
|
65
|
+
let PRIVATE_USER_DATA = null
|
|
66
|
+
if (IS_PRIVATE) {
|
|
67
|
+
PRIVATE_USER_DATA = path.join(os.tmpdir(), `haltija-private-${process.pid}`)
|
|
68
|
+
try {
|
|
69
|
+
fs.mkdirSync(PRIVATE_USER_DATA, { recursive: true })
|
|
70
|
+
app.setPath('userData', PRIVATE_USER_DATA)
|
|
71
|
+
// Also keep the disk cache with it, so nothing is left in the shared profile.
|
|
72
|
+
app.setPath('sessionData', PRIVATE_USER_DATA)
|
|
73
|
+
} catch (err) {
|
|
74
|
+
// Falling back to the shared profile is slow and pollutes it, but it still WORKS — so say so
|
|
75
|
+
// loudly rather than refusing to start a run over a temp-directory failure.
|
|
76
|
+
console.error(
|
|
77
|
+
`[Haltija Desktop] Could not create a private profile at ${PRIVATE_USER_DATA}: ${err.message}. ` +
|
|
78
|
+
`Falling back to the shared profile — concurrent private instances will be slow and are ` +
|
|
79
|
+
`NOT safe to compare against each other (see issue #31).`,
|
|
80
|
+
)
|
|
81
|
+
PRIVATE_USER_DATA = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
47
85
|
// Haltija server config
|
|
48
86
|
let HALTIJA_PORT = IS_PRIVATE ? 0 : parseInt(process.env.HALTIJA_PORT || '8700')
|
|
49
87
|
let HALTIJA_SERVER = `http://localhost:${HALTIJA_PORT}`
|
|
@@ -1694,11 +1732,24 @@ if (!gotTheLock) {
|
|
|
1694
1732
|
}
|
|
1695
1733
|
// Drop a marker so hj's auto-launch knows the user explicitly quit.
|
|
1696
1734
|
// Cleared next time the user manually starts Haltija.
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1735
|
+
//
|
|
1736
|
+
// NOT in private mode: `~/.haltija/last-quit` is SHARED state, and an automated run ending is
|
|
1737
|
+
// not the user quitting anything. Writing it suppressed auto-launch for the interactive app the
|
|
1738
|
+
// developer is actually using — the same class as the profile sharing above (#31), where
|
|
1739
|
+
// "private" turned out to mean "private ports" rather than "touches nothing of yours".
|
|
1740
|
+
if (!IS_PRIVATE) {
|
|
1741
|
+
try {
|
|
1742
|
+
const dir = path.join(os.homedir(), '.haltija')
|
|
1743
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
1744
|
+
fs.writeFileSync(path.join(dir, 'last-quit'), String(Date.now()))
|
|
1745
|
+
} catch {}
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
// NOTE: the private profile is deliberately NOT deleted here. Chromium flushes its caches
|
|
1749
|
+
// AFTER 'will-quit', so removing the directory at this point just gets it recreated — measured:
|
|
1750
|
+
// both dirs survived a clean shutdown. Stale profiles are swept at the START of the next
|
|
1751
|
+
// private run instead (see sweepStalePrivateState in bin/tosijs-dev.mjs), keyed on whether the
|
|
1752
|
+
// owning pid is still alive, which is immune to shutdown ordering.
|
|
1702
1753
|
})
|
|
1703
1754
|
|
|
1704
1755
|
// Handle certificate errors (for self-signed certs in dev)
|
|
@@ -26,10 +26,10 @@ function resolveServerUrl(inputs) {
|
|
|
26
26
|
return injected || persisted || SHARED_PUBLIC_URL;
|
|
27
27
|
}
|
|
28
28
|
export {
|
|
29
|
-
|
|
30
|
-
resolvePublicUrl,
|
|
31
|
-
resolveInternalPort,
|
|
32
|
-
isPrivateInstance,
|
|
29
|
+
SHARED_INTERNAL_PORT,
|
|
33
30
|
SHARED_PUBLIC_URL,
|
|
34
|
-
|
|
31
|
+
isPrivateInstance,
|
|
32
|
+
resolveInternalPort,
|
|
33
|
+
resolvePublicUrl,
|
|
34
|
+
resolveServerUrl
|
|
35
35
|
};
|
|
@@ -40,13 +40,13 @@
|
|
|
40
40
|
// src/component.ts
|
|
41
41
|
var exports_component = {};
|
|
42
42
|
__export(exports_component, {
|
|
43
|
-
|
|
43
|
+
DevChannel: () => DevChannel,
|
|
44
44
|
VERSION: () => VERSION2,
|
|
45
|
-
|
|
45
|
+
inject: () => inject
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.12.
|
|
49
|
+
var VERSION = "1.12.5";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -4095,25 +4095,21 @@
|
|
|
4095
4095
|
let inputDesc = "";
|
|
4096
4096
|
if (isCleared) {
|
|
4097
4097
|
inputDesc = `Clear ${inputLabel}`;
|
|
4098
|
-
} else if (inputType === "range") {
|
|
4099
|
-
inputAction = "
|
|
4100
|
-
inputDesc = `Set ${inputLabel} to ${inputValue}`;
|
|
4101
|
-
} else if (inputType === "select-one" || inputType === "select-multiple") {
|
|
4102
|
-
inputAction = "select";
|
|
4103
|
-
inputDesc = `Select "${inputValue}" in ${inputLabel}`;
|
|
4098
|
+
} else if (inputType === "range" || inputType === "select-one" || inputType === "select-multiple" || inputType === "date" || inputType === "time" || inputType === "color") {
|
|
4099
|
+
inputAction = "eval";
|
|
4100
|
+
inputDesc = `Set ${inputLabel} to "${inputValue}"`;
|
|
4104
4101
|
} else if (inputType === "checkbox" || inputType === "radio") {
|
|
4105
4102
|
inputAction = "check";
|
|
4106
4103
|
inputDesc = `Check ${inputLabel}`;
|
|
4107
|
-
} else if (inputType === "date" || inputType === "time" || inputType === "color") {
|
|
4108
|
-
inputAction = "set";
|
|
4109
|
-
inputDesc = `Set ${inputLabel} to ${inputValue}`;
|
|
4110
4104
|
} else {
|
|
4111
4105
|
inputDesc = `Type "${inputValue}" in ${inputLabel}`;
|
|
4112
4106
|
}
|
|
4113
4107
|
steps.push(withFallback({
|
|
4114
4108
|
action: inputAction,
|
|
4115
4109
|
selector,
|
|
4116
|
-
...inputAction === "
|
|
4110
|
+
...inputAction === "eval" ? {
|
|
4111
|
+
code: `const el = document.querySelector(${JSON.stringify(selector)});` + ` el.value = ${JSON.stringify(inputValue)};` + ` el.dispatchEvent(new Event('input', { bubbles: true }));` + ` el.dispatchEvent(new Event('change', { bubbles: true }));`
|
|
4112
|
+
} : inputAction === "type" ? { text: inputValue } : { value: inputValue },
|
|
4117
4113
|
description: inputDesc,
|
|
4118
4114
|
...delay && delay > 50 ? { delay } : {}
|
|
4119
4115
|
}, event.target));
|
|
@@ -4146,7 +4142,7 @@
|
|
|
4146
4142
|
case "interaction:select":
|
|
4147
4143
|
const selectedText = this.cleanDescription(event.payload?.text || "", 50);
|
|
4148
4144
|
steps.push({
|
|
4149
|
-
action: "select",
|
|
4145
|
+
action: "select-text",
|
|
4150
4146
|
selector,
|
|
4151
4147
|
text: event.payload?.text || "",
|
|
4152
4148
|
description: `Select text "${selectedText}"`,
|
package/bin/cli-commands.mjs
CHANGED
|
@@ -120,9 +120,9 @@ function cliNameForEndpoint(path) {
|
|
|
120
120
|
return isKnownCommand(name) ? name : null;
|
|
121
121
|
}
|
|
122
122
|
export {
|
|
123
|
-
|
|
124
|
-
cliNameForEndpoint,
|
|
125
|
-
ROUTED_COMMANDS,
|
|
123
|
+
LOCAL_COMMANDS,
|
|
126
124
|
LOCAL_COMMAND_HELP,
|
|
127
|
-
|
|
125
|
+
ROUTED_COMMANDS,
|
|
126
|
+
cliNameForEndpoint,
|
|
127
|
+
isKnownCommand
|
|
128
128
|
};
|
package/bin/cli-subcommand.mjs
CHANGED
|
@@ -1065,6 +1065,40 @@ function exitOnTestFailure(json, subcommand) {
|
|
|
1065
1065
|
}
|
|
1066
1066
|
}
|
|
1067
1067
|
|
|
1068
|
+
/**
|
|
1069
|
+
* One endpoint's section out of API.md, or the whole document when no name is given.
|
|
1070
|
+
*
|
|
1071
|
+
* Matches the `### \`METHOD /path\`` headings the generator emits, on the path's last segment, so
|
|
1072
|
+
* `hj api screenshot` and `hj api map` both work. An unknown name returns the full document with a
|
|
1073
|
+
* note on stderr rather than nothing — a lookup that silently prints emptiness is worse than one
|
|
1074
|
+
* that over-delivers, and the caller has already paid for the fetch.
|
|
1075
|
+
*/
|
|
1076
|
+
export function filterApiSection(markdown, args) {
|
|
1077
|
+
const wanted = (args || []).find((a) => !a.startsWith('-'))
|
|
1078
|
+
if (!wanted) return markdown
|
|
1079
|
+
const lines = markdown.split('\n')
|
|
1080
|
+
const needle = wanted.replace(/^\/+/, '').toLowerCase()
|
|
1081
|
+
let start = -1
|
|
1082
|
+
for (let i = 0; i < lines.length; i++) {
|
|
1083
|
+
const m = /^### `[A-Z]+ (\/\S*)`/.exec(lines[i])
|
|
1084
|
+
if (!m) continue
|
|
1085
|
+
const seg = m[1].replace(/^\//, '').toLowerCase()
|
|
1086
|
+
if (seg === needle || seg.split('/').pop() === needle) { start = i; break }
|
|
1087
|
+
}
|
|
1088
|
+
if (start === -1) {
|
|
1089
|
+
process.stderr.write(
|
|
1090
|
+
dim(`[hj] no API section named "${wanted}" — printing the full reference. ` +
|
|
1091
|
+
`Section headings look like \`### \`POST /screenshot\`\`.`) + '\n',
|
|
1092
|
+
)
|
|
1093
|
+
return markdown
|
|
1094
|
+
}
|
|
1095
|
+
let end = lines.length
|
|
1096
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
1097
|
+
if (/^#{2,3} /.test(lines[i])) { end = i; break }
|
|
1098
|
+
}
|
|
1099
|
+
return lines.slice(start, end).join('\n').trimEnd()
|
|
1100
|
+
}
|
|
1101
|
+
|
|
1068
1102
|
export async function runSubcommand(subcommand, subArgs, port = '8700', options = {}) {
|
|
1069
1103
|
const baseUrl = `http://localhost:${port}`
|
|
1070
1104
|
const jsonOutput = subArgs.includes('--json')
|
|
@@ -1184,7 +1218,7 @@ export async function runSubcommand(subcommand, subArgs, port = '8700', options
|
|
|
1184
1218
|
if (isGet) {
|
|
1185
1219
|
const url = new URL(path, baseUrl)
|
|
1186
1220
|
url.searchParams.set('window', targetWindowId)
|
|
1187
|
-
return doRequest(url.toString(), 'GET', undefined, { subcommand, jsonOutput })
|
|
1221
|
+
return doRequest(url.toString(), 'GET', undefined, { subcommand, jsonOutput, args: filteredArgs })
|
|
1188
1222
|
} else {
|
|
1189
1223
|
if (!body) body = {}
|
|
1190
1224
|
body.window = targetWindowId
|
|
@@ -1192,11 +1226,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700', options
|
|
|
1192
1226
|
}
|
|
1193
1227
|
|
|
1194
1228
|
const url = `${baseUrl}${path}`
|
|
1195
|
-
return doRequest(url, isGet ? 'GET' : 'POST', body, { subcommand, jsonOutput })
|
|
1229
|
+
return doRequest(url, isGet ? 'GET' : 'POST', body, { subcommand, jsonOutput, args: filteredArgs })
|
|
1196
1230
|
}
|
|
1197
1231
|
|
|
1198
1232
|
async function doRequest(url, method, body, context = {}) {
|
|
1199
|
-
const { subcommand, jsonOutput } = context
|
|
1233
|
+
const { subcommand, jsonOutput, args: cmdArgs } = context
|
|
1200
1234
|
try {
|
|
1201
1235
|
const headers = {}
|
|
1202
1236
|
if (process.env.HALTIJA_TOKEN) headers['X-Haltija-Token'] = process.env.HALTIJA_TOKEN
|
|
@@ -1369,7 +1403,14 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
1369
1403
|
}
|
|
1370
1404
|
} else {
|
|
1371
1405
|
const text = await resp.text()
|
|
1372
|
-
|
|
1406
|
+
// `hj api <endpoint>` is a LOOKUP, not a dump.
|
|
1407
|
+
//
|
|
1408
|
+
// SKILL.md defers detail to `hj api screenshot` / `hj api map` to keep the always-loaded
|
|
1409
|
+
// prompt small. That only pays off if the pointer works: positionals were dropped for GET
|
|
1410
|
+
// subcommands, so every form returned the whole 64KB file — ~3KB saved from the prompt and
|
|
1411
|
+
// ~16k tokens spent the first time anyone followed the advice. Filtering client-side needs no
|
|
1412
|
+
// server change and no new endpoint.
|
|
1413
|
+
console.log(subcommand === 'api' ? filterApiSection(text, cmdArgs) : text)
|
|
1373
1414
|
}
|
|
1374
1415
|
|
|
1375
1416
|
// Show hint for this command (if available and successful).
|
package/bin/format-test.mjs
CHANGED
|
@@ -46,6 +46,14 @@ export function formatTestResult(result) {
|
|
|
46
46
|
|
|
47
47
|
lines.push(` ${step.index + 1} ${[stepStatus, desc, dur, err].filter(Boolean).join(' ')}`)
|
|
48
48
|
|
|
49
|
+
// A step can PASS and still have something to say — currently a deprecated action spelling.
|
|
50
|
+
// Every loop here filtered on failure, so the notice reached nobody: a suite using an old
|
|
51
|
+
// name ran clean and its author found out when the alias was deleted. The whole point of
|
|
52
|
+
// deprecating rather than breaking is that the warning arrives while there is time to act.
|
|
53
|
+
if (step.warning) {
|
|
54
|
+
lines.push(` ! ${step.warning}`)
|
|
55
|
+
}
|
|
56
|
+
|
|
49
57
|
// Failure context detail
|
|
50
58
|
if (!step.passed && step.context) {
|
|
51
59
|
const detail = formatFailureContext(step.context)
|
package/bin/hj.mjs
CHANGED
|
@@ -196,6 +196,33 @@ async function runWhere(port, portSource, jsonOutput) {
|
|
|
196
196
|
)
|
|
197
197
|
return
|
|
198
198
|
}
|
|
199
|
+
// TRANSPORTS, present and absent (issue #32). The channel is shared, so an instance that came up
|
|
200
|
+
// HTTP-only silently denies HTTPS pages in OTHER projects — and nothing said so. A half-open
|
|
201
|
+
// channel used to look identical to a full one from here.
|
|
202
|
+
const t = serverInfo.transports
|
|
203
|
+
if (t) {
|
|
204
|
+
const parts = []
|
|
205
|
+
if (t.http) parts.push(t.http.listening ? green(`http ${t.http.port} ✓`) : dim(`http ${t.http.port} ✗`))
|
|
206
|
+
if (t.https) {
|
|
207
|
+
// No port when there is nothing listening and none was assigned — `https 0` is noise.
|
|
208
|
+
const label = t.https.port ? `https ${t.https.port}` : 'https'
|
|
209
|
+
parts.push(
|
|
210
|
+
t.https.listening
|
|
211
|
+
? green(`${label} ✓`)
|
|
212
|
+
: `${yellow(`${label} ✗`)} ${dim(`(${t.https.reason})`)}`,
|
|
213
|
+
)
|
|
214
|
+
}
|
|
215
|
+
console.log(`${bold('transports:')} ${parts.join(' ')}`)
|
|
216
|
+
// Only for a SHARED channel. A private instance denies nobody else — saying otherwise would be
|
|
217
|
+
// the alarming answer rather than the true one.
|
|
218
|
+
if (t.https && !t.https.listening && !serverInfo.isPrivate) {
|
|
219
|
+
console.log(
|
|
220
|
+
dim(' an HTTPS page cannot import an HTTP channel (mixed content), so any page served over ' +
|
|
221
|
+
'https has no haltija here — including pages belonging to other projects sharing this channel.'),
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
199
226
|
const desc = [
|
|
200
227
|
`haltija ${serverInfo.serverVersion}`,
|
|
201
228
|
instanceName ? `name=${instanceName}` : null,
|
package/bin/mcp-setup.mjs
CHANGED
|
@@ -62,7 +62,7 @@ function findMcpServerPath() {
|
|
|
62
62
|
try {
|
|
63
63
|
const result = spawnSync('npm', ['root', '-g'], { encoding: 'utf8' })
|
|
64
64
|
if (result.status === 0) {
|
|
65
|
-
const globalPath = join(result.stdout.trim(), '
|
|
65
|
+
const globalPath = join(result.stdout.trim(), 'haltija/apps/mcp/build/index.js')
|
|
66
66
|
if (existsSync(globalPath)) {
|
|
67
67
|
return globalPath
|
|
68
68
|
}
|
|
@@ -70,7 +70,7 @@ function findMcpServerPath() {
|
|
|
70
70
|
} catch {}
|
|
71
71
|
|
|
72
72
|
// Check node_modules in current directory
|
|
73
|
-
const nodeModulesPath = join(process.cwd(), 'node_modules/
|
|
73
|
+
const nodeModulesPath = join(process.cwd(), 'node_modules/haltija/apps/mcp/build/index.js')
|
|
74
74
|
if (existsSync(nodeModulesPath)) {
|
|
75
75
|
return nodeModulesPath
|
|
76
76
|
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/** ⚠️ AUTO-GENERATED FROM src/private-state.ts — DO NOT EDIT. Run: bun run build */
|
|
2
|
+
// src/private-state.ts
|
|
3
|
+
function stalePrivateEntries(names, deps) {
|
|
4
|
+
const out = [];
|
|
5
|
+
for (const name of names) {
|
|
6
|
+
const m = /^haltija-private-(\d+)(\.json)?$/.exec(name);
|
|
7
|
+
if (!m)
|
|
8
|
+
continue;
|
|
9
|
+
const pid = parseInt(m[1], 10);
|
|
10
|
+
if (!pid || pid === deps.selfPid)
|
|
11
|
+
continue;
|
|
12
|
+
if (deps.isAlive(pid))
|
|
13
|
+
continue;
|
|
14
|
+
out.push(name);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
}
|
|
18
|
+
function pidIsAlive(pid) {
|
|
19
|
+
try {
|
|
20
|
+
process.kill(pid, 0);
|
|
21
|
+
return true;
|
|
22
|
+
} catch (err) {
|
|
23
|
+
return err?.code === "EPERM";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
export {
|
|
27
|
+
pidIsAlive,
|
|
28
|
+
stalePrivateEntries
|
|
29
|
+
};
|
package/bin/project-origins.mjs
CHANGED
package/bin/semver.mjs
CHANGED