thursday-agent 0.9.0 → 0.10.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/.next/BUILD_ID +1 -1
- package/.next/build-manifest.json +3 -3
- package/.next/prerender-manifest.json +3 -3
- package/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/server/app/_global-error.html +1 -1
- package/.next/server/app/_global-error.rsc +1 -1
- package/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +2 -2
- package/.next/server/app/_not-found.segments/_full.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
- package/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
- package/.next/server/app/api/artifact/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/keep-working/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/files/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/[id]/route.js.nft.json +1 -1
- package/.next/server/app/api/bot/thread/route.js.nft.json +1 -1
- package/.next/server/app/api/config/route.js.nft.json +1 -1
- package/.next/server/app/api/events/route.js.nft.json +1 -1
- package/.next/server/app/api/favicon/[host]/route.js.nft.json +1 -1
- package/.next/server/app/api/file/[...path]/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/catalog/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/credits/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/route.js.nft.json +1 -1
- package/.next/server/app/api/llm-model/subscription/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/oauth/callback/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/route.js.nft.json +1 -1
- package/.next/server/app/api/mcp/tools/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/edit/route.js.nft.json +1 -1
- package/.next/server/app/api/memory/route.js.nft.json +1 -1
- package/.next/server/app/api/probe/route.js.nft.json +1 -1
- package/.next/server/app/api/reach/route.js.nft.json +1 -1
- package/.next/server/app/api/routine/route.js.nft.json +1 -1
- package/.next/server/app/api/signins/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/[source]/[name]/route.js.nft.json +1 -1
- package/.next/server/app/api/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/call/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/skills/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/text/route.js.nft.json +1 -1
- package/.next/server/app/api/thursday/tool-call/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/files/route.js.nft.json +1 -1
- package/.next/server/app/api/workspace/route.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page/server-reference-manifest.json +1 -1
- package/.next/server/app/artifact/[...path]/page.js.nft.json +1 -1
- package/.next/server/app/artifact/[...path]/page_client-reference-manifest.js +1 -1
- package/.next/server/app/favicon.ico/route.js.nft.json +1 -1
- package/.next/server/app/manifest.webmanifest/route.js.nft.json +1 -1
- package/.next/server/app/page/server-reference-manifest.json +56 -56
- package/.next/server/app/page.js.nft.json +1 -1
- package/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0oo-wlv._.js → [root-of-the-server]__0cxqzfc._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__0tzekbl._.js +1 -1
- package/.next/server/chunks/ssr/{[root-of-the-server]__0zuns7e._.js → [root-of-the-server]__1-d2tgh._.js} +2 -2
- package/.next/server/chunks/ssr/[root-of-the-server]__1se25x0._.js +1 -1
- package/.next/server/chunks/ssr/{_0scf7xy._.js → _05rxyf9._.js} +2 -2
- package/.next/server/chunks/ssr/_070nv1_._.js +1 -1
- package/.next/server/chunks/ssr/_097-ljd._.js +1 -1
- package/.next/server/chunks/ssr/{_1uy0xom._.js → _0bp-_fi._.js} +3 -3
- package/.next/server/chunks/ssr/_0fqhhwi._.js +1 -1
- package/.next/server/chunks/ssr/{_0ltz2q5._.js → _0hlxij-._.js} +2 -2
- package/.next/server/chunks/ssr/{_1a-nuxx._.js → _0p9pmxe._.js} +2 -2
- package/.next/server/chunks/ssr/{_0ital-2._.js → _0s41irp._.js} +2 -2
- package/.next/server/chunks/ssr/{_1z74dmq._.js → _0tg0vf8._.js} +4 -4
- package/.next/server/chunks/ssr/{_1tmu8z-._.js → _173siic._.js} +2 -2
- package/.next/server/chunks/ssr/{_1x-kojb._.js → _17v-33v._.js} +3 -3
- package/.next/server/chunks/ssr/{_0l_mcfz._.js → _19n08g5._.js} +2 -2
- package/.next/server/chunks/ssr/{_0urj3mc._.js → _1doidrw._.js} +2 -2
- package/.next/server/chunks/ssr/{_1-zyvdb._.js → _1flsjeo._.js} +2 -2
- package/.next/server/chunks/ssr/_1mqn9mc._.js +1 -1
- package/.next/server/chunks/ssr/_1neoj22._.js +1 -1
- package/.next/server/chunks/ssr/_1ng831c._.js +3 -0
- package/.next/server/chunks/ssr/{_0kjj5ts._.js → _1u2jw0i._.js} +3 -3
- package/.next/server/chunks/ssr/_1xoch6f._.js +1 -1
- package/.next/server/chunks/ssr/{_1flbs77._.js → _1yk6yak._.js} +2 -2
- package/.next/server/chunks/ssr/features_bot_1yr-_pm._.js +3 -0
- package/.next/server/chunks/ssr/features_connectors_components_mcp-setting_tsx_0jmbgvp._.js +1 -1
- package/.next/server/chunks/ssr/features_memory_components_memory-setting_tsx_0oa43sw._.js +1 -1
- package/.next/server/chunks/ssr/features_routine_components_routine-setting_tsx_055fknz._.js +1 -1
- package/.next/server/chunks/ssr/{node_modules_1ogczzz._.js → node_modules_048wmmj._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1af3w91._.js → node_modules_04t200t._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1wbkzl9._.js → node_modules_0511ha-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0wnlddw._.js → node_modules_08g12f-._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0uyh_vb._.js → node_modules_0cdu6jq._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_03d0rfz._.js → node_modules_0eb4rx7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_05ybmta._.js → node_modules_0f2k5l0._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0sgy-dc._.js → node_modules_0piws2a._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_12et8d5._.js → node_modules_0pk6i-e._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_04_sbbc._.js → node_modules_0s1lwc9._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_068ws5v._.js → node_modules_0wq4-ts._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_13kwejm._.js → node_modules_0zjqwz7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_07rs_p2._.js → node_modules_11hbkuj._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1z1lrbk._.js → node_modules_18woe20._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0yvz9e0._.js → node_modules_1_afpz7._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0t_ez6-._.js → node_modules_1_ban4u._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1uwg-gz._.js → node_modules_1cxc8kn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0th357h._.js → node_modules_1dcbvfw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_06bb4q8._.js → node_modules_1expacw._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1s-gb-1._.js → node_modules_1ksh9gy._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0ubdtk-._.js → node_modules_1m2ho33._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1bucjlk._.js → node_modules_1n0h9sf._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_1lkix61._.js → node_modules_1qehmwm._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0x630an._.js → node_modules_1xzo2wn._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0jljel9._.js → node_modules_1zf46lp._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_0dzm4um._.js → node_modules_1zkpek5._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0r443fh._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_0avl8-1._.js} +1 -1
- package/.next/server/chunks/ssr/{node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_10wnlbk._.js → node_modules_@mermaid-js_parser_dist_chunks_mermaid-parser_core_14oslro._.js} +1 -1
- package/.next/server/middleware-build-manifest.js +3 -3
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/server/server-reference-manifest.js +1 -1
- package/.next/server/server-reference-manifest.json +57 -57
- package/.next/static/chunks/03kgfyrli_0gz.js +1 -0
- package/.next/static/chunks/{245hb85nsy6j4.js → 09120tms63y21.js} +1 -1
- package/.next/static/chunks/{2c64drdevdla3.js → 0o980477kfqez.js} +1 -1
- package/.next/static/chunks/{13ixzheqw7o-w.js → 0oz3rs47yy3p8.js} +2 -2
- package/.next/static/chunks/0w20wnnfrscq7.js +1 -0
- package/.next/static/chunks/{3bbzsitgiltnt.js → 19ojn4lomsk0r.js} +1 -1
- package/.next/static/chunks/{38301ji-85i30.js → 1mwee7xvitlp9.js} +1 -1
- package/.next/static/chunks/1p764d75_0lhv.js +18 -0
- package/.next/static/chunks/{27tvv1i-smrck.js → 1pjygp7hqib6j.js} +1 -1
- package/.next/static/chunks/{0_awn8tucznxc.js → 22shnca_6zfzs.js} +1 -1
- package/.next/static/chunks/{0ge-hx0ed83a1.js → 24dst4mgurbsh.js} +1 -1
- package/.next/static/chunks/{06nt9tatr-dug.js → 25zq8gzhbdsr7.js} +1 -1
- package/.next/static/chunks/{37j27b3e1gt7r.js → 2d4z3er53ng8r.js} +1 -1
- package/.next/static/chunks/2ky81z-jgvasc.js +1 -0
- package/.next/static/chunks/2m6jd3586ijb-.js +1 -0
- package/.next/static/chunks/{1grkt5gr8pbg1.js → 2qod5tqlpz8b7.js} +7 -7
- package/.next/static/chunks/{1yugpvvi8qrbu.js → 30skcjexa57zn.js} +2 -2
- package/.next/static/chunks/{427qsc6kahxyo.js → 35kwn237o4wdn.js} +1 -1
- package/.next/static/chunks/{0qmd8n52hp-6v.js → 3exjsi8pfdvzj.js} +1 -1
- package/.next/static/chunks/3qq4o8w2rv_e9.css +1 -0
- package/.next/static/chunks/{3h2e-xv1zxh_f.js → 3vynnr71ghvom.js} +2 -2
- package/.next/static/chunks/42890jqk049q1.js +1 -0
- package/README.md +4 -0
- package/components/ui/radio-group.tsx +2 -2
- package/features/bot/components/bot-setting.tsx +4 -4
- package/features/routine/components/routine-setting.tsx +1 -1
- package/features/thursday/components/thursday-setting.tsx +5 -5
- package/guide/bots.md +1 -1
- package/knip.json +1 -0
- package/package.json +1 -1
- package/seed-skills/analyst/README.md +19 -0
- package/seed-skills/analyst/data-report/SKILL.md +59 -0
- package/seed-skills/analyst/data-report/references/brief.md +31 -0
- package/seed-skills/analyst/data-report/references/company.md +41 -0
- package/seed-skills/analyst/data-report/references/comparison.md +42 -0
- package/seed-skills/analyst/data-report/references/decision.md +28 -0
- package/seed-skills/analyst/data-report/references/market-size.md +49 -0
- package/seed-skills/analyst/data-report/references/price-check.md +33 -0
- package/seed-skills/analyst/data-report/references/sources.md +51 -0
- package/seed-skills/analyst/data-report/references/trend.md +39 -0
- package/seed-skills/analyst/data-report/scripts/chart.mjs +679 -0
- package/seed-skills/analyst/data-report/scripts/fetch.mjs +521 -0
- package/seed-skills/insta/instagram/SKILL.md +33 -0
- package/seed-skills/insta/instagram/references/dm.md +49 -0
- package/seed-skills/insta/instagram/references/post.md +74 -0
- package/seed-skills/insta/instagram/scripts/compose.mjs +329 -0
- package/seed-skills/insta/instagram/scripts/image-size.mjs +30 -0
- package/seed-skills/insta/instagram/scripts/inbox.mjs +159 -0
- package/seed-skills/insta/instagram/scripts/lib.mjs +91 -0
- package/seed-skills/insta/instagram/scripts/media.mjs +104 -0
- package/seed-skills/insta/instagram/scripts/profile.mjs +139 -0
- package/seed-skills/insta/instagram/scripts/read.mjs +240 -0
- package/seed-skills/insta/instagram/scripts/render.mjs +126 -0
- package/seed-skills/insta/instagram/scripts/search.mjs +133 -0
- package/seed-skills/insta/instagram/scripts/selectors.mjs +56 -0
- package/seed-skills/insta/instagram/scripts/sheet.mjs +100 -0
- package/seed-skills/insta/instagram/scripts/webimage.mjs +105 -0
- package/seed-skills/insta/instagram/templates/carousel.html +62 -0
- package/seed-skills/mail/mail/SKILL.md +37 -0
- package/seed-skills/mail/mail/recipes/gmail.mjs +448 -0
- package/seed-skills/mail/mail/references/recipes.md +55 -0
- package/seed-skills/mail/mail/scripts/mail.mjs +202 -0
- package/seed-skills/marketer/README.md +6 -4
- package/seed-skills/marketer/copywriting/SKILL.md +1 -1
- package/seed-skills/marketer/emails/SKILL.md +1 -1
- package/seed-skills/marketer/launch/SKILL.md +1 -1
- package/seed-skills/marketer/product-marketing/SKILL.md +5 -5
- package/seed-skills/marketer/seo-audit/SKILL.md +1 -1
- package/seed-skills/marketer/social/SKILL.md +1 -1
- package/.next/server/chunks/ssr/_0dshsh4._.js +0 -3
- package/.next/server/chunks/ssr/features_bot_1nr94ks._.js +0 -3
- package/.next/static/chunks/0pda_vf-abwvx.js +0 -1
- package/.next/static/chunks/0rowp9rx3qbxu.js +0 -18
- package/.next/static/chunks/27lkanxvtdn09.js +0 -1
- package/.next/static/chunks/3fec-aeteddr-.js +0 -1
- package/.next/static/chunks/3go3hwp1z2_h2.css +0 -1
- package/.next/static/chunks/3h2bun3hcggg8.js +0 -1
- package/.next/static/chunks/3j1y335o95mj7.js +0 -1
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → Ar5V4kMdL4CE1trx8anrH}/_buildManifest.js +0 -0
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → Ar5V4kMdL4CE1trx8anrH}/_clientMiddlewareManifest.js +0 -0
- /package/.next/static/{u8kJqShUAa49vY7BxAWsw → Ar5V4kMdL4CE1trx8anrH}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Takes pictures through Instagram's post composer up to its last screen — files,
|
|
4
|
+
* crop set to the post's ratio (the composer starts every post at square), past
|
|
5
|
+
* filters, caption typed, AI label set — and stops there. It never presses Share:
|
|
6
|
+
* that is one click on the last screen's header, made only once posting is allowed.
|
|
7
|
+
*
|
|
8
|
+
* node compose.mjs --ratio 4:5 --caption caption.txt [--ai-label] [--shot last.png] <picture>...
|
|
9
|
+
* node compose.mjs --discard close an open composer and throw its draft away
|
|
10
|
+
*/
|
|
11
|
+
import { readFileSync } from "node:fs";
|
|
12
|
+
import { resolve } from "node:path";
|
|
13
|
+
import { fail, inPage, orFail, parseArgs } from "./lib.mjs";
|
|
14
|
+
import { SEL } from "./selectors.mjs";
|
|
15
|
+
|
|
16
|
+
const opts = parseArgs();
|
|
17
|
+
|
|
18
|
+
if (opts.discard) {
|
|
19
|
+
const done = orFail(
|
|
20
|
+
await inPage(
|
|
21
|
+
async (page, { SEL }) => {
|
|
22
|
+
for (let i = 0; i < 3; i++) {
|
|
23
|
+
const open = await page.locator(SEL.composer).count();
|
|
24
|
+
if (!open) return { closed: true };
|
|
25
|
+
const last = page.locator(SEL.composer).last();
|
|
26
|
+
// The discard question: a small dialog of two buttons, the first one destructive
|
|
27
|
+
const confirm =
|
|
28
|
+
(await last.getAttribute("aria-label")) === null &&
|
|
29
|
+
(await last.locator("button, [role=button]").count()) === 2 &&
|
|
30
|
+
(await last.locator(SEL.caption).count()) === 0;
|
|
31
|
+
if (confirm)
|
|
32
|
+
await last.locator("button, [role=button]").first().click();
|
|
33
|
+
else await page.keyboard.press("Escape");
|
|
34
|
+
await page.waitForTimeout(1000);
|
|
35
|
+
}
|
|
36
|
+
return (await page.locator(SEL.composer).count())
|
|
37
|
+
? {
|
|
38
|
+
error:
|
|
39
|
+
"The composer did not close. Snapshot it and close it by hand.",
|
|
40
|
+
}
|
|
41
|
+
: { closed: true };
|
|
42
|
+
},
|
|
43
|
+
{ SEL },
|
|
44
|
+
),
|
|
45
|
+
);
|
|
46
|
+
if (done.closed) console.log("No composer open; any draft was discarded.");
|
|
47
|
+
process.exit(0);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const files = opts._.map((f) => resolve(f));
|
|
51
|
+
const ratio = String(opts.ratio ?? "");
|
|
52
|
+
if (!files.length || !SEL.cropOptions.includes(ratio) || !opts.caption)
|
|
53
|
+
fail(
|
|
54
|
+
`usage: node compose.mjs --ratio ${SEL.cropOptions.join("|")} --caption <file> [--ai-label] [--shot last.png] <picture>...`,
|
|
55
|
+
);
|
|
56
|
+
if (files.length > 20) fail("A post holds at most 20 pictures.");
|
|
57
|
+
const caption = readFileSync(opts.caption, "utf8").replace(/\r/g, "").trim();
|
|
58
|
+
if (caption.length > 2200)
|
|
59
|
+
fail(`The caption is ${caption.length} characters; the limit is 2200.`);
|
|
60
|
+
|
|
61
|
+
const started = Date.now();
|
|
62
|
+
const done = orFail(
|
|
63
|
+
await inPage(
|
|
64
|
+
async (page, { files, ratio, caption, aiLabel, shot, SEL }) => {
|
|
65
|
+
const dialog = () => page.locator(SEL.composer).last();
|
|
66
|
+
if (
|
|
67
|
+
(await page.locator(`${SEL.composer} ${SEL.caption}`).count()) ||
|
|
68
|
+
(await page.locator(`${SEL.composer} ${SEL.fileInput}`).count())
|
|
69
|
+
)
|
|
70
|
+
return {
|
|
71
|
+
error:
|
|
72
|
+
"A composer is already open. Run with --discard first, or finish it by hand.",
|
|
73
|
+
};
|
|
74
|
+
if (
|
|
75
|
+
!/instagram\.com/.test(page.url()) ||
|
|
76
|
+
/\/accounts\/|\/challenge\//.test(page.url())
|
|
77
|
+
)
|
|
78
|
+
await page.goto("https://www.instagram.com/", {
|
|
79
|
+
waitUntil: "domcontentloaded",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// A narrow window swaps the sidebar for a bottom bar in another order: work wide
|
|
83
|
+
const viewport = page.viewportSize();
|
|
84
|
+
if (viewport && viewport.width < SEL.wideWidth)
|
|
85
|
+
await page.setViewportSize({
|
|
86
|
+
width: SEL.wideWidth,
|
|
87
|
+
height: Math.max(viewport.height, 900),
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// Create, then the menu's first entry (Post), until a dialog takes files. The
|
|
91
|
+
// menu's entries land in the page right after Create's own link
|
|
92
|
+
const shown = () =>
|
|
93
|
+
page.locator(SEL.sidebarAction).evaluateAll((all) =>
|
|
94
|
+
all.map((a, i) => ({
|
|
95
|
+
i,
|
|
96
|
+
x: a.getBoundingClientRect().x,
|
|
97
|
+
w: a.getBoundingClientRect().width,
|
|
98
|
+
})),
|
|
99
|
+
);
|
|
100
|
+
await page
|
|
101
|
+
.locator(SEL.sidebarAction)
|
|
102
|
+
.first()
|
|
103
|
+
.waitFor({ timeout: 15000 })
|
|
104
|
+
.catch(() => {});
|
|
105
|
+
const side = (await shown()).filter((a) => a.w > 0 && a.x < 120);
|
|
106
|
+
if (side.length <= SEL.createIndex)
|
|
107
|
+
return {
|
|
108
|
+
error:
|
|
109
|
+
"Selectors changed: no Create entry in the sidebar (SEL.sidebarAction). Snapshot and fix selectors.mjs.",
|
|
110
|
+
};
|
|
111
|
+
const create = side[SEL.createIndex].i;
|
|
112
|
+
const input = page.locator(`${SEL.composer} ${SEL.fileInput}`);
|
|
113
|
+
// Twice at most: a menu left open closes on the first press
|
|
114
|
+
for (let i = 0; i < 2 && !(await input.count()); i++) {
|
|
115
|
+
const count = await page.locator(SEL.sidebarAction).count();
|
|
116
|
+
await page.locator(SEL.sidebarAction).nth(create).click();
|
|
117
|
+
await page.waitForTimeout(1000);
|
|
118
|
+
if ((await page.locator(SEL.sidebarAction).count()) > count) {
|
|
119
|
+
await page
|
|
120
|
+
.locator(SEL.sidebarAction)
|
|
121
|
+
.nth(create + 1)
|
|
122
|
+
.click();
|
|
123
|
+
await input
|
|
124
|
+
.waitFor({ state: "attached", timeout: 8000 })
|
|
125
|
+
.catch(() => {});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!(await input.count())) {
|
|
129
|
+
await page.keyboard.press("Escape");
|
|
130
|
+
return {
|
|
131
|
+
error:
|
|
132
|
+
"Selectors changed: Create did not open a composer that takes files (SEL.createIndex). Snapshot and fix selectors.mjs.",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
await input.setInputFiles(files);
|
|
136
|
+
|
|
137
|
+
// The crop step: files taken, a preview drawn
|
|
138
|
+
const cropping = await page
|
|
139
|
+
.waitForFunction(
|
|
140
|
+
(SEL) => {
|
|
141
|
+
const d = [...document.querySelectorAll(SEL.composer)].at(-1);
|
|
142
|
+
return (
|
|
143
|
+
d &&
|
|
144
|
+
!d.querySelector(SEL.fileInput) &&
|
|
145
|
+
[...d.querySelectorAll(SEL.preview)].some(
|
|
146
|
+
(i) => i.getBoundingClientRect().width > 200,
|
|
147
|
+
)
|
|
148
|
+
);
|
|
149
|
+
},
|
|
150
|
+
SEL,
|
|
151
|
+
{ timeout: 30000 },
|
|
152
|
+
)
|
|
153
|
+
.then(() => true)
|
|
154
|
+
.catch(() => false);
|
|
155
|
+
if (!cropping)
|
|
156
|
+
return {
|
|
157
|
+
error:
|
|
158
|
+
"The composer did not reach its crop step after the files went in (a file refused, or the page changed). Snapshot and look.",
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
// The crop toggle is the leftmost icon along the preview's bottom edge
|
|
162
|
+
const toggled = await dialog().evaluate((d) => {
|
|
163
|
+
const box = d.getBoundingClientRect();
|
|
164
|
+
const icons = [...d.querySelectorAll("button, [role=button]")]
|
|
165
|
+
.filter((b) => !b.innerText.trim() && b.querySelector("svg"))
|
|
166
|
+
.map((b) => ({ b, r: b.getBoundingClientRect() }))
|
|
167
|
+
.filter(({ r }) => r.width > 0 && r.bottom > box.bottom - 90)
|
|
168
|
+
.sort((a, b) => a.r.x - b.r.x);
|
|
169
|
+
if (!icons.length) return false;
|
|
170
|
+
icons[0].b.setAttribute("data-kit-crop", "");
|
|
171
|
+
return true;
|
|
172
|
+
});
|
|
173
|
+
if (!toggled)
|
|
174
|
+
return {
|
|
175
|
+
error:
|
|
176
|
+
"Selectors changed: no crop control on the crop step. Snapshot and fix compose.mjs.",
|
|
177
|
+
};
|
|
178
|
+
await page.locator("[data-kit-crop]").first().click();
|
|
179
|
+
await page.waitForTimeout(700);
|
|
180
|
+
const picked = await dialog().evaluate((d, ratio) => {
|
|
181
|
+
const options = [...d.querySelectorAll("button, [role=button]")];
|
|
182
|
+
const square = options.findIndex((b) =>
|
|
183
|
+
b.textContent.trim().startsWith("1:1"),
|
|
184
|
+
);
|
|
185
|
+
if (square < 1) return false;
|
|
186
|
+
const target =
|
|
187
|
+
ratio === "original"
|
|
188
|
+
? options[square - 1]
|
|
189
|
+
: options.find((b) => b.textContent.trim().startsWith(ratio));
|
|
190
|
+
if (!target) return false;
|
|
191
|
+
target.setAttribute("data-kit-ratio", "");
|
|
192
|
+
return true;
|
|
193
|
+
}, ratio);
|
|
194
|
+
if (!picked)
|
|
195
|
+
return {
|
|
196
|
+
error:
|
|
197
|
+
"Selectors changed: the crop menu has no ratio options. Snapshot and fix compose.mjs.",
|
|
198
|
+
};
|
|
199
|
+
await page.locator("[data-kit-ratio]").click();
|
|
200
|
+
await page.waitForTimeout(800);
|
|
201
|
+
// The crop is the frame the picture shows through, not the picture itself
|
|
202
|
+
const measured = await dialog().evaluate((d, preview) => {
|
|
203
|
+
const el = [...d.querySelectorAll(preview)]
|
|
204
|
+
.filter((e) => e.getBoundingClientRect().width > 200)
|
|
205
|
+
.sort(
|
|
206
|
+
(a, b) =>
|
|
207
|
+
b.getBoundingClientRect().width - a.getBoundingClientRect().width,
|
|
208
|
+
)[0];
|
|
209
|
+
if (!el) return null;
|
|
210
|
+
let { left, top, right, bottom } = el.getBoundingClientRect();
|
|
211
|
+
for (
|
|
212
|
+
let a = el.parentElement;
|
|
213
|
+
a && a !== d.parentElement;
|
|
214
|
+
a = a.parentElement
|
|
215
|
+
) {
|
|
216
|
+
if (getComputedStyle(a).overflow === "visible") continue;
|
|
217
|
+
const r = a.getBoundingClientRect();
|
|
218
|
+
left = Math.max(left, r.left);
|
|
219
|
+
top = Math.max(top, r.top);
|
|
220
|
+
right = Math.min(right, r.right);
|
|
221
|
+
bottom = Math.min(bottom, r.bottom);
|
|
222
|
+
}
|
|
223
|
+
return bottom > top ? (right - left) / (bottom - top) : null;
|
|
224
|
+
}, SEL.preview);
|
|
225
|
+
const want = { "1:1": 1, "4:5": 0.8, "16:9": 16 / 9 }[ratio];
|
|
226
|
+
if (want && (!measured || Math.abs(measured - want) / want > 0.03))
|
|
227
|
+
return {
|
|
228
|
+
error: `The crop did not take: the preview is ${measured?.toFixed(3)} wide per tall, not ${ratio}. Snapshot and set it by hand.`,
|
|
229
|
+
};
|
|
230
|
+
await page
|
|
231
|
+
.locator("[data-kit-crop]")
|
|
232
|
+
.first()
|
|
233
|
+
.click()
|
|
234
|
+
.catch(() => {});
|
|
235
|
+
|
|
236
|
+
// Crop, then filters, then the caption: two Nexts and never a third, since the
|
|
237
|
+
// caption step's header button in the same place is Share
|
|
238
|
+
for (let i = 0; i < 2; i++) {
|
|
239
|
+
if (await dialog().locator(SEL.caption).count()) break;
|
|
240
|
+
const before = await dialog().getAttribute("aria-label");
|
|
241
|
+
const next = await dialog().evaluate((d) => {
|
|
242
|
+
const top = d.getBoundingClientRect().top;
|
|
243
|
+
const header = [...d.querySelectorAll("button, [role=button]")]
|
|
244
|
+
.map((b) => ({ b, r: b.getBoundingClientRect() }))
|
|
245
|
+
.filter(
|
|
246
|
+
({ b, r }) =>
|
|
247
|
+
b.innerText.trim() && r.width > 0 && r.top < top + 60,
|
|
248
|
+
)
|
|
249
|
+
.sort((a, b) => b.r.x - a.r.x)[0];
|
|
250
|
+
if (!header) return false;
|
|
251
|
+
for (const e of d.querySelectorAll("[data-kit-next]"))
|
|
252
|
+
e.removeAttribute("data-kit-next");
|
|
253
|
+
header.b.setAttribute("data-kit-next", "");
|
|
254
|
+
return true;
|
|
255
|
+
});
|
|
256
|
+
if (!next)
|
|
257
|
+
return {
|
|
258
|
+
error:
|
|
259
|
+
"Selectors changed: no Next in the composer's header. Snapshot and go on by hand.",
|
|
260
|
+
};
|
|
261
|
+
await dialog().locator("[data-kit-next]").click();
|
|
262
|
+
await page
|
|
263
|
+
.waitForFunction(
|
|
264
|
+
([sel, before]) =>
|
|
265
|
+
[...document.querySelectorAll(sel)]
|
|
266
|
+
.at(-1)
|
|
267
|
+
?.getAttribute("aria-label") !== before,
|
|
268
|
+
[SEL.composer, before],
|
|
269
|
+
{ timeout: 10000 },
|
|
270
|
+
)
|
|
271
|
+
.catch(() => {});
|
|
272
|
+
}
|
|
273
|
+
const box = dialog().locator(SEL.caption).first();
|
|
274
|
+
await box.waitFor({ timeout: 10000 }).catch(() => {});
|
|
275
|
+
if (!(await box.count()))
|
|
276
|
+
return {
|
|
277
|
+
error:
|
|
278
|
+
"The composer did not reach its caption step in two Nexts. Snapshot and go on by hand.",
|
|
279
|
+
};
|
|
280
|
+
|
|
281
|
+
let label = null;
|
|
282
|
+
const toggle = dialog().locator(SEL.aiLabel).first();
|
|
283
|
+
if (await toggle.count()) {
|
|
284
|
+
// The input itself does not take a click; the switch drawn around it does
|
|
285
|
+
if ((await toggle.isChecked()) !== aiLabel)
|
|
286
|
+
await toggle.locator("xpath=..").click({ timeout: 5000 });
|
|
287
|
+
await page.waitForTimeout(400);
|
|
288
|
+
label = await toggle.isChecked();
|
|
289
|
+
}
|
|
290
|
+
await box.click();
|
|
291
|
+
const lines = caption.split("\n");
|
|
292
|
+
for (let i = 0; i < lines.length; i++) {
|
|
293
|
+
if (i) await page.keyboard.press("Enter");
|
|
294
|
+
if (lines[i]) await page.keyboard.insertText(lines[i]);
|
|
295
|
+
}
|
|
296
|
+
await page.waitForTimeout(500);
|
|
297
|
+
const typed = (await box.innerText()).replace(/\r/g, "").trim();
|
|
298
|
+
|
|
299
|
+
// Close the tag suggestions typing a # opens, without Escape (which asks to discard)
|
|
300
|
+
await dialog()
|
|
301
|
+
.locator("h1, h2, [role=heading]")
|
|
302
|
+
.first()
|
|
303
|
+
.click({ timeout: 3000 })
|
|
304
|
+
.catch(() => {});
|
|
305
|
+
await page.waitForTimeout(300);
|
|
306
|
+
if (shot) await dialog().screenshot({ path: shot, scale: "css" });
|
|
307
|
+
if (viewport && viewport.width < SEL.wideWidth)
|
|
308
|
+
await page.setViewportSize(viewport);
|
|
309
|
+
return { measured, typed, label };
|
|
310
|
+
},
|
|
311
|
+
{
|
|
312
|
+
files,
|
|
313
|
+
ratio,
|
|
314
|
+
caption,
|
|
315
|
+
aiLabel: Boolean(opts["ai-label"]),
|
|
316
|
+
shot: opts.shot ? resolve(opts.shot) : null,
|
|
317
|
+
SEL,
|
|
318
|
+
},
|
|
319
|
+
),
|
|
320
|
+
);
|
|
321
|
+
|
|
322
|
+
const same = done.typed.replace(/\s+/g, " ") === caption.replace(/\s+/g, " ");
|
|
323
|
+
console.log(
|
|
324
|
+
`On the last screen: ${files.length} picture(s) at ${ratio}${done.measured ? ` (preview ${done.measured.toFixed(3)})` : ""}, ` +
|
|
325
|
+
`caption ${done.typed.length} chars${same ? "" : " — NOT the same as the file; fix it in the composer"}, ` +
|
|
326
|
+
`AI label ${done.label === null ? "not offered" : done.label ? "on" : "off"}. Share is not pressed.` +
|
|
327
|
+
`${opts.shot ? ` Screenshot: ${resolve(opts.shot)}.` : ""} ${((Date.now() - started) / 1000).toFixed(1)}s`,
|
|
328
|
+
);
|
|
329
|
+
if (!same) process.exit(1);
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { closeSync, openSync, readSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
/** Width and height of a PNG or JPEG, read from its header; null for anything else. */
|
|
4
|
+
export function imageSize(path) {
|
|
5
|
+
const fd = openSync(path, "r");
|
|
6
|
+
try {
|
|
7
|
+
const head = Buffer.alloc(64 * 1024);
|
|
8
|
+
const n = readSync(fd, head, 0, head.length, 0);
|
|
9
|
+
if (head.readUInt32BE(0) === 0x89504e47)
|
|
10
|
+
return { w: head.readUInt32BE(16), h: head.readUInt32BE(20) };
|
|
11
|
+
if (head[0] === 0xff && head[1] === 0xd8) {
|
|
12
|
+
// Walk the JPEG segments to the first frame header
|
|
13
|
+
for (let i = 2; i + 9 < n; ) {
|
|
14
|
+
if (head[i] !== 0xff) return null;
|
|
15
|
+
const marker = head[i + 1];
|
|
16
|
+
const len = head.readUInt16BE(i + 2);
|
|
17
|
+
if (
|
|
18
|
+
marker >= 0xc0 &&
|
|
19
|
+
marker <= 0xcf &&
|
|
20
|
+
![0xc4, 0xc8, 0xcc].includes(marker)
|
|
21
|
+
)
|
|
22
|
+
return { w: head.readUInt16BE(i + 7), h: head.readUInt16BE(i + 5) };
|
|
23
|
+
i += 2 + len;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return null;
|
|
27
|
+
} finally {
|
|
28
|
+
closeSync(fd);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Lists DM threads from the inbox page without opening any of them — opening a
|
|
4
|
+
* thread marks it seen. One line a thread: unread mark, name, last message, when, id.
|
|
5
|
+
*
|
|
6
|
+
* node inbox.mjs [--folder primary|general|requests] [--max 30] [--unread] [--json]
|
|
7
|
+
*/
|
|
8
|
+
import { fail, inPage, oneLine, orFail, parseArgs, stamp } from "./lib.mjs";
|
|
9
|
+
import { SEL } from "./selectors.mjs";
|
|
10
|
+
|
|
11
|
+
const opts = parseArgs();
|
|
12
|
+
const folder = opts.folder ?? "primary";
|
|
13
|
+
if (!SEL.folders[folder])
|
|
14
|
+
fail(`--folder is one of ${Object.keys(SEL.folders).join(", ")}`);
|
|
15
|
+
const max = Number(opts.max ?? 30);
|
|
16
|
+
|
|
17
|
+
const started = Date.now();
|
|
18
|
+
const found = orFail(
|
|
19
|
+
await inPage(
|
|
20
|
+
async (page, { url, tab, SEL, max }) => {
|
|
21
|
+
if (!page.url().startsWith(url))
|
|
22
|
+
await page.goto(url, { waitUntil: "domcontentloaded" });
|
|
23
|
+
if (tab != null) {
|
|
24
|
+
const pick = page.locator(SEL.folderTab).nth(tab);
|
|
25
|
+
await pick.waitFor({ timeout: 15000 }).catch(() => {});
|
|
26
|
+
if (
|
|
27
|
+
(await pick.count()) &&
|
|
28
|
+
(await pick.getAttribute("aria-selected")) !== "true"
|
|
29
|
+
)
|
|
30
|
+
await pick.click();
|
|
31
|
+
}
|
|
32
|
+
// Loaded when the list is there and its loading placeholders are gone
|
|
33
|
+
await page
|
|
34
|
+
.waitForFunction(
|
|
35
|
+
(SEL) => {
|
|
36
|
+
const list = document.querySelector(SEL.inboxList);
|
|
37
|
+
return (
|
|
38
|
+
list &&
|
|
39
|
+
(list.querySelector(SEL.inboxRow) ||
|
|
40
|
+
!list.querySelector("[role=status]"))
|
|
41
|
+
);
|
|
42
|
+
},
|
|
43
|
+
SEL,
|
|
44
|
+
{ timeout: 15000 },
|
|
45
|
+
)
|
|
46
|
+
.catch(() => {});
|
|
47
|
+
await page.waitForTimeout(500);
|
|
48
|
+
|
|
49
|
+
const count = () =>
|
|
50
|
+
page.evaluate(
|
|
51
|
+
(SEL) =>
|
|
52
|
+
document.querySelectorAll(`${SEL.inboxList} ${SEL.inboxRow}`)
|
|
53
|
+
.length,
|
|
54
|
+
SEL,
|
|
55
|
+
);
|
|
56
|
+
// More threads load as the list scrolls
|
|
57
|
+
for (let n = await count(), tries = 0; n < max && tries < 20; tries++) {
|
|
58
|
+
await page.evaluate((SEL) => {
|
|
59
|
+
const row = document.querySelector(
|
|
60
|
+
`${SEL.inboxList} ${SEL.inboxRow}`,
|
|
61
|
+
);
|
|
62
|
+
for (let e = row?.parentElement; e; e = e.parentElement)
|
|
63
|
+
if (e.scrollHeight > e.clientHeight + 4) {
|
|
64
|
+
e.scrollTop = e.scrollHeight;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
}, SEL);
|
|
68
|
+
const grew = await page
|
|
69
|
+
.waitForFunction(
|
|
70
|
+
([SEL, n]) =>
|
|
71
|
+
document.querySelectorAll(`${SEL.inboxList} ${SEL.inboxRow}`)
|
|
72
|
+
.length > n,
|
|
73
|
+
[SEL, n],
|
|
74
|
+
{ timeout: 3000 },
|
|
75
|
+
)
|
|
76
|
+
.then(() => true)
|
|
77
|
+
.catch(() => false);
|
|
78
|
+
if (!grew) break;
|
|
79
|
+
n = await count();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return page.evaluate(
|
|
83
|
+
({ SEL, max }) => {
|
|
84
|
+
if (/\/accounts\/login|\/challenge\//.test(location.pathname))
|
|
85
|
+
return {
|
|
86
|
+
error: `Not signed in (${location.pathname}). Borrow the kept sign-in, goto the inbox, run again.`,
|
|
87
|
+
};
|
|
88
|
+
const list = document.querySelector(SEL.inboxList);
|
|
89
|
+
if (!list)
|
|
90
|
+
return {
|
|
91
|
+
error: document.querySelector("main")
|
|
92
|
+
? "Selectors changed: no thread list (SEL.inboxList). Snapshot the inbox once and fix selectors.mjs."
|
|
93
|
+
: "No inbox on the page: something covers it (a dialog, a daily time-limit screen). Snapshot, close it, run again.",
|
|
94
|
+
};
|
|
95
|
+
const fiberOf = (el) =>
|
|
96
|
+
el[Object.keys(el).find((k) => k.startsWith("__reactFiber"))];
|
|
97
|
+
const findProp = (el, name) => {
|
|
98
|
+
for (let f = fiberOf(el), d = 0; f && d < 30; f = f.return, d++) {
|
|
99
|
+
const p = f.memoizedProps;
|
|
100
|
+
if (!p || typeof p !== "object") continue;
|
|
101
|
+
if (p[name] != null) return p[name];
|
|
102
|
+
for (const v of Object.values(p))
|
|
103
|
+
if (v && typeof v === "object" && v[name] != null)
|
|
104
|
+
return v[name];
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
};
|
|
108
|
+
const rows = [...list.querySelectorAll(SEL.inboxRow)].slice(0, max);
|
|
109
|
+
const threads = rows.map((row) => {
|
|
110
|
+
const lines = row.innerText
|
|
111
|
+
.split("\n")
|
|
112
|
+
.map((l) => l.trim())
|
|
113
|
+
.filter((l) => l && l !== "·");
|
|
114
|
+
const abbr = row.querySelector("abbr");
|
|
115
|
+
return {
|
|
116
|
+
id: findProp(row, SEL.threadIdProp),
|
|
117
|
+
name: lines[0] ?? "",
|
|
118
|
+
last: lines[1] ?? "",
|
|
119
|
+
ago: abbr?.getAttribute("aria-label") || abbr?.innerText || "",
|
|
120
|
+
at: Number(findProp(row, SEL.lastActivityProp)) || null,
|
|
121
|
+
unread: [...row.querySelectorAll("span")].some(
|
|
122
|
+
(s) =>
|
|
123
|
+
Number(getComputedStyle(s).fontWeight) >= SEL.unreadWeight,
|
|
124
|
+
),
|
|
125
|
+
};
|
|
126
|
+
});
|
|
127
|
+
if (threads.length && threads.every((t) => !t.id))
|
|
128
|
+
return {
|
|
129
|
+
error:
|
|
130
|
+
"Selectors changed: rows found but no thread id (SEL.threadIdProp). Fix selectors.mjs.",
|
|
131
|
+
};
|
|
132
|
+
return { threads };
|
|
133
|
+
},
|
|
134
|
+
{ SEL, max },
|
|
135
|
+
);
|
|
136
|
+
},
|
|
137
|
+
{
|
|
138
|
+
url: `https://www.instagram.com${SEL.folders[folder].url}`,
|
|
139
|
+
tab: SEL.folders[folder].tab,
|
|
140
|
+
SEL,
|
|
141
|
+
max,
|
|
142
|
+
},
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
let threads = found.threads;
|
|
147
|
+
if (opts.unread) threads = threads.filter((t) => t.unread);
|
|
148
|
+
if (opts.json) {
|
|
149
|
+
process.stdout.write(`${JSON.stringify(threads)}\n`);
|
|
150
|
+
} else {
|
|
151
|
+
for (const t of threads)
|
|
152
|
+
console.log(
|
|
153
|
+
`${t.unread ? "* " : " "}${oneLine(t.name, 40)} — ${oneLine(t.last, 80)} · ${t.at ? stamp(t.at) : t.ago} · ${t.id ?? "no id"}`,
|
|
154
|
+
);
|
|
155
|
+
const unread = found.threads.filter((t) => t.unread).length;
|
|
156
|
+
console.log(
|
|
157
|
+
`${found.threads.length} threads in ${folder}, ${unread} unread (*). ${((Date.now() - started) / 1000).toFixed(1)}s`,
|
|
158
|
+
);
|
|
159
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Runs `fn(page, args, helpers)` in this shell's own browser session
|
|
8
|
+
* (PLAYWRIGHT_CLI_SESSION) through `playwright-cli run-code`, and resolves to what
|
|
9
|
+
* it returned. `fn` and each helper are sent as source, so they see nothing of
|
|
10
|
+
* their module: what they need comes in `args`. They run in a bare VM beside the
|
|
11
|
+
* browser — no timers, no require — so waits are `page.waitForTimeout` and
|
|
12
|
+
* `page.waitForFunction`, and files are written by the caller from what returns.
|
|
13
|
+
*/
|
|
14
|
+
export async function inPage(fn, args = {}, helpers = {}) {
|
|
15
|
+
const lib = Object.entries(helpers)
|
|
16
|
+
.map(([name, helper]) => `${name}: ${helper}`)
|
|
17
|
+
.join(", ");
|
|
18
|
+
const code = `async page => (${fn})(page, ${JSON.stringify(args)}, { ${lib} })`;
|
|
19
|
+
// One argument is capped (128 KB on Linux): longer code goes as a file
|
|
20
|
+
const dir = code.length > 64_000 ? mkdtempSync(join(tmpdir(), "run-")) : null;
|
|
21
|
+
if (dir) writeFileSync(join(dir, "code.js"), code);
|
|
22
|
+
const out = await new Promise((resolve) =>
|
|
23
|
+
execFile(
|
|
24
|
+
"playwright-cli",
|
|
25
|
+
dir
|
|
26
|
+
? ["--raw", "run-code", `--filename=${join(dir, "code.js")}`]
|
|
27
|
+
: ["--raw", "run-code", code],
|
|
28
|
+
{ encoding: "utf8", maxBuffer: 256 * 1024 * 1024 },
|
|
29
|
+
(error, stdout, stderr) => {
|
|
30
|
+
if (dir) rmSync(dir, { recursive: true, force: true });
|
|
31
|
+
if (!error) return resolve(stdout.trim());
|
|
32
|
+
const said = `${stdout ?? ""}${stderr ?? ""}`.trim();
|
|
33
|
+
fail(
|
|
34
|
+
/not open|no (open )?browser/i.test(said)
|
|
35
|
+
? "No browser is open in this session. `playwright-cli open`, sign in, then run this again."
|
|
36
|
+
: `The browser answered with an error:\n${said.slice(0, 800)}`,
|
|
37
|
+
);
|
|
38
|
+
},
|
|
39
|
+
),
|
|
40
|
+
);
|
|
41
|
+
if (!out) return undefined;
|
|
42
|
+
try {
|
|
43
|
+
return JSON.parse(out);
|
|
44
|
+
} catch {
|
|
45
|
+
fail(`Unexpected answer from the browser:\n${out.slice(0, 800)}`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** `--name value` and `--flag` pairs, and the rest as positionals. */
|
|
50
|
+
export function parseArgs(argv = process.argv.slice(2)) {
|
|
51
|
+
const opts = { _: [] };
|
|
52
|
+
for (let i = 0; i < argv.length; i++) {
|
|
53
|
+
const a = argv[i];
|
|
54
|
+
if (!a.startsWith("--")) {
|
|
55
|
+
opts._.push(a);
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
const [key, inline] = a.slice(2).split("=", 2);
|
|
59
|
+
if (inline !== undefined) opts[key] = inline;
|
|
60
|
+
else if (argv[i + 1] !== undefined && !argv[i + 1].startsWith("--"))
|
|
61
|
+
opts[key] = argv[++i];
|
|
62
|
+
else opts[key] = true;
|
|
63
|
+
}
|
|
64
|
+
return opts;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function fail(message) {
|
|
68
|
+
process.stderr.write(`${message}\n`);
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/** A page-side answer `{ error }` stops the script with that message. */
|
|
73
|
+
export function orFail(result) {
|
|
74
|
+
if (result && typeof result === "object" && result.error) fail(result.error);
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Local time, minutes precision: what a person reads, and what `--since` takes. */
|
|
79
|
+
export const stamp = (ms) => {
|
|
80
|
+
if (!ms) return "?";
|
|
81
|
+
const d = new Date(Number(ms));
|
|
82
|
+
const p = (n) => String(n).padStart(2, "0");
|
|
83
|
+
return `${d.getFullYear()}-${p(d.getMonth() + 1)}-${p(d.getDate())} ${p(d.getHours())}:${p(d.getMinutes())}`;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const oneLine = (text, max) => {
|
|
87
|
+
const flat = String(text ?? "")
|
|
88
|
+
.replace(/\s+/g, " ")
|
|
89
|
+
.trim();
|
|
90
|
+
return flat.length > max ? `${flat.slice(0, max - 1)}…` : flat;
|
|
91
|
+
};
|