@asrunjs/asrun 0.0.0-stage → 0.2.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.
Files changed (286) hide show
  1. package/.claude/commands/explore-case.md +15 -0
  2. package/.claude/commands/gen-spec.md +13 -0
  3. package/.claude/commands/heal-spec.md +23 -0
  4. package/.claude/commands/make-case.md +8 -0
  5. package/.claude/settings.json +5 -0
  6. package/.claude/skills/exploring-cases/SKILL.md +132 -0
  7. package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -0
  8. package/.claude/skills/exploring-cases/references/handoff-card.js +128 -0
  9. package/.claude/skills/exploring-cases/references/open-questions.md +112 -0
  10. package/.claude/skills/exploring-cases/references/readiness.md +137 -0
  11. package/.claude/skills/exploring-cases/references/user-handoff.md +147 -0
  12. package/.claude/skills/generating-specs/SKILL.md +207 -0
  13. package/.claude/skills/generating-specs/references/conventions.md +148 -0
  14. package/.claude/skills/making-cases/SKILL.md +64 -0
  15. package/.claude/skills/making-cases/references/gaps.md +36 -0
  16. package/.claude/skills/making-cases/references/normalizing.md +38 -0
  17. package/.claude/skills/making-cases/references/splitting.md +28 -0
  18. package/.claude/skills/making-cases/references/writes-and-email.md +36 -0
  19. package/AGENTS.md +154 -0
  20. package/CLAUDE.md +1 -0
  21. package/LICENSE +21 -0
  22. package/README.md +410 -3
  23. package/bin/asrun.mjs +49 -0
  24. package/dist/lib/account-lock.d.ts +6 -0
  25. package/dist/lib/account-lock.js +52 -0
  26. package/dist/lib/assertions.d.ts +14 -0
  27. package/dist/lib/assertions.js +39 -0
  28. package/dist/lib/baselines.d.ts +43 -0
  29. package/dist/lib/baselines.js +108 -0
  30. package/dist/lib/case-id.d.ts +11 -0
  31. package/dist/lib/case-id.js +17 -0
  32. package/dist/lib/case-ops.d.ts +26 -0
  33. package/dist/lib/case-ops.js +110 -0
  34. package/dist/lib/cases.d.ts +29 -0
  35. package/dist/lib/cases.js +124 -0
  36. package/dist/lib/cli.d.ts +10 -0
  37. package/dist/lib/cli.js +18 -0
  38. package/dist/lib/downloads.d.ts +42 -0
  39. package/dist/lib/downloads.js +84 -0
  40. package/dist/lib/email.d.ts +10 -0
  41. package/dist/lib/email.js +19 -0
  42. package/dist/lib/ffmpeg.d.ts +8 -0
  43. package/dist/lib/ffmpeg.js +50 -0
  44. package/dist/lib/fixtures.d.ts +40 -0
  45. package/dist/lib/fixtures.js +149 -0
  46. package/dist/lib/node-cli.d.ts +11 -0
  47. package/dist/lib/node-cli.js +26 -0
  48. package/dist/lib/paths.d.ts +25 -0
  49. package/dist/lib/paths.js +88 -0
  50. package/dist/lib/pdf.d.ts +71 -0
  51. package/dist/lib/pdf.js +140 -0
  52. package/dist/lib/profiles.d.ts +38 -0
  53. package/dist/lib/profiles.js +121 -0
  54. package/dist/lib/ready.d.ts +36 -0
  55. package/dist/lib/ready.js +53 -0
  56. package/dist/lib/records.d.ts +33 -0
  57. package/dist/lib/records.js +61 -0
  58. package/dist/lib/run-filters.d.ts +19 -0
  59. package/dist/lib/run-filters.js +30 -0
  60. package/dist/lib/run-summary.d.ts +90 -0
  61. package/dist/lib/run-summary.js +35 -0
  62. package/dist/lib/session.d.ts +18 -0
  63. package/dist/lib/session.js +45 -0
  64. package/dist/lib/summary-reporter.d.ts +16 -0
  65. package/dist/lib/summary-reporter.js +146 -0
  66. package/dist/lib/targets.d.ts +27 -0
  67. package/dist/lib/targets.js +51 -0
  68. package/dist/lib/ui-build.d.ts +10 -0
  69. package/dist/lib/ui-build.js +30 -0
  70. package/dist/lib/values.d.ts +29 -0
  71. package/dist/lib/values.js +85 -0
  72. package/dist/lib/write-gate.d.ts +41 -0
  73. package/dist/lib/write-gate.js +129 -0
  74. package/dist/playwright.config.d.ts +2 -0
  75. package/dist/playwright.config.js +92 -0
  76. package/dist/scripts/build.d.ts +1 -0
  77. package/dist/scripts/build.js +20 -0
  78. package/dist/scripts/check-cases.d.ts +1 -0
  79. package/dist/scripts/check-cases.js +81 -0
  80. package/dist/scripts/cleanup.d.ts +1 -0
  81. package/dist/scripts/cleanup.js +25 -0
  82. package/dist/scripts/cli.d.ts +1 -0
  83. package/dist/scripts/cli.js +28 -0
  84. package/dist/scripts/doctor.d.ts +1 -0
  85. package/dist/scripts/doctor.js +185 -0
  86. package/dist/scripts/explore.d.ts +1 -0
  87. package/dist/scripts/explore.js +54 -0
  88. package/dist/scripts/go.mjs +188 -0
  89. package/dist/scripts/heal-evidence.mjs +143 -0
  90. package/dist/scripts/init.d.ts +16 -0
  91. package/dist/scripts/init.js +284 -0
  92. package/dist/scripts/lint-specs.d.ts +1 -0
  93. package/dist/scripts/lint-specs.js +138 -0
  94. package/dist/scripts/login.d.ts +1 -0
  95. package/dist/scripts/login.js +47 -0
  96. package/dist/scripts/new-case.d.ts +1 -0
  97. package/dist/scripts/new-case.js +19 -0
  98. package/dist/scripts/render-pdf.d.ts +1 -0
  99. package/dist/scripts/render-pdf.js +44 -0
  100. package/dist/scripts/report.d.ts +1 -0
  101. package/dist/scripts/report.js +14 -0
  102. package/dist/scripts/review-baselines.d.ts +1 -0
  103. package/dist/scripts/review-baselines.js +30 -0
  104. package/dist/scripts/run.d.ts +1 -0
  105. package/dist/scripts/run.js +423 -0
  106. package/dist/scripts/status.d.ts +1 -0
  107. package/dist/scripts/status.js +31 -0
  108. package/dist/scripts/ui-build.d.ts +1 -0
  109. package/dist/scripts/ui-build.js +7 -0
  110. package/dist/scripts/ui-dev.d.ts +1 -0
  111. package/dist/scripts/ui-dev.js +3 -0
  112. package/dist/scripts/ui.d.ts +1 -0
  113. package/dist/scripts/ui.js +13 -0
  114. package/dist/ui/dev.d.ts +1 -0
  115. package/dist/ui/dev.js +36 -0
  116. package/dist/ui/server/actions.d.ts +91 -0
  117. package/dist/ui/server/actions.js +352 -0
  118. package/dist/ui/server/ai-stream.d.ts +41 -0
  119. package/dist/ui/server/ai-stream.js +202 -0
  120. package/dist/ui/server/app.d.ts +4 -0
  121. package/dist/ui/server/app.js +333 -0
  122. package/dist/ui/server/cases.d.ts +23 -0
  123. package/dist/ui/server/cases.js +170 -0
  124. package/dist/ui/server/files.d.ts +5 -0
  125. package/dist/ui/server/files.js +67 -0
  126. package/dist/ui/server/index.d.ts +1 -0
  127. package/dist/ui/server/index.js +33 -0
  128. package/dist/ui/server/jobs.d.ts +74 -0
  129. package/dist/ui/server/jobs.js +263 -0
  130. package/dist/ui/server/profiles.d.ts +16 -0
  131. package/dist/ui/server/profiles.js +195 -0
  132. package/dist/ui/server/watch-files.d.ts +4 -0
  133. package/dist/ui/server/watch-files.js +36 -0
  134. package/dist/ui/shared/ai-commands.d.ts +78 -0
  135. package/dist/ui/shared/ai-commands.js +154 -0
  136. package/dist/ui/shared/case-id.d.ts +1 -0
  137. package/dist/ui/shared/case-id.js +1 -0
  138. package/dist/ui/shared/jobs.d.ts +6 -0
  139. package/dist/ui/shared/jobs.js +7 -0
  140. package/dist/ui/shared/product.d.ts +3 -0
  141. package/dist/ui/shared/product.js +3 -0
  142. package/dist/ui/shared/run-filters.d.ts +1 -0
  143. package/dist/ui/shared/run-filters.js +1 -0
  144. package/dist/ui/shared/site-address.d.ts +6 -0
  145. package/dist/ui/shared/site-address.js +11 -0
  146. package/dist/ui/shared/types.d.ts +257 -0
  147. package/dist/ui/shared/types.js +1 -0
  148. package/docs/theme-guide.html +692 -0
  149. package/package.json +98 -3
  150. package/templates/.claude/commands/explore-case.md +21 -0
  151. package/templates/.claude/commands/gen-spec.md +20 -0
  152. package/templates/.claude/commands/heal-spec.md +30 -0
  153. package/templates/.claude/commands/make-case.md +12 -0
  154. package/templates/.claude/settings.json +5 -0
  155. package/templates/.mcp.json +8 -0
  156. package/templates/AGENTS.md +45 -0
  157. package/templates/CLAUDE.md +1 -0
  158. package/templates/cases/_template.md +63 -0
  159. package/templates/gitignore +13 -0
  160. package/templates/opencode.jsonc +11 -0
  161. package/templates/package.json +31 -0
  162. package/templates/playwright.config.ts +3 -0
  163. package/templates/pnpm-workspace.yaml +5 -0
  164. package/templates/profiles.yaml +29 -0
  165. package/templates/seed.spec.ts +9 -0
  166. package/templates/tsconfig.json +14 -0
  167. package/ui/dist/apple-touch-icon.png +0 -0
  168. package/ui/dist/assets/apl-gfMMiTNf.js +1 -0
  169. package/ui/dist/assets/asciiarmor-C_eisQCp.js +1 -0
  170. package/ui/dist/assets/asn1-DCl_8MCW.js +1 -0
  171. package/ui/dist/assets/asterisk-BEn0Cwpx.js +1 -0
  172. package/ui/dist/assets/brainfuck-D5EjA2JK.js +1 -0
  173. package/ui/dist/assets/clike-xvONpD0Z.js +1 -0
  174. package/ui/dist/assets/clojure-BflJGmDX.js +1 -0
  175. package/ui/dist/assets/cmake-84nbDH0I.js +1 -0
  176. package/ui/dist/assets/cobol-DHLQUbN7.js +1 -0
  177. package/ui/dist/assets/coffeescript-De_zI4J0.js +1 -0
  178. package/ui/dist/assets/commonlisp-DgAKBe0r.js +1 -0
  179. package/ui/dist/assets/crystal-Bl6YKCG_.js +1 -0
  180. package/ui/dist/assets/css-CnaYoQib.js +1 -0
  181. package/ui/dist/assets/cypher-p9eYesGn.js +1 -0
  182. package/ui/dist/assets/d-DWkQyNnU.js +1 -0
  183. package/ui/dist/assets/diff-ChtP43wD.js +1 -0
  184. package/ui/dist/assets/dist-BHaIOZdW.js +1 -0
  185. package/ui/dist/assets/dist-BSJMM_jp.js +1 -0
  186. package/ui/dist/assets/dist-BiE4suo9.js +1 -0
  187. package/ui/dist/assets/dist-BqAROxzI.js +1 -0
  188. package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
  189. package/ui/dist/assets/dist-BwYRbe5s.js +9 -0
  190. package/ui/dist/assets/dist-BzulKuec.js +1 -0
  191. package/ui/dist/assets/dist-CBV7_vDN.js +2 -0
  192. package/ui/dist/assets/dist-CgIQYY1o.js +1 -0
  193. package/ui/dist/assets/dist-DDJNvivM.js +1 -0
  194. package/ui/dist/assets/dist-DF3F_I8N.js +1 -0
  195. package/ui/dist/assets/dist-DHCY9KpV.js +1 -0
  196. package/ui/dist/assets/dist-DX8knLVr.js +1 -0
  197. package/ui/dist/assets/dist-D_I45xgJ.js +11 -0
  198. package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
  199. package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
  200. package/ui/dist/assets/dist-gF2bFn-5.js +1 -0
  201. package/ui/dist/assets/dockerfile-DJkHQgkl.js +1 -0
  202. package/ui/dist/assets/dtd-DTF72YNO.js +1 -0
  203. package/ui/dist/assets/dylan-S7ucR4pR.js +1 -0
  204. package/ui/dist/assets/ebnf-D4c0_ac3.js +1 -0
  205. package/ui/dist/assets/ecl-BVIeWmwY.js +1 -0
  206. package/ui/dist/assets/eiffel-BEjRio4Q.js +1 -0
  207. package/ui/dist/assets/elm-Cshzl8qu.js +1 -0
  208. package/ui/dist/assets/erlang-BV0c4Hdn.js +1 -0
  209. package/ui/dist/assets/factor-LKH5gN8L.js +1 -0
  210. package/ui/dist/assets/fcl-ClOhbFR7.js +1 -0
  211. package/ui/dist/assets/forth-D6RzAl64.js +1 -0
  212. package/ui/dist/assets/fortran-Bt6PBEDR.js +1 -0
  213. package/ui/dist/assets/gas-BHEdbvp9.js +1 -0
  214. package/ui/dist/assets/gherkin-oBAE_ms0.js +1 -0
  215. package/ui/dist/assets/groovy-CUrWKPia.js +1 -0
  216. package/ui/dist/assets/haskell-BRsoo5mP.js +1 -0
  217. package/ui/dist/assets/haxe-DjVOVvNP.js +1 -0
  218. package/ui/dist/assets/http-CLVgA2GD.js +1 -0
  219. package/ui/dist/assets/idl-BUZw3wgd.js +1 -0
  220. package/ui/dist/assets/index-DmasQ6Rq.css +2 -0
  221. package/ui/dist/assets/index-I7uU93kk.js +294 -0
  222. package/ui/dist/assets/javascript-lS2r2ifg.js +1 -0
  223. package/ui/dist/assets/julia-CQfgDRfP.js +1 -0
  224. package/ui/dist/assets/livescript-BXxG0Yva.js +1 -0
  225. package/ui/dist/assets/lua-DPCEkfD_.js +1 -0
  226. package/ui/dist/assets/mathematica-u8YMmbU0.js +1 -0
  227. package/ui/dist/assets/mbox-BEMVcqjs.js +1 -0
  228. package/ui/dist/assets/mirc-C9zpzAZU.js +1 -0
  229. package/ui/dist/assets/mllike-B45xgp2S.js +1 -0
  230. package/ui/dist/assets/modelica-CqHI_q-D.js +1 -0
  231. package/ui/dist/assets/mscgen-qgSLujhx.js +1 -0
  232. package/ui/dist/assets/mumps-BSzj6Xyz.js +1 -0
  233. package/ui/dist/assets/nginx-C-ORUuei.js +1 -0
  234. package/ui/dist/assets/nsis-viPvXFiq.js +1 -0
  235. package/ui/dist/assets/ntriples-CzMBulkp.js +1 -0
  236. package/ui/dist/assets/octave-D2Q8cUp1.js +1 -0
  237. package/ui/dist/assets/oz-BwTct_5T.js +1 -0
  238. package/ui/dist/assets/pascal-Dp_KdFgX.js +1 -0
  239. package/ui/dist/assets/perl-BkSkiI9m.js +1 -0
  240. package/ui/dist/assets/pig-AF4Hwhju.js +1 -0
  241. package/ui/dist/assets/powershell-D8WpQPaX.js +1 -0
  242. package/ui/dist/assets/properties-C357ku5U.js +1 -0
  243. package/ui/dist/assets/protobuf-D1ij_kNL.js +1 -0
  244. package/ui/dist/assets/pug-BqCHZ5xT.js +1 -0
  245. package/ui/dist/assets/puppet-BZyAS7I4.js +1 -0
  246. package/ui/dist/assets/python-B_1Jd8HM.js +1 -0
  247. package/ui/dist/assets/q-B9NhS7L_.js +1 -0
  248. package/ui/dist/assets/r-CxDmU1I4.js +1 -0
  249. package/ui/dist/assets/rpm-gpDK5sbt.js +1 -0
  250. package/ui/dist/assets/ruby-ChfNwlCY.js +1 -0
  251. package/ui/dist/assets/sas-C2OyD2Y6.js +1 -0
  252. package/ui/dist/assets/scheme-D9qYN0V5.js +1 -0
  253. package/ui/dist/assets/shell-DwuoZtxw.js +1 -0
  254. package/ui/dist/assets/sieve-tLWDfEkZ.js +1 -0
  255. package/ui/dist/assets/simple-mode-DXiLM1-G.js +1 -0
  256. package/ui/dist/assets/smalltalk-BLp5-jwC.js +1 -0
  257. package/ui/dist/assets/solr-B78wSQJQ.js +1 -0
  258. package/ui/dist/assets/sparql-Dz-mlCAC.js +1 -0
  259. package/ui/dist/assets/spreadsheet-CTIncYxj.js +1 -0
  260. package/ui/dist/assets/sql-DuQMFDvy.js +1 -0
  261. package/ui/dist/assets/stex-DNzd62Q_.js +1 -0
  262. package/ui/dist/assets/stylus-BOVgKnng.js +1 -0
  263. package/ui/dist/assets/swift-DKB6_1j6.js +1 -0
  264. package/ui/dist/assets/tcl-BGfruO1u.js +1 -0
  265. package/ui/dist/assets/textile-C4jVoHG1.js +1 -0
  266. package/ui/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
  267. package/ui/dist/assets/tiki-CvUEFusT.js +1 -0
  268. package/ui/dist/assets/toml-BPTmHmyx.js +1 -0
  269. package/ui/dist/assets/troff-C2dAgh7P.js +1 -0
  270. package/ui/dist/assets/ttcn-PPSTk1ow.js +1 -0
  271. package/ui/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
  272. package/ui/dist/assets/turtle-Cx3rYX2g.js +1 -0
  273. package/ui/dist/assets/vb-BthdM_4N.js +1 -0
  274. package/ui/dist/assets/vbscript-D9f6rSsU.js +1 -0
  275. package/ui/dist/assets/velocity-CqftRsjg.js +1 -0
  276. package/ui/dist/assets/verilog-CjI9Y6RI.js +1 -0
  277. package/ui/dist/assets/vhdl-kS471soi.js +1 -0
  278. package/ui/dist/assets/webidl-D5jiJvOU.js +1 -0
  279. package/ui/dist/assets/xquery-BKJx4mf9.js +1 -0
  280. package/ui/dist/assets/yacas-CXXgtw0p.js +1 -0
  281. package/ui/dist/assets/z80-Co3PbHdE.js +1 -0
  282. package/ui/dist/favicon.ico +0 -0
  283. package/ui/dist/favicon.svg +11 -0
  284. package/ui/dist/index.html +17 -0
  285. package/ui/dist/logos/claude.svg +1 -0
  286. package/ui/dist/logos/opencode.svg +1 -0
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,44 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { corner, openPdf, selectPages } from '../lib/pdf.js';
4
+ import { ROOT } from '../lib/paths.js';
5
+ // Renders the pages of a PDF to PNG files and prints the text of each page, so an exploring agent can LOOK at the
6
+ // pages (open the images) before it writes a target card for a PDF check. It never opens a browser and writes only
7
+ // below `.explore/`.
8
+ const args = process.argv.slice(2);
9
+ const option = (name) => {
10
+ const index = args.indexOf(name);
11
+ return index >= 0 ? args[index + 1] : undefined;
12
+ };
13
+ const file = args.find((arg) => !arg.startsWith('--') && arg !== option('--out') && arg !== option('--pages') && arg !== option('--corner'));
14
+ const out = option('--out');
15
+ const pagesArg = option('--pages') ?? 'all';
16
+ const cornerArg = option('--corner');
17
+ if (!file || !out) {
18
+ console.error('Usage: pnpm pdf:render <file.pdf> --out .explore/targets/<case-id>/<name> [--pages all|first|last|1,3] [--corner top-left|top-right|bottom-left|bottom-right]\n' +
19
+ 'Writes <out>-p<page>.png (the whole page) and, with --corner, <out>-p<page>-<corner>.png (that corner only).');
20
+ process.exit(1);
21
+ }
22
+ const outBase = path.resolve(ROOT, out);
23
+ if (!outBase.startsWith(path.join(ROOT, '.explore') + path.sep)) {
24
+ console.error('--out must be inside .explore/ (it is git-ignored, the pages are of logged-in accounts).');
25
+ process.exit(1);
26
+ }
27
+ const selection = pagesArg === 'all' || pagesArg === 'first' || pagesArg === 'last' ? pagesArg : pagesArg.split(',').map(Number);
28
+ const pdf = await openPdf(fs.readFileSync(path.resolve(file)));
29
+ fs.mkdirSync(path.dirname(outBase), { recursive: true });
30
+ console.log(`${path.basename(file)}: ${pdf.pageCount} page(s)`);
31
+ for (const page of selectPages(selection, pdf.pageCount)) {
32
+ const whole = await pdf.render(page);
33
+ const target = `${outBase}-p${page}.png`;
34
+ fs.writeFileSync(target, whole.png);
35
+ console.log(`\npage ${page}: ${path.relative(ROOT, target)} (${whole.width}x${whole.height})`);
36
+ if (cornerArg) {
37
+ const part = await pdf.render(page, corner(cornerArg));
38
+ const partFile = `${outBase}-p${page}-${cornerArg}.png`;
39
+ fs.writeFileSync(partFile, part.png);
40
+ console.log(` ${cornerArg}: ${path.relative(ROOT, partFile)} ink ${(part.inkRatio * 100).toFixed(1)}%${part.inkRatio < 0.002 ? ' (BLANK)' : ''}`);
41
+ }
42
+ console.log(` text: ${pdf.texts[page - 1].slice(0, 300) || '(none)'}`);
43
+ }
44
+ await pdf.close();
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,14 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { ROOT } from '../lib/paths.js';
5
+ import { commandScript } from '../lib/node-cli.js';
6
+ const latestHtml = path.join(ROOT, 'reports', 'latest', 'html');
7
+ if (!fs.existsSync(latestHtml)) {
8
+ console.error('No report yet. Run: pnpm asrun');
9
+ process.exit(1);
10
+ }
11
+ spawnSync(process.execPath, [commandScript('@playwright/test', 'playwright'), 'show-report', latestHtml], {
12
+ cwd: ROOT,
13
+ stdio: 'inherit',
14
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,30 @@
1
+ import { listBaselines } from '../lib/baselines.js';
2
+ import { listCaseFileIds } from '../lib/cases.js';
3
+ // Lists the screenshot baselines of the specs, marks the ones that are new or changed (git), and prints for each the
4
+ // target card that says what the image must show. Opening and looking at each image is the review, and it is a
5
+ // person's or an agent's job: this script only lines them up. It never writes anything.
6
+ const onlyNew = process.argv.includes('--new');
7
+ const wanted = process.argv.slice(2).filter((arg) => !arg.startsWith('-'));
8
+ let problems = 0;
9
+ let shown = 0;
10
+ for (const item of listBaselines(wanted.length > 0 ? wanted.filter((id) => listCaseFileIds().includes(id)) : undefined)) {
11
+ if (onlyNew && !item.changed)
12
+ continue;
13
+ shown++;
14
+ console.log(`${item.changed ? 'NEW/CHANGED' : 'unchanged '} ${item.rel}`);
15
+ if (item.problem) {
16
+ problems++;
17
+ console.log(` PROBLEM: ${item.problem}`);
18
+ }
19
+ else if (item.card) {
20
+ console.log(` must show (${item.card.id} ${item.card.name}): ${item.card.what ?? '(no "What")'}`);
21
+ console.log(` where: ${item.card.where ?? '(none)'}`);
22
+ if (item.card.not)
23
+ console.log(` must not show: ${item.card.not}`);
24
+ const page = /-p(\d+)-[^-]+-[^-]+-[^-]+\.png$/i.exec(item.file)?.[1];
25
+ if (page)
26
+ console.log(` (a region of PDF page ${page}; pages: ${item.card.pages ?? 'all'}, region: ${item.card.region ?? '(see Where)'})`);
27
+ }
28
+ }
29
+ console.log(`\n${shown} baseline image(s) listed. Open every NEW/CHANGED image and compare it with "must show"; a baseline that shows something else (a content image instead of a logo) must be deleted, not kept.`);
30
+ process.exit(problems > 0 ? 1 : 0);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,423 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ import fs from 'node:fs';
3
+ import path from 'node:path';
4
+ import { chromium } from '@playwright/test';
5
+ import { listEnvs, listProfiles, loadProfiles } from '../lib/profiles.js';
6
+ import readline from 'node:readline/promises';
7
+ import { listCaseFileIds, listSuites } from '../lib/cases.js';
8
+ import { ROOT } from '../lib/paths.js';
9
+ import { commandScript } from '../lib/node-cli.js';
10
+ import { RUN_SETTINGS_ENV } from '../lib/run-summary.js';
11
+ import { describeLeftovers, findLeftovers } from '../lib/records.js';
12
+ import { filterArgs } from '../lib/run-filters.js';
13
+ import { ALLOW_WRITES_ENV, describeSkippedWriteCases, describeWriteCases, findWriteCases, isProtectedEnv, planRun, } from '../lib/write-gate.js';
14
+ import { checkSession, formatNotLoggedIn, NOT_LOGGED_IN_ENV, skipProfileHint, } from '../lib/session.js';
15
+ const USAGE = `Usage: pnpm asrun --env <env> [--profile a,b] [--skip-profile a,b] [--tag name] [--logged-in] [--suite name] [--grep text] [--read-only] [--allow-writes=<env>] [--update-baselines <case,case>] [--report-id <id>] [playwright options...]
16
+
17
+ --env is required and has no default (the environments are listed in profiles.yaml, e.g. production, staging).
18
+
19
+ pnpm asrun --env staging all profiles, all cases
20
+ pnpm asrun --env production --profile example-production one profile
21
+ pnpm asrun --env production --skip-profile example-production all profiles except these
22
+ pnpm asrun --env production --tag us only the profiles that carry the tag "us"
23
+ pnpm asrun --env production --logged-in only the profiles that are logged in (the others are left out, not failed)
24
+ pnpm asrun --env production --suite smoke only cases whose front matter lists "smoke" under "suites"
25
+ (comma-separated for several: --suite smoke,regression; a case in
26
+ both runs once). --priority is the old name of --suite.
27
+ pnpm asrun --env production --read-only leave out the cases that write data (front matter "writes")
28
+ pnpm asrun --env production --allow-writes=production run cases that write data in production WITHOUT the prompt. For
29
+ people running unattended; AI agents must never add this flag.
30
+ pnpm asrun --env production --grep landing one case (matches the test title)
31
+ pnpm asrun --env staging --grep documents --update-baselines documents/landing-page
32
+ run the chosen cases as usual, except the listed ones: those are run separately
33
+ and their screenshot baselines are overwritten with what the page shows now
34
+ (--update-snapshots=all). Look at the new images afterwards:
35
+ pnpm baselines:review <case-id>
36
+ pnpm asrun --env staging --ui interactive mode
37
+ pnpm asrun --env production --step-text overlay the current test step on the videos
38
+ (also --step-text=1|true|yes|on; off by default; 0|false|no|off disables)`;
39
+ const TRUE_VALUES = ['1', 'true', 'yes', 'on'];
40
+ const FALSE_VALUES = ['0', 'false', 'no', 'off'];
41
+ const args = process.argv.slice(2);
42
+ if (args.includes('--help')) {
43
+ console.log(USAGE);
44
+ process.exit(0);
45
+ }
46
+ const passthrough = [];
47
+ let env;
48
+ let selectedProfiles = [];
49
+ let skippedProfiles = [];
50
+ let selectedTags = [];
51
+ let selectedSuites = [];
52
+ let readOnly = false;
53
+ let loggedInOnly = false;
54
+ let allowWrites;
55
+ let userGrep;
56
+ let stepText = false;
57
+ let updateBaselines = [];
58
+ let reportId;
59
+ for (let i = 0; i < args.length; i++) {
60
+ const arg = args[i];
61
+ if (arg === '--env') {
62
+ env = args[++i];
63
+ }
64
+ else if (arg.startsWith('--env=')) {
65
+ env = arg.slice('--env='.length);
66
+ }
67
+ else if (arg === '--profile') {
68
+ selectedProfiles = (args[++i] ?? '').split(',').filter(Boolean);
69
+ }
70
+ else if (arg.startsWith('--profile=')) {
71
+ selectedProfiles = arg.slice('--profile='.length).split(',').filter(Boolean);
72
+ }
73
+ else if (arg === '--skip-profile') {
74
+ skippedProfiles = (args[++i] ?? '').split(',').filter(Boolean);
75
+ }
76
+ else if (arg.startsWith('--skip-profile=')) {
77
+ skippedProfiles = arg.slice('--skip-profile='.length).split(',').filter(Boolean);
78
+ }
79
+ else if (arg === '--tag') {
80
+ selectedTags = (args[++i] ?? '').split(',').filter(Boolean);
81
+ }
82
+ else if (arg.startsWith('--tag=')) {
83
+ selectedTags = arg.slice('--tag='.length).split(',').filter(Boolean);
84
+ }
85
+ else if (arg === '--suite' || arg === '--priority') {
86
+ selectedSuites = (args[++i] ?? '').split(',').filter(Boolean);
87
+ }
88
+ else if (arg.startsWith('--suite=') || arg.startsWith('--priority=')) {
89
+ selectedSuites = arg.slice(arg.indexOf('=') + 1).split(',').filter(Boolean);
90
+ }
91
+ else if (arg === '--logged-in') {
92
+ loggedInOnly = true;
93
+ }
94
+ else if (arg === '--read-only') {
95
+ readOnly = true;
96
+ }
97
+ else if (arg.startsWith('--allow-writes=')) {
98
+ allowWrites = arg.slice('--allow-writes='.length);
99
+ }
100
+ else if (arg === '--allow-writes') {
101
+ console.error('--allow-writes needs the environment it applies to, e.g. --allow-writes=production');
102
+ process.exit(1);
103
+ }
104
+ else if (arg === '--report-id') {
105
+ reportId = args[++i];
106
+ }
107
+ else if (arg.startsWith('--report-id=')) {
108
+ reportId = arg.slice('--report-id='.length);
109
+ }
110
+ else if (arg === '--update-baselines') {
111
+ updateBaselines = (args[++i] ?? '').split(',').filter(Boolean);
112
+ }
113
+ else if (arg.startsWith('--update-baselines=')) {
114
+ updateBaselines = arg.slice('--update-baselines='.length).split(',').filter(Boolean);
115
+ }
116
+ else if (arg === '--grep' || arg === '-g') {
117
+ userGrep = args[++i];
118
+ }
119
+ else if (arg.startsWith('--grep=')) {
120
+ userGrep = arg.slice('--grep='.length);
121
+ }
122
+ else if (arg === '--step-text' || arg.startsWith('--step-text=')) {
123
+ // A bare flag means true; an explicit value may follow as `=value` or as the next argument.
124
+ let value = arg.includes('=') ? arg.slice(arg.indexOf('=') + 1) : undefined;
125
+ if (value === undefined && [...TRUE_VALUES, ...FALSE_VALUES].includes(args[i + 1]?.toLowerCase())) {
126
+ value = args[++i];
127
+ }
128
+ value = (value ?? 'true').toLowerCase();
129
+ if (!TRUE_VALUES.includes(value) && !FALSE_VALUES.includes(value)) {
130
+ console.error(`Invalid value for --step-text: "${value}". Use 1/true/yes/on or 0/false/no/off.`);
131
+ process.exit(1);
132
+ }
133
+ stepText = TRUE_VALUES.includes(value);
134
+ }
135
+ else {
136
+ passthrough.push(arg);
137
+ }
138
+ }
139
+ const allProfiles = loadProfiles();
140
+ if (allProfiles.length === 0) {
141
+ console.error('profiles.yaml has no profiles. Add one entry per environment (see the comments in the file), ' +
142
+ 'then log in with "pnpm signin <profile-id>".');
143
+ process.exit(1);
144
+ }
145
+ const knownProfileIds = allProfiles.map((profile) => profile.id);
146
+ const knownEnvs = listEnvs(allProfiles);
147
+ const listOnly = passthrough.includes('--list');
148
+ if (!env && !listOnly) {
149
+ console.error(`Choose the environment to test: --env <env> (${knownEnvs.join(' | ')}). There is no default.\n` +
150
+ `Example: pnpm asrun --env ${knownEnvs.includes('staging') ? 'staging' : (knownEnvs[0] ?? '<env>')}`);
151
+ process.exit(1);
152
+ }
153
+ if (env && !knownEnvs.includes(env)) {
154
+ console.error(`Unknown environment "${env}". Known: ${knownEnvs.join(', ')}`);
155
+ process.exit(1);
156
+ }
157
+ const unknown = [...selectedProfiles, ...skippedProfiles].filter((id) => !knownProfileIds.includes(id));
158
+ if (unknown.length > 0) {
159
+ console.error(`Unknown profile(s): ${unknown.join(', ')}. Known: ${knownProfileIds.join(', ') || '(none)'}`);
160
+ process.exit(1);
161
+ }
162
+ const knownTags = [...new Set(allProfiles.flatMap((profile) => profile.tags))].sort();
163
+ const unknownTags = selectedTags.filter((tag) => !knownTags.includes(tag));
164
+ if (unknownTags.length > 0) {
165
+ console.error(`Unknown tag(s): ${unknownTags.join(', ')}. Used by the profiles: ${knownTags.join(', ') || '(none)'}`);
166
+ process.exit(1);
167
+ }
168
+ const unknownCases = updateBaselines.filter((id) => !listCaseFileIds().includes(id));
169
+ if (unknownCases.length > 0) {
170
+ console.error(`--update-baselines: unknown case(s): ${unknownCases.join(', ')}`);
171
+ process.exit(1);
172
+ }
173
+ if (updateBaselines.length > 0 && passthrough.some((arg) => arg === '-u' || arg.startsWith('--update-snapshots'))) {
174
+ console.error('--update-baselines already sets --update-snapshots for the listed cases; do not pass both.');
175
+ process.exit(1);
176
+ }
177
+ const knownSuites = listSuites();
178
+ const unknownSuites = selectedSuites.filter((name) => !knownSuites.includes(name));
179
+ if (unknownSuites.length > 0) {
180
+ console.error(`Unknown suite: ${unknownSuites.join(', ')}. Used by the cases: ${knownSuites.join(', ')}`);
181
+ process.exit(1);
182
+ }
183
+ if (allowWrites !== undefined && allowWrites !== env) {
184
+ console.error(`--allow-writes=${allowWrites} does not match --env ${env ?? '(none)'}. Name the environment you mean.`);
185
+ process.exit(1);
186
+ }
187
+ if (readOnly && allowWrites) {
188
+ console.error('--read-only and --allow-writes cannot be combined.');
189
+ process.exit(1);
190
+ }
191
+ // Login is not a test case: a profile without a valid session cannot be tested, so find out up front.
192
+ const profilesInEnv = listProfiles(env);
193
+ const profilesWithTags = selectedTags.length > 0
194
+ ? profilesInEnv.filter((profile) => profile.tags.some((tag) => selectedTags.includes(tag)))
195
+ : profilesInEnv;
196
+ const taggedIds = profilesWithTags.map((profile) => profile.id);
197
+ const noEnvProfiles = selectedProfiles.filter((id) => env && !taggedIds.includes(id));
198
+ if (noEnvProfiles.length > 0) {
199
+ const reason = selectedTags.length > 0 ? `no tag ${selectedTags.join(', ')}` : `no "${env}" environment`;
200
+ console.error(`Profile(s) with ${reason} in profiles.yaml: ${noEnvProfiles.join(', ')}`);
201
+ process.exit(1);
202
+ }
203
+ let profilesToRun = (selectedProfiles.length > 0 ? selectedProfiles : taggedIds).filter((id) => !skippedProfiles.includes(id));
204
+ if (profilesToRun.length === 0) {
205
+ console.error('No profile left to test: every profile was skipped.');
206
+ process.exit(1);
207
+ }
208
+ // A failed check (network error, timeout) says nothing about the login, so it is retried and, if it keeps
209
+ // failing, left for the workers to re-check instead of being reported as an expired session.
210
+ async function checkWithRetry(browser, profile, attempts = 3) {
211
+ for (let attempt = 1; attempt <= attempts; attempt++) {
212
+ try {
213
+ return await checkSession(browser, profile);
214
+ }
215
+ catch {
216
+ if (attempt < attempts)
217
+ await new Promise((resolve) => setTimeout(resolve, 1000 * attempt));
218
+ }
219
+ }
220
+ return 'unknown';
221
+ }
222
+ const notLoggedIn = new Map();
223
+ const unverified = [];
224
+ // The list of tests needs neither a browser nor a login.
225
+ if (!listOnly) {
226
+ const browser = await chromium.launch();
227
+ try {
228
+ const checks = await Promise.all(profilesInEnv
229
+ .filter((profile) => profilesToRun.includes(profile.id))
230
+ .map(async (profile) => [profile.id, await checkWithRetry(browser, profile)]));
231
+ for (const [id, status] of checks) {
232
+ if (status === 'unknown')
233
+ unverified.push(id);
234
+ else if (status !== 'ok')
235
+ notLoggedIn.set(id, status);
236
+ }
237
+ }
238
+ finally {
239
+ await browser.close();
240
+ }
241
+ }
242
+ if (unverified.length > 0) {
243
+ console.error(`Could not verify the login of: ${unverified.join(', ')} (network problem?). ` +
244
+ 'Each test worker will check again before running.\n');
245
+ }
246
+ if (notLoggedIn.size > 0 && loggedInOnly) {
247
+ const left = [...notLoggedIn.entries()].map(([id, status]) => `${id} (${status})`).join(', ');
248
+ console.log(`Skipped, not logged in (--logged-in): ${left}\n`);
249
+ profilesToRun = profilesToRun.filter((id) => !notLoggedIn.has(id));
250
+ if (profilesToRun.length === 0) {
251
+ console.error('No profile left to test: none of the chosen profiles is logged in.');
252
+ process.exit(1);
253
+ }
254
+ }
255
+ else if (notLoggedIn.size > 0) {
256
+ console.error('Not logged in (tests for these profiles will FAIL):');
257
+ for (const [id, status] of notLoggedIn) {
258
+ console.error(` ${id.padEnd(24)} ${status.padEnd(8)} -> pnpm signin ${id}`);
259
+ }
260
+ console.error(`Or skip them for this run: ${skipProfileHint([...notLoggedIn.keys()], env)}\n`);
261
+ }
262
+ // Which tests will run, as Playwright selects them. Always leaves out the @seed helper test.
263
+ const profilesFilter = selectedProfiles.length > 0 || skippedProfiles.length > 0 || selectedTags.length > 0 || loggedInOnly;
264
+ const filters = {
265
+ env: env ?? '',
266
+ profiles: profilesFilter ? profilesToRun : null,
267
+ suites: selectedSuites,
268
+ grep: userGrep,
269
+ readOnly,
270
+ };
271
+ const selectionArgs = filterArgs(filters);
272
+ // Permission to write test data. A case writes only in the environments it lists under `writes`; anywhere else
273
+ // it is skipped with a notice (not failed). Staging needs no confirmation. In any other environment a human has to
274
+ // confirm, every time: by typing the environment name, or up front with --allow-writes=<env> (for unattended runs).
275
+ // An AI agent has no terminal, so the prompt refuses to continue there; agents must not add the flag.
276
+ let writesAllowedIn;
277
+ if (env && !listOnly) {
278
+ const { writing: writeCases, skipped } = findWriteCases(filters);
279
+ if (skipped.length > 0 && !readOnly)
280
+ console.log(`\n${describeSkippedWriteCases(env, skipped)}\n`);
281
+ if (writeCases.length > 0 && isProtectedEnv(env)) {
282
+ if (allowWrites === env) {
283
+ console.log(`${describeWriteCases(env, writeCases)}\nPermission given with --allow-writes=${env}.\n`);
284
+ writesAllowedIn = env;
285
+ }
286
+ else if (process.stdin.isTTY && process.stdout.isTTY) {
287
+ console.log(`\n\x1b[41;97;1m WRITE CASES IN ${env.toUpperCase()} \x1b[0m\n${describeWriteCases(env, writeCases)}\n`);
288
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
289
+ const answer = (await rl.question(`Type "${env}" to run them, or anything else to cancel: `)).trim();
290
+ rl.close();
291
+ if (answer !== env) {
292
+ console.error('Cancelled. Nothing was run. (Leave these cases out with --read-only.)');
293
+ process.exit(1);
294
+ }
295
+ writesAllowedIn = env;
296
+ }
297
+ else {
298
+ console.error(`${describeWriteCases(env, writeCases)}\n\n` +
299
+ `Refusing to run: these cases write data in ${env.toUpperCase()} and nobody confirmed it.\n` +
300
+ ` - leave them out: add --read-only\n` +
301
+ ` - run them anyway: a person adds --allow-writes=${env} (AI agents must not)\n` +
302
+ ` - or test in staging: --env staging`);
303
+ process.exit(1);
304
+ }
305
+ }
306
+ }
307
+ let phases = [{ suffix: '', selection: selectionArgs, extra: [] }];
308
+ if (updateBaselines.length > 0 && !listOnly) {
309
+ const planned = await planRun(filters);
310
+ const runnable = planned.filter((item) => item.skip === null).map((item) => item.id);
311
+ const notInRun = updateBaselines.filter((id) => !runnable.includes(id));
312
+ if (notInRun.length > 0) {
313
+ console.error(`--update-baselines: ${notInRun.join(', ')} would not run with these filters (not selected, or skipped in ${env?.toUpperCase()}).`);
314
+ process.exit(1);
315
+ }
316
+ const rest = planned.map((item) => item.id).filter((id) => !updateBaselines.includes(id));
317
+ phases = [
318
+ ...(rest.length > 0 ? [{ suffix: '', selection: filterArgs({ ...filters, only: rest }), extra: [], only: rest }] : []),
319
+ {
320
+ suffix: '-baselines',
321
+ selection: filterArgs({ ...filters, only: updateBaselines }),
322
+ extra: ['--update-snapshots=all'],
323
+ label: `Updating the screenshot baselines of: ${updateBaselines.join(', ')}`,
324
+ only: updateBaselines,
325
+ },
326
+ ];
327
+ }
328
+ // The environment is part of the run id, so it shows in the report folder, the UI and every path.
329
+ // The UI passes its own id, so it can link to the report of the job before the run has finished.
330
+ const baseRunId = reportId ?? `${new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19)}-${env ?? 'list'}`;
331
+ // Started with the current Node: the shim in `node_modules/.bin` is a shell script on Windows.
332
+ const PHASE_SCRIPT = commandScript('@playwright/test', 'playwright');
333
+ // The permission must come from the confirmation above, never from a variable that was already in the shell.
334
+ const childEnv = { ...process.env };
335
+ delete childEnv[ALLOW_WRITES_ENV];
336
+ function runPhase(phase) {
337
+ const runId = `${baseRunId}${phase.suffix}`;
338
+ if (phase.label)
339
+ console.log(`\n=== ${phase.label} ===`);
340
+ if (env && !listOnly) {
341
+ const text = ` ENV: ${env.toUpperCase()} `;
342
+ const color = env === 'production' ? '\x1b[41;97;1m' : '\x1b[44;97;1m';
343
+ console.log(`\n${process.stdout.isTTY ? `${color}${text}\x1b[0m` : text.trim()} ${profilesInEnv
344
+ .filter((profile) => profilesToRun.includes(profile.id))
345
+ .map((profile) => profile.baseURL)
346
+ .join(' ')}\n`);
347
+ }
348
+ const result = spawnSync(process.execPath, [PHASE_SCRIPT, 'test', ...phase.selection, ...phase.extra, ...passthrough], {
349
+ cwd: ROOT,
350
+ stdio: 'inherit',
351
+ env: {
352
+ ...childEnv,
353
+ ASRUN_RUN_ID: runId,
354
+ ...(env && { ASRUN_ENV: env }),
355
+ ...(writesAllowedIn && { [ALLOW_WRITES_ENV]: writesAllowedIn }),
356
+ ASRUN_STEP_TEXT: stepText ? '1' : '0',
357
+ [RUN_SETTINGS_ENV]: JSON.stringify({
358
+ stepText,
359
+ readOnly,
360
+ profiles: filters.profiles,
361
+ suites: selectedSuites,
362
+ ...(selectedTags.length > 0 && { tags: selectedTags }),
363
+ ...(userGrep && { grep: userGrep }),
364
+ ...(phase.only && { only: phase.only }),
365
+ }),
366
+ [NOT_LOGGED_IN_ENV]: formatNotLoggedIn(notLoggedIn),
367
+ },
368
+ });
369
+ if (passthrough.includes('--list')) {
370
+ // Nothing ran, so there are no recordings and the reporters' empty output must not become "latest".
371
+ fs.rmSync(path.join(ROOT, 'reports', runId), { recursive: true, force: true });
372
+ return result.status ?? 1;
373
+ }
374
+ const resultsDir = path.join(ROOT, '.tmp', 'results');
375
+ const recordings = [];
376
+ if (fs.existsSync(resultsDir)) {
377
+ for (const entry of fs.readdirSync(resultsDir, { withFileTypes: true })) {
378
+ if (!entry.isDirectory())
379
+ continue;
380
+ const files = fs
381
+ .readdirSync(path.join(resultsDir, entry.name))
382
+ .filter((name) => name.endsWith('.webm') || name === 'trace.zip')
383
+ .sort();
384
+ if (files.length > 0)
385
+ recordings.push({ testDir: entry.name, files });
386
+ }
387
+ }
388
+ if (recordings.length > 0) {
389
+ console.log('\nRecordings (overwritten by the next run):');
390
+ for (const { testDir, files } of recordings) {
391
+ console.log(` ${testDir}`);
392
+ for (const file of files) {
393
+ const full = path.join(resultsDir, testDir, file);
394
+ console.log(` ${file === 'trace.zip' ? 'trace:' : 'video:'} ${full}`);
395
+ }
396
+ if (files.includes('trace.zip')) {
397
+ console.log(` open trace: pnpm exec playwright show-trace ${path.join(resultsDir, testDir, 'trace.zip')}`);
398
+ }
399
+ }
400
+ }
401
+ const leftovers = findLeftovers({ runId });
402
+ if (leftovers.length > 0) {
403
+ console.error(`\nTEST DATA LEFT BEHIND in ${env?.toUpperCase()}: delete it by hand, then run "pnpm cleanup --env ${env} --forget"\n${describeLeftovers(leftovers)}`);
404
+ }
405
+ const reportDir = path.join(ROOT, 'reports', runId, 'html');
406
+ if (fs.existsSync(reportDir)) {
407
+ const latest = path.join(ROOT, 'reports', 'latest');
408
+ fs.rmSync(latest, { force: true });
409
+ fs.symlinkSync(path.join(ROOT, 'reports', runId), latest);
410
+ console.log(`\nReport (videos, screenshots, traces): pnpm report`);
411
+ }
412
+ return result.status ?? 1;
413
+ }
414
+ let exitCode = 0;
415
+ for (const phase of phases) {
416
+ const status = runPhase(phase);
417
+ if (status !== 0)
418
+ exitCode = status;
419
+ }
420
+ if (updateBaselines.length > 0 && !listOnly) {
421
+ console.log(`\nBaselines rewritten for: ${updateBaselines.join(', ')}. Open the new images before you keep them: pnpm baselines:review ${updateBaselines.join(' ')}`);
422
+ }
423
+ process.exit(exitCode);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,31 @@
1
+ import fs from 'node:fs';
2
+ import { chromium } from '@playwright/test';
3
+ import { listEnvs, listProfiles } from '../lib/profiles.js';
4
+ import { authFile } from '../lib/paths.js';
5
+ import { checkSession, signinCommand } from '../lib/session.js';
6
+ const args = process.argv.slice(2);
7
+ const envArg = args.find((arg) => arg.startsWith('--env='))?.slice('--env='.length) ?? (args.includes('--env') ? args[args.indexOf('--env') + 1] : undefined);
8
+ if (envArg && !listEnvs().includes(envArg)) {
9
+ console.error(`Unknown environment "${envArg}". Known: ${listEnvs().join(', ')}`);
10
+ process.exit(1);
11
+ }
12
+ const rows = listProfiles(envArg);
13
+ if (rows.length === 0) {
14
+ console.log(`No profiles${envArg ? ` for ${envArg.toUpperCase()}` : ''}. Add one to profiles.yaml first.`);
15
+ process.exit(0);
16
+ }
17
+ const browser = await chromium.launch();
18
+ const failing = [];
19
+ console.log('Profile Env Account Session Saved at');
20
+ for (const profile of rows) {
21
+ const status = await checkSession(browser, profile);
22
+ const file = authFile(profile.id);
23
+ const savedAt = fs.existsSync(file) ? fs.statSync(file).mtime.toLocaleString() : '-';
24
+ console.log(`${profile.id.padEnd(24)} ${profile.env.padEnd(10)} ${(profile.account ?? '-').padEnd(20)} ${status.padEnd(9)} ${savedAt}`);
25
+ if (status !== 'ok')
26
+ failing.push(profile);
27
+ }
28
+ await browser.close();
29
+ if (failing.length > 0) {
30
+ console.log(`\nFix with: ${signinCommand({ id: '<profile-id>' })}`);
31
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import { buildUi } from '../lib/ui-build.js';
2
+ const status = buildUi();
3
+ if (status === null) {
4
+ console.error('No front-end toolchain here: the web UI can only be built in the framework repository.');
5
+ process.exit(1);
6
+ }
7
+ process.exit(status);
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,3 @@
1
+ // Vite dev server plus the API in watch mode.
2
+ await import('../ui/dev.js');
3
+ export {};
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,13 @@
1
+ import { buildUi, uiIsBuilt } from '../lib/ui-build.js';
2
+ // An installed package ships the built app; the framework repository builds it (or refreshes it with `asrun ui --build`).
3
+ if (!uiIsBuilt() || process.argv.includes('--build')) {
4
+ const status = buildUi();
5
+ if (status === null) {
6
+ console.error('This asrun installation has no built web UI. Reinstall it: "pnpm add -D @asrunjs/asrun".');
7
+ process.exit(1);
8
+ }
9
+ if (status !== 0)
10
+ process.exit(status);
11
+ }
12
+ // Serves the built app and the API on one port (see ui/server/index.ts).
13
+ await import('../ui/server/index.js');
@@ -0,0 +1 @@
1
+ export {};
package/dist/ui/dev.js ADDED
@@ -0,0 +1,36 @@
1
+ import path from 'node:path';
2
+ import { spawn } from 'node:child_process';
3
+ import { commandScript } from '../lib/node-cli.js';
4
+ import { FRAMEWORK_ROOT, ROOT } from '../lib/paths.js';
5
+ const webPort = process.env.ASRUN_UI_WEB_PORT ?? '5173';
6
+ const tools = [
7
+ {
8
+ name: 'tsx',
9
+ script: commandScript('tsx'),
10
+ args: ['watch', path.join(FRAMEWORK_ROOT, 'ui', 'server', 'index.ts')],
11
+ env: { ...process.env, ASRUN_UI_DEV_ORIGIN: `http://127.0.0.1:${webPort}` },
12
+ },
13
+ {
14
+ name: 'vite',
15
+ script: commandScript('vite'),
16
+ args: ['--config', path.join(FRAMEWORK_ROOT, 'ui', 'web', 'vite.config.ts')],
17
+ env: process.env,
18
+ },
19
+ ];
20
+ const children = [];
21
+ const stop = () => children.forEach((child) => child.kill('SIGTERM'));
22
+ process.on('SIGINT', stop);
23
+ process.on('SIGTERM', stop);
24
+ for (const tool of tools) {
25
+ const child = spawn(process.execPath, [tool.script, ...tool.args], { cwd: ROOT, stdio: 'inherit', env: tool.env });
26
+ children.push(child);
27
+ child.on('error', (error) => {
28
+ console.error(`Failed to start ${tool.name}: ${error.message}`);
29
+ stop();
30
+ process.exit(1);
31
+ });
32
+ child.on('exit', (code) => {
33
+ stop();
34
+ process.exit(code ?? 0);
35
+ });
36
+ }