haltija 1.6.0 → 1.6.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/CHANGELOG.md +35 -0
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/resources/component.js +1 -1
- package/bin/cli-subcommand.mjs +13 -1
- package/bin/hj.mjs +135 -0
- package/bin/version.mjs +1 -1
- package/dist/api-handlers.d.ts +2 -0
- package/dist/component.d.ts +1 -1
- package/dist/component.esm.js +1 -1
- package/dist/component.js +1 -1
- package/dist/hj.js +102 -3
- package/dist/index.js +51 -14
- package/dist/server.js +51 -14
- package/dist/types.d.ts +6 -0
- package/dist/version.d.ts +1 -1
- package/docs/CI-INTEGRATION.md +32 -15
- package/llms.txt +5 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,40 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.6.1
|
|
4
|
+
|
|
5
|
+
Makes haltija's detection reachable by automation ([#8](https://github.com/tonioloewald/haltija/issues/8),
|
|
6
|
+
[#11](https://github.com/tonioloewald/haltija/issues/11)).
|
|
7
|
+
|
|
8
|
+
haltija already *detects* the situations that wreck a test lane — the wrong project's browser, a
|
|
9
|
+
hidden tab returning stale results, a server with nothing to drive. It only ever **warned on
|
|
10
|
+
stderr**, so scripts consumed plausible-but-wrong results and failed much later, pointing at the
|
|
11
|
+
caller's own code.
|
|
12
|
+
|
|
13
|
+
### New: `ready` — "server is up" is not "server is drivable"
|
|
14
|
+
|
|
15
|
+
`/status` and `/windows` now return **`ready`**: true when at least one top-level tab is connected.
|
|
16
|
+
A server running with *zero* windows answers `/status` 200, so an adopter's reuse probe skipped
|
|
17
|
+
starting its own browser and then had nothing to navigate. Gate a lane on `ready`, not on the 200.
|
|
18
|
+
|
|
19
|
+
### New: `hj doctor`
|
|
20
|
+
|
|
21
|
+
One-command preflight that **exits non-zero**: server reachable → a tab is connected → the target
|
|
22
|
+
isn't ambiguous → tabs aren't all hidden → versions aligned. `--json` for machine-readable output.
|
|
23
|
+
Use it as the wait-loop condition in CI.
|
|
24
|
+
|
|
25
|
+
### New: `hj --strict` / `HALTIJA_STRICT=1`
|
|
26
|
+
|
|
27
|
+
Turns the advisory warnings into **non-zero exits**, and refuses to print a suspect result to
|
|
28
|
+
stdout at all — a script must not consume a value that may be wrong. A warning is the right default
|
|
29
|
+
for a human at a prompt and the wrong one for a lane.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Warning de-duplication silently defeated strict mode.** The server withheld a repeated warning
|
|
34
|
+
entirely, so the first command in a lane failed and every later one within the cooldown passed.
|
|
35
|
+
The server now always reports the condition and marks repeats (`warningRepeated`); de-dup is a
|
|
36
|
+
presentation concern, so `hj` stays quiet on a repeat while `--strict` fails on any warning.
|
|
37
|
+
|
|
3
38
|
## 1.6.0
|
|
4
39
|
|
|
5
40
|
Consolidating release: rolls up everything from 1.5.2–1.5.7 (the last npm-published version was
|
package/bin/cli-subcommand.mjs
CHANGED
|
@@ -942,7 +942,19 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
942
942
|
// content never mounted, so an empty selector means "not mounted", not "broken" (issue #3).
|
|
943
943
|
// Print it on stderr so it can't be mistaken for output, and so --json stdout stays clean.
|
|
944
944
|
if (json && typeof json.warning === 'string' && json.warning) {
|
|
945
|
-
|
|
945
|
+
if (process.env.HALTIJA_STRICT === '1') {
|
|
946
|
+
// Strict mode (issue #8): if the result may be wrong, a script must not consume it. Fail
|
|
947
|
+
// fast with the real reason instead of emitting a plausible-but-wrong value that makes the
|
|
948
|
+
// lane fail later, pointing at the caller's own code. stdout stays empty on purpose.
|
|
949
|
+
// NB: fails on `warningRepeated` too — the condition still holds, and suppressing repeats
|
|
950
|
+
// here would let every command after the first silently pass.
|
|
951
|
+
console.error(`hj: ERROR (strict) — ${json.warning}`)
|
|
952
|
+
console.error(`hj: refusing to return a result that may be wrong. Fix the condition above, or drop --strict/HALTIJA_STRICT to proceed anyway.`)
|
|
953
|
+
process.exit(1)
|
|
954
|
+
}
|
|
955
|
+
// Non-strict: stay quiet on a repeat within the cooldown, so a burst of commands doesn't
|
|
956
|
+
// re-print the same block and train the reader to ignore it.
|
|
957
|
+
if (!json.warningRepeated) console.error(`hj: warning — ${json.warning}`)
|
|
946
958
|
}
|
|
947
959
|
|
|
948
960
|
// Text format for supported subcommands (unless --json)
|
package/bin/hj.mjs
CHANGED
|
@@ -191,6 +191,104 @@ async function runServers(resolvedPort) {
|
|
|
191
191
|
console.log(dim('\nPick one: ') + `hj --port <n> <cmd>` + dim(' or ') + `hj --name <name> <cmd>`)
|
|
192
192
|
}
|
|
193
193
|
|
|
194
|
+
/**
|
|
195
|
+
* `hj doctor` — one-command preflight for a test lane: "is the thing I'm about to drive the thing
|
|
196
|
+
* I mean, and can it actually be driven?" (issues #8, #11). Exits NON-ZERO when it isn't, which is
|
|
197
|
+
* the whole point: "is a server up?" is a cheap probe that does NOT predict success, so adopters
|
|
198
|
+
* who used it skipped spawning their own browser and failed much later on a timeout.
|
|
199
|
+
*
|
|
200
|
+
* Checks, in the order they bite: server reachable → drivable (a tab is connected) → targeting is
|
|
201
|
+
* unambiguous (cwd matches, or the choice was explicit) → tabs visible → versions aligned.
|
|
202
|
+
*/
|
|
203
|
+
async function runDoctor(port, portSource, jsonOutput) {
|
|
204
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`
|
|
205
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`
|
|
206
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`
|
|
207
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`
|
|
208
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`
|
|
209
|
+
const token = process.env.HALTIJA_TOKEN
|
|
210
|
+
|
|
211
|
+
const problems = [] // fatal → exit 1
|
|
212
|
+
const notes = [] // advisory
|
|
213
|
+
let status = null
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
217
|
+
headers: token ? { 'X-Haltija-Token': token } : {},
|
|
218
|
+
signal: AbortSignal.timeout(3000),
|
|
219
|
+
})
|
|
220
|
+
if (resp.ok) status = await resp.json()
|
|
221
|
+
else problems.push(`server on port ${port} returned HTTP ${resp.status}`)
|
|
222
|
+
} catch (err) {
|
|
223
|
+
const refused = err.code === 'ConnectionRefused' || err.cause?.code === 'ECONNREFUSED'
|
|
224
|
+
problems.push(
|
|
225
|
+
refused
|
|
226
|
+
? `no haltija server is listening on port ${port} — start one (bunx haltija) or check the target`
|
|
227
|
+
: `could not reach the server on port ${port}: ${err.message}`,
|
|
228
|
+
)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (status) {
|
|
232
|
+
// The signal that actually predicts success. Older servers (<1.6.1) don't send `ready`; fall
|
|
233
|
+
// back to counting tabs rather than inventing a pass.
|
|
234
|
+
const tabs = Array.isArray(status.windows) ? status.windows : []
|
|
235
|
+
const ready = typeof status.ready === 'boolean' ? status.ready : tabs.length > 0
|
|
236
|
+
if (!ready) {
|
|
237
|
+
problems.push(
|
|
238
|
+
`the server on port ${port} is up but has NO connected browser tab — nothing to drive. ` +
|
|
239
|
+
`Open a tab in the desktop app, or inject the widget into a page. ` +
|
|
240
|
+
`("server is up" is not "server is drivable" — that's what this check exists for.)`,
|
|
241
|
+
)
|
|
242
|
+
}
|
|
243
|
+
const hidden = tabs.filter((w) => w.hidden)
|
|
244
|
+
if (ready && hidden.length === tabs.length) {
|
|
245
|
+
problems.push(
|
|
246
|
+
`every connected tab reports HIDDEN — results from a backgrounded tab can be ` +
|
|
247
|
+
`plausible-but-wrong (rAF/timers throttled). Bring one to the front.`,
|
|
248
|
+
)
|
|
249
|
+
} else if (hidden.length) {
|
|
250
|
+
notes.push(`${hidden.length} of ${tabs.length} tab(s) are hidden; commands targeting them may return stale results`)
|
|
251
|
+
}
|
|
252
|
+
if (status.serverVersion && differsBeyondPatch(HJ_VERSION, status.serverVersion)) {
|
|
253
|
+
notes.push(`hj ${HJ_VERSION} is driving server ${status.serverVersion} (version skew)`)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Ambiguous targeting: we fell back to the shared default while other projects' servers are live.
|
|
258
|
+
const live = listLiveInstances()
|
|
259
|
+
const ambiguous = portSource === '8700 (default)' && live.length > 0
|
|
260
|
+
if (ambiguous) {
|
|
261
|
+
problems.push(
|
|
262
|
+
`targeting the shared default port 8700, but ${live.length} other haltija server(s) are ` +
|
|
263
|
+
`running and none matches this directory (${process.cwd()}) — the target is ambiguous. ` +
|
|
264
|
+
`Pick one with --name/--port, or run from the project's directory.`,
|
|
265
|
+
)
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const ok = problems.length === 0
|
|
269
|
+
|
|
270
|
+
if (jsonOutput) {
|
|
271
|
+
console.log(JSON.stringify({
|
|
272
|
+
ok, port, portSource,
|
|
273
|
+
serverVersion: status?.serverVersion ?? null,
|
|
274
|
+
ready: status ? (typeof status.ready === 'boolean' ? status.ready : (status.windows?.length ?? 0) > 0) : false,
|
|
275
|
+
tabs: status?.windows?.length ?? 0,
|
|
276
|
+
problems, notes,
|
|
277
|
+
}, null, 2))
|
|
278
|
+
return ok
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
console.log(`${bold('target:')} port ${port} ${dim(`(${portSource})`)}`)
|
|
282
|
+
if (status) {
|
|
283
|
+
const tabCount = status.windows?.length ?? 0
|
|
284
|
+
console.log(`${bold('server:')} haltija ${status.serverVersion || '?'}${status.desktopApp ? dim(' (desktop app)') : ''}, ${tabCount} tab${tabCount === 1 ? '' : 's'}`)
|
|
285
|
+
}
|
|
286
|
+
for (const n of notes) console.log(`${yellow('!')} ${n}`)
|
|
287
|
+
for (const p of problems) console.log(`${red('✗')} ${p}`)
|
|
288
|
+
if (ok) console.log(`${green('✓')} ready to drive`)
|
|
289
|
+
return ok
|
|
290
|
+
}
|
|
291
|
+
|
|
194
292
|
/**
|
|
195
293
|
* Resolve a named haltija instance to its port by reading
|
|
196
294
|
* ~/.haltija/servers/<name>.json. Returns null if the file is missing,
|
|
@@ -280,7 +378,13 @@ ${dim('Overriding that (per-shell):')}
|
|
|
280
378
|
${dim('Lifecycle:')}
|
|
281
379
|
${dim('hj where')} # which server this shell targets + what is alive there
|
|
282
380
|
${dim('hj servers')} # list ALL live servers (pick one with --port/--name)
|
|
381
|
+
${dim('hj doctor')} # preflight: drivable + unambiguous? EXITS 1 if not
|
|
283
382
|
${dim('hj shutdown')} # stop the targeted server (a private --app: Electron + all)
|
|
383
|
+
|
|
384
|
+
${dim('For scripts / CI:')}
|
|
385
|
+
${dim('hj --strict <cmd>')} # turn advisory warnings (wrong project, hidden tab)
|
|
386
|
+
${dim('HALTIJA_STRICT=1')} # into non-zero exits, so a lane fails fast on the
|
|
387
|
+
${dim('# real cause instead of a later timeout')}
|
|
284
388
|
${listSubcommands()}
|
|
285
389
|
Run ${dim('hj --help')} for this help.
|
|
286
390
|
Run ${dim('haltija --help')} for server/app options.
|
|
@@ -288,6 +392,23 @@ Run ${dim('haltija --help')} for server/app options.
|
|
|
288
392
|
process.exit(0)
|
|
289
393
|
}
|
|
290
394
|
|
|
395
|
+
// Parse --strict FIRST — before port resolution, which is itself one of the things strict mode
|
|
396
|
+
// turns from a warning into an error. (A check placed before the input it depends on is a
|
|
397
|
+
// recurring bug shape: parse the flag, then run the code that reads it.) Sets HALTIJA_STRICT so
|
|
398
|
+
// cli-subcommand.mjs sees it too.
|
|
399
|
+
//
|
|
400
|
+
// In strict mode the advisory warnings — cross-project targeting, hidden tab, focus ambiguity —
|
|
401
|
+
// become non-zero exits (issue #8). haltija already DETECTS these precisely; the gap was that
|
|
402
|
+
// detection never reached the exit code, so a lane consumed a plausible-but-wrong result and failed
|
|
403
|
+
// much later pointing at the caller's own code. A warning is right for a human at a prompt and
|
|
404
|
+
// wrong for a script.
|
|
405
|
+
const strictIdx = args.indexOf('--strict')
|
|
406
|
+
if (strictIdx !== -1) {
|
|
407
|
+
process.env.HALTIJA_STRICT = '1'
|
|
408
|
+
args.splice(strictIdx, 1)
|
|
409
|
+
}
|
|
410
|
+
const STRICT = process.env.HALTIJA_STRICT === '1'
|
|
411
|
+
|
|
291
412
|
// Parse --name option (or HALTIJA_NAME env): resolve to a port via
|
|
292
413
|
// ~/.haltija/servers/<name>.json, written by `haltija --name <foo>`.
|
|
293
414
|
let resolvedName = process.env.HALTIJA_NAME || ''
|
|
@@ -358,6 +479,13 @@ if (portFlag) {
|
|
|
358
479
|
// explicit choice.
|
|
359
480
|
if (live.length) {
|
|
360
481
|
const names = live.map((e) => `${e.name} (${e.cwd})`).join(', ')
|
|
482
|
+
if (STRICT) {
|
|
483
|
+
// A lane must not silently drive another project's browser (issue #8, case 1).
|
|
484
|
+
console.error(`hj: ERROR (strict) — refusing to fall back to the default port 8700 while other haltija servers are running: ${names}`)
|
|
485
|
+
console.error(`hj: this shell's cwd (${process.cwd()}) matches none of them, so the target is ambiguous.`)
|
|
486
|
+
console.error(`hj: pick one explicitly with --name/--port (or cd into its directory), or drop --strict to proceed anyway.`)
|
|
487
|
+
process.exit(1)
|
|
488
|
+
}
|
|
361
489
|
console.error(`hj: warning — targeting the default port 8700, but these haltija servers are running: ${names}`)
|
|
362
490
|
console.error(`hj: if you meant one of them, cd into its directory, or use --name/--port. See \`hj where\`.`)
|
|
363
491
|
}
|
|
@@ -444,6 +572,13 @@ if (subcommand === 'servers' || subcommand === 'ls') {
|
|
|
444
572
|
process.exit(0)
|
|
445
573
|
}
|
|
446
574
|
|
|
575
|
+
// `hj doctor` — preflight for a test lane. EXITS NON-ZERO when the target isn't drivable or is
|
|
576
|
+
// ambiguous, so a lane can fail fast with the real reason (issues #8, #11). Never auto-launches.
|
|
577
|
+
if (subcommand === 'doctor') {
|
|
578
|
+
const ok = await runDoctor(port, portSource, subArgs.includes('--json'))
|
|
579
|
+
process.exit(ok ? 0 : 1)
|
|
580
|
+
}
|
|
581
|
+
|
|
447
582
|
// `hj shutdown` / `hj quit` — cleanly stop the targeted server. For a private `--app` instance this
|
|
448
583
|
// tears down the WHOLE thing (Electron + its child servers); for a plain server it stops that
|
|
449
584
|
// server. Never auto-launches (it's a stop command), so it's handled here before the routing table.
|
package/bin/version.mjs
CHANGED
package/dist/api-handlers.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface DevResponse {
|
|
|
18
18
|
timestamp: number;
|
|
19
19
|
/** Hidden-tab / focus-ambiguity caveat attached by the server (see requestFromBrowser). */
|
|
20
20
|
warning?: string;
|
|
21
|
+
/** True when this exact warning was already reported within the cooldown (see types.ts). */
|
|
22
|
+
warningRepeated?: boolean;
|
|
21
23
|
}
|
|
22
24
|
/** Function to send request to browser widget */
|
|
23
25
|
export type RequestFromBrowserFn = (channel: string, action: string, payload: any, timeoutMs?: number, windowId?: string) => Promise<DevResponse>;
|
package/dist/component.d.ts
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
* - Option+Tab toggles visibility (but active state always shows briefly)
|
|
21
21
|
* - Localhost only by default
|
|
22
22
|
*/
|
|
23
|
-
export declare const VERSION = "1.6.
|
|
23
|
+
export declare const VERSION = "1.6.1";
|
|
24
24
|
export declare class DevChannel extends HTMLElement {
|
|
25
25
|
static get tagName(): string;
|
|
26
26
|
static elementCreator(): () => DevChannel;
|
package/dist/component.esm.js
CHANGED
package/dist/component.js
CHANGED
package/dist/hj.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
|
-
// haltija-cli:do-not-edit v1.6.
|
|
2
|
+
// haltija-cli:do-not-edit v1.6.1
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
5
5
|
|
|
@@ -756,7 +756,7 @@ function substituteGeneratedVars(text, seed) {
|
|
|
756
756
|
}
|
|
757
757
|
|
|
758
758
|
// bin/version.mjs
|
|
759
|
-
var HJ_VERSION = "1.6.
|
|
759
|
+
var HJ_VERSION = "1.6.1";
|
|
760
760
|
|
|
761
761
|
// bin/semver.mjs
|
|
762
762
|
function parseVersion(v) {
|
|
@@ -1629,7 +1629,13 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
1629
1629
|
if (contentType.includes("application/json")) {
|
|
1630
1630
|
const json = await resp.json();
|
|
1631
1631
|
if (json && typeof json.warning === "string" && json.warning) {
|
|
1632
|
-
|
|
1632
|
+
if (process.env.HALTIJA_STRICT === "1") {
|
|
1633
|
+
console.error(`hj: ERROR (strict) — ${json.warning}`);
|
|
1634
|
+
console.error(`hj: refusing to return a result that may be wrong. Fix the condition above, or drop --strict/HALTIJA_STRICT to proceed anyway.`);
|
|
1635
|
+
process.exit(1);
|
|
1636
|
+
}
|
|
1637
|
+
if (!json.warningRepeated)
|
|
1638
|
+
console.error(`hj: warning — ${json.warning}`);
|
|
1633
1639
|
}
|
|
1634
1640
|
if (!jsonOutput && subcommand === "tree" && json.success && json.data) {
|
|
1635
1641
|
console.log(formatTree(json.data, 0, { depth: body?.depth }));
|
|
@@ -2063,6 +2069,77 @@ This shell targets :${resolvedPort}, but nothing is listening there.`));
|
|
|
2063
2069
|
console.log(dim3(`
|
|
2064
2070
|
Pick one: `) + `hj --port <n> <cmd>` + dim3(" or ") + `hj --name <name> <cmd>`);
|
|
2065
2071
|
}
|
|
2072
|
+
async function runDoctor(port, portSource, jsonOutput) {
|
|
2073
|
+
const bold2 = (s) => `\x1B[1m${s}\x1B[0m`;
|
|
2074
|
+
const dim3 = (s) => `\x1B[2m${s}\x1B[0m`;
|
|
2075
|
+
const green2 = (s) => `\x1B[32m${s}\x1B[0m`;
|
|
2076
|
+
const red2 = (s) => `\x1B[31m${s}\x1B[0m`;
|
|
2077
|
+
const yellow2 = (s) => `\x1B[33m${s}\x1B[0m`;
|
|
2078
|
+
const token = process.env.HALTIJA_TOKEN;
|
|
2079
|
+
const problems = [];
|
|
2080
|
+
const notes = [];
|
|
2081
|
+
let status = null;
|
|
2082
|
+
try {
|
|
2083
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
2084
|
+
headers: token ? { "X-Haltija-Token": token } : {},
|
|
2085
|
+
signal: AbortSignal.timeout(3000)
|
|
2086
|
+
});
|
|
2087
|
+
if (resp.ok)
|
|
2088
|
+
status = await resp.json();
|
|
2089
|
+
else
|
|
2090
|
+
problems.push(`server on port ${port} returned HTTP ${resp.status}`);
|
|
2091
|
+
} catch (err) {
|
|
2092
|
+
const refused = err.code === "ConnectionRefused" || err.cause?.code === "ECONNREFUSED";
|
|
2093
|
+
problems.push(refused ? `no haltija server is listening on port ${port} — start one (bunx haltija) or check the target` : `could not reach the server on port ${port}: ${err.message}`);
|
|
2094
|
+
}
|
|
2095
|
+
if (status) {
|
|
2096
|
+
const tabs = Array.isArray(status.windows) ? status.windows : [];
|
|
2097
|
+
const ready = typeof status.ready === "boolean" ? status.ready : tabs.length > 0;
|
|
2098
|
+
if (!ready) {
|
|
2099
|
+
problems.push(`the server on port ${port} is up but has NO connected browser tab — nothing to drive. ` + `Open a tab in the desktop app, or inject the widget into a page. ` + `("server is up" is not "server is drivable" — that's what this check exists for.)`);
|
|
2100
|
+
}
|
|
2101
|
+
const hidden = tabs.filter((w) => w.hidden);
|
|
2102
|
+
if (ready && hidden.length === tabs.length) {
|
|
2103
|
+
problems.push(`every connected tab reports HIDDEN — results from a backgrounded tab can be ` + `plausible-but-wrong (rAF/timers throttled). Bring one to the front.`);
|
|
2104
|
+
} else if (hidden.length) {
|
|
2105
|
+
notes.push(`${hidden.length} of ${tabs.length} tab(s) are hidden; commands targeting them may return stale results`);
|
|
2106
|
+
}
|
|
2107
|
+
if (status.serverVersion && differsBeyondPatch(HJ_VERSION, status.serverVersion)) {
|
|
2108
|
+
notes.push(`hj ${HJ_VERSION} is driving server ${status.serverVersion} (version skew)`);
|
|
2109
|
+
}
|
|
2110
|
+
}
|
|
2111
|
+
const live = listLiveInstances();
|
|
2112
|
+
const ambiguous = portSource === "8700 (default)" && live.length > 0;
|
|
2113
|
+
if (ambiguous) {
|
|
2114
|
+
problems.push(`targeting the shared default port 8700, but ${live.length} other haltija server(s) are ` + `running and none matches this directory (${process.cwd()}) — the target is ambiguous. ` + `Pick one with --name/--port, or run from the project's directory.`);
|
|
2115
|
+
}
|
|
2116
|
+
const ok = problems.length === 0;
|
|
2117
|
+
if (jsonOutput) {
|
|
2118
|
+
console.log(JSON.stringify({
|
|
2119
|
+
ok,
|
|
2120
|
+
port,
|
|
2121
|
+
portSource,
|
|
2122
|
+
serverVersion: status?.serverVersion ?? null,
|
|
2123
|
+
ready: status ? typeof status.ready === "boolean" ? status.ready : (status.windows?.length ?? 0) > 0 : false,
|
|
2124
|
+
tabs: status?.windows?.length ?? 0,
|
|
2125
|
+
problems,
|
|
2126
|
+
notes
|
|
2127
|
+
}, null, 2));
|
|
2128
|
+
return ok;
|
|
2129
|
+
}
|
|
2130
|
+
console.log(`${bold2("target:")} port ${port} ${dim3(`(${portSource})`)}`);
|
|
2131
|
+
if (status) {
|
|
2132
|
+
const tabCount = status.windows?.length ?? 0;
|
|
2133
|
+
console.log(`${bold2("server:")} haltija ${status.serverVersion || "?"}${status.desktopApp ? dim3(" (desktop app)") : ""}, ${tabCount} tab${tabCount === 1 ? "" : "s"}`);
|
|
2134
|
+
}
|
|
2135
|
+
for (const n of notes)
|
|
2136
|
+
console.log(`${yellow2("!")} ${n}`);
|
|
2137
|
+
for (const p of problems)
|
|
2138
|
+
console.log(`${red2("✗")} ${p}`);
|
|
2139
|
+
if (ok)
|
|
2140
|
+
console.log(`${green2("✓")} ready to drive`);
|
|
2141
|
+
return ok;
|
|
2142
|
+
}
|
|
2066
2143
|
function lookupNamedInstance(name) {
|
|
2067
2144
|
const path = join2(REGISTRY_DIR, `${name}.json`);
|
|
2068
2145
|
if (!existsSync2(path))
|
|
@@ -2138,13 +2215,25 @@ ${dim3("Overriding that (per-shell):")}
|
|
|
2138
2215
|
${dim3("Lifecycle:")}
|
|
2139
2216
|
${dim3("hj where")} # which server this shell targets + what is alive there
|
|
2140
2217
|
${dim3("hj servers")} # list ALL live servers (pick one with --port/--name)
|
|
2218
|
+
${dim3("hj doctor")} # preflight: drivable + unambiguous? EXITS 1 if not
|
|
2141
2219
|
${dim3("hj shutdown")} # stop the targeted server (a private --app: Electron + all)
|
|
2220
|
+
|
|
2221
|
+
${dim3("For scripts / CI:")}
|
|
2222
|
+
${dim3("hj --strict <cmd>")} # turn advisory warnings (wrong project, hidden tab)
|
|
2223
|
+
${dim3("HALTIJA_STRICT=1")} # into non-zero exits, so a lane fails fast on the
|
|
2224
|
+
${dim3("# real cause instead of a later timeout")}
|
|
2142
2225
|
${listSubcommands()}
|
|
2143
2226
|
Run ${dim3("hj --help")} for this help.
|
|
2144
2227
|
Run ${dim3("haltija --help")} for server/app options.
|
|
2145
2228
|
`);
|
|
2146
2229
|
process.exit(0);
|
|
2147
2230
|
}
|
|
2231
|
+
var strictIdx = args.indexOf("--strict");
|
|
2232
|
+
if (strictIdx !== -1) {
|
|
2233
|
+
process.env.HALTIJA_STRICT = "1";
|
|
2234
|
+
args.splice(strictIdx, 1);
|
|
2235
|
+
}
|
|
2236
|
+
var STRICT = process.env.HALTIJA_STRICT === "1";
|
|
2148
2237
|
var resolvedName = process.env.HALTIJA_NAME || "";
|
|
2149
2238
|
var nameSource = resolvedName ? "HALTIJA_NAME env" : "";
|
|
2150
2239
|
var nameIdx = args.indexOf("--name");
|
|
@@ -2190,6 +2279,12 @@ if (portFlag) {
|
|
|
2190
2279
|
portSource = "8700 (default)";
|
|
2191
2280
|
if (live.length) {
|
|
2192
2281
|
const names = live.map((e) => `${e.name} (${e.cwd})`).join(", ");
|
|
2282
|
+
if (STRICT) {
|
|
2283
|
+
console.error(`hj: ERROR (strict) — refusing to fall back to the default port 8700 while other haltija servers are running: ${names}`);
|
|
2284
|
+
console.error(`hj: this shell's cwd (${process.cwd()}) matches none of them, so the target is ambiguous.`);
|
|
2285
|
+
console.error(`hj: pick one explicitly with --name/--port (or cd into its directory), or drop --strict to proceed anyway.`);
|
|
2286
|
+
process.exit(1);
|
|
2287
|
+
}
|
|
2193
2288
|
console.error(`hj: warning — targeting the default port 8700, but these haltija servers are running: ${names}`);
|
|
2194
2289
|
console.error(`hj: if you meant one of them, cd into its directory, or use --name/--port. See \`hj where\`.`);
|
|
2195
2290
|
}
|
|
@@ -2238,6 +2333,10 @@ if (subcommand === "servers" || subcommand === "ls") {
|
|
|
2238
2333
|
await runServers(port);
|
|
2239
2334
|
process.exit(0);
|
|
2240
2335
|
}
|
|
2336
|
+
if (subcommand === "doctor") {
|
|
2337
|
+
const ok = await runDoctor(port, portSource, subArgs.includes("--json"));
|
|
2338
|
+
process.exit(ok ? 0 : 1);
|
|
2339
|
+
}
|
|
2241
2340
|
if (subcommand === "shutdown" || subcommand === "quit") {
|
|
2242
2341
|
const token = process.env.HALTIJA_TOKEN;
|
|
2243
2342
|
try {
|
package/dist/index.js
CHANGED
|
@@ -674,7 +674,7 @@ var injectorCode = `
|
|
|
674
674
|
`;
|
|
675
675
|
|
|
676
676
|
// src/version.ts
|
|
677
|
-
var VERSION = "1.6.
|
|
677
|
+
var VERSION = "1.6.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -1008,9 +1008,13 @@ curl -X POST localhost:8700/click -d '{"selector":"#submit"}'
|
|
|
1008
1008
|
|
|
1009
1009
|
Returns server info and connected browser count.
|
|
1010
1010
|
|
|
1011
|
-
Response: {
|
|
1011
|
+
Response: { serverVersion, ready, windows: [...], browsers: n, desktopApp, pid, ... }
|
|
1012
1012
|
|
|
1013
|
-
Use to verify server is running
|
|
1013
|
+
Use to verify the server is running \u2014 but gate a test lane on **\`ready\`**, not on the 200. A
|
|
1014
|
+
server can be up with zero connected tabs: /status answers fine and there is still nothing to
|
|
1015
|
+
drive, so a lane that adopts it fails later on a timeout that points at the caller's own code.
|
|
1016
|
+
\`ready\` is true when at least one top-level tab is connected. \`hj doctor\` checks this (plus
|
|
1017
|
+
ambiguous targeting) and exits non-zero, which is the one-command preflight for a lane.
|
|
1014
1018
|
|
|
1015
1019
|
---
|
|
1016
1020
|
|
|
@@ -2166,10 +2170,16 @@ Deprecated: Use POST /select {"action":"clear"} instead.
|
|
|
2166
2170
|
|
|
2167
2171
|
Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
2168
2172
|
|
|
2169
|
-
Response: { windows: [{ id, url, title, focused }] }
|
|
2173
|
+
Response: { windows: [{ id, url, title, focused }], count, ready, hint }
|
|
2170
2174
|
|
|
2171
2175
|
Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
2172
2176
|
|
|
2177
|
+
**\`ready\` is the signal to gate a test lane on, not "is the server up".** A server can be running
|
|
2178
|
+
with zero connected tabs \u2014 it answers /status 200 but there is nothing to drive, and a lane that
|
|
2179
|
+
adopts it fails later on a confusing timeout. \`ready\` is true when at least one top-level tab is
|
|
2180
|
+
connected. (Hidden tabs count as ready \u2014 they're reachable, just possibly stale; see the hidden-tab
|
|
2181
|
+
warning.) \`hj doctor\` checks this and exits non-zero, so a lane can fail fast on the real cause.
|
|
2182
|
+
|
|
2173
2183
|
---
|
|
2174
2184
|
|
|
2175
2185
|
### \`POST /tabs/open\`
|
|
@@ -2867,8 +2877,16 @@ hj tree # ...and plain hj reaches it
|
|
|
2867
2877
|
|
|
2868
2878
|
hj where # which port, WHY, and what is alive there
|
|
2869
2879
|
hj servers # list ALL live servers; pick with --port/--name
|
|
2880
|
+
hj doctor # preflight for a script: EXITS 1 if not drivable
|
|
2881
|
+
hj --strict <cmd> # warnings (wrong project / hidden tab) become errors
|
|
2870
2882
|
\`\`\`
|
|
2871
2883
|
|
|
2884
|
+
**In a script or CI lane, gate on \`hj doctor\` (or the \`ready\` field), not on "the server
|
|
2885
|
+
answered".** A server can be up with zero connected tabs \u2014 \`/status\` returns 200 and there
|
|
2886
|
+
is still nothing to drive, so a lane that adopts it fails later on a timeout that points at
|
|
2887
|
+
your own code. \`hj doctor\` exits non-zero on exactly that, and \`--strict\` turns the advisory
|
|
2888
|
+
warnings into failures so a suspect result is never consumed.
|
|
2889
|
+
|
|
2872
2890
|
When several haltijas run at once (e.g. a project server AND the desktop app),
|
|
2873
2891
|
\`hj servers\` (alias \`hj ls\`) lists them all \u2014 the desktop app is reachable as
|
|
2874
2892
|
\`hj --name desktop\`. If no server owns your directory, \`hj\` falls back to the shared default port
|
|
@@ -3040,6 +3058,11 @@ Two first-party ways to run (plus embedding, below):
|
|
|
3040
3058
|
port a shell targets and WHY; override with \`--port\` or \`--name\`. When several servers
|
|
3041
3059
|
run at once (e.g. a project server AND the desktop app), \`hj servers\` lists them all \u2014
|
|
3042
3060
|
the desktop app is reachable as \`hj --name desktop\`.
|
|
3061
|
+
- **Scripts/CI:** gate on \`hj doctor\` (exits non-zero when the target is not drivable or is
|
|
3062
|
+
ambiguous) or on the \`ready\` field of \`/status\`/\`/windows\` \u2014 NOT on "the server answered".
|
|
3063
|
+
A server can be up with zero connected tabs, so a lane that adopts it fails later on a
|
|
3064
|
+
confusing timeout. \`hj --strict\` (or HALTIJA_STRICT=1) turns the hidden-tab and
|
|
3065
|
+
cross-project warnings into non-zero exits so a suspect result is never consumed.
|
|
3043
3066
|
- **CI:** two engines, and the choice matters \u2014 both need one external browser, but
|
|
3044
3067
|
a *different* one:
|
|
3045
3068
|
- \`haltija --ci\` (or \`--private --app\` for an isolated instance) drives **Electron**
|
|
@@ -3126,7 +3149,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3126
3149
|
});
|
|
3127
3150
|
|
|
3128
3151
|
// src/version.ts
|
|
3129
|
-
var VERSION = "1.6.
|
|
3152
|
+
var VERSION = "1.6.1";
|
|
3130
3153
|
|
|
3131
3154
|
// src/text-selector.ts
|
|
3132
3155
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -12906,9 +12929,15 @@ var windows = endpoint({
|
|
|
12906
12929
|
summary: "List connected windows",
|
|
12907
12930
|
description: `Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
12908
12931
|
|
|
12909
|
-
Response: { windows: [{ id, url, title, focused }] }
|
|
12932
|
+
Response: { windows: [{ id, url, title, focused }], count, ready, hint }
|
|
12933
|
+
|
|
12934
|
+
Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
12910
12935
|
|
|
12911
|
-
|
|
12936
|
+
**\`ready\` is the signal to gate a test lane on, not "is the server up".** A server can be running
|
|
12937
|
+
with zero connected tabs \u2014 it answers /status 200 but there is nothing to drive, and a lane that
|
|
12938
|
+
adopts it fails later on a confusing timeout. \`ready\` is true when at least one top-level tab is
|
|
12939
|
+
connected. (Hidden tabs count as ready \u2014 they're reachable, just possibly stale; see the hidden-tab
|
|
12940
|
+
warning.) \`hj doctor\` checks this and exits non-zero, so a lane can fail fast on the real cause.`,
|
|
12912
12941
|
category: "windows",
|
|
12913
12942
|
hints: "--json | see: tabs-open, tabs-close, tabs-focus, status"
|
|
12914
12943
|
});
|
|
@@ -13379,9 +13408,13 @@ var status = endpoint({
|
|
|
13379
13408
|
summary: "Server status",
|
|
13380
13409
|
description: `Returns server info and connected browser count.
|
|
13381
13410
|
|
|
13382
|
-
Response: {
|
|
13411
|
+
Response: { serverVersion, ready, windows: [...], browsers: n, desktopApp, pid, ... }
|
|
13383
13412
|
|
|
13384
|
-
Use to verify server is running
|
|
13413
|
+
Use to verify the server is running \u2014 but gate a test lane on **\`ready\`**, not on the 200. A
|
|
13414
|
+
server can be up with zero connected tabs: /status answers fine and there is still nothing to
|
|
13415
|
+
drive, so a lane that adopts it fails later on a timeout that points at the caller's own code.
|
|
13416
|
+
\`ready\` is true when at least one top-level tab is connected. \`hj doctor\` checks this (plus
|
|
13417
|
+
ambiguous targeting) and exits non-zero, which is the one-command preflight for a lane.`,
|
|
13385
13418
|
category: "meta",
|
|
13386
13419
|
hints: "--json | see: windows, stats, console"
|
|
13387
13420
|
});
|
|
@@ -13840,7 +13873,9 @@ function inferSuggestion(step, pageContext) {
|
|
|
13840
13873
|
|
|
13841
13874
|
// src/api-handlers.ts
|
|
13842
13875
|
function withWarning(body, response) {
|
|
13843
|
-
|
|
13876
|
+
if (!response.warning)
|
|
13877
|
+
return body;
|
|
13878
|
+
return response.warningRepeated ? { ...body, warning: response.warning, warningRepeated: true } : { ...body, warning: response.warning };
|
|
13844
13879
|
}
|
|
13845
13880
|
var handlers = new Map;
|
|
13846
13881
|
function registerHandler(endpoint2, handler) {
|
|
@@ -16611,10 +16646,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16611
16646
|
`);
|
|
16612
16647
|
if (!warning)
|
|
16613
16648
|
return res;
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
}
|
|
16617
|
-
return { ...res, warning };
|
|
16649
|
+
const repeated = !shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS);
|
|
16650
|
+
return repeated ? { ...res, warning, warningRepeated: true } : { ...res, warning };
|
|
16618
16651
|
};
|
|
16619
16652
|
const timeout = setTimeout(() => {
|
|
16620
16653
|
pendingResponses.delete(id);
|
|
@@ -17041,6 +17074,7 @@ async function handleRest(req) {
|
|
|
17041
17074
|
return Response.json({
|
|
17042
17075
|
ok: allWindows.length > 0,
|
|
17043
17076
|
windows: windowList,
|
|
17077
|
+
ready: allWindows.filter((w) => (w.windowType || "tab") === "tab").length > 0,
|
|
17044
17078
|
serverVersion: SERVER_VERSION,
|
|
17045
17079
|
pid: process.pid,
|
|
17046
17080
|
recording: activeRecordings > 0,
|
|
@@ -18988,11 +19022,14 @@ ${messageText}`;
|
|
|
18988
19022
|
label: w.label,
|
|
18989
19023
|
windowType: w.windowType || "tab"
|
|
18990
19024
|
}));
|
|
19025
|
+
const drivableTabs = windowList.filter((w) => (w.windowType || "tab") === "tab");
|
|
19026
|
+
const ready = drivableTabs.length > 0;
|
|
18991
19027
|
const hint = windowList.length > 1 ? "Multiple tabs connected. Use ?window=<id> to target specific tab (e.g., /tree?window=abc123)" : windowList.length === 1 ? "One tab connected. Commands automatically target it." : "No tabs connected. Inject the widget into a browser tab.";
|
|
18992
19028
|
return Response.json({
|
|
18993
19029
|
windows: windowList,
|
|
18994
19030
|
focused: focusedWindowId,
|
|
18995
19031
|
count: windowList.length,
|
|
19032
|
+
ready,
|
|
18996
19033
|
hint
|
|
18997
19034
|
}, { headers });
|
|
18998
19035
|
}
|
package/dist/server.js
CHANGED
|
@@ -674,7 +674,7 @@ var injectorCode = `
|
|
|
674
674
|
`;
|
|
675
675
|
|
|
676
676
|
// src/version.ts
|
|
677
|
-
var VERSION = "1.6.
|
|
677
|
+
var VERSION = "1.6.1";
|
|
678
678
|
|
|
679
679
|
// src/embedded-assets.ts
|
|
680
680
|
var APP_MD = `# Haltija App
|
|
@@ -1008,9 +1008,13 @@ curl -X POST localhost:8700/click -d '{"selector":"#submit"}'
|
|
|
1008
1008
|
|
|
1009
1009
|
Returns server info and connected browser count.
|
|
1010
1010
|
|
|
1011
|
-
Response: {
|
|
1011
|
+
Response: { serverVersion, ready, windows: [...], browsers: n, desktopApp, pid, ... }
|
|
1012
1012
|
|
|
1013
|
-
Use to verify server is running
|
|
1013
|
+
Use to verify the server is running \u2014 but gate a test lane on **\`ready\`**, not on the 200. A
|
|
1014
|
+
server can be up with zero connected tabs: /status answers fine and there is still nothing to
|
|
1015
|
+
drive, so a lane that adopts it fails later on a timeout that points at the caller's own code.
|
|
1016
|
+
\`ready\` is true when at least one top-level tab is connected. \`hj doctor\` checks this (plus
|
|
1017
|
+
ambiguous targeting) and exits non-zero, which is the one-command preflight for a lane.
|
|
1014
1018
|
|
|
1015
1019
|
---
|
|
1016
1020
|
|
|
@@ -2166,10 +2170,16 @@ Deprecated: Use POST /select {"action":"clear"} instead.
|
|
|
2166
2170
|
|
|
2167
2171
|
Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
2168
2172
|
|
|
2169
|
-
Response: { windows: [{ id, url, title, focused }] }
|
|
2173
|
+
Response: { windows: [{ id, url, title, focused }], count, ready, hint }
|
|
2170
2174
|
|
|
2171
2175
|
Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
2172
2176
|
|
|
2177
|
+
**\`ready\` is the signal to gate a test lane on, not "is the server up".** A server can be running
|
|
2178
|
+
with zero connected tabs \u2014 it answers /status 200 but there is nothing to drive, and a lane that
|
|
2179
|
+
adopts it fails later on a confusing timeout. \`ready\` is true when at least one top-level tab is
|
|
2180
|
+
connected. (Hidden tabs count as ready \u2014 they're reachable, just possibly stale; see the hidden-tab
|
|
2181
|
+
warning.) \`hj doctor\` checks this and exits non-zero, so a lane can fail fast on the real cause.
|
|
2182
|
+
|
|
2173
2183
|
---
|
|
2174
2184
|
|
|
2175
2185
|
### \`POST /tabs/open\`
|
|
@@ -2867,8 +2877,16 @@ hj tree # ...and plain hj reaches it
|
|
|
2867
2877
|
|
|
2868
2878
|
hj where # which port, WHY, and what is alive there
|
|
2869
2879
|
hj servers # list ALL live servers; pick with --port/--name
|
|
2880
|
+
hj doctor # preflight for a script: EXITS 1 if not drivable
|
|
2881
|
+
hj --strict <cmd> # warnings (wrong project / hidden tab) become errors
|
|
2870
2882
|
\`\`\`
|
|
2871
2883
|
|
|
2884
|
+
**In a script or CI lane, gate on \`hj doctor\` (or the \`ready\` field), not on "the server
|
|
2885
|
+
answered".** A server can be up with zero connected tabs \u2014 \`/status\` returns 200 and there
|
|
2886
|
+
is still nothing to drive, so a lane that adopts it fails later on a timeout that points at
|
|
2887
|
+
your own code. \`hj doctor\` exits non-zero on exactly that, and \`--strict\` turns the advisory
|
|
2888
|
+
warnings into failures so a suspect result is never consumed.
|
|
2889
|
+
|
|
2872
2890
|
When several haltijas run at once (e.g. a project server AND the desktop app),
|
|
2873
2891
|
\`hj servers\` (alias \`hj ls\`) lists them all \u2014 the desktop app is reachable as
|
|
2874
2892
|
\`hj --name desktop\`. If no server owns your directory, \`hj\` falls back to the shared default port
|
|
@@ -3040,6 +3058,11 @@ Two first-party ways to run (plus embedding, below):
|
|
|
3040
3058
|
port a shell targets and WHY; override with \`--port\` or \`--name\`. When several servers
|
|
3041
3059
|
run at once (e.g. a project server AND the desktop app), \`hj servers\` lists them all \u2014
|
|
3042
3060
|
the desktop app is reachable as \`hj --name desktop\`.
|
|
3061
|
+
- **Scripts/CI:** gate on \`hj doctor\` (exits non-zero when the target is not drivable or is
|
|
3062
|
+
ambiguous) or on the \`ready\` field of \`/status\`/\`/windows\` \u2014 NOT on "the server answered".
|
|
3063
|
+
A server can be up with zero connected tabs, so a lane that adopts it fails later on a
|
|
3064
|
+
confusing timeout. \`hj --strict\` (or HALTIJA_STRICT=1) turns the hidden-tab and
|
|
3065
|
+
cross-project warnings into non-zero exits so a suspect result is never consumed.
|
|
3043
3066
|
- **CI:** two engines, and the choice matters \u2014 both need one external browser, but
|
|
3044
3067
|
a *different* one:
|
|
3045
3068
|
- \`haltija --ci\` (or \`--private --app\` for an isolated instance) drives **Electron**
|
|
@@ -3126,7 +3149,7 @@ var COMPONENT_JS = `(() => {
|
|
|
3126
3149
|
});
|
|
3127
3150
|
|
|
3128
3151
|
// src/version.ts
|
|
3129
|
-
var VERSION = "1.6.
|
|
3152
|
+
var VERSION = "1.6.1";
|
|
3130
3153
|
|
|
3131
3154
|
// src/text-selector.ts
|
|
3132
3155
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\\(/;
|
|
@@ -12906,9 +12929,15 @@ var windows = endpoint({
|
|
|
12906
12929
|
summary: "List connected windows",
|
|
12907
12930
|
description: `Returns all connected browser windows/tabs with IDs, URLs, and titles.
|
|
12908
12931
|
|
|
12909
|
-
Response: { windows: [{ id, url, title, focused }] }
|
|
12932
|
+
Response: { windows: [{ id, url, title, focused }], count, ready, hint }
|
|
12933
|
+
|
|
12934
|
+
Use window IDs in other endpoints (e.g., /click, /tree) to target specific tabs.
|
|
12910
12935
|
|
|
12911
|
-
|
|
12936
|
+
**\`ready\` is the signal to gate a test lane on, not "is the server up".** A server can be running
|
|
12937
|
+
with zero connected tabs \u2014 it answers /status 200 but there is nothing to drive, and a lane that
|
|
12938
|
+
adopts it fails later on a confusing timeout. \`ready\` is true when at least one top-level tab is
|
|
12939
|
+
connected. (Hidden tabs count as ready \u2014 they're reachable, just possibly stale; see the hidden-tab
|
|
12940
|
+
warning.) \`hj doctor\` checks this and exits non-zero, so a lane can fail fast on the real cause.`,
|
|
12912
12941
|
category: "windows",
|
|
12913
12942
|
hints: "--json | see: tabs-open, tabs-close, tabs-focus, status"
|
|
12914
12943
|
});
|
|
@@ -13379,9 +13408,13 @@ var status = endpoint({
|
|
|
13379
13408
|
summary: "Server status",
|
|
13380
13409
|
description: `Returns server info and connected browser count.
|
|
13381
13410
|
|
|
13382
|
-
Response: {
|
|
13411
|
+
Response: { serverVersion, ready, windows: [...], browsers: n, desktopApp, pid, ... }
|
|
13383
13412
|
|
|
13384
|
-
Use to verify server is running
|
|
13413
|
+
Use to verify the server is running \u2014 but gate a test lane on **\`ready\`**, not on the 200. A
|
|
13414
|
+
server can be up with zero connected tabs: /status answers fine and there is still nothing to
|
|
13415
|
+
drive, so a lane that adopts it fails later on a timeout that points at the caller's own code.
|
|
13416
|
+
\`ready\` is true when at least one top-level tab is connected. \`hj doctor\` checks this (plus
|
|
13417
|
+
ambiguous targeting) and exits non-zero, which is the one-command preflight for a lane.`,
|
|
13385
13418
|
category: "meta",
|
|
13386
13419
|
hints: "--json | see: windows, stats, console"
|
|
13387
13420
|
});
|
|
@@ -13840,7 +13873,9 @@ function inferSuggestion(step, pageContext) {
|
|
|
13840
13873
|
|
|
13841
13874
|
// src/api-handlers.ts
|
|
13842
13875
|
function withWarning(body, response) {
|
|
13843
|
-
|
|
13876
|
+
if (!response.warning)
|
|
13877
|
+
return body;
|
|
13878
|
+
return response.warningRepeated ? { ...body, warning: response.warning, warningRepeated: true } : { ...body, warning: response.warning };
|
|
13844
13879
|
}
|
|
13845
13880
|
var handlers = new Map;
|
|
13846
13881
|
function registerHandler(endpoint2, handler) {
|
|
@@ -16611,10 +16646,8 @@ async function requestFromBrowser(channel, action, payload, timeoutMs = 5000, wi
|
|
|
16611
16646
|
`);
|
|
16612
16647
|
if (!warning)
|
|
16613
16648
|
return res;
|
|
16614
|
-
|
|
16615
|
-
|
|
16616
|
-
}
|
|
16617
|
-
return { ...res, warning };
|
|
16649
|
+
const repeated = !shouldEmitWarning(warning, recentTabWarnings, Date.now(), TAB_WARN_COOLDOWN_MS);
|
|
16650
|
+
return repeated ? { ...res, warning, warningRepeated: true } : { ...res, warning };
|
|
16618
16651
|
};
|
|
16619
16652
|
const timeout = setTimeout(() => {
|
|
16620
16653
|
pendingResponses.delete(id);
|
|
@@ -17041,6 +17074,7 @@ async function handleRest(req) {
|
|
|
17041
17074
|
return Response.json({
|
|
17042
17075
|
ok: allWindows.length > 0,
|
|
17043
17076
|
windows: windowList,
|
|
17077
|
+
ready: allWindows.filter((w) => (w.windowType || "tab") === "tab").length > 0,
|
|
17044
17078
|
serverVersion: SERVER_VERSION,
|
|
17045
17079
|
pid: process.pid,
|
|
17046
17080
|
recording: activeRecordings > 0,
|
|
@@ -18988,11 +19022,14 @@ ${messageText}`;
|
|
|
18988
19022
|
label: w.label,
|
|
18989
19023
|
windowType: w.windowType || "tab"
|
|
18990
19024
|
}));
|
|
19025
|
+
const drivableTabs = windowList.filter((w) => (w.windowType || "tab") === "tab");
|
|
19026
|
+
const ready = drivableTabs.length > 0;
|
|
18991
19027
|
const hint = windowList.length > 1 ? "Multiple tabs connected. Use ?window=<id> to target specific tab (e.g., /tree?window=abc123)" : windowList.length === 1 ? "One tab connected. Commands automatically target it." : "No tabs connected. Inject the widget into a browser tab.";
|
|
18992
19028
|
return Response.json({
|
|
18993
19029
|
windows: windowList,
|
|
18994
19030
|
focused: focusedWindowId,
|
|
18995
19031
|
count: windowList.length,
|
|
19032
|
+
ready,
|
|
18996
19033
|
hint
|
|
18997
19034
|
}, { headers });
|
|
18998
19035
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -32,6 +32,12 @@ export interface DevResponse {
|
|
|
32
32
|
* mounted. The command succeeded; the number may still be wrong. See src/tab-liveness.ts.
|
|
33
33
|
*/
|
|
34
34
|
warning?: string;
|
|
35
|
+
/**
|
|
36
|
+
* True when `warning` describes a condition already reported within the recent cooldown. The
|
|
37
|
+
* condition still holds — this only says "you've been told". Clients suppress the repeat for
|
|
38
|
+
* humans; strict/CI consumers must fail on `warning` regardless of this flag.
|
|
39
|
+
*/
|
|
40
|
+
warningRepeated?: boolean;
|
|
35
41
|
}
|
|
36
42
|
export interface DomQueryRequest {
|
|
37
43
|
selector?: string;
|
package/dist/version.d.ts
CHANGED
package/docs/CI-INTEGRATION.md
CHANGED
|
@@ -168,29 +168,46 @@ xvfb-run --auto-servernum npx electron . &
|
|
|
168
168
|
|
|
169
169
|
## Waiting for Ready State
|
|
170
170
|
|
|
171
|
-
Don't use `sleep
|
|
171
|
+
Don't use `sleep`, and **don't gate on "the server answered"** — that is the single most common
|
|
172
|
+
way a lane fails confusingly. A haltija server can be up with **zero connected tabs**: `/status`
|
|
173
|
+
returns 200, your lane decides haltija is available and skips starting its own browser, and then
|
|
174
|
+
`navigate` fails with "no browser reachable" — or worse, times out somewhere that looks like your
|
|
175
|
+
code's fault. *Server up ≠ drivable.*
|
|
176
|
+
|
|
177
|
+
Use the built-in preflight, which exits non-zero on exactly that case:
|
|
172
178
|
|
|
173
179
|
```bash
|
|
174
|
-
# Wait
|
|
180
|
+
# Wait until the target is actually drivable (not merely alive)
|
|
175
181
|
for i in $(seq 1 30); do
|
|
176
|
-
if
|
|
177
|
-
echo "Haltija server ready after ${i}s"
|
|
178
|
-
break
|
|
179
|
-
fi
|
|
182
|
+
if hj doctor >/dev/null 2>&1; then break; fi
|
|
180
183
|
sleep 1
|
|
181
184
|
done
|
|
182
185
|
|
|
183
|
-
#
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
186
|
+
hj doctor # print the verdict; exits 1 if it's not drivable or the target is ambiguous
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
`hj doctor` checks, in the order they bite: server reachable → a tab is actually connected → the
|
|
190
|
+
target isn't ambiguous (your cwd matches, or you chose explicitly) → tabs aren't all hidden →
|
|
191
|
+
versions aligned. Add `--json` for machine-readable output.
|
|
192
|
+
|
|
193
|
+
Checking by hand instead? Gate on the **`ready`** field, not on the HTTP status:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
curl -sf http://localhost:8700/status | jq -e '.ready' # true only when a tab is connected
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
And for a lane, add **`--strict`** (or `HALTIJA_STRICT=1`) to your `hj` calls so advisory warnings —
|
|
200
|
+
cross-project targeting, a hidden tab returning stale results — become non-zero exits instead of
|
|
201
|
+
stderr noise your script ignores:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
hj --strict navigate "$URL"
|
|
192
205
|
```
|
|
193
206
|
|
|
207
|
+
Best of all, don't share state at all: `haltija --private --app` (or `--private --headless`) gives
|
|
208
|
+
the lane its own isolated server and browser on an ephemeral port, which nothing else can adopt and
|
|
209
|
+
which tears down with the run.
|
|
210
|
+
|
|
194
211
|
## The hj CLI
|
|
195
212
|
|
|
196
213
|
The `hj` command is installed to `~/.local/bin` when Haltija starts. Add it to PATH:
|
package/llms.txt
CHANGED
|
@@ -42,6 +42,11 @@ Two first-party ways to run (plus embedding, below):
|
|
|
42
42
|
port a shell targets and WHY; override with `--port` or `--name`. When several servers
|
|
43
43
|
run at once (e.g. a project server AND the desktop app), `hj servers` lists them all —
|
|
44
44
|
the desktop app is reachable as `hj --name desktop`.
|
|
45
|
+
- **Scripts/CI:** gate on `hj doctor` (exits non-zero when the target is not drivable or is
|
|
46
|
+
ambiguous) or on the `ready` field of `/status`/`/windows` — NOT on "the server answered".
|
|
47
|
+
A server can be up with zero connected tabs, so a lane that adopts it fails later on a
|
|
48
|
+
confusing timeout. `hj --strict` (or HALTIJA_STRICT=1) turns the hidden-tab and
|
|
49
|
+
cross-project warnings into non-zero exits so a suspect result is never consumed.
|
|
45
50
|
- **CI:** two engines, and the choice matters — both need one external browser, but
|
|
46
51
|
a *different* one:
|
|
47
52
|
- `haltija --ci` (or `--private --app` for an isolated instance) drives **Electron**
|