@asrunjs/asrun 0.0.0-stage → 0.5.2
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/.claude/commands/explore-case.md +18 -0
- package/.claude/commands/gen-spec.md +16 -0
- package/.claude/commands/heal-spec.md +26 -0
- package/.claude/commands/make-case.md +11 -0
- package/.claude/settings.json +5 -0
- package/.claude/skills/exploring-cases/SKILL.md +146 -0
- package/.claude/skills/exploring-cases/references/assertion-targets.md +103 -0
- package/.claude/skills/exploring-cases/references/handoff-card.js +128 -0
- package/.claude/skills/exploring-cases/references/open-questions.md +116 -0
- package/.claude/skills/exploring-cases/references/readiness.md +146 -0
- package/.claude/skills/exploring-cases/references/user-handoff.md +157 -0
- package/.claude/skills/generating-specs/SKILL.md +219 -0
- package/.claude/skills/generating-specs/references/conventions.md +148 -0
- package/.claude/skills/making-cases/SKILL.md +72 -0
- package/.claude/skills/making-cases/references/gaps.md +36 -0
- package/.claude/skills/making-cases/references/normalizing.md +39 -0
- package/.claude/skills/making-cases/references/splitting.md +28 -0
- package/.claude/skills/making-cases/references/writes-and-email.md +37 -0
- package/AGENTS.md +159 -0
- package/CLAUDE.md +2 -0
- package/LICENSE +21 -0
- package/README.md +511 -2
- package/bin/asrun.mjs +49 -0
- package/dist/lib/account-lock.d.ts +6 -0
- package/dist/lib/account-lock.js +52 -0
- package/dist/lib/assertions.d.ts +14 -0
- package/dist/lib/assertions.js +39 -0
- package/dist/lib/baselines.d.ts +43 -0
- package/dist/lib/baselines.js +108 -0
- package/dist/lib/build-artifacts.d.ts +8 -0
- package/dist/lib/build-artifacts.js +10 -0
- package/dist/lib/case-doc.d.ts +311 -0
- package/dist/lib/case-doc.js +1117 -0
- package/dist/lib/case-id.d.ts +11 -0
- package/dist/lib/case-id.js +17 -0
- package/dist/lib/case-ops.d.ts +26 -0
- package/dist/lib/case-ops.js +110 -0
- package/dist/lib/cases.d.ts +16 -0
- package/dist/lib/cases.js +51 -0
- package/dist/lib/cli.d.ts +10 -0
- package/dist/lib/cli.js +18 -0
- package/dist/lib/command.d.ts +24 -0
- package/dist/lib/command.js +90 -0
- package/dist/lib/downloads.d.ts +42 -0
- package/dist/lib/downloads.js +84 -0
- package/dist/lib/email.d.ts +10 -0
- package/dist/lib/email.js +19 -0
- package/dist/lib/ffmpeg.d.ts +8 -0
- package/dist/lib/ffmpeg.js +50 -0
- package/dist/lib/fixtures.d.ts +40 -0
- package/dist/lib/fixtures.js +149 -0
- package/dist/lib/login-card.d.ts +22 -0
- package/dist/lib/login-card.js +183 -0
- package/dist/lib/node-cli.d.ts +15 -0
- package/dist/lib/node-cli.js +36 -0
- package/dist/lib/paths.d.ts +25 -0
- package/dist/lib/paths.js +88 -0
- package/dist/lib/pdf.d.ts +71 -0
- package/dist/lib/pdf.js +140 -0
- package/dist/lib/profiles.d.ts +62 -0
- package/dist/lib/profiles.js +174 -0
- package/dist/lib/ready.d.ts +36 -0
- package/dist/lib/ready.js +53 -0
- package/dist/lib/records.d.ts +33 -0
- package/dist/lib/records.js +61 -0
- package/dist/lib/run-filters.d.ts +19 -0
- package/dist/lib/run-filters.js +38 -0
- package/dist/lib/run-summary.d.ts +90 -0
- package/dist/lib/run-summary.js +35 -0
- package/dist/lib/session.d.ts +30 -0
- package/dist/lib/session.js +111 -0
- package/dist/lib/summary-reporter.d.ts +16 -0
- package/dist/lib/summary-reporter.js +146 -0
- package/dist/lib/targets.d.ts +19 -0
- package/dist/lib/targets.js +26 -0
- package/dist/lib/ui-build.d.ts +10 -0
- package/dist/lib/ui-build.js +30 -0
- package/dist/lib/values.d.ts +29 -0
- package/dist/lib/values.js +85 -0
- package/dist/lib/write-gate.d.ts +41 -0
- package/dist/lib/write-gate.js +129 -0
- package/dist/playwright.config.d.ts +2 -0
- package/dist/playwright.config.js +92 -0
- package/dist/scripts/build.d.ts +1 -0
- package/dist/scripts/build.js +21 -0
- package/dist/scripts/check-cases.d.ts +1 -0
- package/dist/scripts/check-cases.js +149 -0
- package/dist/scripts/cleanup.d.ts +1 -0
- package/dist/scripts/cleanup.js +25 -0
- package/dist/scripts/cli.d.ts +1 -0
- package/dist/scripts/cli.js +28 -0
- package/dist/scripts/doctor.d.ts +1 -0
- package/dist/scripts/doctor.js +296 -0
- package/dist/scripts/explore.d.ts +1 -0
- package/dist/scripts/explore.js +75 -0
- package/dist/scripts/go.mjs +188 -0
- package/dist/scripts/heal-evidence.mjs +143 -0
- package/dist/scripts/init.d.ts +22 -0
- package/dist/scripts/init.js +332 -0
- package/dist/scripts/lint-specs.d.ts +1 -0
- package/dist/scripts/lint-specs.js +141 -0
- package/dist/scripts/login.d.ts +1 -0
- package/dist/scripts/login.js +75 -0
- package/dist/scripts/new-case.d.ts +1 -0
- package/dist/scripts/new-case.js +19 -0
- package/dist/scripts/render-pdf.d.ts +1 -0
- package/dist/scripts/render-pdf.js +44 -0
- package/dist/scripts/report.d.ts +1 -0
- package/dist/scripts/report.js +14 -0
- package/dist/scripts/review-baselines.d.ts +1 -0
- package/dist/scripts/review-baselines.js +30 -0
- package/dist/scripts/run.d.ts +1 -0
- package/dist/scripts/run.js +423 -0
- package/dist/scripts/status.d.ts +1 -0
- package/dist/scripts/status.js +31 -0
- package/dist/scripts/ui-build.d.ts +1 -0
- package/dist/scripts/ui-build.js +7 -0
- package/dist/scripts/ui-dev.d.ts +1 -0
- package/dist/scripts/ui-dev.js +3 -0
- package/dist/scripts/ui.d.ts +1 -0
- package/dist/scripts/ui.js +13 -0
- package/dist/ui/dev.d.ts +1 -0
- package/dist/ui/dev.js +36 -0
- package/dist/ui/server/actions.d.ts +95 -0
- package/dist/ui/server/actions.js +365 -0
- package/dist/ui/server/ai-models.d.ts +3 -0
- package/dist/ui/server/ai-models.js +347 -0
- package/dist/ui/server/ai-stream.d.ts +66 -0
- package/dist/ui/server/ai-stream.js +408 -0
- package/dist/ui/server/app.d.ts +4 -0
- package/dist/ui/server/app.js +395 -0
- package/dist/ui/server/cases.d.ts +23 -0
- package/dist/ui/server/cases.js +170 -0
- package/dist/ui/server/files.d.ts +5 -0
- package/dist/ui/server/files.js +67 -0
- package/dist/ui/server/index.d.ts +1 -0
- package/dist/ui/server/index.js +33 -0
- package/dist/ui/server/jobs.d.ts +80 -0
- package/dist/ui/server/jobs.js +317 -0
- package/dist/ui/server/profiles.d.ts +35 -0
- package/dist/ui/server/profiles.js +371 -0
- package/dist/ui/server/watch-files.d.ts +4 -0
- package/dist/ui/server/watch-files.js +36 -0
- package/dist/ui/shared/ai-commands.d.ts +88 -0
- package/dist/ui/shared/ai-commands.js +177 -0
- package/dist/ui/shared/case-id.d.ts +1 -0
- package/dist/ui/shared/case-id.js +1 -0
- package/dist/ui/shared/jobs.d.ts +6 -0
- package/dist/ui/shared/jobs.js +7 -0
- package/dist/ui/shared/product.d.ts +3 -0
- package/dist/ui/shared/product.js +3 -0
- package/dist/ui/shared/run-filters.d.ts +1 -0
- package/dist/ui/shared/run-filters.js +1 -0
- package/dist/ui/shared/site-address.d.ts +6 -0
- package/dist/ui/shared/site-address.js +11 -0
- package/dist/ui/shared/types.d.ts +371 -0
- package/dist/ui/shared/types.js +1 -0
- package/docs/agents/framework-dev.md +79 -0
- package/docs/theme-guide.html +922 -0
- package/package.json +109 -3
- package/templates/.claude/commands/explore-case.md +22 -0
- package/templates/.claude/commands/gen-spec.md +21 -0
- package/templates/.claude/commands/heal-spec.md +31 -0
- package/templates/.claude/commands/make-case.md +13 -0
- package/templates/.claude/settings.json +5 -0
- package/templates/.mcp.json +8 -0
- package/templates/AGENTS.md +64 -0
- package/templates/CLAUDE.md +1 -0
- package/templates/cases/_template.md +57 -0
- package/templates/gitignore +13 -0
- package/templates/opencode.jsonc +16 -0
- package/templates/package.json +31 -0
- package/templates/playwright.config.ts +3 -0
- package/templates/pnpm-workspace.yaml +5 -0
- package/templates/profiles.yaml +42 -0
- package/templates/seed.spec.ts +9 -0
- package/templates/tsconfig.json +14 -0
- package/ui/dist/apple-touch-icon.png +0 -0
- package/ui/dist/assets/apl-gfMMiTNf.js +1 -0
- package/ui/dist/assets/asciiarmor-C_eisQCp.js +1 -0
- package/ui/dist/assets/asn1-DCl_8MCW.js +1 -0
- package/ui/dist/assets/asterisk-BEn0Cwpx.js +1 -0
- package/ui/dist/assets/brainfuck-D5EjA2JK.js +1 -0
- package/ui/dist/assets/clike-xvONpD0Z.js +1 -0
- package/ui/dist/assets/clojure-BflJGmDX.js +1 -0
- package/ui/dist/assets/cmake-84nbDH0I.js +1 -0
- package/ui/dist/assets/cobol-DHLQUbN7.js +1 -0
- package/ui/dist/assets/coffeescript-De_zI4J0.js +1 -0
- package/ui/dist/assets/commonlisp-DgAKBe0r.js +1 -0
- package/ui/dist/assets/crystal-Bl6YKCG_.js +1 -0
- package/ui/dist/assets/css-CnaYoQib.js +1 -0
- package/ui/dist/assets/cypher-p9eYesGn.js +1 -0
- package/ui/dist/assets/d-DWkQyNnU.js +1 -0
- package/ui/dist/assets/diff-ChtP43wD.js +1 -0
- package/ui/dist/assets/dist-BGbe0-Aa.js +1 -0
- package/ui/dist/assets/dist-Ba-HC662.js +2 -0
- package/ui/dist/assets/dist-BtMLMthJ.js +1 -0
- package/ui/dist/assets/dist-Bw5-uxs9.js +1 -0
- package/ui/dist/assets/dist-BzulKuec.js +1 -0
- package/ui/dist/assets/dist-C-Oyo1ZT.js +1 -0
- package/ui/dist/assets/dist-C8MdhqgG.js +1 -0
- package/ui/dist/assets/dist-CG1pHn5m.js +1 -0
- package/ui/dist/assets/dist-CgIQYY1o.js +1 -0
- package/ui/dist/assets/dist-CmpteMYY.js +9 -0
- package/ui/dist/assets/dist-DDJNvivM.js +1 -0
- package/ui/dist/assets/dist-DF3F_I8N.js +1 -0
- package/ui/dist/assets/dist-DSy3dVf4.js +11 -0
- package/ui/dist/assets/dist-Dlorc3Sx.js +1 -0
- package/ui/dist/assets/dist-Dq6sW5Ur.js +1 -0
- package/ui/dist/assets/dist-Dr9crbBz.js +13 -0
- package/ui/dist/assets/dist-jjxQutEm.js +1 -0
- package/ui/dist/assets/dockerfile-DJkHQgkl.js +1 -0
- package/ui/dist/assets/dtd-DTF72YNO.js +1 -0
- package/ui/dist/assets/dylan-S7ucR4pR.js +1 -0
- package/ui/dist/assets/ebnf-D4c0_ac3.js +1 -0
- package/ui/dist/assets/ecl-BVIeWmwY.js +1 -0
- package/ui/dist/assets/eiffel-BEjRio4Q.js +1 -0
- package/ui/dist/assets/elm-Cshzl8qu.js +1 -0
- package/ui/dist/assets/erlang-BV0c4Hdn.js +1 -0
- package/ui/dist/assets/factor-LKH5gN8L.js +1 -0
- package/ui/dist/assets/fcl-ClOhbFR7.js +1 -0
- package/ui/dist/assets/forth-D6RzAl64.js +1 -0
- package/ui/dist/assets/fortran-Bt6PBEDR.js +1 -0
- package/ui/dist/assets/gas-BHEdbvp9.js +1 -0
- package/ui/dist/assets/gherkin-oBAE_ms0.js +1 -0
- package/ui/dist/assets/groovy-CUrWKPia.js +1 -0
- package/ui/dist/assets/haskell-BRsoo5mP.js +1 -0
- package/ui/dist/assets/haxe-DjVOVvNP.js +1 -0
- package/ui/dist/assets/http-CLVgA2GD.js +1 -0
- package/ui/dist/assets/idl-BUZw3wgd.js +1 -0
- package/ui/dist/assets/index-CTmnF0Wp.css +2 -0
- package/ui/dist/assets/index-P_Yo0ZZn.js +338 -0
- package/ui/dist/assets/javascript-lS2r2ifg.js +1 -0
- package/ui/dist/assets/julia-CQfgDRfP.js +1 -0
- package/ui/dist/assets/livescript-BXxG0Yva.js +1 -0
- package/ui/dist/assets/lua-DPCEkfD_.js +1 -0
- package/ui/dist/assets/mathematica-u8YMmbU0.js +1 -0
- package/ui/dist/assets/mbox-BEMVcqjs.js +1 -0
- package/ui/dist/assets/mirc-C9zpzAZU.js +1 -0
- package/ui/dist/assets/mllike-B45xgp2S.js +1 -0
- package/ui/dist/assets/modelica-CqHI_q-D.js +1 -0
- package/ui/dist/assets/mscgen-qgSLujhx.js +1 -0
- package/ui/dist/assets/mumps-BSzj6Xyz.js +1 -0
- package/ui/dist/assets/nginx-C-ORUuei.js +1 -0
- package/ui/dist/assets/nsis-viPvXFiq.js +1 -0
- package/ui/dist/assets/ntriples-CzMBulkp.js +1 -0
- package/ui/dist/assets/octave-D2Q8cUp1.js +1 -0
- package/ui/dist/assets/oz-BwTct_5T.js +1 -0
- package/ui/dist/assets/pascal-Dp_KdFgX.js +1 -0
- package/ui/dist/assets/perl-BkSkiI9m.js +1 -0
- package/ui/dist/assets/pig-AF4Hwhju.js +1 -0
- package/ui/dist/assets/powershell-D8WpQPaX.js +1 -0
- package/ui/dist/assets/properties-C357ku5U.js +1 -0
- package/ui/dist/assets/protobuf-D1ij_kNL.js +1 -0
- package/ui/dist/assets/pug-BqCHZ5xT.js +1 -0
- package/ui/dist/assets/puppet-BZyAS7I4.js +1 -0
- package/ui/dist/assets/python-B_1Jd8HM.js +1 -0
- package/ui/dist/assets/q-B9NhS7L_.js +1 -0
- package/ui/dist/assets/r-CxDmU1I4.js +1 -0
- package/ui/dist/assets/rpm-gpDK5sbt.js +1 -0
- package/ui/dist/assets/ruby-ChfNwlCY.js +1 -0
- package/ui/dist/assets/sas-C2OyD2Y6.js +1 -0
- package/ui/dist/assets/scheme-D9qYN0V5.js +1 -0
- package/ui/dist/assets/shell-DwuoZtxw.js +1 -0
- package/ui/dist/assets/sieve-tLWDfEkZ.js +1 -0
- package/ui/dist/assets/simple-mode-DXiLM1-G.js +1 -0
- package/ui/dist/assets/smalltalk-BLp5-jwC.js +1 -0
- package/ui/dist/assets/solr-B78wSQJQ.js +1 -0
- package/ui/dist/assets/sparql-Dz-mlCAC.js +1 -0
- package/ui/dist/assets/spreadsheet-CTIncYxj.js +1 -0
- package/ui/dist/assets/sql-DuQMFDvy.js +1 -0
- package/ui/dist/assets/stex-DNzd62Q_.js +1 -0
- package/ui/dist/assets/stylus-BOVgKnng.js +1 -0
- package/ui/dist/assets/swift-DKB6_1j6.js +1 -0
- package/ui/dist/assets/tcl-BGfruO1u.js +1 -0
- package/ui/dist/assets/textile-C4jVoHG1.js +1 -0
- package/ui/dist/assets/tiddlywiki-VlUiK86J.js +1 -0
- package/ui/dist/assets/tiki-CvUEFusT.js +1 -0
- package/ui/dist/assets/toml-BPTmHmyx.js +1 -0
- package/ui/dist/assets/troff-C2dAgh7P.js +1 -0
- package/ui/dist/assets/ttcn-PPSTk1ow.js +1 -0
- package/ui/dist/assets/ttcn-cfg-2b4Gb5sE.js +1 -0
- package/ui/dist/assets/turtle-Cx3rYX2g.js +1 -0
- package/ui/dist/assets/vb-BthdM_4N.js +1 -0
- package/ui/dist/assets/vbscript-D9f6rSsU.js +1 -0
- package/ui/dist/assets/velocity-CqftRsjg.js +1 -0
- package/ui/dist/assets/verilog-CjI9Y6RI.js +1 -0
- package/ui/dist/assets/vhdl-kS471soi.js +1 -0
- package/ui/dist/assets/webidl-D5jiJvOU.js +1 -0
- package/ui/dist/assets/xquery-BKJx4mf9.js +1 -0
- package/ui/dist/assets/yacas-CXXgtw0p.js +1 -0
- package/ui/dist/assets/z80-Co3PbHdE.js +1 -0
- package/ui/dist/favicon.ico +0 -0
- package/ui/dist/favicon.svg +11 -0
- package/ui/dist/help/ai-tools.png +0 -0
- package/ui/dist/help/case-explore.png +0 -0
- package/ui/dist/help/case-generate.png +0 -0
- package/ui/dist/help/cases-memo.png +0 -0
- package/ui/dist/help/profile-form.png +0 -0
- package/ui/dist/help/profiles-login.png +0 -0
- package/ui/dist/help/reports.png +0 -0
- package/ui/dist/help/run-tests.png +0 -0
- package/ui/dist/index.html +23 -0
- package/ui/dist/logos/claude.svg +1 -0
- package/ui/dist/logos/opencode.svg +1 -0
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { chromium } from '@playwright/test';
|
|
3
|
+
import { isMap, isSeq, parseDocument } from 'yaml';
|
|
4
|
+
import { DEFAULT_SESSION_CHECK_PATH, DEFAULT_SESSION_CHECK, listEnvs, listProfiles, loadProfiles, PROFILES_FILE, validateSessionCheck } from '../../lib/profiles.js';
|
|
5
|
+
import { authFile } from '../../lib/paths.js';
|
|
6
|
+
import { checkSession } from '../../lib/session.js';
|
|
7
|
+
import { withScheme } from '../shared/site-address.js';
|
|
8
|
+
// Keyed by profile id.
|
|
9
|
+
const verifications = new Map();
|
|
10
|
+
const verifying = new Set();
|
|
11
|
+
/** Only looks at file timestamps; the content of the session file is never read here. */
|
|
12
|
+
function savedAt(profileId) {
|
|
13
|
+
const file = authFile(profileId);
|
|
14
|
+
return fs.existsSync(file) ? fs.statSync(file).mtime.toISOString() : null;
|
|
15
|
+
}
|
|
16
|
+
const toInfo = (profile) => {
|
|
17
|
+
const saved = savedAt(profile.id);
|
|
18
|
+
const verification = verifications.get(profile.id);
|
|
19
|
+
// A session file saved after the check (a new login from the CLI, another browser tab) makes the old
|
|
20
|
+
// result stale, so the row shows "unverified" until it is checked again.
|
|
21
|
+
const stale = Boolean(saved && verification && saved > verification.checkedAt);
|
|
22
|
+
let status;
|
|
23
|
+
if (!saved)
|
|
24
|
+
status = 'missing';
|
|
25
|
+
else if (!verification || stale)
|
|
26
|
+
status = 'unverified';
|
|
27
|
+
else
|
|
28
|
+
status = verification.status === 'missing' ? 'unverified' : verification.status;
|
|
29
|
+
return {
|
|
30
|
+
id: profile.id,
|
|
31
|
+
name: profile.name,
|
|
32
|
+
env: profile.env,
|
|
33
|
+
baseURL: profile.baseURL,
|
|
34
|
+
account: profile.account,
|
|
35
|
+
tags: profile.tags,
|
|
36
|
+
startPath: profile.startPath,
|
|
37
|
+
sessionCheckPath: profile.sessionCheckPath,
|
|
38
|
+
sessionCheck: profile.sessionCheck,
|
|
39
|
+
email: profile.email,
|
|
40
|
+
status,
|
|
41
|
+
verifying: verifying.has(profile.id),
|
|
42
|
+
savedAt: saved,
|
|
43
|
+
checkedAt: verification?.checkedAt ?? null,
|
|
44
|
+
error: verification?.error,
|
|
45
|
+
};
|
|
46
|
+
};
|
|
47
|
+
export function listProfileRows() {
|
|
48
|
+
return { profiles: listProfiles().map(toInfo), envs: listEnvs(), verifying: verifying.size > 0 };
|
|
49
|
+
}
|
|
50
|
+
/** Opens each saved session on the real site (read-only) and records whether it is still logged in. */
|
|
51
|
+
export async function verifyProfiles(only) {
|
|
52
|
+
const profiles = listProfiles().filter((profile) => (!only?.ids || only.ids.includes(profile.id)) && (!only?.env || only.env === profile.env) && !verifying.has(profile.id));
|
|
53
|
+
const withSession = profiles.filter((profile) => savedAt(profile.id));
|
|
54
|
+
for (const profile of profiles) {
|
|
55
|
+
if (!savedAt(profile.id))
|
|
56
|
+
verifications.delete(profile.id);
|
|
57
|
+
}
|
|
58
|
+
if (withSession.length === 0)
|
|
59
|
+
return;
|
|
60
|
+
for (const profile of withSession)
|
|
61
|
+
verifying.add(profile.id);
|
|
62
|
+
let browser;
|
|
63
|
+
try {
|
|
64
|
+
browser = await chromium.launch();
|
|
65
|
+
await Promise.all(withSession.map(async (profile) => {
|
|
66
|
+
try {
|
|
67
|
+
const status = await checkSession(browser, profile);
|
|
68
|
+
verifications.set(profile.id, { status, checkedAt: new Date().toISOString() });
|
|
69
|
+
}
|
|
70
|
+
catch (error) {
|
|
71
|
+
verifications.set(profile.id, {
|
|
72
|
+
status: 'error',
|
|
73
|
+
checkedAt: new Date().toISOString(),
|
|
74
|
+
error: error.message.split('\n')[0],
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
finally {
|
|
78
|
+
verifying.delete(profile.id);
|
|
79
|
+
}
|
|
80
|
+
}));
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
for (const profile of withSession) {
|
|
84
|
+
verifications.set(profile.id, {
|
|
85
|
+
status: 'error',
|
|
86
|
+
checkedAt: new Date().toISOString(),
|
|
87
|
+
error: error.message.split('\n')[0],
|
|
88
|
+
});
|
|
89
|
+
verifying.delete(profile.id);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
finally {
|
|
93
|
+
await browser?.close();
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
const PROFILE_ID = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
97
|
+
const ENV_NAME = /^[a-z0-9]+(-[a-z0-9]+)*$/;
|
|
98
|
+
export class ProfileError extends Error {
|
|
99
|
+
status;
|
|
100
|
+
constructor(message, status) {
|
|
101
|
+
super(message);
|
|
102
|
+
this.status = status;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function parseSiteAddress(text, label) {
|
|
106
|
+
let url;
|
|
107
|
+
try {
|
|
108
|
+
// A typed address without a scheme (`example.com`) is completed with https://.
|
|
109
|
+
url = new URL(withScheme(text));
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
throw new ProfileError(`${label} must be a full URL, e.g. https://www.example.com.`, 400);
|
|
113
|
+
}
|
|
114
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:') {
|
|
115
|
+
throw new ProfileError(`${label} must start with https:// (or http://).`, 400);
|
|
116
|
+
}
|
|
117
|
+
if (url.pathname !== '/' || url.search || url.hash || url.username || url.password) {
|
|
118
|
+
throw new ProfileError(`${label} must be only the site address, without a path, query or credentials.`, 400);
|
|
119
|
+
}
|
|
120
|
+
return url;
|
|
121
|
+
}
|
|
122
|
+
const normalTags = (tags) => [...new Set((tags ?? []).map((tag) => tag.trim()).filter(Boolean))];
|
|
123
|
+
function readFields(request) {
|
|
124
|
+
const name = request.name.trim();
|
|
125
|
+
const env = request.env.trim();
|
|
126
|
+
const account = request.account?.trim() || undefined;
|
|
127
|
+
const startPath = request.startPath?.trim() || undefined;
|
|
128
|
+
const sessionCheckPath = request.sessionCheckPath?.trim() || undefined;
|
|
129
|
+
// Same rules as the loader, so the file the form writes is one `pnpm asrun` accepts.
|
|
130
|
+
const sessionCheck = validateSessionCheck('Session check', request.sessionCheck ?? null);
|
|
131
|
+
if (!name)
|
|
132
|
+
throw new ProfileError('Name is required.', 400);
|
|
133
|
+
if (!ENV_NAME.test(env)) {
|
|
134
|
+
throw new ProfileError('Environment must be lowercase letters and digits, separated by single dashes (e.g. "production", "staging").', 400);
|
|
135
|
+
}
|
|
136
|
+
if (env === 'production' && request.email === true) {
|
|
137
|
+
throw new ProfileError('email: true is not allowed. Tests never send email in production.', 400);
|
|
138
|
+
}
|
|
139
|
+
const baseURL = parseSiteAddress(request.baseURL, 'Base URL').origin;
|
|
140
|
+
if (startPath && (!startPath.startsWith('/') || startPath.startsWith('//') || /\s/.test(startPath))) {
|
|
141
|
+
throw new ProfileError('Start path must be empty (the site root) or a path on the site that starts with a single "/" and contains no spaces.', 400);
|
|
142
|
+
}
|
|
143
|
+
if (sessionCheckPath && (!sessionCheckPath.startsWith('/') || /\s/.test(sessionCheckPath))) {
|
|
144
|
+
throw new ProfileError('Session check path must start with "/" and contain no spaces.', 400);
|
|
145
|
+
}
|
|
146
|
+
return {
|
|
147
|
+
name,
|
|
148
|
+
env,
|
|
149
|
+
baseURL,
|
|
150
|
+
account,
|
|
151
|
+
tags: normalTags(request.tags),
|
|
152
|
+
startPath,
|
|
153
|
+
sessionCheckPath,
|
|
154
|
+
sessionCheck,
|
|
155
|
+
email: request.email === true,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
/** The `sessionCheck` keys that differ from the defaults, or null when the profile can rely on them. */
|
|
159
|
+
function sessionCheckEntry(check) {
|
|
160
|
+
const entry = {};
|
|
161
|
+
if (check.loggedOutUrl !== DEFAULT_SESSION_CHECK.loggedOutUrl)
|
|
162
|
+
entry.loggedOutUrl = check.loggedOutUrl;
|
|
163
|
+
if (check.loggedInSelector)
|
|
164
|
+
entry.loggedInSelector = check.loggedInSelector;
|
|
165
|
+
if (check.loggedOutSelector)
|
|
166
|
+
entry.loggedOutSelector = check.loggedOutSelector;
|
|
167
|
+
if (check.timeoutMs !== DEFAULT_SESSION_CHECK.timeoutMs)
|
|
168
|
+
entry.timeoutMs = check.timeoutMs;
|
|
169
|
+
return Object.keys(entry).length > 0 ? entry : null;
|
|
170
|
+
}
|
|
171
|
+
const sameSessionCheck = (a, b) => a.loggedOutUrl === b.loggedOutUrl &&
|
|
172
|
+
a.loggedInSelector === b.loggedInSelector &&
|
|
173
|
+
a.loggedOutSelector === b.loggedOutSelector &&
|
|
174
|
+
a.timeoutMs === b.timeoutMs;
|
|
175
|
+
/**
|
|
176
|
+
* Writes the edited YAML document and restores the old file when the result cannot be loaded, so a
|
|
177
|
+
* failed save never leaves a profiles.yaml behind that the test runner cannot read.
|
|
178
|
+
*/
|
|
179
|
+
function saveDocument(file, doc, original, action) {
|
|
180
|
+
const temp = `${file}.${process.pid}.tmp`;
|
|
181
|
+
fs.writeFileSync(temp, doc.toString());
|
|
182
|
+
fs.renameSync(temp, file);
|
|
183
|
+
try {
|
|
184
|
+
loadProfiles(file);
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
fs.writeFileSync(file, original);
|
|
188
|
+
throw new ProfileError(`Could not ${action}: ${error.message}`, 400);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
const sameList = (a, b) => a.length === b.length && a.every((value, index) => value === b[index]);
|
|
192
|
+
/**
|
|
193
|
+
* Appends one profile to profiles.yaml. The file is edited as a YAML document, so comments and the
|
|
194
|
+
* existing entries stay as they are.
|
|
195
|
+
*/
|
|
196
|
+
export function addProfile(request, file = PROFILES_FILE) {
|
|
197
|
+
const id = request.id.trim();
|
|
198
|
+
const fields = readFields(request);
|
|
199
|
+
if (!PROFILE_ID.test(id)) {
|
|
200
|
+
throw new ProfileError('Profile id must be lowercase letters and digits, separated by single dashes (e.g. "example-staging").', 400);
|
|
201
|
+
}
|
|
202
|
+
if (loadProfiles(file).some((profile) => profile.id === id)) {
|
|
203
|
+
throw new ProfileError(`Profile "${id}" already exists.`, 409);
|
|
204
|
+
}
|
|
205
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
206
|
+
const doc = parseDocument(original, { merge: true });
|
|
207
|
+
if (doc.errors.length > 0) {
|
|
208
|
+
throw new ProfileError('profiles.yaml has syntax errors; fix it by hand first.', 400);
|
|
209
|
+
}
|
|
210
|
+
const profiles = doc.get('profiles');
|
|
211
|
+
if (!isSeq(profiles))
|
|
212
|
+
throw new ProfileError('profiles.yaml has no "profiles" list.', 400);
|
|
213
|
+
// `profiles: []` is a flow sequence; adding a map entry to it would render `[ { ... } ]`. Keep the file block-style.
|
|
214
|
+
profiles.flow = false;
|
|
215
|
+
const hadEntries = profiles.items.length > 0;
|
|
216
|
+
const check = sessionCheckEntry(fields.sessionCheck);
|
|
217
|
+
const entry = doc.createNode({
|
|
218
|
+
id,
|
|
219
|
+
name: fields.name,
|
|
220
|
+
env: fields.env,
|
|
221
|
+
baseURL: fields.baseURL,
|
|
222
|
+
...(fields.account && { account: fields.account }),
|
|
223
|
+
...(fields.tags.length > 0 && { tags: fields.tags }),
|
|
224
|
+
...(fields.startPath && { startPath: fields.startPath }),
|
|
225
|
+
...(fields.sessionCheckPath && { sessionCheckPath: fields.sessionCheckPath }),
|
|
226
|
+
...(check && { sessionCheck: check }),
|
|
227
|
+
// Never send email from a new profile until a human has checked that a mailbox is connected.
|
|
228
|
+
email: fields.email,
|
|
229
|
+
});
|
|
230
|
+
if (isMap(entry)) {
|
|
231
|
+
entry.spaceBefore = hadEntries;
|
|
232
|
+
entry.flow = false;
|
|
233
|
+
}
|
|
234
|
+
profiles.add(entry);
|
|
235
|
+
saveDocument(file, doc, original, 'add the profile');
|
|
236
|
+
return toInfo(loadProfiles(file).find((profile) => profile.id === id));
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Deletes the saved login of one profile, so it has to log in again. The file holds the session cookies
|
|
240
|
+
* and is never read here, only removed.
|
|
241
|
+
*/
|
|
242
|
+
export function forgetSession(id, file = PROFILES_FILE) {
|
|
243
|
+
const profile = loadProfiles(file).find((candidate) => candidate.id === id);
|
|
244
|
+
if (!profile)
|
|
245
|
+
throw new ProfileError(`Profile "${id}" is not in profiles.yaml any more. Reload the page.`, 409);
|
|
246
|
+
fs.rmSync(authFile(id), { force: true });
|
|
247
|
+
verifications.delete(id);
|
|
248
|
+
return toInfo(profile);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Removes one profile from profiles.yaml and deletes its saved login. Only that entry is written, so the
|
|
252
|
+
* comments and the other entries stay in the file. Screenshot baselines named after the id stay on disk:
|
|
253
|
+
* they are files of the cases, not of the profile.
|
|
254
|
+
*/
|
|
255
|
+
export function deleteProfile(id, file = PROFILES_FILE) {
|
|
256
|
+
const profile = loadProfiles(file).find((candidate) => candidate.id === id);
|
|
257
|
+
if (!profile)
|
|
258
|
+
throw new ProfileError(`Profile "${id}" is not in profiles.yaml any more. Reload the page.`, 409);
|
|
259
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
260
|
+
const doc = parseDocument(original, { merge: true });
|
|
261
|
+
if (doc.errors.length > 0) {
|
|
262
|
+
throw new ProfileError('profiles.yaml has syntax errors; fix it by hand first.', 400);
|
|
263
|
+
}
|
|
264
|
+
const profiles = doc.get('profiles');
|
|
265
|
+
if (!isSeq(profiles))
|
|
266
|
+
throw new ProfileError('profiles.yaml has no "profiles" list.', 400);
|
|
267
|
+
const index = profiles.items.findIndex((item) => isMap(item) && item.get('id') === id);
|
|
268
|
+
if (index < 0)
|
|
269
|
+
throw new ProfileError(`Profile "${id}" is not in profiles.yaml any more. Reload the page.`, 409);
|
|
270
|
+
profiles.delete(index);
|
|
271
|
+
// The entry that is first now keeps the blank line of the one before it; drop it so `profiles:` is followed
|
|
272
|
+
// directly by the entry, like a hand-written file.
|
|
273
|
+
const first = profiles.items[0];
|
|
274
|
+
if (isMap(first))
|
|
275
|
+
first.spaceBefore = false;
|
|
276
|
+
// A `profiles:` with nothing under it is null; the loader needs a list (it may be empty while the suite is
|
|
277
|
+
// set up), so an emptied list is written as `[]`.
|
|
278
|
+
if (profiles.items.length === 0)
|
|
279
|
+
profiles.flow = true;
|
|
280
|
+
saveDocument(file, doc, original, 'delete the profile');
|
|
281
|
+
// The saved login belonged to this profile. The file is never read here, only removed.
|
|
282
|
+
fs.rmSync(authFile(id), { force: true });
|
|
283
|
+
verifications.delete(id);
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Changes the fields of one existing profile. Only the fields that actually changed are written, so
|
|
287
|
+
* comments on the others stay in the file. When the site address changes, the saved login belongs to
|
|
288
|
+
* the old site and is deleted: the profile has to log in again on the new one.
|
|
289
|
+
*/
|
|
290
|
+
export function editProfile(request, file = PROFILES_FILE) {
|
|
291
|
+
const id = request.id.trim();
|
|
292
|
+
if (!PROFILE_ID.test(id)) {
|
|
293
|
+
throw new ProfileError('Profile id must be lowercase letters and digits, separated by single dashes (e.g. "example-staging").', 400);
|
|
294
|
+
}
|
|
295
|
+
const fields = readFields(request);
|
|
296
|
+
const before = loadProfiles(file).find((profile) => profile.id === id);
|
|
297
|
+
if (!before)
|
|
298
|
+
throw new ProfileError(`Profile "${id}" is not in profiles.yaml any more. Reload the page.`, 409);
|
|
299
|
+
const original = fs.readFileSync(file, 'utf8');
|
|
300
|
+
const doc = parseDocument(original, { merge: true });
|
|
301
|
+
if (doc.errors.length > 0) {
|
|
302
|
+
throw new ProfileError('profiles.yaml has syntax errors; fix it by hand first.', 400);
|
|
303
|
+
}
|
|
304
|
+
const profiles = doc.get('profiles');
|
|
305
|
+
if (!isSeq(profiles))
|
|
306
|
+
throw new ProfileError('profiles.yaml has no "profiles" list.', 400);
|
|
307
|
+
const entry = profiles.items.find((item) => isMap(item) && item.get('id') === id);
|
|
308
|
+
if (!isMap(entry))
|
|
309
|
+
throw new ProfileError(`Profile "${id}" is not in profiles.yaml any more. Reload the page.`, 409);
|
|
310
|
+
if (fields.name !== before.name)
|
|
311
|
+
entry.set('name', fields.name);
|
|
312
|
+
if (fields.env !== before.env)
|
|
313
|
+
entry.set('env', fields.env);
|
|
314
|
+
if (fields.baseURL !== before.baseURL)
|
|
315
|
+
entry.set('baseURL', fields.baseURL);
|
|
316
|
+
if (fields.account) {
|
|
317
|
+
if (fields.account !== before.account)
|
|
318
|
+
entry.set('account', fields.account);
|
|
319
|
+
}
|
|
320
|
+
else if (entry.has('account')) {
|
|
321
|
+
entry.delete('account');
|
|
322
|
+
}
|
|
323
|
+
if (!sameList(fields.tags, before.tags)) {
|
|
324
|
+
if (fields.tags.length > 0)
|
|
325
|
+
entry.set('tags', fields.tags);
|
|
326
|
+
else
|
|
327
|
+
entry.delete('tags');
|
|
328
|
+
}
|
|
329
|
+
// An empty path means the site root; drop an explicit key instead of writing an empty one.
|
|
330
|
+
if (fields.startPath) {
|
|
331
|
+
if (fields.startPath !== before.startPath || !entry.has('startPath')) {
|
|
332
|
+
entry.set('startPath', fields.startPath);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
else if (entry.has('startPath')) {
|
|
336
|
+
entry.delete('startPath');
|
|
337
|
+
}
|
|
338
|
+
// An omitted path means the default; drop an explicit key instead of writing the default out.
|
|
339
|
+
if (fields.sessionCheckPath && fields.sessionCheckPath !== DEFAULT_SESSION_CHECK_PATH) {
|
|
340
|
+
if (fields.sessionCheckPath !== before.sessionCheckPath || !entry.has('sessionCheckPath')) {
|
|
341
|
+
entry.set('sessionCheckPath', fields.sessionCheckPath);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
else if (entry.has('sessionCheckPath')) {
|
|
345
|
+
entry.delete('sessionCheckPath');
|
|
346
|
+
}
|
|
347
|
+
// The same for the session check signals: write only what differs from the defaults, so the entry
|
|
348
|
+
// stays readable (and a comment above it stays in place when nothing about it changed).
|
|
349
|
+
if (!sameSessionCheck(fields.sessionCheck, before.sessionCheck)) {
|
|
350
|
+
const check = sessionCheckEntry(fields.sessionCheck);
|
|
351
|
+
if (check)
|
|
352
|
+
entry.set('sessionCheck', check);
|
|
353
|
+
else
|
|
354
|
+
entry.delete('sessionCheck');
|
|
355
|
+
}
|
|
356
|
+
if (fields.email !== before.email)
|
|
357
|
+
entry.set('email', fields.email);
|
|
358
|
+
saveDocument(file, doc, original, 'save the profile');
|
|
359
|
+
const siteChanged = fields.baseURL !== before.baseURL;
|
|
360
|
+
const hadSession = siteChanged && fs.existsSync(authFile(id));
|
|
361
|
+
if (siteChanged) {
|
|
362
|
+
// The saved login holds cookies of the old site. Deleting the file (never reading it) makes the
|
|
363
|
+
// profile log in again on the new address.
|
|
364
|
+
fs.rmSync(authFile(id), { force: true });
|
|
365
|
+
verifications.delete(id);
|
|
366
|
+
}
|
|
367
|
+
return {
|
|
368
|
+
profile: toInfo(loadProfiles(file).find((profile) => profile.id === id)),
|
|
369
|
+
sessionCleared: hadSession,
|
|
370
|
+
};
|
|
371
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import { CASES_DIR, TESTS_DIR } from '../../lib/paths.js';
|
|
4
|
+
const DEBOUNCE_MS = 200;
|
|
5
|
+
/** Emits `files` when a case file or a spec changes on disk, whoever changed it (the UI, an agent, an editor). */
|
|
6
|
+
export const fileEvents = new EventEmitter();
|
|
7
|
+
fileEvents.setMaxListeners(0);
|
|
8
|
+
export function startFileWatcher() {
|
|
9
|
+
const pending = { cases: false, specs: false };
|
|
10
|
+
let timer = null;
|
|
11
|
+
const schedule = (kind) => {
|
|
12
|
+
pending[kind] = true;
|
|
13
|
+
timer ??= setTimeout(() => {
|
|
14
|
+
timer = null;
|
|
15
|
+
const change = { ...pending };
|
|
16
|
+
pending.cases = false;
|
|
17
|
+
pending.specs = false;
|
|
18
|
+
fileEvents.emit('files', change);
|
|
19
|
+
}, DEBOUNCE_MS);
|
|
20
|
+
};
|
|
21
|
+
const watch = (dir, kind, suffix) => {
|
|
22
|
+
try {
|
|
23
|
+
const watcher = fs.watch(dir, { recursive: true }, (_event, filename) => {
|
|
24
|
+
if (filename && filename.toString().endsWith(suffix))
|
|
25
|
+
schedule(kind);
|
|
26
|
+
});
|
|
27
|
+
watcher.on('error', () => watcher.close());
|
|
28
|
+
watcher.unref();
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
// The folder does not exist (yet) or cannot be watched; the pages still refresh when the window gets focus.
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
watch(CASES_DIR, 'cases', '.md');
|
|
35
|
+
watch(TESTS_DIR, 'specs', '.spec.ts');
|
|
36
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export declare const AI_TOOLS: readonly ['claude', 'opencode'];
|
|
2
|
+
export type AiTool = (typeof AI_TOOLS)[number];
|
|
3
|
+
export declare const DEFAULT_AI_TOOL: AiTool;
|
|
4
|
+
export declare const AI_TOOL_LABEL: Record<AiTool, string>;
|
|
5
|
+
export declare const AI_COMMANDS: readonly ['explore-case', 'gen-spec', 'heal-spec'];
|
|
6
|
+
export type AiCommand = (typeof AI_COMMANDS)[number];
|
|
7
|
+
export declare const AI_COMMAND_LABEL: Record<AiCommand, string>;
|
|
8
|
+
/**
|
|
9
|
+
* What a one-shot run of a command may change, so the permissions can be as narrow as possible.
|
|
10
|
+
* `case`: only cases/*.md. `spec`: also the spec, but it never runs the tests.
|
|
11
|
+
* `files`: also snapshots, and it runs the tests.
|
|
12
|
+
*/
|
|
13
|
+
export type AiWrites = 'case' | 'spec' | 'files';
|
|
14
|
+
export declare const AI_COMMAND_WRITES: Record<AiCommand, AiWrites>;
|
|
15
|
+
/**
|
|
16
|
+
* Every command opens real pages, so every command needs an environment and there is no default.
|
|
17
|
+
* Agents never write data in any of them: a person does those steps.
|
|
18
|
+
*/
|
|
19
|
+
export declare const AI_ENV_HINT: Record<AiCommand, string>;
|
|
20
|
+
/** What a run of a command on a case may be told besides the case id. */
|
|
21
|
+
export interface AiRunOptions {
|
|
22
|
+
/** The environment to explore. There is no default. */
|
|
23
|
+
env: string;
|
|
24
|
+
/**
|
|
25
|
+
* The model to run the agent with, as `provider/model` (`#variant` for OpenCode) or a Claude alias/name.
|
|
26
|
+
* Omitted = whatever the CLI would pick by itself, which for OpenCode is its built-in free model.
|
|
27
|
+
*/
|
|
28
|
+
model?: string;
|
|
29
|
+
/** `gen-spec` only: regenerate and overwrite an existing spec instead of leaving it alone. */
|
|
30
|
+
force?: boolean;
|
|
31
|
+
/** `explore-case` only: a person is at the machine, so steps that write data are handed to them in the browser. */
|
|
32
|
+
attended?: boolean;
|
|
33
|
+
/** Show the agent's reasoning in the log (`--thinking` for OpenCode). Off by default: it costs tokens and time. */
|
|
34
|
+
thinking?: boolean;
|
|
35
|
+
/** `heal-spec` only: the profiles to heal. Omitted = every profile that is logged in in `env`. */
|
|
36
|
+
profiles?: string[];
|
|
37
|
+
/** `heal-spec` only: file (relative to the project root) with the user's description and clues. */
|
|
38
|
+
hints?: string;
|
|
39
|
+
/** `heal-spec` only: the report (folder name under reports/) to read first instead of the newest one. */
|
|
40
|
+
report?: string;
|
|
41
|
+
}
|
|
42
|
+
export declare const slashCommand: (command: AiCommand, caseId: string, options: AiRunOptions) => string;
|
|
43
|
+
/**
|
|
44
|
+
* The conversation of a one-shot run. The server picks the id, so a later turn can resume it: the first turn
|
|
45
|
+
* creates the session, every following one (`resume`) continues it with the same permissions.
|
|
46
|
+
*/
|
|
47
|
+
export interface AiSession {
|
|
48
|
+
id: string;
|
|
49
|
+
resume: boolean;
|
|
50
|
+
}
|
|
51
|
+
/** The prompt of a one-shot run. `heal-spec` has no questions of its own, so it keeps the plain slash command. */
|
|
52
|
+
export declare function headlessPrompt(command: AiCommand, caseId: string, options: AiRunOptions): string;
|
|
53
|
+
/** Executable and arguments of the one-shot (non-interactive) run. */
|
|
54
|
+
export declare function headlessInvocation(tool: AiTool, command: AiCommand, caseId: string, options: AiRunOptions, session?: AiSession): {
|
|
55
|
+
file: string;
|
|
56
|
+
args: string[];
|
|
57
|
+
};
|
|
58
|
+
/** The one-shot command as it is typed in a shell. */
|
|
59
|
+
export declare function headlessCommandLine(tool: AiTool, command: AiCommand, caseId: string, options: AiRunOptions, session?: AiSession): string;
|
|
60
|
+
/** Folder (relative to the AsRun root, git-ignored) where the clues typed into the Heal dialog are stored. */
|
|
61
|
+
export declare const HINTS_DIR = ".tmp/hints";
|
|
62
|
+
export declare const HINTS_MAX_CHARS = 8000;
|
|
63
|
+
/** Folder (relative to the AsRun root, git-ignored) where the answers typed into the Continue box are stored. */
|
|
64
|
+
export declare const REPLY_DIR = ".tmp/replies";
|
|
65
|
+
export declare const REPLY_MAX_CHARS = 4000;
|
|
66
|
+
export declare const USE_DEFAULTS_REPLY = "Accept the suggestion of every open question: delete its TODO block and keep the sentence above it as confirmed text.";
|
|
67
|
+
/**
|
|
68
|
+
* The next turn of a run that ended with `RESULT: needs-input`: the user's answers. They go into the case file, which
|
|
69
|
+
* stays the record of truth, and the task of the original command is finished. The answers come from a file
|
|
70
|
+
* (`.tmp/replies/…md`) rather than from the prompt, so a line break or a quote in them cannot break the command line.
|
|
71
|
+
*/
|
|
72
|
+
export declare function replyPrompt(command: AiCommand, replyFile: string): string;
|
|
73
|
+
/** The next turn of an earlier run, in its session and with exactly the permissions of the first turn. */
|
|
74
|
+
export declare function continueInvocation(tool: AiTool, command: AiCommand, replyFile: string, sessionId: string, model?: string, thinking?: boolean): {
|
|
75
|
+
file: string;
|
|
76
|
+
args: string[];
|
|
77
|
+
};
|
|
78
|
+
export declare const continueCommandLine: (tool: AiTool, command: AiCommand, replyFile: string, sessionId: string, model?: string, thinking?: boolean) => string;
|
|
79
|
+
/** Folder (relative to the AsRun root, git-ignored) where memos from the Cases page are stored. */
|
|
80
|
+
export declare const MEMO_DIR = ".tmp/memos";
|
|
81
|
+
export declare const MEMO_MAX_CHARS = 100000;
|
|
82
|
+
/** /make-case is interactive by design; a one-shot run cannot answer questions, so it must use the defaults. */
|
|
83
|
+
export declare const makeCasePrompt: (memoPath: string) => string;
|
|
84
|
+
export declare const makeCaseInvocation: (tool: AiTool, memoPath: string, model?: string, thinking?: boolean) => {
|
|
85
|
+
file: string;
|
|
86
|
+
args: string[];
|
|
87
|
+
};
|
|
88
|
+
export declare const makeCaseCommandLine: (tool: AiTool, memoPath: string, model?: string, thinking?: boolean) => string;
|