haltija 1.2.4 → 1.2.7
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/apps/desktop/main.js +16 -3
- package/apps/desktop/package.json +1 -1
- package/apps/desktop/renderer.js +1 -1
- package/apps/desktop/resources/component.js +83 -7
- package/apps/desktop/webview-preload.js +1 -1
- package/bin/cli-subcommand.mjs +109 -7
- package/bin/format-tree.mjs +5 -3
- package/bin/hints.json +1 -1
- package/bin/hj.mjs +16 -1
- package/bin/tosijs-dev.mjs +2 -0
- package/dist/component.js +83 -7
- package/dist/hj.js +108 -9
- package/dist/index.js +186 -28
- package/dist/server.js +186 -28
- package/package.json +1 -1
package/apps/desktop/main.js
CHANGED
|
@@ -36,6 +36,10 @@ const HALTIJA_SERVER = `http://localhost:${HALTIJA_PORT}`
|
|
|
36
36
|
// Combined with webContents.id to create globally unique tab identifiers
|
|
37
37
|
const APP_INSTANCE_ID = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}`
|
|
38
38
|
|
|
39
|
+
// Pending session: when an agent opens a tab with a session, store it here
|
|
40
|
+
// so injectWidget can include it in the widget config for the next new tab
|
|
41
|
+
let pendingTabSession = null
|
|
42
|
+
|
|
39
43
|
// ============================================
|
|
40
44
|
// Preferences
|
|
41
45
|
// ============================================
|
|
@@ -741,8 +745,16 @@ async function injectWidget(webContents) {
|
|
|
741
745
|
// This is stable across navigations (even cross-origin) enabling cross-page recording
|
|
742
746
|
const wsUrl = HALTIJA_SERVER.replace('http:', 'ws:') + '/ws/browser'
|
|
743
747
|
const windowId = `hj-${APP_INSTANCE_ID}-${webContents.id}`
|
|
748
|
+
const configObj = { serverUrl: wsUrl, windowId }
|
|
749
|
+
// Session priority: pending session from agent's tabs/open > env var
|
|
750
|
+
if (pendingTabSession) {
|
|
751
|
+
configObj.session = pendingTabSession
|
|
752
|
+
pendingTabSession = null // Consume — only applies to the next tab
|
|
753
|
+
} else if (process.env.HALTIJA_SESSION) {
|
|
754
|
+
configObj.session = process.env.HALTIJA_SESSION
|
|
755
|
+
}
|
|
744
756
|
await webContents.executeJavaScript(
|
|
745
|
-
`window.__haltija_config__ =
|
|
757
|
+
`window.__haltija_config__ = ${JSON.stringify(configObj)};`,
|
|
746
758
|
)
|
|
747
759
|
await webContents.executeJavaScript(componentCode)
|
|
748
760
|
|
|
@@ -882,9 +894,10 @@ function setupScreenCapture() {
|
|
|
882
894
|
})
|
|
883
895
|
|
|
884
896
|
// Tab management — forwarded to renderer
|
|
885
|
-
ipcMain.handle('open-tab', async (event, url) => {
|
|
897
|
+
ipcMain.handle('open-tab', async (event, url, session) => {
|
|
886
898
|
if (!mainWindow) return false
|
|
887
|
-
|
|
899
|
+
if (session) pendingTabSession = session
|
|
900
|
+
mainWindow.webContents.send('open-tab', { url, session })
|
|
888
901
|
return true
|
|
889
902
|
})
|
|
890
903
|
|
package/apps/desktop/renderer.js
CHANGED
|
@@ -130,7 +130,7 @@ document.addEventListener('keydown', (e) => {
|
|
|
130
130
|
// ============================================
|
|
131
131
|
|
|
132
132
|
// Tab management from widget (via main process IPC)
|
|
133
|
-
window.haltija?.onOpenTab?.((data) => createTab(data.url))
|
|
133
|
+
window.haltija?.onOpenTab?.((data) => createTab(data.url, { session: data.session }))
|
|
134
134
|
window.haltija?.onCloseTab?.((data) => {
|
|
135
135
|
const tab = findTabByWindowId(data.windowId)
|
|
136
136
|
if (tab) closeTab(tab.id)
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
});
|
|
47
47
|
|
|
48
48
|
// src/version.ts
|
|
49
|
-
var VERSION = "1.2.
|
|
49
|
+
var VERSION = "1.2.7";
|
|
50
50
|
|
|
51
51
|
// src/text-selector.ts
|
|
52
52
|
var TEXT_PSEUDO_RE = /:(?:text-is|has-text|text)\(/;
|
|
@@ -1310,9 +1310,22 @@
|
|
|
1310
1310
|
}
|
|
1311
1311
|
return summary;
|
|
1312
1312
|
}
|
|
1313
|
+
function pruneNonInteractive(node) {
|
|
1314
|
+
const isInteractive = node.flags?.interactive || node.flags?.hasEvents;
|
|
1315
|
+
const children = node.children?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
|
|
1316
|
+
const shadowChildren = node.shadowChildren?.map((c) => pruneNonInteractive(c)).filter((c) => c !== null);
|
|
1317
|
+
const hasInteractiveDescendant = children && children.length > 0 || shadowChildren && shadowChildren.length > 0;
|
|
1318
|
+
if (!isInteractive && !hasInteractiveDescendant)
|
|
1319
|
+
return null;
|
|
1320
|
+
return {
|
|
1321
|
+
...node,
|
|
1322
|
+
children: children?.length ? children : undefined,
|
|
1323
|
+
shadowChildren: shadowChildren?.length ? shadowChildren : undefined
|
|
1324
|
+
};
|
|
1325
|
+
}
|
|
1313
1326
|
function buildDomTree(el, options, currentDepth = 0) {
|
|
1314
1327
|
const {
|
|
1315
|
-
depth =
|
|
1328
|
+
depth = -1,
|
|
1316
1329
|
includeText = true,
|
|
1317
1330
|
allAttributes = false,
|
|
1318
1331
|
includeStyles = false,
|
|
@@ -1703,6 +1716,7 @@
|
|
|
1703
1716
|
windowId;
|
|
1704
1717
|
isElectron = false;
|
|
1705
1718
|
browserId = uid();
|
|
1719
|
+
sessionToken = "";
|
|
1706
1720
|
killed = false;
|
|
1707
1721
|
isActive = true;
|
|
1708
1722
|
homeLeft = 0;
|
|
@@ -1775,7 +1789,7 @@
|
|
|
1775
1789
|
selectionBox = null;
|
|
1776
1790
|
highlightedElements = [];
|
|
1777
1791
|
static get observedAttributes() {
|
|
1778
|
-
return ["server", "hidden"];
|
|
1792
|
+
return ["server", "hidden", "session"];
|
|
1779
1793
|
}
|
|
1780
1794
|
static async runTests() {
|
|
1781
1795
|
const el = document.querySelector(TAG_NAME);
|
|
@@ -1871,10 +1885,16 @@
|
|
|
1871
1885
|
}
|
|
1872
1886
|
this.windowId = storedWindowId;
|
|
1873
1887
|
}
|
|
1888
|
+
if (config?.session) {
|
|
1889
|
+
this.sessionToken = config.session;
|
|
1890
|
+
} else {
|
|
1891
|
+
this.sessionToken = uid();
|
|
1892
|
+
}
|
|
1874
1893
|
}
|
|
1875
1894
|
connectedCallback() {
|
|
1876
1895
|
this.killed = false;
|
|
1877
1896
|
this.serverUrl = this.getAttribute("server") || this.serverUrl;
|
|
1897
|
+
this.sessionToken = this.getAttribute("session") || this.sessionToken;
|
|
1878
1898
|
this.render();
|
|
1879
1899
|
const rect = this.getBoundingClientRect();
|
|
1880
1900
|
this.homeLeft = window.innerWidth - rect.width - 16;
|
|
@@ -1902,6 +1922,11 @@
|
|
|
1902
1922
|
this.connect();
|
|
1903
1923
|
}
|
|
1904
1924
|
}
|
|
1925
|
+
if (name === "session") {
|
|
1926
|
+
this.sessionToken = value;
|
|
1927
|
+
this.disconnect();
|
|
1928
|
+
this.connect();
|
|
1929
|
+
}
|
|
1905
1930
|
}
|
|
1906
1931
|
render() {
|
|
1907
1932
|
if (this.shadowRoot.querySelector(".widget")) {
|
|
@@ -2087,6 +2112,25 @@
|
|
|
2087
2112
|
.btn.info-btn:hover {
|
|
2088
2113
|
background: #2563eb;
|
|
2089
2114
|
}
|
|
2115
|
+
.session-badge {
|
|
2116
|
+
display: flex;
|
|
2117
|
+
align-items: center;
|
|
2118
|
+
gap: 2px;
|
|
2119
|
+
font-size: 9px;
|
|
2120
|
+
color: #666;
|
|
2121
|
+
background: rgba(255,255,255,0.05);
|
|
2122
|
+
padding: 2px 4px;
|
|
2123
|
+
border-radius: 3px;
|
|
2124
|
+
cursor: pointer;
|
|
2125
|
+
user-select: none;
|
|
2126
|
+
}
|
|
2127
|
+
.session-badge:hover {
|
|
2128
|
+
background: rgba(255,255,255,0.1);
|
|
2129
|
+
color: #aaa;
|
|
2130
|
+
}
|
|
2131
|
+
.session-badge.copied {
|
|
2132
|
+
color: #22c55e;
|
|
2133
|
+
}
|
|
2090
2134
|
@keyframes pulse {
|
|
2091
2135
|
0%, 100% { opacity: 1; }
|
|
2092
2136
|
50% { opacity: 0.5; }
|
|
@@ -2449,6 +2493,8 @@
|
|
|
2449
2493
|
<span class="logo">\uD83E\uDDDD</span>
|
|
2450
2494
|
</div>
|
|
2451
2495
|
<div class="title">${PRODUCT_NAME}</div>
|
|
2496
|
+
<span class="session-badge" data-action="copy-session" title="Session: ${this.sessionToken}
|
|
2497
|
+
Click to copy session command">${this.sessionToken.slice(0, 8)}</span>
|
|
2452
2498
|
<div class="controls">
|
|
2453
2499
|
<button class="btn" data-action="select" title="Select elements (drag to select area)" aria-label="Select elements">\uD83D\uDC46</button>
|
|
2454
2500
|
<button class="btn" data-action="record" title="Record test (click to start/stop)" aria-label="Record test">REC</button>
|
|
@@ -2535,6 +2581,8 @@
|
|
|
2535
2581
|
this.startSelection();
|
|
2536
2582
|
if (action2 === "stats")
|
|
2537
2583
|
this.copyStatsToClipboard();
|
|
2584
|
+
if (action2 === "copy-session")
|
|
2585
|
+
this.copySessionToken(e.currentTarget);
|
|
2538
2586
|
if (action2 === "close-modal")
|
|
2539
2587
|
this.closeTestModal();
|
|
2540
2588
|
if (action2 === "copy-test")
|
|
@@ -3160,6 +3208,28 @@
|
|
|
3160
3208
|
});
|
|
3161
3209
|
}
|
|
3162
3210
|
}
|
|
3211
|
+
async copySessionToken(badge) {
|
|
3212
|
+
const cmd = `export HALTIJA_SESSION=${this.sessionToken}`;
|
|
3213
|
+
try {
|
|
3214
|
+
await navigator.clipboard.writeText(cmd);
|
|
3215
|
+
badge.textContent = "copied!";
|
|
3216
|
+
badge.classList.add("copied");
|
|
3217
|
+
setTimeout(() => {
|
|
3218
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3219
|
+
badge.classList.remove("copied");
|
|
3220
|
+
}, 1500);
|
|
3221
|
+
} catch {
|
|
3222
|
+
try {
|
|
3223
|
+
await navigator.clipboard.writeText(this.sessionToken);
|
|
3224
|
+
badge.textContent = "copied!";
|
|
3225
|
+
badge.classList.add("copied");
|
|
3226
|
+
setTimeout(() => {
|
|
3227
|
+
badge.textContent = this.sessionToken.slice(0, 8);
|
|
3228
|
+
badge.classList.remove("copied");
|
|
3229
|
+
}, 1500);
|
|
3230
|
+
} catch {}
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3163
3233
|
async copyStatsToClipboard() {
|
|
3164
3234
|
const refStats = refRegistry.getStats();
|
|
3165
3235
|
stats.refsStale = refStats.stale;
|
|
@@ -4926,6 +4996,7 @@ ${elementSummary}${moreText}`;
|
|
|
4926
4996
|
this.send("system", "connected", {
|
|
4927
4997
|
windowId: this.windowId,
|
|
4928
4998
|
browserId: this.browserId,
|
|
4999
|
+
session: this.sessionToken,
|
|
4929
5000
|
version: VERSION2,
|
|
4930
5001
|
serverSessionId: SERVER_SESSION_ID,
|
|
4931
5002
|
url: location.href,
|
|
@@ -5351,7 +5422,7 @@ ${elementSummary}${moreText}`;
|
|
|
5351
5422
|
const haltija = window.haltija;
|
|
5352
5423
|
if (action2 === "open") {
|
|
5353
5424
|
if (haltija?.openTab) {
|
|
5354
|
-
haltija.openTab(payload2.url).then((opened) => {
|
|
5425
|
+
haltija.openTab(payload2.url, payload2.session).then((opened) => {
|
|
5355
5426
|
this.respond(msg2.id, true, { opened });
|
|
5356
5427
|
}).catch((err) => {
|
|
5357
5428
|
this.respond(msg2.id, false, null, err.message);
|
|
@@ -5471,7 +5542,10 @@ ${elementSummary}${moreText}`;
|
|
|
5471
5542
|
const summary = buildActionableSummary(el);
|
|
5472
5543
|
this.respond(msg2.id, true, summary);
|
|
5473
5544
|
} else {
|
|
5474
|
-
|
|
5545
|
+
let tree = buildDomTree(el, request);
|
|
5546
|
+
if (request.interactiveOnly && tree) {
|
|
5547
|
+
tree = pruneNonInteractive(tree);
|
|
5548
|
+
}
|
|
5475
5549
|
if (request.ancestors && tree) {
|
|
5476
5550
|
const ancestors = [];
|
|
5477
5551
|
let parent = el.parentElement;
|
|
@@ -7207,7 +7281,7 @@ ${elementSummary}${moreText}`;
|
|
|
7207
7281
|
}
|
|
7208
7282
|
registerDevChannel();
|
|
7209
7283
|
var WIDGET_ID = "haltija-widget";
|
|
7210
|
-
function inject(serverUrl2 = "wss://localhost:8700/ws/browser") {
|
|
7284
|
+
function inject(serverUrl2 = "wss://localhost:8700/ws/browser", options) {
|
|
7211
7285
|
const existing = document.getElementById(WIDGET_ID);
|
|
7212
7286
|
if (existing) {
|
|
7213
7287
|
console.log(`${LOG_PREFIX} Already injected`);
|
|
@@ -7222,6 +7296,8 @@ ${elementSummary}${moreText}`;
|
|
|
7222
7296
|
const el = DevChannel.elementCreator()();
|
|
7223
7297
|
el.id = WIDGET_ID;
|
|
7224
7298
|
el.setAttribute("server", serverUrl2);
|
|
7299
|
+
if (options?.session)
|
|
7300
|
+
el.setAttribute("session", options.session);
|
|
7225
7301
|
el.setAttribute("data-version", VERSION2);
|
|
7226
7302
|
document.body.appendChild(el);
|
|
7227
7303
|
console.log(`${LOG_PREFIX} Injected`);
|
|
@@ -7233,7 +7309,7 @@ ${elementSummary}${moreText}`;
|
|
|
7233
7309
|
const config = window.__haltija_config__;
|
|
7234
7310
|
if (config?.autoInject !== false) {
|
|
7235
7311
|
if (config) {
|
|
7236
|
-
inject(config.serverUrl || config.wsUrl);
|
|
7312
|
+
inject(config.serverUrl || config.wsUrl, { session: config.session });
|
|
7237
7313
|
return;
|
|
7238
7314
|
}
|
|
7239
7315
|
}
|
|
@@ -25,7 +25,7 @@ contextBridge.exposeInMainWorld('haltija', {
|
|
|
25
25
|
return ipcRenderer.invoke('navigate-url', url)
|
|
26
26
|
},
|
|
27
27
|
// Tab management — routed through main process to renderer
|
|
28
|
-
openTab: (url) => ipcRenderer.invoke('open-tab', url),
|
|
28
|
+
openTab: (url, session) => ipcRenderer.invoke('open-tab', url, session),
|
|
29
29
|
closeTab: (windowId) => ipcRenderer.invoke('close-tab', windowId),
|
|
30
30
|
focusTab: (windowId) => ipcRenderer.invoke('focus-tab', windowId),
|
|
31
31
|
openAgentTab: () => {
|
package/bin/cli-subcommand.mjs
CHANGED
|
@@ -204,7 +204,8 @@ export function parseTreeArgs(args) {
|
|
|
204
204
|
if (a === '--depth' || a === '-d') { body.depth = num(args[++i]); continue }
|
|
205
205
|
if (a === '--selector' || a === '-s') { body.selector = args[++i]; continue }
|
|
206
206
|
if (a === '--compact' || a === '-c') { body.compact = true; continue }
|
|
207
|
-
if (a === '--
|
|
207
|
+
if (a === '--interactive' || a === '-i') { body.interactiveOnly = true; continue }
|
|
208
|
+
if (a === '--visible' || a === '-v') { body.visibleOnly = true; continue }
|
|
208
209
|
if (a === '--text') { body.includeText = true; continue }
|
|
209
210
|
if (a === '--no-text') { body.includeText = false; continue }
|
|
210
211
|
if (a === '--shadow') { body.pierceShadow = true; continue }
|
|
@@ -432,6 +433,18 @@ export function clean(obj) {
|
|
|
432
433
|
return Object.keys(result).length ? result : undefined
|
|
433
434
|
}
|
|
434
435
|
|
|
436
|
+
// ============================================
|
|
437
|
+
// Session ID for multi-agent isolation
|
|
438
|
+
// ============================================
|
|
439
|
+
|
|
440
|
+
export function getSessionId() {
|
|
441
|
+
if (process.env.HALTIJA_SESSION) return process.env.HALTIJA_SESSION
|
|
442
|
+
// Auto-generate a session ID for this shell — persisted in env for subsequent calls
|
|
443
|
+
const id = `hj_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`
|
|
444
|
+
process.env.HALTIJA_SESSION = id
|
|
445
|
+
return id
|
|
446
|
+
}
|
|
447
|
+
|
|
435
448
|
// ============================================
|
|
436
449
|
// Server auto-start
|
|
437
450
|
// ============================================
|
|
@@ -509,14 +522,92 @@ async function startServerInBackground(port) {
|
|
|
509
522
|
return false
|
|
510
523
|
}
|
|
511
524
|
|
|
525
|
+
// ============================================
|
|
526
|
+
// Auto-launch Electron app when no browser windows connected
|
|
527
|
+
// ============================================
|
|
528
|
+
|
|
529
|
+
async function launchElectronApp() {
|
|
530
|
+
const { execSync, spawn: spawnChild } = await import('child_process')
|
|
531
|
+
|
|
532
|
+
if (process.platform === 'darwin') {
|
|
533
|
+
// Check common locations for Haltija.app
|
|
534
|
+
const appPaths = [
|
|
535
|
+
'/Applications/Haltija.app',
|
|
536
|
+
`${process.env.HOME}/Applications/Haltija.app`,
|
|
537
|
+
]
|
|
538
|
+
for (const p of appPaths) {
|
|
539
|
+
if (existsSync(p)) {
|
|
540
|
+
spawnChild('open', ['-a', p], { stdio: 'ignore', detached: true }).unref()
|
|
541
|
+
return true
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// Try spotlight search as fallback
|
|
545
|
+
try {
|
|
546
|
+
const result = execSync('mdfind "kMDItemCFBundleIdentifier == com.electron.haltija" | head -1', { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'ignore'] }).trim()
|
|
547
|
+
if (result) {
|
|
548
|
+
spawnChild('open', ['-a', result], { stdio: 'ignore', detached: true }).unref()
|
|
549
|
+
return true
|
|
550
|
+
}
|
|
551
|
+
} catch {}
|
|
552
|
+
return false
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// Linux/Windows: not yet supported
|
|
556
|
+
return false
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
async function ensureBrowserConnected(port) {
|
|
560
|
+
try {
|
|
561
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
562
|
+
signal: AbortSignal.timeout(2000)
|
|
563
|
+
})
|
|
564
|
+
const status = await resp.json()
|
|
565
|
+
// Use status.ok (global, not session-filtered) to check if any browser is connected
|
|
566
|
+
if (status.ok) return true
|
|
567
|
+
} catch { return false }
|
|
568
|
+
|
|
569
|
+
// No windows connected — try to launch Electron app (macOS only)
|
|
570
|
+
if (process.platform !== 'darwin') return false
|
|
571
|
+
|
|
572
|
+
process.stderr.write('\x1b[2mLaunching Haltija browser...\x1b[0m')
|
|
573
|
+
const launched = await launchElectronApp()
|
|
574
|
+
if (!launched) {
|
|
575
|
+
process.stderr.write('\x1b[2m not found\x1b[0m\n')
|
|
576
|
+
return false
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
// Wait for a window to connect (up to 10s)
|
|
580
|
+
const maxWait = 10000
|
|
581
|
+
const start = Date.now()
|
|
582
|
+
while (Date.now() - start < maxWait) {
|
|
583
|
+
try {
|
|
584
|
+
const resp = await fetch(`http://localhost:${port}/status`, {
|
|
585
|
+
signal: AbortSignal.timeout(1000)
|
|
586
|
+
})
|
|
587
|
+
const status = await resp.json()
|
|
588
|
+
if (status.ok) {
|
|
589
|
+
process.stderr.write('\x1b[2m ready\x1b[0m\n')
|
|
590
|
+
return true
|
|
591
|
+
}
|
|
592
|
+
} catch {}
|
|
593
|
+
await new Promise(r => setTimeout(r, 500))
|
|
594
|
+
}
|
|
595
|
+
process.stderr.write('\x1b[2m timeout\x1b[0m\n')
|
|
596
|
+
return false
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
// Commands that don't need a browser window to be connected
|
|
600
|
+
const INFO_COMMANDS = new Set(['status', 'windows', 'version', 'help'])
|
|
601
|
+
|
|
512
602
|
// ============================================
|
|
513
603
|
// Main subcommand execution
|
|
514
604
|
// ============================================
|
|
515
605
|
|
|
516
|
-
export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
606
|
+
export async function runSubcommand(subcommand, subArgs, port = '8700', options = {}) {
|
|
517
607
|
const baseUrl = `http://localhost:${port}`
|
|
518
608
|
const jsonOutput = subArgs.includes('--json')
|
|
519
|
-
|
|
609
|
+
const noLaunch = options.noLaunch || false
|
|
610
|
+
// Remove --json and extract --window/--session before processing
|
|
520
611
|
let filteredArgs = subArgs.filter(a => a !== '--json')
|
|
521
612
|
let targetWindowId = undefined
|
|
522
613
|
const windowIdx = filteredArgs.indexOf('--window')
|
|
@@ -524,6 +615,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
524
615
|
targetWindowId = filteredArgs[windowIdx + 1]
|
|
525
616
|
filteredArgs = [...filteredArgs.slice(0, windowIdx), ...filteredArgs.slice(windowIdx + 2)]
|
|
526
617
|
}
|
|
618
|
+
const sessionIdx = filteredArgs.indexOf('--session')
|
|
619
|
+
if (sessionIdx !== -1) {
|
|
620
|
+
process.env.HALTIJA_SESSION = filteredArgs[sessionIdx + 1]
|
|
621
|
+
filteredArgs = [...filteredArgs.slice(0, sessionIdx), ...filteredArgs.slice(sessionIdx + 2)]
|
|
622
|
+
}
|
|
527
623
|
|
|
528
624
|
// Check if server is running, auto-start if not
|
|
529
625
|
if (!(await isServerRunning(port))) {
|
|
@@ -538,6 +634,11 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
538
634
|
}
|
|
539
635
|
}
|
|
540
636
|
|
|
637
|
+
// Auto-launch browser if no windows connected (skip for info commands and --no-launch)
|
|
638
|
+
if (!noLaunch && !INFO_COMMANDS.has(subcommand)) {
|
|
639
|
+
await ensureBrowserConnected(port)
|
|
640
|
+
}
|
|
641
|
+
|
|
541
642
|
// Special handling for 'send' command - route to appropriate endpoint
|
|
542
643
|
// hj send selection [agent] → /send/selection
|
|
543
644
|
// hj send recording [agent] → /send/recording
|
|
@@ -596,9 +697,10 @@ export async function runSubcommand(subcommand, subArgs, port = '8700') {
|
|
|
596
697
|
async function doRequest(url, method, body, context = {}) {
|
|
597
698
|
const { subcommand, jsonOutput } = context
|
|
598
699
|
try {
|
|
599
|
-
const
|
|
700
|
+
const sessionId = getSessionId()
|
|
701
|
+
const opts = { method, headers: { 'X-Haltija-Session': sessionId } }
|
|
600
702
|
if (body) {
|
|
601
|
-
opts.headers
|
|
703
|
+
opts.headers['Content-Type'] = 'application/json'
|
|
602
704
|
opts.body = JSON.stringify(body)
|
|
603
705
|
}
|
|
604
706
|
|
|
@@ -610,7 +712,7 @@ async function doRequest(url, method, body, context = {}) {
|
|
|
610
712
|
|
|
611
713
|
// Text format for supported subcommands (unless --json)
|
|
612
714
|
if (!jsonOutput && subcommand === 'tree' && json.success && json.data) {
|
|
613
|
-
console.log(formatTree(json.data))
|
|
715
|
+
console.log(formatTree(json.data, 0, { depth: body?.depth }))
|
|
614
716
|
} else if (!jsonOutput && subcommand === 'events' && (json.events || Array.isArray(json))) {
|
|
615
717
|
console.log(formatEvents(json))
|
|
616
718
|
} else if (!jsonOutput && subcommand === 'test-run' && json.test) {
|
|
@@ -730,7 +832,7 @@ export function listSubcommands() {
|
|
|
730
832
|
return `
|
|
731
833
|
Subcommands (replace curl with simple commands):
|
|
732
834
|
${bold('Inspect')}
|
|
733
|
-
tree [selector] [-d
|
|
835
|
+
tree [selector] [-d N] [-i] [-v] DOM tree (full depth, -i=interactive, -v=visible)
|
|
734
836
|
query <selector> Find elements matching selector
|
|
735
837
|
inspect <@ref|selector> Detailed element info
|
|
736
838
|
inspectAll <selector> Deep inspect all matches
|
package/bin/format-tree.mjs
CHANGED
|
@@ -27,15 +27,17 @@ const MAX_TEXT_LEN = 80
|
|
|
27
27
|
* @param {number} indent - current indentation level (internal)
|
|
28
28
|
* @returns {string} formatted text output with footer
|
|
29
29
|
*/
|
|
30
|
-
export function formatTree(node, indent = 0) {
|
|
30
|
+
export function formatTree(node, indent = 0, { depth } = {}) {
|
|
31
31
|
if (!node) return ''
|
|
32
32
|
|
|
33
33
|
const lines = []
|
|
34
34
|
formatNode(node, indent, lines)
|
|
35
35
|
|
|
36
|
-
// Footer:
|
|
36
|
+
// Footer: options hint
|
|
37
|
+
const d = depth ?? -1
|
|
38
|
+
const depthLabel = d === -1 ? 'unlimited' : String(d)
|
|
37
39
|
lines.push('---')
|
|
38
|
-
lines.push(
|
|
40
|
+
lines.push(`depth=${depthLabel} | -d N | -i (interactive) | --visible | --json`)
|
|
39
41
|
|
|
40
42
|
return lines.join('\n')
|
|
41
43
|
}
|
package/bin/hints.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"tree": "-d
|
|
2
|
+
"tree": "-d 3 (shallow), -i (interactive only), --visible, --compact | see: inspect, query, click",
|
|
3
3
|
"query": "@ref or \"selector\", --all | see: tree, inspect",
|
|
4
4
|
"inspect": "@ref or \"selector\", --styles, --rules, --ancestors | see: tree, query",
|
|
5
5
|
"click": "@ref or \"selector\", :text(Button), --diff | see: tree, wait, type",
|
package/bin/hj.mjs
CHANGED
|
@@ -36,6 +36,21 @@ if (portIdx !== -1 && args[portIdx + 1]) {
|
|
|
36
36
|
args.splice(portIdx, 2)
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
// Parse --session option (set session token)
|
|
40
|
+
const sessionIdx = args.indexOf('--session')
|
|
41
|
+
if (sessionIdx !== -1 && args[sessionIdx + 1]) {
|
|
42
|
+
process.env.HALTIJA_SESSION = args[sessionIdx + 1]
|
|
43
|
+
args.splice(sessionIdx, 2)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Parse --no-launch option (skip auto-launching Electron app)
|
|
47
|
+
let noLaunch = false
|
|
48
|
+
const noLaunchIdx = args.indexOf('--no-launch')
|
|
49
|
+
if (noLaunchIdx !== -1) {
|
|
50
|
+
noLaunch = true
|
|
51
|
+
args.splice(noLaunchIdx, 1)
|
|
52
|
+
}
|
|
53
|
+
|
|
39
54
|
const subcommand = args[0]
|
|
40
55
|
const subArgs = args.slice(1).filter(a => a !== '--window' || true) // keep all args
|
|
41
56
|
|
|
@@ -61,7 +76,7 @@ if (!isSubcommand(subcommand)) {
|
|
|
61
76
|
console.error(`Run 'hj' for docs.`)
|
|
62
77
|
process.exit(1)
|
|
63
78
|
} else {
|
|
64
|
-
runSubcommand(subcommand, subArgs, port)
|
|
79
|
+
runSubcommand(subcommand, subArgs, port, { noLaunch })
|
|
65
80
|
}
|
|
66
81
|
|
|
67
82
|
function filterHelp(topic) {
|
package/bin/tosijs-dev.mjs
CHANGED
|
@@ -362,6 +362,8 @@ if (docsDirIdx !== -1 && args[docsDirIdx + 1]) {
|
|
|
362
362
|
// ============================================
|
|
363
363
|
|
|
364
364
|
const ciMode = args.includes('--ci')
|
|
365
|
+
const secureMode = args.includes('--secure')
|
|
366
|
+
if (secureMode) process.env.HALTIJA_SECURE = '1'
|
|
365
367
|
const headlessMode = args.includes('--headless') // Playwright headless (separate from CI)
|
|
366
368
|
const headlessUrlIdx = args.indexOf('--headless-url')
|
|
367
369
|
const headlessUrl = headlessUrlIdx !== -1 ? args[headlessUrlIdx + 1] : null
|