@asrunjs/asrun 0.2.0 → 0.5.3
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 -15
- package/.claude/commands/gen-spec.md +16 -13
- package/.claude/commands/heal-spec.md +26 -23
- package/.claude/commands/make-case.md +11 -8
- package/.claude/skills/exploring-cases/SKILL.md +146 -132
- package/.claude/skills/exploring-cases/references/open-questions.md +116 -112
- package/.claude/skills/exploring-cases/references/readiness.md +146 -137
- package/.claude/skills/exploring-cases/references/user-handoff.md +157 -147
- package/.claude/skills/generating-specs/SKILL.md +219 -207
- package/.claude/skills/generating-specs/references/conventions.md +148 -148
- package/.claude/skills/making-cases/SKILL.md +72 -64
- package/.claude/skills/making-cases/references/normalizing.md +39 -38
- package/.claude/skills/making-cases/references/writes-and-email.md +37 -36
- package/AGENTS.md +169 -154
- package/CLAUDE.md +2 -1
- package/README.md +221 -410
- 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/cases.d.ts +2 -15
- package/dist/lib/cases.js +5 -78
- package/dist/lib/command.d.ts +24 -0
- package/dist/lib/command.js +90 -0
- package/dist/lib/fixtures.js +2 -2
- package/dist/lib/login-card.d.ts +22 -0
- package/dist/lib/login-card.js +183 -0
- package/dist/lib/node-cli.d.ts +6 -2
- package/dist/lib/node-cli.js +16 -6
- package/dist/lib/profiles.d.ts +25 -1
- package/dist/lib/profiles.js +54 -1
- package/dist/lib/run-filters.js +10 -2
- package/dist/lib/session.d.ts +13 -1
- package/dist/lib/session.js +73 -7
- package/dist/lib/targets.d.ts +5 -13
- package/dist/lib/targets.js +4 -29
- package/dist/lib/values.js +2 -2
- package/dist/scripts/build.js +2 -1
- package/dist/scripts/check-cases.js +74 -6
- package/dist/scripts/doctor.js +115 -4
- package/dist/scripts/explore.js +22 -1
- package/dist/scripts/go.mjs +188 -188
- package/dist/scripts/init.d.ts +7 -1
- package/dist/scripts/init.js +62 -14
- package/dist/scripts/lint-specs.js +4 -1
- package/dist/scripts/login.js +30 -2
- package/dist/ui/server/actions.d.ts +4 -0
- package/dist/ui/server/actions.js +25 -12
- 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 +34 -9
- package/dist/ui/server/ai-stream.js +283 -77
- package/dist/ui/server/app.js +74 -12
- package/dist/ui/server/jobs.d.ts +7 -1
- package/dist/ui/server/jobs.js +69 -15
- package/dist/ui/server/profiles.d.ts +20 -1
- package/dist/ui/server/profiles.js +214 -38
- package/dist/ui/shared/ai-commands.d.ts +16 -6
- package/dist/ui/shared/ai-commands.js +35 -12
- package/dist/ui/shared/types.d.ts +114 -0
- package/docs/agents/case-format.md +193 -0
- package/docs/agents/framework-dev.md +167 -0
- package/docs/ai-workflow.md +235 -0
- package/docs/cases.md +189 -0
- package/docs/commands.md +175 -0
- package/docs/install.md +174 -0
- package/docs/profiles.md +139 -0
- package/docs/theme-guide.html +922 -692
- package/docs/troubleshooting.md +101 -0
- package/docs/web-ui.md +139 -0
- package/package.json +20 -1
- package/templates/.claude/commands/explore-case.md +22 -21
- package/templates/.claude/commands/gen-spec.md +21 -20
- package/templates/.claude/commands/heal-spec.md +31 -30
- package/templates/.claude/commands/make-case.md +13 -12
- package/templates/.mcp.json +8 -8
- package/templates/AGENTS.md +66 -45
- package/templates/cases/_template.md +57 -63
- package/templates/opencode.jsonc +16 -11
- package/templates/profiles.yaml +42 -29
- package/ui/dist/assets/{dist-D_I45xgJ.js → dist-BudNAjOv.js} +1 -1
- package/ui/dist/assets/{dist-CBV7_vDN.js → dist-C-eCSFja.js} +1 -1
- package/ui/dist/assets/{dist-gF2bFn-5.js → dist-CO4heSzh.js} +1 -1
- package/ui/dist/assets/{dist-DX8knLVr.js → dist-CPLhRzom.js} +1 -1
- package/ui/dist/assets/{dist-BqAROxzI.js → dist-DEPJZ3Me.js} +1 -1
- package/ui/dist/assets/{dist-BwYRbe5s.js → dist-DZ7RiqmA.js} +1 -1
- package/ui/dist/assets/{dist-BHaIOZdW.js → dist-LixzGniF.js} +1 -1
- package/ui/dist/assets/{dist-DHCY9KpV.js → dist-O_RVi4tP.js} +1 -1
- package/ui/dist/assets/{dist-BiE4suo9.js → dist-jPS6HCJ5.js} +1 -1
- package/ui/dist/assets/{dist-BSJMM_jp.js → dist-vmf3fnkM.js} +1 -1
- package/ui/dist/assets/index-BIGt8Hfz.js +338 -0
- package/ui/dist/assets/index-CTmnF0Wp.css +2 -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 +22 -16
- package/ui/dist/assets/index-DmasQ6Rq.css +0 -2
- package/ui/dist/assets/index-I7uU93kk.js +0 -294
package/dist/scripts/go.mjs
CHANGED
|
@@ -1,188 +1,188 @@
|
|
|
1
|
-
// One-liner for a fresh machine: install everything, prepare the workspace, then start the web UI and open it.
|
|
2
|
-
// Plain Node on purpose: it may run before the dependencies are installed.
|
|
3
|
-
//
|
|
4
|
-
// pnpm add -D @asrunjs/asrun (or github:wizcas/asrun
|
|
5
|
-
// pnpm exec asrun init
|
|
6
|
-
// pnpm go
|
|
7
|
-
import {spawn, spawnSync} from 'node:child_process';
|
|
8
|
-
import fs from 'node:fs';
|
|
9
|
-
import path from 'node:path';
|
|
10
|
-
|
|
11
|
-
const FRAMEWORK = path.resolve(import.meta.dirname, '..');
|
|
12
|
-
const PACKAGE = '@asrunjs/asrun';
|
|
13
|
-
const MARKERS = ['.asrun.json', 'profiles.yaml'];
|
|
14
|
-
const port = Number(process.env.ASRUN_UI_PORT ?? 4173);
|
|
15
|
-
const url = `http://127.0.0.1:${port}`;
|
|
16
|
-
const openBrowser = !process.argv.includes('--no-open');
|
|
17
|
-
const shell = process.platform === 'win32';
|
|
18
|
-
|
|
19
|
-
// `.env` next to the command, or the framework's own `.env`; a real environment variable always wins.
|
|
20
|
-
function loadRootEnv(file) {
|
|
21
|
-
try {
|
|
22
|
-
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
23
|
-
if (process.env.ASRUN_ROOT !== undefined) return;
|
|
24
|
-
const match = /^(?:export\s+)?ASRUN_ROOT\s*=\s*(.*)$/.exec(raw.trim());
|
|
25
|
-
if (match) {
|
|
26
|
-
process.env.ASRUN_ROOT = path.resolve(path.dirname(file), match[1].trim().replace(/^(['"])(.*)\1$/, '$2'));
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
} catch {
|
|
30
|
-
// no .env: nothing to do
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
loadRootEnv(path.join(process.cwd(), '.env'));
|
|
34
|
-
if (process.env.ASRUN_ROOT === undefined) loadRootEnv(path.join(FRAMEWORK, '.env'));
|
|
35
|
-
|
|
36
|
-
// The workspace: the project that owns profiles.yaml, cases/ and tests/. Inside the framework repository
|
|
37
|
-
// (which has none of those) the demo workspace is used, so the framework itself can be developed this way.
|
|
38
|
-
function findWorkspace(from) {
|
|
39
|
-
for (let dir = path.resolve(from); ; dir = path.dirname(dir)) {
|
|
40
|
-
if (MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) return dir;
|
|
41
|
-
if (path.dirname(dir) === dir) return null;
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
const demo = path.join(FRAMEWORK, 'examples', 'demo');
|
|
45
|
-
const ROOT = (process.env.ASRUN_ROOT && path.resolve(process.env.ASRUN_ROOT)) || findWorkspace(process.cwd()) || demo;
|
|
46
|
-
if (ROOT === FRAMEWORK && !fs.existsSync(demo)) {
|
|
47
|
-
console.error('No workspace found: run this command in the project that uses asrun, or set ASRUN_ROOT.');
|
|
48
|
-
process.exit(1);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
const step = (n, text) => console.log(`\n\x1b[1;36m[${n}/4] ${text}\x1b[0m`);
|
|
52
|
-
const warn = (text) => console.log(`\x1b[33m! ${text}\x1b[0m`);
|
|
53
|
-
const fail = (text) => {
|
|
54
|
-
console.error(`\x1b[31m${text}\x1b[0m`);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
};
|
|
57
|
-
const run = (command, args, options = {}) =>
|
|
58
|
-
spawnSync(command, args, {cwd: ROOT, stdio: 'inherit', shell, ...options}).status === 0;
|
|
59
|
-
const exists = (command) => spawnSync(command, ['--version'], {stdio: 'ignore', shell}).status === 0;
|
|
60
|
-
|
|
61
|
-
/** The JavaScript entry point of a package's command-line tool, without relying on node_modules/.bin shims. */
|
|
62
|
-
function packageBin(name, command = name) {
|
|
63
|
-
for (const base of [FRAMEWORK, path.dirname(path.dirname(FRAMEWORK)), ROOT]) {
|
|
64
|
-
const dir = path.join(base, 'node_modules', name);
|
|
65
|
-
const manifest = path.join(dir, 'package.json');
|
|
66
|
-
if (!fs.existsSync(manifest)) continue;
|
|
67
|
-
const bin = JSON.parse(fs.readFileSync(manifest, 'utf8')).bin;
|
|
68
|
-
const entry = typeof bin === 'string' ? bin : bin?.[command];
|
|
69
|
-
if (entry) return path.join(dir, entry);
|
|
70
|
-
}
|
|
71
|
-
return null;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/** The installed command of this workspace, or the framework's own bin while developing it here. */
|
|
75
|
-
const bin = packageBin(PACKAGE) ?? path.join(FRAMEWORK, 'bin', 'asrun.mjs');
|
|
76
|
-
/** The workspace's copy of the package, if any. */
|
|
77
|
-
const installed = fs.existsSync(path.join(ROOT, 'node_modules', PACKAGE, 'package.json'));
|
|
78
|
-
const frameworkRelative = installed
|
|
79
|
-
? './' + path.relative(ROOT, path.join(ROOT, 'node_modules', PACKAGE)).split(path.sep).join('/')
|
|
80
|
-
: (() => {
|
|
81
|
-
const relative = path.relative(ROOT, FRAMEWORK).split(path.sep).join('/');
|
|
82
|
-
return relative === '' ? '.' : relative.startsWith('.') ? relative : `./${relative}`;
|
|
83
|
-
})();
|
|
84
|
-
|
|
85
|
-
/** pnpm only installs dependencies normally; its build scripts need an explicit approval in pnpm-workspace.yaml. */
|
|
86
|
-
function ensurePnpmWorkspace() {
|
|
87
|
-
if (frameworkRelative.startsWith('..')) return 'skipped';
|
|
88
|
-
const file = path.join(ROOT, 'pnpm-workspace.yaml');
|
|
89
|
-
const block = 'allowBuilds:\n esbuild: true\n ffmpeg-static: true\n';
|
|
90
|
-
if (!fs.existsSync(file)) {
|
|
91
|
-
fs.writeFileSync(file, `# asrun's dependencies (esbuild through tsx, ffmpeg-static) compile a binary on install.\n${block}`);
|
|
92
|
-
return 'created';
|
|
93
|
-
}
|
|
94
|
-
const text = fs.readFileSync(file, 'utf8');
|
|
95
|
-
if (text.includes('allowBuilds')) return 'kept';
|
|
96
|
-
fs.writeFileSync(file, `${text.replace(/\n*$/, '\n')}\n${block}`);
|
|
97
|
-
return 'updated';
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
async function uiIsUp() {
|
|
101
|
-
try {
|
|
102
|
-
return (await fetch(`${url}/api/info`, {signal: AbortSignal.timeout(1500)})).ok;
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function open(target) {
|
|
109
|
-
const [command, args] =
|
|
110
|
-
process.platform === 'darwin'
|
|
111
|
-
? ['open', [target]]
|
|
112
|
-
: process.platform === 'win32'
|
|
113
|
-
? ['cmd', ['/c', 'start', '', target]]
|
|
114
|
-
: ['xdg-open', [target]];
|
|
115
|
-
spawn(command, args, {stdio: 'ignore', detached: true}).on('error', () => {}).unref();
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// ffmpeg joins the videos of a test's browser tabs into one. Never fatal: the optional ffmpeg-static package is the fallback.
|
|
119
|
-
function ensureFfmpeg() {
|
|
120
|
-
// ffmpeg does not know `--version`, only `-version`.
|
|
121
|
-
if (spawnSync('ffmpeg', ['-version'], {stdio: 'ignore', shell}).status === 0) return console.log('ffmpeg: ok');
|
|
122
|
-
const installer =
|
|
123
|
-
process.platform === 'darwin' && exists('brew')
|
|
124
|
-
? ['brew', ['install', 'ffmpeg']]
|
|
125
|
-
: process.platform === 'win32' && exists('winget')
|
|
126
|
-
? ['winget', ['install', '--id', 'Gyan.FFmpeg', '-e', '--accept-source-agreements', '--accept-package-agreements']]
|
|
127
|
-
: null;
|
|
128
|
-
if (!installer) {
|
|
129
|
-
return warn('ffmpeg not found; the bundled ffmpeg-static is used instead. To install it yourself: brew install ffmpeg | winget install Gyan.FFmpeg | sudo apt install ffmpeg');
|
|
130
|
-
}
|
|
131
|
-
console.log(`ffmpeg not found, installing it: ${installer[0]} ${installer[1].join(' ')}`);
|
|
132
|
-
if (run(...installer)) console.log('ffmpeg: installed');
|
|
133
|
-
else warn('Could not install ffmpeg; the bundled ffmpeg-static is used instead.');
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
step(1, 'Checking this machine');
|
|
137
|
-
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
138
|
-
if (nodeMajor < 22) fail(`Node 22 or newer is required (this is ${process.versions.node}). Install it from https://nodejs.org and run "pnpm go" again.`);
|
|
139
|
-
console.log(`Node ${process.versions.node}: ok`);
|
|
140
|
-
console.log(`workspace: ${ROOT}`);
|
|
141
|
-
console.log(`framework: ${FRAMEWORK}${installed ? ' (installed)' : ' (development checkout)'}`);
|
|
142
|
-
ensureFfmpeg();
|
|
143
|
-
if (!exists('claude') && !exists('opencode')) {
|
|
144
|
-
warn('Neither "claude" (Claude Code) nor "opencode" found. Everything works except the AI steps (make cases, review, generate specs).');
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
step(2, 'Installing dependencies and the browser (a minute the first time, seconds after that)');
|
|
148
|
-
const workspaceFile = ensurePnpmWorkspace();
|
|
149
|
-
if (workspaceFile === 'created') console.log('pnpm-workspace.yaml: created (allowBuilds)');
|
|
150
|
-
else if (workspaceFile === 'updated') console.log('pnpm-workspace.yaml: updated (allowBuilds)');
|
|
151
|
-
const installRoot = frameworkRelative.startsWith('..') ? FRAMEWORK : ROOT;
|
|
152
|
-
if (!run('pnpm', ['install'], {cwd: installRoot})) fail('Install failed. Fix the error above and run "pnpm go" again.');
|
|
153
|
-
const playwright = packageBin('@playwright/test', 'playwright');
|
|
154
|
-
if (playwright) {
|
|
155
|
-
if (!run(process.execPath, [playwright, 'install', 'chromium'])) warn('Could not install the Chromium browser; run "pnpm exec playwright install chromium".');
|
|
156
|
-
} else {
|
|
157
|
-
warn('@playwright/test is not installed yet. Run this command again after the workspace files are in place.');
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
step(3, 'Preparing the workspace');
|
|
161
|
-
// `--root` names the workspace, but the command runs where the person is: that is where `.env` belongs.
|
|
162
|
-
if (!run(process.execPath, [bin, 'init', '--root', ROOT], {cwd: process.cwd()})) {
|
|
163
|
-
fail('Could not initialize the workspace. Run "pnpm asrun init" and read the message.');
|
|
164
|
-
}
|
|
165
|
-
run(process.execPath, [bin, 'doctor']);
|
|
166
|
-
|
|
167
|
-
step(4, `Starting the web UI at ${url}`);
|
|
168
|
-
if (await uiIsUp()) {
|
|
169
|
-
console.log('The UI is already running.');
|
|
170
|
-
if (openBrowser) open(url);
|
|
171
|
-
process.exit(0);
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const env = {...process.env, ASRUN_ROOT: ROOT};
|
|
175
|
-
const child = spawn(process.execPath, [bin, 'ui'], {cwd: ROOT, stdio: 'inherit', env, shell});
|
|
176
|
-
// Ctrl+C reaches the UI server directly (same terminal); we only wait for it to shut down.
|
|
177
|
-
process.on('SIGINT', () => {});
|
|
178
|
-
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
179
|
-
child.on('exit', (code) => process.exit(code ?? 0));
|
|
180
|
-
|
|
181
|
-
for (let i = 0; i < 240 && child.exitCode === null; i++) {
|
|
182
|
-
if (await uiIsUp()) {
|
|
183
|
-
console.log(`\n\x1b[1;32mReady: ${url}\x1b[0m (Ctrl+C to stop)`);
|
|
184
|
-
if (openBrowser) open(url);
|
|
185
|
-
break;
|
|
186
|
-
}
|
|
187
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
188
|
-
}
|
|
1
|
+
// One-liner for a fresh machine: install everything, prepare the workspace, then start the web UI and open it.
|
|
2
|
+
// Plain Node on purpose: it may run before the dependencies are installed.
|
|
3
|
+
//
|
|
4
|
+
// pnpm add -D @asrunjs/asrun (or github:wizcas/asrun#<tag>)
|
|
5
|
+
// pnpm exec asrun init
|
|
6
|
+
// pnpm go
|
|
7
|
+
import {spawn, spawnSync} from 'node:child_process';
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
const FRAMEWORK = path.resolve(import.meta.dirname, '..');
|
|
12
|
+
const PACKAGE = '@asrunjs/asrun';
|
|
13
|
+
const MARKERS = ['.asrun.json', 'profiles.yaml'];
|
|
14
|
+
const port = Number(process.env.ASRUN_UI_PORT ?? 4173);
|
|
15
|
+
const url = `http://127.0.0.1:${port}`;
|
|
16
|
+
const openBrowser = !process.argv.includes('--no-open');
|
|
17
|
+
const shell = process.platform === 'win32';
|
|
18
|
+
|
|
19
|
+
// `.env` next to the command, or the framework's own `.env`; a real environment variable always wins.
|
|
20
|
+
function loadRootEnv(file) {
|
|
21
|
+
try {
|
|
22
|
+
for (const raw of fs.readFileSync(file, 'utf8').split(/\r?\n/)) {
|
|
23
|
+
if (process.env.ASRUN_ROOT !== undefined) return;
|
|
24
|
+
const match = /^(?:export\s+)?ASRUN_ROOT\s*=\s*(.*)$/.exec(raw.trim());
|
|
25
|
+
if (match) {
|
|
26
|
+
process.env.ASRUN_ROOT = path.resolve(path.dirname(file), match[1].trim().replace(/^(['"])(.*)\1$/, '$2'));
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
} catch {
|
|
30
|
+
// no .env: nothing to do
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
loadRootEnv(path.join(process.cwd(), '.env'));
|
|
34
|
+
if (process.env.ASRUN_ROOT === undefined) loadRootEnv(path.join(FRAMEWORK, '.env'));
|
|
35
|
+
|
|
36
|
+
// The workspace: the project that owns profiles.yaml, cases/ and tests/. Inside the framework repository
|
|
37
|
+
// (which has none of those) the demo workspace is used, so the framework itself can be developed this way.
|
|
38
|
+
function findWorkspace(from) {
|
|
39
|
+
for (let dir = path.resolve(from); ; dir = path.dirname(dir)) {
|
|
40
|
+
if (MARKERS.some((marker) => fs.existsSync(path.join(dir, marker)))) return dir;
|
|
41
|
+
if (path.dirname(dir) === dir) return null;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
const demo = path.join(FRAMEWORK, 'examples', 'demo');
|
|
45
|
+
const ROOT = (process.env.ASRUN_ROOT && path.resolve(process.env.ASRUN_ROOT)) || findWorkspace(process.cwd()) || demo;
|
|
46
|
+
if (ROOT === FRAMEWORK && !fs.existsSync(demo)) {
|
|
47
|
+
console.error('No workspace found: run this command in the project that uses asrun, or set ASRUN_ROOT.');
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const step = (n, text) => console.log(`\n\x1b[1;36m[${n}/4] ${text}\x1b[0m`);
|
|
52
|
+
const warn = (text) => console.log(`\x1b[33m! ${text}\x1b[0m`);
|
|
53
|
+
const fail = (text) => {
|
|
54
|
+
console.error(`\x1b[31m${text}\x1b[0m`);
|
|
55
|
+
process.exit(1);
|
|
56
|
+
};
|
|
57
|
+
const run = (command, args, options = {}) =>
|
|
58
|
+
spawnSync(command, args, {cwd: ROOT, stdio: 'inherit', shell, ...options}).status === 0;
|
|
59
|
+
const exists = (command) => spawnSync(command, ['--version'], {stdio: 'ignore', shell}).status === 0;
|
|
60
|
+
|
|
61
|
+
/** The JavaScript entry point of a package's command-line tool, without relying on node_modules/.bin shims. */
|
|
62
|
+
function packageBin(name, command = name) {
|
|
63
|
+
for (const base of [FRAMEWORK, path.dirname(path.dirname(FRAMEWORK)), ROOT]) {
|
|
64
|
+
const dir = path.join(base, 'node_modules', name);
|
|
65
|
+
const manifest = path.join(dir, 'package.json');
|
|
66
|
+
if (!fs.existsSync(manifest)) continue;
|
|
67
|
+
const bin = JSON.parse(fs.readFileSync(manifest, 'utf8')).bin;
|
|
68
|
+
const entry = typeof bin === 'string' ? bin : bin?.[command];
|
|
69
|
+
if (entry) return path.join(dir, entry);
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** The installed command of this workspace, or the framework's own bin while developing it here. */
|
|
75
|
+
const bin = packageBin(PACKAGE) ?? path.join(FRAMEWORK, 'bin', 'asrun.mjs');
|
|
76
|
+
/** The workspace's copy of the package, if any. */
|
|
77
|
+
const installed = fs.existsSync(path.join(ROOT, 'node_modules', PACKAGE, 'package.json'));
|
|
78
|
+
const frameworkRelative = installed
|
|
79
|
+
? './' + path.relative(ROOT, path.join(ROOT, 'node_modules', PACKAGE)).split(path.sep).join('/')
|
|
80
|
+
: (() => {
|
|
81
|
+
const relative = path.relative(ROOT, FRAMEWORK).split(path.sep).join('/');
|
|
82
|
+
return relative === '' ? '.' : relative.startsWith('.') ? relative : `./${relative}`;
|
|
83
|
+
})();
|
|
84
|
+
|
|
85
|
+
/** pnpm only installs dependencies normally; its build scripts need an explicit approval in pnpm-workspace.yaml. */
|
|
86
|
+
function ensurePnpmWorkspace() {
|
|
87
|
+
if (frameworkRelative.startsWith('..')) return 'skipped';
|
|
88
|
+
const file = path.join(ROOT, 'pnpm-workspace.yaml');
|
|
89
|
+
const block = 'allowBuilds:\n esbuild: true\n ffmpeg-static: true\n';
|
|
90
|
+
if (!fs.existsSync(file)) {
|
|
91
|
+
fs.writeFileSync(file, `# asrun's dependencies (esbuild through tsx, ffmpeg-static) compile a binary on install.\n${block}`);
|
|
92
|
+
return 'created';
|
|
93
|
+
}
|
|
94
|
+
const text = fs.readFileSync(file, 'utf8');
|
|
95
|
+
if (text.includes('allowBuilds')) return 'kept';
|
|
96
|
+
fs.writeFileSync(file, `${text.replace(/\n*$/, '\n')}\n${block}`);
|
|
97
|
+
return 'updated';
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function uiIsUp() {
|
|
101
|
+
try {
|
|
102
|
+
return (await fetch(`${url}/api/info`, {signal: AbortSignal.timeout(1500)})).ok;
|
|
103
|
+
} catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function open(target) {
|
|
109
|
+
const [command, args] =
|
|
110
|
+
process.platform === 'darwin'
|
|
111
|
+
? ['open', [target]]
|
|
112
|
+
: process.platform === 'win32'
|
|
113
|
+
? ['cmd', ['/c', 'start', '', target]]
|
|
114
|
+
: ['xdg-open', [target]];
|
|
115
|
+
spawn(command, args, {stdio: 'ignore', detached: true}).on('error', () => {}).unref();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ffmpeg joins the videos of a test's browser tabs into one. Never fatal: the optional ffmpeg-static package is the fallback.
|
|
119
|
+
function ensureFfmpeg() {
|
|
120
|
+
// ffmpeg does not know `--version`, only `-version`.
|
|
121
|
+
if (spawnSync('ffmpeg', ['-version'], {stdio: 'ignore', shell}).status === 0) return console.log('ffmpeg: ok');
|
|
122
|
+
const installer =
|
|
123
|
+
process.platform === 'darwin' && exists('brew')
|
|
124
|
+
? ['brew', ['install', 'ffmpeg']]
|
|
125
|
+
: process.platform === 'win32' && exists('winget')
|
|
126
|
+
? ['winget', ['install', '--id', 'Gyan.FFmpeg', '-e', '--accept-source-agreements', '--accept-package-agreements']]
|
|
127
|
+
: null;
|
|
128
|
+
if (!installer) {
|
|
129
|
+
return warn('ffmpeg not found; the bundled ffmpeg-static is used instead. To install it yourself: brew install ffmpeg | winget install Gyan.FFmpeg | sudo apt install ffmpeg');
|
|
130
|
+
}
|
|
131
|
+
console.log(`ffmpeg not found, installing it: ${installer[0]} ${installer[1].join(' ')}`);
|
|
132
|
+
if (run(...installer)) console.log('ffmpeg: installed');
|
|
133
|
+
else warn('Could not install ffmpeg; the bundled ffmpeg-static is used instead.');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
step(1, 'Checking this machine');
|
|
137
|
+
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
138
|
+
if (nodeMajor < 22) fail(`Node 22 or newer is required (this is ${process.versions.node}). Install it from https://nodejs.org and run "pnpm go" again.`);
|
|
139
|
+
console.log(`Node ${process.versions.node}: ok`);
|
|
140
|
+
console.log(`workspace: ${ROOT}`);
|
|
141
|
+
console.log(`framework: ${FRAMEWORK}${installed ? ' (installed)' : ' (development checkout)'}`);
|
|
142
|
+
ensureFfmpeg();
|
|
143
|
+
if (!exists('claude') && !exists('opencode')) {
|
|
144
|
+
warn('Neither "claude" (Claude Code) nor "opencode" found. Everything works except the AI steps (make cases, review, generate specs).');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
step(2, 'Installing dependencies and the browser (a minute the first time, seconds after that)');
|
|
148
|
+
const workspaceFile = ensurePnpmWorkspace();
|
|
149
|
+
if (workspaceFile === 'created') console.log('pnpm-workspace.yaml: created (allowBuilds)');
|
|
150
|
+
else if (workspaceFile === 'updated') console.log('pnpm-workspace.yaml: updated (allowBuilds)');
|
|
151
|
+
const installRoot = frameworkRelative.startsWith('..') ? FRAMEWORK : ROOT;
|
|
152
|
+
if (!run('pnpm', ['install'], {cwd: installRoot})) fail('Install failed. Fix the error above and run "pnpm go" again.');
|
|
153
|
+
const playwright = packageBin('@playwright/test', 'playwright');
|
|
154
|
+
if (playwright) {
|
|
155
|
+
if (!run(process.execPath, [playwright, 'install', 'chromium'])) warn('Could not install the Chromium browser; run "pnpm exec playwright install chromium".');
|
|
156
|
+
} else {
|
|
157
|
+
warn('@playwright/test is not installed yet. Run this command again after the workspace files are in place.');
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
step(3, 'Preparing the workspace');
|
|
161
|
+
// `--root` names the workspace, but the command runs where the person is: that is where `.env` belongs.
|
|
162
|
+
if (!run(process.execPath, [bin, 'init', '--root', ROOT], {cwd: process.cwd()})) {
|
|
163
|
+
fail('Could not initialize the workspace. Run "pnpm asrun init" and read the message.');
|
|
164
|
+
}
|
|
165
|
+
run(process.execPath, [bin, 'doctor']);
|
|
166
|
+
|
|
167
|
+
step(4, `Starting the web UI at ${url}`);
|
|
168
|
+
if (await uiIsUp()) {
|
|
169
|
+
console.log('The UI is already running.');
|
|
170
|
+
if (openBrowser) open(url);
|
|
171
|
+
process.exit(0);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
const env = {...process.env, ASRUN_ROOT: ROOT};
|
|
175
|
+
const child = spawn(process.execPath, [bin, 'ui'], {cwd: ROOT, stdio: 'inherit', env, shell});
|
|
176
|
+
// Ctrl+C reaches the UI server directly (same terminal); we only wait for it to shut down.
|
|
177
|
+
process.on('SIGINT', () => {});
|
|
178
|
+
process.on('SIGTERM', () => child.kill('SIGTERM'));
|
|
179
|
+
child.on('exit', (code) => process.exit(code ?? 0));
|
|
180
|
+
|
|
181
|
+
for (let i = 0; i < 240 && child.exitCode === null; i++) {
|
|
182
|
+
if (await uiIsUp()) {
|
|
183
|
+
console.log(`\n\x1b[1;32mReady: ${url}\x1b[0m (Ctrl+C to stop)`);
|
|
184
|
+
if (openBrowser) open(url);
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
188
|
+
}
|
package/dist/scripts/init.d.ts
CHANGED
|
@@ -7,8 +7,14 @@ export declare const GENERATED: {
|
|
|
7
7
|
export declare function frameworkPath(root: string): string;
|
|
8
8
|
/** True when this workspace has the package installed (or is the framework's own checkout). */
|
|
9
9
|
export declare function isInstalled(root: string): boolean;
|
|
10
|
-
/** Fills the `{{framework}}`
|
|
10
|
+
/** Fills the `{{framework}}` and `{{playwright}}` placeholders of a template. */
|
|
11
11
|
export declare function renderTemplate(name: string, root: string): string;
|
|
12
|
+
/**
|
|
13
|
+
* The Playwright CLI the MCP server runs. The workspace's own install is the normal place; a workspace that lives
|
|
14
|
+
* inside the framework repository (examples/demo) has no `node_modules` of its own, so it points at the framework's.
|
|
15
|
+
* Kept relative: the file is committed and must not name a machine.
|
|
16
|
+
*/
|
|
17
|
+
export declare function playwrightBin(root: string): string;
|
|
12
18
|
export declare function frameworkVersion(): {
|
|
13
19
|
version: string;
|
|
14
20
|
commit: string;
|
package/dist/scripts/init.js
CHANGED
|
@@ -6,13 +6,15 @@ import { FRAMEWORK_ROOT, TEMPLATES_DIR } from '../lib/paths.js';
|
|
|
6
6
|
* Creates the files a workspace needs. It is safe to run many times: it never overwrites a file the project owns, and
|
|
7
7
|
* `--update` refreshes only the files generated from the framework templates.
|
|
8
8
|
*/
|
|
9
|
-
const USAGE = `Usage: pnpm asrun init [--root <dir>] [--update] [--force <path>] [--no-env] [--install [spec]]
|
|
10
|
-
|
|
11
|
-
--root <dir> the workspace to initialize (default: the current folder)
|
|
12
|
-
--update refresh the generated files (playwright.config.ts, tests/seed.spec.ts, .claude/commands
|
|
13
|
-
|
|
14
|
-
--
|
|
15
|
-
--
|
|
9
|
+
const USAGE = `Usage: pnpm asrun init [--root <dir>] [--update] [--force <path>] [--no-env] [--install [spec]]
|
|
10
|
+
|
|
11
|
+
--root <dir> the workspace to initialize (default: the current folder)
|
|
12
|
+
--update refresh the generated files (playwright.config.ts, tests/seed.spec.ts, .claude/commands/*,
|
|
13
|
+
.opencode/commands/*)
|
|
14
|
+
--force <path> overwrite one file this tool writes, e.g. --force playwright.config.ts (may be repeated)
|
|
15
|
+
--reset-rules allow --force on AGENTS.md, CLAUDE.md or .claude/skills/** (project-owned rules)
|
|
16
|
+
--no-env do not write .env (ASRUN_ROOT)
|
|
17
|
+
--install [spec] also add the dependency (default "asrun"; e.g. --install github:<owner>/<repo>#<tag>)`;
|
|
16
18
|
/** Files `init` owns: `--update` rewrites exactly these, and a project never edits them. */
|
|
17
19
|
export const GENERATED = [
|
|
18
20
|
{ file: 'playwright.config.ts', template: 'playwright.config.ts' },
|
|
@@ -21,12 +23,18 @@ export const GENERATED = [
|
|
|
21
23
|
{ file: '.claude/commands/explore-case.md', template: '.claude/commands/explore-case.md' },
|
|
22
24
|
{ file: '.claude/commands/gen-spec.md', template: '.claude/commands/gen-spec.md' },
|
|
23
25
|
{ file: '.claude/commands/heal-spec.md', template: '.claude/commands/heal-spec.md' },
|
|
26
|
+
// OpenCode does not read `.claude/commands` (only `.claude/skills`), so the same commands live here too.
|
|
27
|
+
{ file: '.opencode/commands/make-case.md', template: '.claude/commands/make-case.md' },
|
|
28
|
+
{ file: '.opencode/commands/explore-case.md', template: '.claude/commands/explore-case.md' },
|
|
29
|
+
{ file: '.opencode/commands/gen-spec.md', template: '.claude/commands/gen-spec.md' },
|
|
30
|
+
{ file: '.opencode/commands/heal-spec.md', template: '.claude/commands/heal-spec.md' },
|
|
24
31
|
];
|
|
25
32
|
/** Files `init` creates once; after that the project owns them and may edit them freely. */
|
|
26
33
|
const ONCE = [
|
|
27
34
|
{ file: 'AGENTS.md', template: 'AGENTS.md' },
|
|
28
35
|
{ file: 'CLAUDE.md', template: 'CLAUDE.md' },
|
|
29
36
|
{ file: '.mcp.json', template: '.mcp.json' },
|
|
37
|
+
{ file: 'opencode.jsonc', template: 'opencode.jsonc' },
|
|
30
38
|
{ file: '.claude/settings.json', template: '.claude/settings.json' },
|
|
31
39
|
{ file: 'tsconfig.json', template: 'tsconfig.json' },
|
|
32
40
|
{ file: 'pnpm-workspace.yaml', template: 'pnpm-workspace.yaml' },
|
|
@@ -71,9 +79,22 @@ export function frameworkPath(root) {
|
|
|
71
79
|
export function isInstalled(root) {
|
|
72
80
|
return fs.existsSync(path.join(path.resolve(root, frameworkPath(root)), 'package.json'));
|
|
73
81
|
}
|
|
74
|
-
/** Fills the `{{framework}}`
|
|
82
|
+
/** Fills the `{{framework}}` and `{{playwright}}` placeholders of a template. */
|
|
75
83
|
export function renderTemplate(name, root) {
|
|
76
|
-
return readTemplate(name)
|
|
84
|
+
return readTemplate(name)
|
|
85
|
+
.replaceAll('{{framework}}', frameworkPath(root))
|
|
86
|
+
.replaceAll('{{playwright}}', playwrightBin(root));
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* The Playwright CLI the MCP server runs. The workspace's own install is the normal place; a workspace that lives
|
|
90
|
+
* inside the framework repository (examples/demo) has no `node_modules` of its own, so it points at the framework's.
|
|
91
|
+
* Kept relative: the file is committed and must not name a machine.
|
|
92
|
+
*/
|
|
93
|
+
export function playwrightBin(root) {
|
|
94
|
+
if (fs.existsSync(path.join(root, 'node_modules', '.bin', 'playwright')))
|
|
95
|
+
return './node_modules/.bin/playwright';
|
|
96
|
+
const framework = frameworkPath(root);
|
|
97
|
+
return framework.startsWith('..') ? `${framework}/node_modules/.bin/playwright` : './node_modules/.bin/playwright';
|
|
77
98
|
}
|
|
78
99
|
export function frameworkVersion() {
|
|
79
100
|
let version = '0.0.0';
|
|
@@ -183,7 +204,10 @@ export async function main(args) {
|
|
|
183
204
|
const installAt = args.indexOf('--install');
|
|
184
205
|
const install = installAt >= 0;
|
|
185
206
|
const installSpec = install && args[installAt + 1] && !args[installAt + 1].startsWith('-') ? args[installAt + 1] : PACKAGE;
|
|
186
|
-
const forced = args
|
|
207
|
+
const forced = args
|
|
208
|
+
.flatMap((arg, index) => (arg === '--force' && args[index + 1] ? [args[index + 1]] : []))
|
|
209
|
+
.map((file) => file.split('\\').join('/'));
|
|
210
|
+
const resetRules = args.includes('--reset-rules');
|
|
187
211
|
const skipEnv = args.includes('--no-env');
|
|
188
212
|
if (path.resolve(root) === path.resolve(FRAMEWORK_ROOT)) {
|
|
189
213
|
console.error('This folder is the framework itself, not a workspace. Run "pnpm asrun init" inside the project that uses ' +
|
|
@@ -195,6 +219,20 @@ export async function main(args) {
|
|
|
195
219
|
const kept = [];
|
|
196
220
|
const notes = [];
|
|
197
221
|
const framework = frameworkPath(root);
|
|
222
|
+
// `--force` repairs a file this tool writes. A project's own rules are never overwritten by a framework update:
|
|
223
|
+
// `AGENTS.md`, `CLAUDE.md` and `.claude/skills/**` need `--reset-rules` on top.
|
|
224
|
+
const owned = [...GENERATED, ...ONCE].map((entry) => entry.file);
|
|
225
|
+
const projectRules = (file) => file === 'AGENTS.md' || file === 'CLAUDE.md' || file === '.claude/skills' || file.startsWith('.claude/skills/');
|
|
226
|
+
for (const file of forced) {
|
|
227
|
+
if (!owned.includes(file)) {
|
|
228
|
+
console.error(`--force ${file}: init only writes the files it owns (${owned.join(', ')}).`);
|
|
229
|
+
return 1;
|
|
230
|
+
}
|
|
231
|
+
if (projectRules(file) && !resetRules) {
|
|
232
|
+
console.error(`--force ${file}: this file belongs to the project. Add --reset-rules to replace it with the template.`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
198
236
|
const place = (entry, overwrite) => {
|
|
199
237
|
const target = path.join(root, entry.file);
|
|
200
238
|
if (fs.existsSync(target) && !overwrite) {
|
|
@@ -231,10 +269,20 @@ export async function main(args) {
|
|
|
231
269
|
notes.push('pnpm-workspace.yaml lists "packages"; asrun is a dependency now, so that list can go.');
|
|
232
270
|
}
|
|
233
271
|
}
|
|
234
|
-
if (fs.existsSync(path.join(root, '.mcp.json'))) {
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
272
|
+
if (fs.existsSync(path.join(root, '.mcp.json')) && !fs.readFileSync(path.join(root, '.mcp.json'), 'utf8').includes('playwright-test')) {
|
|
273
|
+
notes.push('Add the "playwright-test" MCP server to .mcp.json (see templates/.mcp.json).');
|
|
274
|
+
}
|
|
275
|
+
if (fs.existsSync(path.join(root, 'opencode.jsonc')) && !fs.readFileSync(path.join(root, 'opencode.jsonc'), 'utf8').includes('playwright-test')) {
|
|
276
|
+
notes.push('Add the "playwright-test" MCP server to opencode.jsonc (see templates/opencode.jsonc).');
|
|
277
|
+
}
|
|
278
|
+
if (fs.existsSync(path.join(root, 'opencode.jsonc'))) {
|
|
279
|
+
const text = fs.readFileSync(path.join(root, 'opencode.jsonc'), 'utf8');
|
|
280
|
+
// A commented-out "model" (the template has one) does not set anything: only a real key counts.
|
|
281
|
+
const withoutComments = text.replace(/^\s*\/\/.*$/gm, '');
|
|
282
|
+
if (!/["']?model["']?\s*:/.test(withoutComments)) {
|
|
283
|
+
notes.push('opencode.jsonc sets no "model": a one-shot "opencode run" then uses OpenCode\'s built-in free model, which has a ' +
|
|
284
|
+
'daily usage limit. Pick a model in the AsRun web UI (top bar), or set "model" there.');
|
|
285
|
+
}
|
|
238
286
|
}
|
|
239
287
|
if (fs.existsSync(path.join(root, '.claude', 'settings.json'))) {
|
|
240
288
|
const settings = fs.readFileSync(path.join(root, '.claude', 'settings.json'), 'utf8');
|
|
@@ -128,8 +128,11 @@ function lint(file) {
|
|
|
128
128
|
TARGET_REF.lastIndex = 0;
|
|
129
129
|
return findings;
|
|
130
130
|
}
|
|
131
|
+
// A case id is written with `/` (`conduit/header-navigation`), while a path on Windows has `\`: without the
|
|
132
|
+
// normalization this filter matched nothing and reported "0 spec(s)" for a spec that was right there.
|
|
133
|
+
const casePath = (file) => file.replace(/\\/g, '/');
|
|
131
134
|
const wanted = process.argv.slice(2).filter((arg) => !arg.startsWith('-'));
|
|
132
|
-
const files = specFiles(TESTS_DIR).filter((file) => wanted.length === 0 || wanted.some((id) => file.endsWith(`${id}.spec.ts`)));
|
|
135
|
+
const files = specFiles(TESTS_DIR).filter((file) => wanted.length === 0 || wanted.some((id) => casePath(file).endsWith(`${casePath(id)}.spec.ts`)));
|
|
133
136
|
const findings = files.flatMap(lint);
|
|
134
137
|
for (const f of findings)
|
|
135
138
|
console.log(`${f.level.toUpperCase().padEnd(4)} ${f.file}:${f.line} [${f.rule}] ${f.message}`);
|
package/dist/scripts/login.js
CHANGED
|
@@ -6,6 +6,7 @@ import { getProfile, listProfiles, profileLabel } from '../lib/profiles.js';
|
|
|
6
6
|
import { parseCliTarget } from '../lib/cli.js';
|
|
7
7
|
import { authFile } from '../lib/paths.js';
|
|
8
8
|
import { checkSession } from '../lib/session.js';
|
|
9
|
+
import { showLoginCard, showLoginCardSaved, waitForLoginCard } from '../lib/login-card.js';
|
|
9
10
|
const parsed = parseCliTarget(process.argv.slice(2));
|
|
10
11
|
if (!parsed.profileId || parsed.help) {
|
|
11
12
|
console.log('Usage: pnpm signin <profile-id>\n\nProfiles:');
|
|
@@ -23,14 +24,41 @@ const file = authFile(profile.id);
|
|
|
23
24
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
24
25
|
const browser = await chromium.launch({ headless: false });
|
|
25
26
|
const context = await browser.newContext({ viewport: null });
|
|
27
|
+
// The card goes in before the first page, so it is there on the login page and again after every navigation.
|
|
28
|
+
showLoginCard(context, `Log in as usual, including the 2FA code. When you can see the logged-in page, press "Done, save the session": ` +
|
|
29
|
+
`the session is saved and this window closes.`);
|
|
26
30
|
const page = await context.newPage();
|
|
27
31
|
await page.goto(new URL('/login/', profile.baseURL).href);
|
|
28
32
|
console.log(`\nA browser window opened for "${profileLabel(profile)}" (${profile.baseURL}).`);
|
|
29
33
|
console.log('1. Log in as usual, including the 2FA code.');
|
|
30
|
-
console.log('2.
|
|
34
|
+
console.log('2. When you can see the logged-in page, press "Done, save the session" on the card in that window.');
|
|
35
|
+
console.log(' The card sits in the bottom-left corner; drag it if it is in the way.');
|
|
31
36
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
32
|
-
|
|
37
|
+
const stopWaiting = new AbortController();
|
|
38
|
+
const card = waitForLoginCard(context, page, stopWaiting.signal);
|
|
39
|
+
const typed = rl
|
|
40
|
+
.question(' Or press Enter here (in the terminal, or the button on the job in the UI): ')
|
|
41
|
+
.then(() => 'enter');
|
|
42
|
+
const answer = await Promise.race([
|
|
43
|
+
card.then((outcome) => ({ via: 'card', outcome })),
|
|
44
|
+
typed.then(() => ({ via: 'enter', outcome: 'done' })),
|
|
45
|
+
]);
|
|
46
|
+
stopWaiting.abort();
|
|
33
47
|
rl.close();
|
|
48
|
+
if (answer.via === 'card' && answer.outcome !== 'done') {
|
|
49
|
+
await browser.close().catch(() => { });
|
|
50
|
+
console.error(answer.outcome === 'cancel'
|
|
51
|
+
? `\nNothing was saved: the login was cancelled on the card. Run \`pnpm signin ${profile.id}\` again when you are ready.`
|
|
52
|
+
: '\nNothing was saved: the browser window was closed before the session was saved. ' +
|
|
53
|
+
`Run \`pnpm signin ${profile.id}\` again.`);
|
|
54
|
+
process.exit(1);
|
|
55
|
+
}
|
|
56
|
+
if (answer.via === 'card') {
|
|
57
|
+
console.log('\nSaving the session from the card...');
|
|
58
|
+
// The person is still looking at the window: say that the session is safe before it disappears.
|
|
59
|
+
await showLoginCardSaved(context, 'Session saved. Closing this window...');
|
|
60
|
+
await new Promise((resolve) => setTimeout(resolve, 1200));
|
|
61
|
+
}
|
|
34
62
|
await context.storageState({ path: file });
|
|
35
63
|
await browser.close();
|
|
36
64
|
fs.chmodSync(file, 0o600);
|
|
@@ -24,6 +24,8 @@ export declare const startJobSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
24
24
|
env: z.ZodString;
|
|
25
25
|
force: z.ZodOptional<z.ZodBoolean>;
|
|
26
26
|
attended: z.ZodOptional<z.ZodBoolean>;
|
|
27
|
+
thinking: z.ZodOptional<z.ZodBoolean>;
|
|
28
|
+
model: z.ZodOptional<z.ZodString>;
|
|
27
29
|
profiles: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
28
30
|
hints: z.ZodOptional<z.ZodString>;
|
|
29
31
|
report: z.ZodOptional<z.ZodString>;
|
|
@@ -38,6 +40,8 @@ export declare const startJobSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
|
38
40
|
opencode: "opencode";
|
|
39
41
|
}>;
|
|
40
42
|
memo: z.ZodString;
|
|
43
|
+
model: z.ZodOptional<z.ZodString>;
|
|
44
|
+
thinking: z.ZodOptional<z.ZodBoolean>;
|
|
41
45
|
}, z.core.$strip>, z.ZodObject<{
|
|
42
46
|
action: z.ZodLiteral<"run">;
|
|
43
47
|
env: z.ZodString;
|