harnery 0.41.0 → 0.42.0
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/dist/commander.js +3 -0
- package/dist/commands/agents.d.ts.map +1 -1
- package/dist/commands/agents.js +9 -1
- package/dist/commands/artifacts.d.ts.map +1 -1
- package/dist/commands/artifacts.js +113 -12
- package/dist/commands/browse.d.ts +164 -0
- package/dist/commands/browse.d.ts.map +1 -1
- package/dist/commands/browse.js +263 -51
- package/dist/commands/claude-desktop.d.ts.map +1 -1
- package/dist/commands/claude-desktop.js +52 -0
- package/dist/commands/disk.d.ts +6 -0
- package/dist/commands/disk.d.ts.map +1 -0
- package/dist/commands/disk.js +80 -0
- package/dist/commands/doctor.d.ts +8 -0
- package/dist/commands/doctor.d.ts.map +1 -1
- package/dist/commands/doctor.js +48 -1
- package/dist/commands/files.d.ts +16 -0
- package/dist/commands/files.d.ts.map +1 -1
- package/dist/commands/files.js +54 -2
- package/dist/commands/init.d.ts +2 -2
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +43 -9
- package/dist/commands/rm.d.ts +4 -0
- package/dist/commands/rm.d.ts.map +1 -0
- package/dist/commands/rm.js +87 -0
- package/dist/commands/servers.d.ts +7 -0
- package/dist/commands/servers.d.ts.map +1 -0
- package/dist/commands/servers.js +394 -0
- package/dist/commands/tunnel.d.ts.map +1 -1
- package/dist/commands/tunnel.js +82 -2
- package/dist/core/adapters/profiles.d.ts +9 -0
- package/dist/core/adapters/profiles.d.ts.map +1 -1
- package/dist/core/adapters/profiles.js +22 -1
- package/dist/core/adapters/types.d.ts +9 -0
- package/dist/core/adapters/types.d.ts.map +1 -1
- package/dist/core/agents/cli.js +13 -6
- package/dist/core/agents/render/session-context.d.ts.map +1 -1
- package/dist/core/agents/render/session-context.js +2 -0
- package/dist/core/agents/state/live-coordination-view.d.ts +20 -0
- package/dist/core/agents/state/live-coordination-view.d.ts.map +1 -1
- package/dist/core/agents/state/live-coordination-view.js +60 -0
- package/dist/core/artifacts/delivery-card.js +9 -2
- package/dist/core/artifacts/index.d.ts +140 -4
- package/dist/core/artifacts/index.d.ts.map +1 -1
- package/dist/core/artifacts/index.js +522 -49
- package/dist/core/config.d.ts +68 -11
- package/dist/core/config.d.ts.map +1 -1
- package/dist/core/config.js +128 -3
- package/dist/core/diagnostics/bundle.d.ts.map +1 -1
- package/dist/core/diagnostics/bundle.js +2 -2
- package/dist/core/events/v3/producers/recorder.d.ts.map +1 -1
- package/dist/core/events/v3/producers/recorder.js +16 -0
- package/dist/core/events/v3/projection.js +9 -1
- package/dist/core/hooks/adapter/events.d.ts +17 -0
- package/dist/core/hooks/adapter/events.d.ts.map +1 -1
- package/dist/core/hooks/adapter/events.js +5 -0
- package/dist/core/hooks/adapter/runtime-telemetry.js +3 -1
- package/dist/core/hooks/adapter/wiring.d.ts +37 -7
- package/dist/core/hooks/adapter/wiring.d.ts.map +1 -1
- package/dist/core/hooks/adapter/wiring.js +62 -4
- package/dist/core/hooks/cli.js +45 -6
- package/dist/core/hooks/run-markers.d.ts +37 -0
- package/dist/core/hooks/run-markers.d.ts.map +1 -0
- package/dist/core/hooks/run-markers.js +109 -0
- package/dist/core/servers/index.d.ts +211 -0
- package/dist/core/servers/index.d.ts.map +1 -0
- package/dist/core/servers/index.js +560 -0
- package/dist/core/servers/net.d.ts +58 -0
- package/dist/core/servers/net.d.ts.map +1 -0
- package/dist/core/servers/net.js +270 -0
- package/dist/core/servers/tunnels.d.ts +14 -0
- package/dist/core/servers/tunnels.d.ts.map +1 -0
- package/dist/core/servers/tunnels.js +33 -0
- package/dist/core/storage/builtins.js +1 -0
- package/dist/core/workflow/engine.d.ts.map +1 -1
- package/dist/core/workflow/engine.js +30 -1
- package/dist/core/workflow/sandbox-projection.d.ts +13 -2
- package/dist/core/workflow/sandbox-projection.d.ts.map +1 -1
- package/dist/core/workflow/sandbox-projection.js +23 -0
- package/dist/core/workflow/spawn-claude.d.ts.map +1 -1
- package/dist/core/workflow/spawn-claude.js +6 -1
- package/dist/core/workflow/spawn-codex.d.ts +2 -0
- package/dist/core/workflow/spawn-codex.d.ts.map +1 -1
- package/dist/core/workflow/spawn-codex.js +20 -1
- package/dist/core/workflow/spawn-cursor.d.ts.map +1 -1
- package/dist/core/workflow/spawn-cursor.js +6 -1
- package/dist/core/workflow/spawn-opencode.d.ts.map +1 -1
- package/dist/core/workflow/spawn-opencode.js +4 -1
- package/dist/core/workflow/types.d.ts +47 -2
- package/dist/core/workflow/types.d.ts.map +1 -1
- package/dist/core/workflow/worker-access.d.ts +92 -0
- package/dist/core/workflow/worker-access.d.ts.map +1 -0
- package/dist/core/workflow/worker-access.js +450 -0
- package/dist/lib/browser/batch-steps.d.ts +18 -0
- package/dist/lib/browser/batch-steps.d.ts.map +1 -0
- package/dist/lib/browser/batch-steps.js +82 -0
- package/dist/lib/browser/collect/collector.d.ts +99 -0
- package/dist/lib/browser/collect/collector.d.ts.map +1 -0
- package/dist/lib/browser/collect/collector.js +202 -0
- package/dist/lib/browser/collect/fields.d.ts +31 -0
- package/dist/lib/browser/collect/fields.d.ts.map +1 -0
- package/dist/lib/browser/collect/fields.js +75 -0
- package/dist/lib/browser/collect/index.d.ts +6 -0
- package/dist/lib/browser/collect/index.d.ts.map +1 -0
- package/dist/lib/browser/collect/index.js +5 -0
- package/dist/lib/browser/collect/page-driver.d.ts +14 -0
- package/dist/lib/browser/collect/page-driver.d.ts.map +1 -0
- package/dist/lib/browser/collect/page-driver.js +306 -0
- package/dist/lib/browser/collect/presets.d.ts +52 -0
- package/dist/lib/browser/collect/presets.d.ts.map +1 -0
- package/dist/lib/browser/collect/presets.js +176 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts +85 -0
- package/dist/lib/browser/collect/scroll-plan.d.ts.map +1 -0
- package/dist/lib/browser/collect/scroll-plan.js +118 -0
- package/dist/lib/browser/index.d.ts +1 -1
- package/dist/lib/browser/index.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.d.ts +2 -0
- package/dist/lib/browser/page-review-judge.d.ts.map +1 -1
- package/dist/lib/browser/page-review-judge.js +13 -0
- package/dist/lib/browser/qa-run-contracts.d.ts +10 -0
- package/dist/lib/browser/qa-run-contracts.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.d.ts.map +1 -1
- package/dist/lib/browser/qa-run.js +1 -0
- package/dist/lib/browser/windows-chrome.d.ts +59 -0
- package/dist/lib/browser/windows-chrome.d.ts.map +1 -0
- package/dist/lib/browser/windows-chrome.js +179 -0
- package/dist/lib/claude-desktop-share.d.ts +136 -0
- package/dist/lib/claude-desktop-share.d.ts.map +1 -0
- package/dist/lib/claude-desktop-share.js +394 -0
- package/dist/lib/disk-usage.d.ts +50 -0
- package/dist/lib/disk-usage.d.ts.map +1 -0
- package/dist/lib/disk-usage.js +303 -0
- package/dist/lib/docs-lint.d.ts +6 -0
- package/dist/lib/docs-lint.d.ts.map +1 -1
- package/dist/lib/docs-lint.js +8 -3
- package/dist/lib/guarded-remove.d.ts +34 -0
- package/dist/lib/guarded-remove.d.ts.map +1 -0
- package/dist/lib/guarded-remove.js +310 -0
- package/dist/lib/instructions/templates.js +8 -8
- package/dist/lib/tunnel/error-page.d.ts +1 -1
- package/dist/lib/tunnel/error-page.d.ts.map +1 -1
- package/dist/lib/tunnel/error-page.js +19 -8
- package/dist/lib/tunnel/gate.js +20 -1
- package/dist/lib/tunnel/path-scope.d.ts +22 -0
- package/dist/lib/tunnel/path-scope.d.ts.map +1 -0
- package/dist/lib/tunnel/path-scope.js +62 -0
- package/dist/lib/tunnel/state.d.ts +5 -0
- package/dist/lib/tunnel/state.d.ts.map +1 -1
- package/dist/lib/tunnel/state.js +17 -3
- package/package.json +6 -1
- package/schemas/config.schema.json +91 -0
- package/src/commander.ts +19 -0
- package/src/commands/agents.ts +14 -0
- package/src/commands/artifacts.ts +171 -19
- package/src/commands/browse.ts +400 -57
- package/src/commands/claude-desktop.ts +61 -0
- package/src/commands/disk.ts +114 -0
- package/src/commands/doctor.ts +52 -1
- package/src/commands/files.ts +93 -2
- package/src/commands/init.ts +55 -5
- package/src/commands/rm.ts +115 -0
- package/src/commands/servers.ts +506 -0
- package/src/commands/tunnel.ts +102 -2
- package/src/core/adapters/profiles.ts +22 -1
- package/src/core/adapters/types.ts +10 -0
- package/src/core/agents/cli.ts +14 -6
- package/src/core/agents/render/session-context.ts +1 -0
- package/src/core/agents/state/live-coordination-view.ts +73 -0
- package/src/core/artifacts/delivery-card.ts +10 -2
- package/src/core/artifacts/index.ts +697 -44
- package/src/core/config.ts +183 -4
- package/src/core/diagnostics/bundle.ts +2 -1
- package/src/core/events/v3/producers/recorder.ts +16 -0
- package/src/core/events/v3/projection.ts +11 -1
- package/src/core/hooks/adapter/events.ts +19 -0
- package/src/core/hooks/adapter/runtime-telemetry.ts +3 -1
- package/src/core/hooks/adapter/wiring.ts +116 -8
- package/src/core/hooks/cli.ts +47 -6
- package/src/core/hooks/run-markers.ts +132 -0
- package/src/core/servers/index.ts +712 -0
- package/src/core/servers/net.ts +283 -0
- package/src/core/servers/tunnels.ts +39 -0
- package/src/core/storage/builtins.ts +1 -0
- package/src/core/workflow/engine.ts +38 -1
- package/src/core/workflow/sandbox-projection.ts +38 -2
- package/src/core/workflow/spawn-claude.ts +5 -1
- package/src/core/workflow/spawn-codex.ts +18 -1
- package/src/core/workflow/spawn-cursor.ts +5 -1
- package/src/core/workflow/spawn-opencode.ts +4 -1
- package/src/core/workflow/types.ts +62 -2
- package/src/core/workflow/worker-access.ts +525 -0
- package/src/lib/browser/batch-steps.ts +79 -0
- package/src/lib/browser/collect/collector.ts +306 -0
- package/src/lib/browser/collect/fields.ts +87 -0
- package/src/lib/browser/collect/index.ts +47 -0
- package/src/lib/browser/collect/page-driver.ts +327 -0
- package/src/lib/browser/collect/presets.ts +237 -0
- package/src/lib/browser/collect/scroll-plan.ts +179 -0
- package/src/lib/browser/index.ts +1 -0
- package/src/lib/browser/page-review-judge.ts +16 -0
- package/src/lib/browser/qa-run-contracts.ts +11 -0
- package/src/lib/browser/qa-run.ts +1 -0
- package/src/lib/browser/windows-chrome.ts +227 -0
- package/src/lib/claude-desktop-share.ts +511 -0
- package/src/lib/disk-usage.ts +347 -0
- package/src/lib/docs-lint.ts +8 -3
- package/src/lib/guarded-remove.ts +376 -0
- package/src/lib/instructions/templates.ts +8 -8
- package/src/lib/tunnel/error-page.ts +25 -9
- package/src/lib/tunnel/gate.ts +22 -1
- package/src/lib/tunnel/path-scope.ts +73 -0
- package/src/lib/tunnel/state.ts +20 -3
package/src/commands/browse.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { chromium } from "playwright";
|
|
|
7
7
|
import { PNG } from "pngjs";
|
|
8
8
|
import type { EmitContext, HarneryProgramContext } from "../commander.ts";
|
|
9
9
|
import { resolveBinName } from "../core/config.ts";
|
|
10
|
+
import { splitBatchSteps } from "../lib/browser/batch-steps.ts";
|
|
10
11
|
import {
|
|
11
12
|
CAPTURE_FIDELITY_MISMATCH_THRESHOLD,
|
|
12
13
|
compareBand,
|
|
@@ -15,6 +16,22 @@ import {
|
|
|
15
16
|
type FidelityProbe,
|
|
16
17
|
pngDimensions,
|
|
17
18
|
} from "../lib/browser/capture-fidelity.ts";
|
|
19
|
+
import {
|
|
20
|
+
type CollectLimits,
|
|
21
|
+
createPageDriver,
|
|
22
|
+
createRng,
|
|
23
|
+
DEFAULT_COLLECT_LIMITS,
|
|
24
|
+
PACING_LIMITS,
|
|
25
|
+
parseProbability,
|
|
26
|
+
parseRange,
|
|
27
|
+
presetNames,
|
|
28
|
+
type ResolvedCollectSpec,
|
|
29
|
+
randomSeed,
|
|
30
|
+
resolveCollectSpec,
|
|
31
|
+
runCollect,
|
|
32
|
+
type ScrollPacing,
|
|
33
|
+
scrollPacingFromEnv,
|
|
34
|
+
} from "../lib/browser/collect/index.ts";
|
|
18
35
|
import {
|
|
19
36
|
type AssertResult,
|
|
20
37
|
Browser,
|
|
@@ -50,6 +67,7 @@ import {
|
|
|
50
67
|
writeNetscapeCookieFile,
|
|
51
68
|
wslHeadedLaunchArgs,
|
|
52
69
|
} from "../lib/browser/index.ts";
|
|
70
|
+
import { isWSL } from "../lib/browser/launch-args.ts";
|
|
53
71
|
import {
|
|
54
72
|
allocateTileBudget,
|
|
55
73
|
validatePageReviewAllocation,
|
|
@@ -113,7 +131,16 @@ import {
|
|
|
113
131
|
type SaveBaselineResult,
|
|
114
132
|
saveBaseline,
|
|
115
133
|
} from "../lib/browser/visual-diff.ts";
|
|
116
|
-
import {
|
|
134
|
+
import {
|
|
135
|
+
harvestDomain,
|
|
136
|
+
harvestWindowsChromeCookies,
|
|
137
|
+
stopWindowsChromeProfile,
|
|
138
|
+
windowsChromePath,
|
|
139
|
+
windowsLocalAppData,
|
|
140
|
+
windowsPlainProfile,
|
|
141
|
+
wslPathToWindows,
|
|
142
|
+
} from "../lib/browser/windows-chrome.ts";
|
|
143
|
+
import { applyExtraCookies, CookieJar, mergeCookies } from "../lib/cookies/index.ts";
|
|
117
144
|
import { commandPaceGate } from "../lib/pace/index.ts";
|
|
118
145
|
|
|
119
146
|
/**
|
|
@@ -167,9 +194,28 @@ interface BrowseOpts {
|
|
|
167
194
|
evaluate?: string;
|
|
168
195
|
captureEvaluate?: string;
|
|
169
196
|
batch?: string;
|
|
197
|
+
// Scroll-and-collect mode (--collect [preset])
|
|
198
|
+
collect?: string | boolean;
|
|
199
|
+
collectItem?: string;
|
|
200
|
+
collectKey?: string;
|
|
201
|
+
collectKeyPattern?: string;
|
|
202
|
+
collectField?: string[];
|
|
203
|
+
collectExpand?: string | boolean;
|
|
204
|
+
collectMaxItems?: string;
|
|
205
|
+
collectMaxScrolls?: string;
|
|
206
|
+
collectMaxSeconds?: string;
|
|
207
|
+
collectIdle?: string;
|
|
208
|
+
collectStep?: string;
|
|
209
|
+
collectPause?: string;
|
|
210
|
+
collectReadChance?: string;
|
|
211
|
+
collectReadPause?: string;
|
|
212
|
+
collectBackChance?: string;
|
|
213
|
+
collectSeed?: string;
|
|
214
|
+
collectFormat?: string;
|
|
215
|
+
collectOut?: string;
|
|
170
216
|
networkHar?: string;
|
|
171
217
|
login?: boolean;
|
|
172
|
-
|
|
218
|
+
attached?: boolean;
|
|
173
219
|
loginCloseFile?: string;
|
|
174
220
|
controlFile?: string;
|
|
175
221
|
headed?: boolean;
|
|
@@ -345,8 +391,69 @@ export function registerBrowseCommand(
|
|
|
345
391
|
"--batch <steps>",
|
|
346
392
|
"Run multiple steps in one session, semicolon-separated. Each step is one of: " +
|
|
347
393
|
"`click <selector>`, `fill <selector=>value>`, `press <key>`, `wait <selector|ms>`, `eval <js>`, `reload`. " +
|
|
348
|
-
'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state.'
|
|
394
|
+
'Example: `--batch "click button; wait 1500; reload; wait 3000"`. `reload` preserves sessionStorage + cookies, which is how to repro sessionStorage-restored UI state. ' +
|
|
395
|
+
"An `eval` step keeps semicolons inside JS quotes and (), [], {}; `\\;` is always a literal semicolon.",
|
|
349
396
|
)
|
|
397
|
+
.option(
|
|
398
|
+
"--collect [preset]",
|
|
399
|
+
"Scroll an infinite feed like a reader and collect every item that passes through the DOM, " +
|
|
400
|
+
`de-duplicated, as JSON. Presets: ${presetNames().join(", ")} (default generic). ` +
|
|
401
|
+
"Skips the trio, checks and --evaluate; runs after --wait-for and --batch.",
|
|
402
|
+
)
|
|
403
|
+
.option("--collect-item <css>", "Item selector (overrides the preset's).")
|
|
404
|
+
.option(
|
|
405
|
+
"--collect-key <extractor>",
|
|
406
|
+
"Dedupe key extractor, <selector>[@attr] inside the item. Default: the preset key, else the " +
|
|
407
|
+
"first permalink-like link, else a hash of the item text.",
|
|
408
|
+
)
|
|
409
|
+
.option("--collect-key-pattern <regex>", "Regex a link must match to count as a permalink key.")
|
|
410
|
+
.option(
|
|
411
|
+
"--collect-field <name=extractor>",
|
|
412
|
+
"Add or override a field: name=<selector>[@attr]. name[]= collects every match. Empty selector " +
|
|
413
|
+
"or :scope is the item itself; no @attr reads visible text (repeatable).",
|
|
414
|
+
(value: string, previous: string[] = []) => [...previous, value],
|
|
415
|
+
[] as string[],
|
|
416
|
+
)
|
|
417
|
+
.option(
|
|
418
|
+
"--collect-expand [selector]",
|
|
419
|
+
"Click 'show more' buttons inside visible items before harvesting (preset selector by default). " +
|
|
420
|
+
"At most 3 per step and 50 per run; links and action buttons are never clicked.",
|
|
421
|
+
)
|
|
422
|
+
.option("--collect-max-items <n>", "Stop after this many unique items.")
|
|
423
|
+
.option("--collect-max-scrolls <n>", "Stop after this many scroll steps (default 60).", "60")
|
|
424
|
+
.option("--collect-max-seconds <n>", "Stop after this much wall time (default 300).", "300")
|
|
425
|
+
.option(
|
|
426
|
+
"--collect-idle <n>",
|
|
427
|
+
"Stop after this many steps with no new items and no page growth (default 5).",
|
|
428
|
+
"5",
|
|
429
|
+
)
|
|
430
|
+
.option(
|
|
431
|
+
"--collect-step <min-max>",
|
|
432
|
+
"Scroll distance per step as a viewport fraction (default 0.6-1.1; env HARNERY_COLLECT_STEP).",
|
|
433
|
+
)
|
|
434
|
+
.option(
|
|
435
|
+
"--collect-pause <min-max>",
|
|
436
|
+
"Pause after each step in ms (default 1200-3500; env HARNERY_COLLECT_PAUSE_MS).",
|
|
437
|
+
)
|
|
438
|
+
.option(
|
|
439
|
+
"--collect-read-chance <p>",
|
|
440
|
+
"Chance of a longer reading pause per step (default 0.12).",
|
|
441
|
+
)
|
|
442
|
+
.option("--collect-read-pause <min-max>", "Reading pause in ms (default 4000-9000).")
|
|
443
|
+
.option(
|
|
444
|
+
"--collect-back-chance <p>",
|
|
445
|
+
"Chance a step starts with a small scroll back up (default 0.08).",
|
|
446
|
+
)
|
|
447
|
+
.option(
|
|
448
|
+
"--collect-seed <n>",
|
|
449
|
+
"Seed for the scroll schedule; the envelope reports the seed used.",
|
|
450
|
+
)
|
|
451
|
+
.option(
|
|
452
|
+
"--collect-format <format>",
|
|
453
|
+
"json (envelope, default) or jsonl (one item per line).",
|
|
454
|
+
"json",
|
|
455
|
+
)
|
|
456
|
+
.option("--collect-out <path>", "Write the collection to this file instead of stdout.")
|
|
350
457
|
.option("--network-har <path>", "Record network traffic to a HAR file (finalized on close)")
|
|
351
458
|
.option(
|
|
352
459
|
"--viewport <preset|WxH>",
|
|
@@ -358,14 +465,18 @@ export function registerBrowseCommand(
|
|
|
358
465
|
"Renders theme-aware pages in that scheme without page-specific toggles. " +
|
|
359
466
|
"Absent = browser default (unchanged behavior).",
|
|
360
467
|
)
|
|
361
|
-
.option("--login", "Headed mode for one-time auth flow (cookies persist in profile)")
|
|
362
468
|
.option(
|
|
363
|
-
"--
|
|
364
|
-
"
|
|
365
|
-
"
|
|
366
|
-
"
|
|
367
|
-
"
|
|
368
|
-
|
|
469
|
+
"--login",
|
|
470
|
+
"Open the URL in the installed Google Chrome with nothing attached (no DevTools protocol, no " +
|
|
471
|
+
"Playwright) so a person can sign in; under WSL this is the Windows host's Chrome. Sign in, then " +
|
|
472
|
+
"close the window or create the --login-close-file; later browse and browse-session runs reuse " +
|
|
473
|
+
"the cookies. Sites cannot tell this window from the person's own browser.",
|
|
474
|
+
)
|
|
475
|
+
.option(
|
|
476
|
+
"--attached",
|
|
477
|
+
"With --login: open an automation-controlled Chrome that this process and --control-file can drive. " +
|
|
478
|
+
"Sites can detect it and may lock the account, so use it only for agent-driven headed sessions on " +
|
|
479
|
+
"sites you own. Never use it to sign in to a third-party account.",
|
|
369
480
|
)
|
|
370
481
|
.option(
|
|
371
482
|
"--login-close-file <path>",
|
|
@@ -373,9 +484,12 @@ export function registerBrowseCommand(
|
|
|
373
484
|
)
|
|
374
485
|
.option(
|
|
375
486
|
"--control-file <path>",
|
|
376
|
-
"With --login, publish an owner-only descriptor for repeated browse-session control",
|
|
487
|
+
"With --login --attached, publish an owner-only descriptor for repeated browse-session control",
|
|
488
|
+
)
|
|
489
|
+
.option(
|
|
490
|
+
"--headed",
|
|
491
|
+
"Automation-controlled headed window for one-off visual checks. Not for signing in; use --login.",
|
|
377
492
|
)
|
|
378
|
-
.option("--headed", "Headed mode for one-off (no auth-flow framing)")
|
|
379
493
|
.option(
|
|
380
494
|
"--browser-channel <name>",
|
|
381
495
|
"Browser to launch: chrome | chrome-beta | msedge | chromium. Headed launches (--login, --headed) " +
|
|
@@ -893,7 +1007,8 @@ async function runBrowse(
|
|
|
893
1007
|
? null
|
|
894
1008
|
: new CookieJar({ path: opts.store ?? DEFAULT_STORE, source: "harn-browse" });
|
|
895
1009
|
if (jar) applyExtraCookies(url, jar, context?.extraCookies);
|
|
896
|
-
const
|
|
1010
|
+
const loginMode = resolveLoginMode(opts);
|
|
1011
|
+
const headed = loginMode === "attached" || opts.headed;
|
|
897
1012
|
const viewport = parseViewport(opts.viewport ?? "desktop");
|
|
898
1013
|
const deviceScaleFactor = parseDeviceScaleFactor(opts.deviceScaleFactor);
|
|
899
1014
|
if (opts.reviewPackAllocation && !opts.reviewPack) {
|
|
@@ -925,12 +1040,6 @@ async function runBrowse(
|
|
|
925
1040
|
}
|
|
926
1041
|
const proxy = opts.proxyFromEnv ? browserProxyFromEnv() : undefined;
|
|
927
1042
|
const proxyGate = opts.proxyFromEnv ? browserProxyGateFromEnv() : null;
|
|
928
|
-
if (opts.loginCloseFile && !opts.login) {
|
|
929
|
-
throw new Error("--login-close-file requires --login.");
|
|
930
|
-
}
|
|
931
|
-
if (opts.controlFile && !opts.login) {
|
|
932
|
-
throw new Error("--control-file requires --login.");
|
|
933
|
-
}
|
|
934
1043
|
const loginCloseFile = opts.loginCloseFile ? resolve(opts.loginCloseFile) : null;
|
|
935
1044
|
if (loginCloseFile && existsSync(loginCloseFile)) {
|
|
936
1045
|
throw new Error(
|
|
@@ -938,14 +1047,10 @@ async function runBrowse(
|
|
|
938
1047
|
);
|
|
939
1048
|
}
|
|
940
1049
|
|
|
1050
|
+
const collectPlan = opts.collect ? buildCollectPlan(opts) : null;
|
|
1051
|
+
|
|
941
1052
|
const pace = commandPaceGate(opts.pace !== false, (message) => emit.log(message, "info"));
|
|
942
|
-
if (
|
|
943
|
-
if (!opts.login) throw new Error("--plain requires --login.");
|
|
944
|
-
if (opts.controlFile) {
|
|
945
|
-
throw new Error(
|
|
946
|
-
"--plain cannot take --control-file: nothing is attached to drive the window.",
|
|
947
|
-
);
|
|
948
|
-
}
|
|
1053
|
+
if (loginMode === "plain") {
|
|
949
1054
|
const plainExecutable = installedChromePath();
|
|
950
1055
|
const plainUa = resolveUserAgent({
|
|
951
1056
|
requested: opts.userAgent,
|
|
@@ -953,6 +1058,10 @@ async function runBrowse(
|
|
|
953
1058
|
chromeMajor: plainExecutable ? chromeMajorFromExecutable(plainExecutable) : undefined,
|
|
954
1059
|
});
|
|
955
1060
|
await pace?.before(url);
|
|
1061
|
+
if (useWindowsPlainLogin()) {
|
|
1062
|
+
await runWindowsPlainLogin(url, opts.store ?? DEFAULT_STORE, loginCloseFile, emit);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
956
1065
|
await runPlainLogin(url, opts.profile ?? DEFAULT_PROFILE, loginCloseFile, plainUa, emit);
|
|
957
1066
|
return;
|
|
958
1067
|
}
|
|
@@ -1030,6 +1139,11 @@ async function runBrowse(
|
|
|
1030
1139
|
batchResult = await runBatch(browser, opts.batch, Number.parseInt(opts.timeout, 10));
|
|
1031
1140
|
}
|
|
1032
1141
|
|
|
1142
|
+
if (collectPlan) {
|
|
1143
|
+
await runCollectMode(browser, collectPlan);
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
|
|
1033
1147
|
let evalResult: unknown;
|
|
1034
1148
|
if (opts.evaluate) {
|
|
1035
1149
|
evalResult = await browser.evaluate<unknown>(opts.evaluate);
|
|
@@ -1484,13 +1598,13 @@ async function runBrowse(
|
|
|
1484
1598
|
const closeWaits: Promise<void>[] = [];
|
|
1485
1599
|
if (loginCloseFile) {
|
|
1486
1600
|
emit.log(
|
|
1487
|
-
`[--login]
|
|
1601
|
+
`[--login --attached] Automation-controlled Chrome is open. Drive the visible window now. Create ${loginCloseFile} to close cleanly.`,
|
|
1488
1602
|
"info",
|
|
1489
1603
|
);
|
|
1490
1604
|
closeWaits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
|
|
1491
1605
|
} else {
|
|
1492
1606
|
emit.log(
|
|
1493
|
-
"[--login]
|
|
1607
|
+
"[--login --attached] Automation-controlled Chrome is open. Drive the visible window now. Press Enter here to close + persist cookies into the profile.",
|
|
1494
1608
|
"info",
|
|
1495
1609
|
);
|
|
1496
1610
|
closeWaits.push(waitForTerminalEnter(lifecycleAbort.signal));
|
|
@@ -1687,9 +1801,161 @@ async function runBrowse(
|
|
|
1687
1801
|
}
|
|
1688
1802
|
|
|
1689
1803
|
// ---------------------------------------------------------------------------
|
|
1690
|
-
//
|
|
1691
|
-
//
|
|
1692
|
-
//
|
|
1804
|
+
// Scroll-and-collect mode (--collect). Extends browse rather than adding a
|
|
1805
|
+
// sibling command so it reuses the exact session path: persistent profile,
|
|
1806
|
+
// cookie jar, browser channel, user agent, proxy, page-load pace gate,
|
|
1807
|
+
// --wait-for and --batch setup steps.
|
|
1808
|
+
// ---------------------------------------------------------------------------
|
|
1809
|
+
|
|
1810
|
+
interface CollectPlan {
|
|
1811
|
+
spec: ResolvedCollectSpec;
|
|
1812
|
+
limits: CollectLimits;
|
|
1813
|
+
pacing: ScrollPacing;
|
|
1814
|
+
seed: number;
|
|
1815
|
+
format: "json" | "jsonl";
|
|
1816
|
+
out: string | null;
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1819
|
+
function parsePositiveInt(raw: string, flag: string): number {
|
|
1820
|
+
if (!/^\d+$/.test(raw.trim()) || Number.parseInt(raw, 10) < 1) {
|
|
1821
|
+
throw new Error(`${flag} must be a positive integer (got "${raw}").`);
|
|
1822
|
+
}
|
|
1823
|
+
return Number.parseInt(raw, 10);
|
|
1824
|
+
}
|
|
1825
|
+
|
|
1826
|
+
function buildCollectPlan(opts: BrowseOpts): CollectPlan {
|
|
1827
|
+
if (opts.login) throw new Error("--collect cannot run with --login.");
|
|
1828
|
+
if (opts.snapshot || opts.html || opts.json) {
|
|
1829
|
+
throw new Error("--collect prints its own JSON; drop --snapshot, --html and --json.");
|
|
1830
|
+
}
|
|
1831
|
+
const presetName = typeof opts.collect === "string" ? opts.collect : "generic";
|
|
1832
|
+
const spec = resolveCollectSpec(presetName, {
|
|
1833
|
+
item: opts.collectItem,
|
|
1834
|
+
key: opts.collectKey,
|
|
1835
|
+
keyPattern: opts.collectKeyPattern,
|
|
1836
|
+
fields: opts.collectField,
|
|
1837
|
+
expand: opts.collectExpand,
|
|
1838
|
+
});
|
|
1839
|
+
const limits: CollectLimits = {
|
|
1840
|
+
...DEFAULT_COLLECT_LIMITS,
|
|
1841
|
+
maxItems:
|
|
1842
|
+
opts.collectMaxItems === undefined
|
|
1843
|
+
? null
|
|
1844
|
+
: parsePositiveInt(opts.collectMaxItems, "--collect-max-items"),
|
|
1845
|
+
maxScrolls: parsePositiveInt(opts.collectMaxScrolls ?? "60", "--collect-max-scrolls"),
|
|
1846
|
+
maxMs: parsePositiveInt(opts.collectMaxSeconds ?? "300", "--collect-max-seconds") * 1_000,
|
|
1847
|
+
idleSteps: parsePositiveInt(opts.collectIdle ?? "5", "--collect-idle"),
|
|
1848
|
+
expandPerStep: spec.expand ? 3 : 0,
|
|
1849
|
+
};
|
|
1850
|
+
const pacing = scrollPacingFromEnv();
|
|
1851
|
+
const msBounds = { min: 0, max: PACING_LIMITS.maxPauseMs };
|
|
1852
|
+
if (opts.collectStep) {
|
|
1853
|
+
pacing.step = parseRange(opts.collectStep, "--collect-step", {
|
|
1854
|
+
min: 0.05,
|
|
1855
|
+
max: PACING_LIMITS.maxStepFraction,
|
|
1856
|
+
});
|
|
1857
|
+
}
|
|
1858
|
+
if (opts.collectPause)
|
|
1859
|
+
pacing.pauseMs = parseRange(opts.collectPause, "--collect-pause", msBounds);
|
|
1860
|
+
if (opts.collectReadPause) {
|
|
1861
|
+
pacing.readPauseMs = parseRange(opts.collectReadPause, "--collect-read-pause", msBounds);
|
|
1862
|
+
}
|
|
1863
|
+
if (opts.collectReadChance !== undefined) {
|
|
1864
|
+
pacing.readChance = parseProbability(opts.collectReadChance, "--collect-read-chance");
|
|
1865
|
+
}
|
|
1866
|
+
if (opts.collectBackChance !== undefined) {
|
|
1867
|
+
pacing.backChance = parseProbability(opts.collectBackChance, "--collect-back-chance");
|
|
1868
|
+
}
|
|
1869
|
+
let seed = randomSeed();
|
|
1870
|
+
if (opts.collectSeed !== undefined) {
|
|
1871
|
+
if (!/^\d+$/.test(opts.collectSeed.trim())) {
|
|
1872
|
+
throw new Error(`--collect-seed must be a non-negative integer (got "${opts.collectSeed}").`);
|
|
1873
|
+
}
|
|
1874
|
+
seed = Number(BigInt(opts.collectSeed.trim()) % 4294967296n);
|
|
1875
|
+
}
|
|
1876
|
+
const format = opts.collectFormat ?? "json";
|
|
1877
|
+
if (format !== "json" && format !== "jsonl") {
|
|
1878
|
+
throw new Error(`--collect-format must be json or jsonl (got "${format}").`);
|
|
1879
|
+
}
|
|
1880
|
+
return {
|
|
1881
|
+
spec,
|
|
1882
|
+
limits,
|
|
1883
|
+
pacing,
|
|
1884
|
+
seed,
|
|
1885
|
+
format,
|
|
1886
|
+
out: opts.collectOut ? resolve(opts.collectOut) : null,
|
|
1887
|
+
};
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
async function runCollectMode(browser: Browser, plan: CollectPlan): Promise<void> {
|
|
1891
|
+
const page = browser.currentPage;
|
|
1892
|
+
const rng = createRng(plan.seed);
|
|
1893
|
+
let interrupted = false;
|
|
1894
|
+
const onSignal = () => {
|
|
1895
|
+
interrupted = true;
|
|
1896
|
+
};
|
|
1897
|
+
process.on("SIGINT", onSignal);
|
|
1898
|
+
process.on("SIGTERM", onSignal);
|
|
1899
|
+
let result: Awaited<ReturnType<typeof runCollect>>;
|
|
1900
|
+
try {
|
|
1901
|
+
emit.log(
|
|
1902
|
+
`collect: preset ${plan.spec.preset}, item ${plan.spec.item}, seed ${plan.seed}`,
|
|
1903
|
+
"info",
|
|
1904
|
+
);
|
|
1905
|
+
const driver = createPageDriver(page, plan.spec, {
|
|
1906
|
+
rng,
|
|
1907
|
+
log: (message) => emit.log(message, "warn"),
|
|
1908
|
+
});
|
|
1909
|
+
result = await runCollect(driver, {
|
|
1910
|
+
limits: plan.limits,
|
|
1911
|
+
pacing: plan.pacing,
|
|
1912
|
+
rng,
|
|
1913
|
+
shouldStop: () => interrupted,
|
|
1914
|
+
onStep: ({ step, items, added }) =>
|
|
1915
|
+
emit.log(`collect: step ${step}, ${items} items (+${added})`, "debug"),
|
|
1916
|
+
});
|
|
1917
|
+
} finally {
|
|
1918
|
+
process.removeListener("SIGINT", onSignal);
|
|
1919
|
+
process.removeListener("SIGTERM", onSignal);
|
|
1920
|
+
}
|
|
1921
|
+
const { items, stats } = result;
|
|
1922
|
+
emit.log(
|
|
1923
|
+
`collect: ${items.length} items in ${stats.steps} steps, stopped on ${stats.stopReason}` +
|
|
1924
|
+
(stats.stopDetail ? ` (${stats.stopDetail})` : ""),
|
|
1925
|
+
"info",
|
|
1926
|
+
);
|
|
1927
|
+
const envelope = {
|
|
1928
|
+
url: page.url(),
|
|
1929
|
+
preset: plan.spec.preset,
|
|
1930
|
+
itemSelector: plan.spec.item,
|
|
1931
|
+
seed: plan.seed,
|
|
1932
|
+
stopReason: stats.stopReason,
|
|
1933
|
+
stats,
|
|
1934
|
+
items,
|
|
1935
|
+
};
|
|
1936
|
+
if (plan.format === "jsonl") {
|
|
1937
|
+
const body = items.map((item) => JSON.stringify(item)).join("\n");
|
|
1938
|
+
if (plan.out) {
|
|
1939
|
+
mkdirSync(dirname(plan.out), { recursive: true });
|
|
1940
|
+
writeFileSync(plan.out, body ? `${body}\n` : "");
|
|
1941
|
+
emit.file(plan.out, { items: items.length, stopReason: stats.stopReason, stats });
|
|
1942
|
+
} else if (body) {
|
|
1943
|
+
process.stdout.write(`${body}\n`);
|
|
1944
|
+
}
|
|
1945
|
+
} else if (plan.out) {
|
|
1946
|
+
mkdirSync(dirname(plan.out), { recursive: true });
|
|
1947
|
+
writeFileSync(plan.out, `${JSON.stringify(envelope, null, 2)}\n`);
|
|
1948
|
+
emit.file(plan.out, { items: items.length, stopReason: stats.stopReason, stats });
|
|
1949
|
+
} else {
|
|
1950
|
+
emit.data(envelope);
|
|
1951
|
+
}
|
|
1952
|
+
if (interrupted) process.exitCode = 130;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// ---------------------------------------------------------------------------
|
|
1956
|
+
// Batch mode runner. Each step is `<verb> <args>`; see splitBatchSteps for
|
|
1957
|
+
// the separator rule (eval steps keep semicolons inside JS quotes and
|
|
1958
|
+
// brackets; `\;` is always a literal semicolon).
|
|
1693
1959
|
// ---------------------------------------------------------------------------
|
|
1694
1960
|
|
|
1695
1961
|
interface BatchResult {
|
|
@@ -1769,27 +2035,6 @@ async function runBatch(
|
|
|
1769
2035
|
return result;
|
|
1770
2036
|
}
|
|
1771
2037
|
|
|
1772
|
-
function splitBatchSteps(input: string): string[] {
|
|
1773
|
-
const steps: string[] = [];
|
|
1774
|
-
let buf = "";
|
|
1775
|
-
for (let i = 0; i < input.length; i++) {
|
|
1776
|
-
const ch = input[i];
|
|
1777
|
-
if (ch === "\\" && input[i + 1] === ";") {
|
|
1778
|
-
buf += ";";
|
|
1779
|
-
i++;
|
|
1780
|
-
continue;
|
|
1781
|
-
}
|
|
1782
|
-
if (ch === ";") {
|
|
1783
|
-
steps.push(buf);
|
|
1784
|
-
buf = "";
|
|
1785
|
-
continue;
|
|
1786
|
-
}
|
|
1787
|
-
buf += ch;
|
|
1788
|
-
}
|
|
1789
|
-
if (buf.trim()) steps.push(buf);
|
|
1790
|
-
return steps;
|
|
1791
|
-
}
|
|
1792
|
-
|
|
1793
2038
|
// ---------------------------------------------------------------------------
|
|
1794
2039
|
// Print mode (--snapshot / --html / --json): stdout, no file writes
|
|
1795
2040
|
// ---------------------------------------------------------------------------
|
|
@@ -3123,7 +3368,28 @@ async function verifyBrowserProxyGate(
|
|
|
3123
3368
|
}
|
|
3124
3369
|
|
|
3125
3370
|
/**
|
|
3126
|
-
*
|
|
3371
|
+
* How a `browse` run handles sign-in. `--login` opens the installed Google
|
|
3372
|
+
* Chrome with nothing attached, because sites can detect an
|
|
3373
|
+
* automation-controlled browser and disable the account that signed in with
|
|
3374
|
+
* it. The attached window is reachable only through an explicit `--attached`.
|
|
3375
|
+
*/
|
|
3376
|
+
export function resolveLoginMode(
|
|
3377
|
+
opts: Pick<BrowseOpts, "login" | "attached" | "controlFile" | "loginCloseFile">,
|
|
3378
|
+
): "none" | "plain" | "attached" {
|
|
3379
|
+
if (opts.attached && !opts.login) throw new Error("--attached requires --login.");
|
|
3380
|
+
if (opts.loginCloseFile && !opts.login) throw new Error("--login-close-file requires --login.");
|
|
3381
|
+
if (opts.controlFile && !(opts.login && opts.attached)) {
|
|
3382
|
+
throw new Error(
|
|
3383
|
+
"--control-file requires --login --attached. A sign-in to a third-party account uses plain " +
|
|
3384
|
+
"--login, which nothing can drive.",
|
|
3385
|
+
);
|
|
3386
|
+
}
|
|
3387
|
+
if (!opts.login) return "none";
|
|
3388
|
+
return opts.attached ? "attached" : "plain";
|
|
3389
|
+
}
|
|
3390
|
+
|
|
3391
|
+
/**
|
|
3392
|
+
* `--login`: spawn the installed Google Chrome directly on the shared
|
|
3127
3393
|
* persistent profile, with no DevTools protocol attached. Playwright enables
|
|
3128
3394
|
* its runtime hooks in every frame it drives, and sign-in verification vendors
|
|
3129
3395
|
* (X's is the reported case) detect that and fail the flow with no explanation.
|
|
@@ -3141,7 +3407,8 @@ async function runPlainLogin(
|
|
|
3141
3407
|
const executable = installedChromePath();
|
|
3142
3408
|
if (!executable) {
|
|
3143
3409
|
throw new Error(
|
|
3144
|
-
"--
|
|
3410
|
+
"--login needs an installed Google Chrome at a standard path; none was found on this machine. " +
|
|
3411
|
+
"Install Google Chrome; do not fall back to --attached for a third-party sign-in.",
|
|
3145
3412
|
);
|
|
3146
3413
|
}
|
|
3147
3414
|
mkdirSync(profileDir, { recursive: true });
|
|
@@ -3164,13 +3431,13 @@ async function runPlainLogin(
|
|
|
3164
3431
|
const waits: Promise<void>[] = [exited];
|
|
3165
3432
|
if (loginCloseFile) {
|
|
3166
3433
|
emit.log(
|
|
3167
|
-
`[--login
|
|
3434
|
+
`[--login] Plain Google Chrome is open with nothing attached. Sign in, then close the window or create ${loginCloseFile}.`,
|
|
3168
3435
|
"info",
|
|
3169
3436
|
);
|
|
3170
3437
|
waits.push(waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal));
|
|
3171
3438
|
} else {
|
|
3172
3439
|
emit.log(
|
|
3173
|
-
"[--login
|
|
3440
|
+
"[--login] Plain Google Chrome is open with nothing attached. Sign in, then close the window or press Enter here.",
|
|
3174
3441
|
"info",
|
|
3175
3442
|
);
|
|
3176
3443
|
waits.push(waitForTerminalEnter(lifecycleAbort.signal));
|
|
@@ -3181,7 +3448,83 @@ async function runPlainLogin(
|
|
|
3181
3448
|
child.kill("SIGTERM");
|
|
3182
3449
|
await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
|
|
3183
3450
|
}
|
|
3184
|
-
emit.log(`[--login
|
|
3451
|
+
emit.log(`[--login] Chrome closed; cookies persisted in ${profileDir}.`, "info");
|
|
3452
|
+
}
|
|
3453
|
+
|
|
3454
|
+
/**
|
|
3455
|
+
* Under WSL, `--login` opens the Windows host's Google Chrome when one
|
|
3456
|
+
* is installed. Some sign-in flows reject every browser running inside WSL,
|
|
3457
|
+
* even a plain Linux Chrome, while the Windows desktop Chrome signs in
|
|
3458
|
+
* normally. `HARNERY_BROWSER_WSL_PLAIN=linux` keeps the Linux Chrome.
|
|
3459
|
+
*/
|
|
3460
|
+
function useWindowsPlainLogin(): boolean {
|
|
3461
|
+
if (!isWSL()) return false;
|
|
3462
|
+
if ((process.env.HARNERY_BROWSER_WSL_PLAIN ?? "").toLowerCase() === "linux") return false;
|
|
3463
|
+
return windowsChromePath() !== undefined;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3466
|
+
/**
|
|
3467
|
+
* The WSL form of `--login`: the person signs in through the Windows
|
|
3468
|
+
* Chrome on a dedicated Windows profile with nothing attached. When the window
|
|
3469
|
+
* closes, the profile's cookies for the sign-in site are read and merged into
|
|
3470
|
+
* the shared cookie store, which later `browse` runs attach.
|
|
3471
|
+
*/
|
|
3472
|
+
async function runWindowsPlainLogin(
|
|
3473
|
+
url: string,
|
|
3474
|
+
storePath: string,
|
|
3475
|
+
loginCloseFile: string | null,
|
|
3476
|
+
emit: { log: (message: string, level: "info" | "warn" | "error" | "debug") => void },
|
|
3477
|
+
): Promise<void> {
|
|
3478
|
+
const chromeLinux = windowsChromePath();
|
|
3479
|
+
const localAppData = windowsLocalAppData();
|
|
3480
|
+
if (!chromeLinux || !localAppData) {
|
|
3481
|
+
throw new Error(
|
|
3482
|
+
"--login under WSL needs the Windows Google Chrome and %LOCALAPPDATA%; neither was readable.",
|
|
3483
|
+
);
|
|
3484
|
+
}
|
|
3485
|
+
const chromeWindows = wslPathToWindows(chromeLinux);
|
|
3486
|
+
const profileWindows = windowsPlainProfile(localAppData);
|
|
3487
|
+
const child = spawn(
|
|
3488
|
+
chromeLinux,
|
|
3489
|
+
[`--user-data-dir=${profileWindows}`, "--no-first-run", "--no-default-browser-check", url],
|
|
3490
|
+
{ stdio: "ignore" },
|
|
3491
|
+
);
|
|
3492
|
+
const exited = new Promise<void>((resolveExit) => {
|
|
3493
|
+
child.once("exit", () => resolveExit());
|
|
3494
|
+
child.once("error", () => resolveExit());
|
|
3495
|
+
});
|
|
3496
|
+
const lifecycleAbort = new AbortController();
|
|
3497
|
+
const waits: Promise<void>[] = [exited];
|
|
3498
|
+
const how = loginCloseFile ? `create ${loginCloseFile}` : "press Enter here";
|
|
3499
|
+
emit.log(
|
|
3500
|
+
`[--login] Windows Google Chrome is open with nothing attached (profile ${profileWindows}). Sign in, then close the window or ${how}.`,
|
|
3501
|
+
"info",
|
|
3502
|
+
);
|
|
3503
|
+
waits.push(
|
|
3504
|
+
loginCloseFile
|
|
3505
|
+
? waitForLoginCloseFile(loginCloseFile, lifecycleAbort.signal)
|
|
3506
|
+
: waitForTerminalEnter(lifecycleAbort.signal),
|
|
3507
|
+
);
|
|
3508
|
+
await Promise.race(waits);
|
|
3509
|
+
lifecycleAbort.abort();
|
|
3510
|
+
stopWindowsChromeProfile(profileWindows);
|
|
3511
|
+
await Promise.race([exited, new Promise((r) => setTimeout(r, 5000))]);
|
|
3512
|
+
const domain = harvestDomain(url);
|
|
3513
|
+
const cookies = harvestWindowsChromeCookies({ chromeWindows, profileWindows, domain });
|
|
3514
|
+
if (cookies.length === 0) {
|
|
3515
|
+
emit.log(
|
|
3516
|
+
`[--login] No ${domain} cookies were found in the Windows profile; the sign-in may not have completed.`,
|
|
3517
|
+
"warn",
|
|
3518
|
+
);
|
|
3519
|
+
return;
|
|
3520
|
+
}
|
|
3521
|
+
const jar = new CookieJar({ path: storePath, source: "harn-browse" });
|
|
3522
|
+
jar.save(mergeCookies(jar.load(), cookies));
|
|
3523
|
+
const names = [...new Set(cookies.map((cookie) => cookie.name))].sort().join(", ");
|
|
3524
|
+
emit.log(
|
|
3525
|
+
`[--login] Copied ${cookies.length} ${domain} cookies into ${storePath}: ${names}.`,
|
|
3526
|
+
"info",
|
|
3527
|
+
);
|
|
3185
3528
|
}
|
|
3186
3529
|
|
|
3187
3530
|
async function waitForLoginCloseFile(path: string, signal?: AbortSignal): Promise<void> {
|
|
@@ -8,6 +8,13 @@ import {
|
|
|
8
8
|
planMirror,
|
|
9
9
|
readCliAccount,
|
|
10
10
|
} from "../lib/claude-desktop.ts";
|
|
11
|
+
import {
|
|
12
|
+
applyShare,
|
|
13
|
+
defaultShareDir,
|
|
14
|
+
planShare,
|
|
15
|
+
shareSummary,
|
|
16
|
+
} from "../lib/claude-desktop-share.ts";
|
|
17
|
+
import { resolveMachineLabel } from "../lib/machine.ts";
|
|
11
18
|
|
|
12
19
|
/**
|
|
13
20
|
* `claude-desktop`: make Claude desktop-app sessions survive account
|
|
@@ -186,6 +193,60 @@ export function registerClaudeDesktopCommand(
|
|
|
186
193
|
}
|
|
187
194
|
},
|
|
188
195
|
);
|
|
196
|
+
|
|
197
|
+
cmd
|
|
198
|
+
.command("share")
|
|
199
|
+
.description(
|
|
200
|
+
"Share sessions with your other computers through a synced folder (iCloud Drive by default): " +
|
|
201
|
+
"publish this machine's recent sessions and list + resume every other machine's here. " +
|
|
202
|
+
"Dry-run by default; --yes applies. Restart the desktop app afterward to see new entries.",
|
|
203
|
+
)
|
|
204
|
+
.option("--dir <path>", "Shared folder (default: iCloud Drive/Harnery/claude-sessions)")
|
|
205
|
+
.option("--days <n>", "Only sessions active in the last N days", "14")
|
|
206
|
+
.option("--data-dir <path>", "Explicit desktop-app data directory")
|
|
207
|
+
.option("--yes", "Apply the plan (default is dry-run)")
|
|
208
|
+
.action((opts: { dir?: string; days: string; dataDir?: string; yes?: boolean }) => {
|
|
209
|
+
const shareDir = opts.dir ?? process.env.HARNERY_SESSION_SHARE_DIR ?? defaultShareDir();
|
|
210
|
+
if (!shareDir) {
|
|
211
|
+
emit.error({
|
|
212
|
+
code: "no_share_dir",
|
|
213
|
+
message: "no shared folder: pass --dir <path> or set HARNERY_SESSION_SHARE_DIR",
|
|
214
|
+
});
|
|
215
|
+
process.exit(2);
|
|
216
|
+
}
|
|
217
|
+
const cli = readCliAccount();
|
|
218
|
+
if (!cli) {
|
|
219
|
+
emit.error({
|
|
220
|
+
code: "no_account",
|
|
221
|
+
message: "cannot read the signed-in account from ~/.claude.json",
|
|
222
|
+
});
|
|
223
|
+
process.exit(2);
|
|
224
|
+
}
|
|
225
|
+
const [dataDir] = requireDataDirs(emit, opts.dataDir);
|
|
226
|
+
const shareOpts = {
|
|
227
|
+
shareDir,
|
|
228
|
+
machine: resolveMachineLabel(),
|
|
229
|
+
days: Number(opts.days) || 14,
|
|
230
|
+
dataDir: dataDir as string,
|
|
231
|
+
targetAccountUuid: cli.accountUuid,
|
|
232
|
+
};
|
|
233
|
+
const plan = planShare(shareOpts);
|
|
234
|
+
const summary = { share_dir: shareDir, machine: shareOpts.machine, ...shareSummary(plan) };
|
|
235
|
+
if (!opts.yes) {
|
|
236
|
+
emit.data({ dry_run: true, ...summary, hint: "re-run with --yes to apply" });
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
const result = applyShare(plan, shareOpts);
|
|
240
|
+
emit.data({
|
|
241
|
+
dry_run: false,
|
|
242
|
+
...summary,
|
|
243
|
+
...result,
|
|
244
|
+
hint:
|
|
245
|
+
result.imported > 0
|
|
246
|
+
? "quit the Claude desktop app fully (Cmd+Q) and reopen it to see the new sessions"
|
|
247
|
+
: "sidebar already current",
|
|
248
|
+
});
|
|
249
|
+
});
|
|
189
250
|
}
|
|
190
251
|
|
|
191
252
|
function collect(value: string, previous: string[]): string[] {
|