@aloud/runner 0.3.0 → 0.3.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/README.md +15 -2
- package/dist/cli.js +367 -42
- package/package.json +2 -2
- package/src/cli.ts +371 -31
- package/src/protocol/presence.ts +83 -0
- package/src/version.ts +56 -1
package/README.md
CHANGED
|
@@ -9,7 +9,7 @@ outbound only: nothing listens on a port, and the server holds no address for th
|
|
|
9
9
|
```sh
|
|
10
10
|
npm install -g @aloud/runner
|
|
11
11
|
|
|
12
|
-
aloud login #
|
|
12
|
+
aloud login # approve this machine in your browser
|
|
13
13
|
aloud start # waits for studies and runs them here
|
|
14
14
|
```
|
|
15
15
|
|
|
@@ -19,12 +19,25 @@ The first `aloud start` downloads Chromium, about 350 MB, once.
|
|
|
19
19
|
|
|
20
20
|
| Command | What it does |
|
|
21
21
|
| --- | --- |
|
|
22
|
+
| `aloud setup` | Inspect this machine and complete whatever setup remains |
|
|
22
23
|
| `aloud login [--token <token>]` | Connect this machine to your workspace |
|
|
23
|
-
| `aloud start [--once] [--quiet]` |
|
|
24
|
+
| `aloud start [--once] [--quiet] [--no-update]` | Update, then wait for studies and run them here |
|
|
24
25
|
| `aloud status` | What is set up, what is not, and whether it is running |
|
|
25
26
|
| `aloud allow <host>` | Let studies open this host from this machine |
|
|
26
27
|
| `aloud mcp` | Connect an MCP host to the web workspace selected by its MCP access token |
|
|
27
28
|
| `aloud logout` | Forget the token on this machine |
|
|
29
|
+
| `aloud --version` | Print the installed runner version |
|
|
30
|
+
|
|
31
|
+
## Updates
|
|
32
|
+
|
|
33
|
+
Starting with 0.3.1, `aloud start` asks the connected Aloud server which official runner release
|
|
34
|
+
it recommends. If a newer one is available, it installs that exact `@aloud/runner` version and
|
|
35
|
+
relaunches it before claiming work. It never replaces code during an active study.
|
|
36
|
+
|
|
37
|
+
If an optional update cannot be installed, the terminal says why and a still-compatible runner may
|
|
38
|
+
continue. If the server requires the update, nothing starts and the command exits non-zero with the
|
|
39
|
+
manual repair command. `--no-update` is available for managed environments, but it cannot make an
|
|
40
|
+
incompatible runner claim work.
|
|
28
41
|
|
|
29
42
|
## MCP access
|
|
30
43
|
|
package/dist/cli.js
CHANGED
|
@@ -2286,7 +2286,7 @@ var init_registry = __esm({
|
|
|
2286
2286
|
PinnedPromptVersions = {
|
|
2287
2287
|
product_discovery: "1.0.0",
|
|
2288
2288
|
study_design: "1.0.0",
|
|
2289
|
-
persona_derivation: "2.
|
|
2289
|
+
persona_derivation: "2.2.0",
|
|
2290
2290
|
success_criteria: "2.0.0",
|
|
2291
2291
|
participant_decision: "2.2.0",
|
|
2292
2292
|
participant_reaction: "1.2.0",
|
|
@@ -2685,6 +2685,11 @@ What the site appears to be, from its public pages:
|
|
|
2685
2685
|
The flow being tested: {{targetFlow}}
|
|
2686
2686
|
The goal each participant will be given: {{goal}}
|
|
2687
2687
|
|
|
2688
|
+
The device for this study is fixed by the operator: {{deviceDescription}} ({{device}}).
|
|
2689
|
+
Every person will use that device for this study. Do not put anyone on another device in their role,
|
|
2690
|
+
jobToBeDone, background, attentionBias, derivedFrom, rationale, or any other prose. Device is not a
|
|
2691
|
+
persona choice and must not appear as a field in your reply.
|
|
2692
|
+
|
|
2688
2693
|
Audience segments relevant to this particular goal:
|
|
2689
2694
|
{{relevantAudiences}}
|
|
2690
2695
|
|
|
@@ -2707,7 +2712,6 @@ For each person give:
|
|
|
2707
2712
|
- "familiarityWithProduct", "familiarityWithDomain", "technicalConfidence", "patience", "urgency":
|
|
2708
2713
|
each one of very_low, low, moderate, high, very_high
|
|
2709
2714
|
- "emotionalStance": curious, neutral, sceptical, hurried, anxious, or enthusiastic
|
|
2710
|
-
- "device": desktop or mobile_web
|
|
2711
2715
|
- "background": two sentences of context that would change their behaviour
|
|
2712
2716
|
- "attentionBias": what this person notices or cares about that the others would not
|
|
2713
2717
|
- "completionOriented": true for exactly the one completion-oriented person, false for everyone else
|
|
@@ -2724,9 +2728,9 @@ Return JSON: {"personas": [ ... ]}
|
|
|
2724
2728
|
`.trim();
|
|
2725
2729
|
personaDerivationPrompt = definePrompt(
|
|
2726
2730
|
"persona_derivation",
|
|
2727
|
-
"2.
|
|
2731
|
+
"2.2.0",
|
|
2728
2732
|
personaSource,
|
|
2729
|
-
(vars) => personaSource.replace("{{productSignals}}", s(vars.productSignals)).replace("{{targetFlow}}", s(vars.targetFlow)).replace("{{goal}}", s(vars.goal)).replace("{{relevantAudiences}}", s(vars.relevantAudiences) || "The intended audience is not yet known.").replace("{{count}}", s(vars.count))
|
|
2733
|
+
(vars) => personaSource.replace("{{productSignals}}", s(vars.productSignals)).replace("{{targetFlow}}", s(vars.targetFlow)).replace("{{goal}}", s(vars.goal)).replace("{{deviceDescription}}", s(vars.deviceDescription)).replace("{{device}}", s(vars.device)).replace("{{relevantAudiences}}", s(vars.relevantAudiences) || "The intended audience is not yet known.").replace("{{count}}", s(vars.count))
|
|
2730
2734
|
);
|
|
2731
2735
|
criteriaSource = `
|
|
2732
2736
|
A team wants to test this goal on their website: {{goal}}
|
|
@@ -3148,6 +3152,83 @@ async function waitForApproval(start2, deps) {
|
|
|
3148
3152
|
}
|
|
3149
3153
|
}
|
|
3150
3154
|
|
|
3155
|
+
// src/version.ts
|
|
3156
|
+
var RUNNER_VERSION = "0.3.2";
|
|
3157
|
+
var RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|
|
3158
|
+
function versionParts(value) {
|
|
3159
|
+
const match = /^(\d+)\.(\d+)\.(\d+)$/.exec(value);
|
|
3160
|
+
if (!match) return null;
|
|
3161
|
+
const parts = [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
3162
|
+
return parts.every(Number.isSafeInteger) ? parts : null;
|
|
3163
|
+
}
|
|
3164
|
+
function compareRunnerVersions(left, right) {
|
|
3165
|
+
const a = versionParts(left);
|
|
3166
|
+
const b = versionParts(right);
|
|
3167
|
+
if (!a || !b) return null;
|
|
3168
|
+
for (let index = 0; index < a.length; index += 1) {
|
|
3169
|
+
if (a[index] > b[index]) return 1;
|
|
3170
|
+
if (a[index] < b[index]) return -1;
|
|
3171
|
+
}
|
|
3172
|
+
return 0;
|
|
3173
|
+
}
|
|
3174
|
+
function runnerVersionPolicyFrom(value) {
|
|
3175
|
+
if (!value || typeof value !== "object") return null;
|
|
3176
|
+
const policy = value;
|
|
3177
|
+
if (typeof policy.minimum !== "string" || typeof policy.recommended !== "string") return null;
|
|
3178
|
+
const order = compareRunnerVersions(policy.recommended, policy.minimum);
|
|
3179
|
+
if (order === null || order < 0) return null;
|
|
3180
|
+
return { minimum: policy.minimum, recommended: policy.recommended };
|
|
3181
|
+
}
|
|
3182
|
+
function runnerUpdateFor(policy, current = RUNNER_VERSION) {
|
|
3183
|
+
const recommendedOrder = compareRunnerVersions(policy.recommended, current);
|
|
3184
|
+
const minimumOrder = compareRunnerVersions(policy.minimum, current);
|
|
3185
|
+
if (recommendedOrder === null || minimumOrder === null) return null;
|
|
3186
|
+
if (recommendedOrder <= 0) return null;
|
|
3187
|
+
return { target: policy.recommended, required: minimumOrder > 0 };
|
|
3188
|
+
}
|
|
3189
|
+
|
|
3190
|
+
// src/protocol/presence.ts
|
|
3191
|
+
async function readRunnerPresence(input) {
|
|
3192
|
+
const fetchImpl = input.fetchImpl ?? fetch;
|
|
3193
|
+
try {
|
|
3194
|
+
const response = await fetchImpl(new URL("api/runner/me", input.server + "/"), {
|
|
3195
|
+
headers: {
|
|
3196
|
+
authorization: `Bearer ${input.token}`,
|
|
3197
|
+
accept: "application/json",
|
|
3198
|
+
[RUNNER_VERSION_HEADER]: RUNNER_VERSION
|
|
3199
|
+
},
|
|
3200
|
+
signal: AbortSignal.timeout(input.timeoutMs ?? 5e3)
|
|
3201
|
+
});
|
|
3202
|
+
if (response.status === 401 || response.status === 403) return { state: "revoked" };
|
|
3203
|
+
if (!response.ok) return { state: "unreachable" };
|
|
3204
|
+
const body = await response.json().catch(() => ({}));
|
|
3205
|
+
return {
|
|
3206
|
+
state: "ok",
|
|
3207
|
+
presence: {
|
|
3208
|
+
online: body.online === true,
|
|
3209
|
+
lastSeenAt: typeof body.lastSeenAt === "string" ? body.lastSeenAt : null,
|
|
3210
|
+
lastVersion: typeof body.lastVersion === "string" ? body.lastVersion : null
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
} catch {
|
|
3214
|
+
return { state: "unreachable" };
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
async function waitForRunnerCheckIn(input) {
|
|
3218
|
+
const now = input.now ?? Date.now;
|
|
3219
|
+
const sleep = input.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
|
|
3220
|
+
const deadline = now() + (input.timeoutMs ?? 15e3);
|
|
3221
|
+
for (; ; ) {
|
|
3222
|
+
const result = await readRunnerPresence(input);
|
|
3223
|
+
if (result.state === "revoked") return "revoked";
|
|
3224
|
+
if (result.state === "ok" && result.presence.online && result.presence.lastSeenAt !== null && result.presence.lastSeenAt !== input.previousLastSeenAt && result.presence.lastVersion === (input.expectedVersion ?? RUNNER_VERSION)) {
|
|
3225
|
+
return "confirmed";
|
|
3226
|
+
}
|
|
3227
|
+
if (now() >= deadline) return "timeout";
|
|
3228
|
+
await sleep(Math.min(input.pollMs ?? 750, Math.max(0, deadline - now())));
|
|
3229
|
+
}
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3151
3232
|
// src/config/mcp-credentials.ts
|
|
3152
3233
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
3153
3234
|
import { constants } from "node:fs";
|
|
@@ -3380,10 +3461,6 @@ function alive(pid) {
|
|
|
3380
3461
|
}
|
|
3381
3462
|
}
|
|
3382
3463
|
|
|
3383
|
-
// src/version.ts
|
|
3384
|
-
var RUNNER_VERSION = "0.3.0";
|
|
3385
|
-
var RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|
|
3386
|
-
|
|
3387
3464
|
// src/protocol/client.ts
|
|
3388
3465
|
var LeaseLostError = class extends Error {
|
|
3389
3466
|
constructor(message) {
|
|
@@ -7485,7 +7562,7 @@ function buildRunLimits(input) {
|
|
|
7485
7562
|
const { synthesis, snapshot } = input;
|
|
7486
7563
|
if (snapshot.cast.some((persona) => persona.completionOriented === true)) {
|
|
7487
7564
|
limits.push(
|
|
7488
|
-
"One participant was deliberately
|
|
7565
|
+
"One participant was deliberately assigned the resilient goal-finisher role to exercise more of the task path. They were highly motivated to finish, but still used only visible controls, documented friction, and could stop at a genuine blocker; their persistence is a study-design choice, not a population estimate."
|
|
7489
7566
|
);
|
|
7490
7567
|
}
|
|
7491
7568
|
if (synthesis.usableSessions.length < snapshot.cast.length) {
|
|
@@ -9378,7 +9455,6 @@ var PersonaProposalResponse = z24.object({
|
|
|
9378
9455
|
patience: Level,
|
|
9379
9456
|
urgency: Level,
|
|
9380
9457
|
emotionalStance: z24.enum(["curious", "neutral", "sceptical", "hurried", "anxious", "enthusiastic"]),
|
|
9381
|
-
device: DeviceContext,
|
|
9382
9458
|
background: z24.string().max(1200).default(""),
|
|
9383
9459
|
attentionBias: z24.string().max(400).default(""),
|
|
9384
9460
|
completionOriented: z24.boolean().default(false),
|
|
@@ -9446,6 +9522,15 @@ init_src();
|
|
|
9446
9522
|
init_src();
|
|
9447
9523
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
9448
9524
|
|
|
9525
|
+
// ../app/src/services/runner-lease.ts
|
|
9526
|
+
init_src();
|
|
9527
|
+
|
|
9528
|
+
// ../app/src/services/run-finalize.ts
|
|
9529
|
+
init_src();
|
|
9530
|
+
|
|
9531
|
+
// ../app/src/services/runner-lease.ts
|
|
9532
|
+
var DEFAULT_LEASE_SECONDS = 20 * 60;
|
|
9533
|
+
|
|
9449
9534
|
// ../app/src/application.ts
|
|
9450
9535
|
var PreflightFailedError = class extends Error {
|
|
9451
9536
|
constructor(preflight2) {
|
|
@@ -9661,10 +9746,6 @@ init_src();
|
|
|
9661
9746
|
// ../app/src/services/recommendations.ts
|
|
9662
9747
|
init_src();
|
|
9663
9748
|
|
|
9664
|
-
// ../app/src/services/runner-lease.ts
|
|
9665
|
-
init_src();
|
|
9666
|
-
var DEFAULT_LEASE_SECONDS = 20 * 60;
|
|
9667
|
-
|
|
9668
9749
|
// ../app/src/services/scheduling.ts
|
|
9669
9750
|
init_src();
|
|
9670
9751
|
|
|
@@ -9832,7 +9913,7 @@ function text(value) {
|
|
|
9832
9913
|
function json(value) {
|
|
9833
9914
|
return text(JSON.stringify(value, null, 2));
|
|
9834
9915
|
}
|
|
9835
|
-
async function handleTool(context, name, input, execute) {
|
|
9916
|
+
async function handleTool(context, name, input, execute, prepare) {
|
|
9836
9917
|
const { app, actor, workspaceId } = context;
|
|
9837
9918
|
switch (name) {
|
|
9838
9919
|
case "create_study": {
|
|
@@ -9965,6 +10046,21 @@ async function handleTool(context, name, input, execute) {
|
|
|
9965
10046
|
}
|
|
9966
10047
|
case "start_study": {
|
|
9967
10048
|
const parsed = StartStudyInput.parse(input);
|
|
10049
|
+
const blocked = await prepare?.(parsed.studyId, parsed.idempotencyKey);
|
|
10050
|
+
if (blocked) {
|
|
10051
|
+
return {
|
|
10052
|
+
...json({
|
|
10053
|
+
started: false,
|
|
10054
|
+
reason: "runner_not_ready",
|
|
10055
|
+
code: blocked.code,
|
|
10056
|
+
blockedHost: blocked.blockedHost,
|
|
10057
|
+
message: blocked.message,
|
|
10058
|
+
runId: blocked.existingRunId ?? null,
|
|
10059
|
+
note: blocked.existingRunId ? "The existing run was not duplicated. Fix the named machine condition and call start_study again with the same runId and idempotencyKey." : "No run was created. Fix the named machine condition and call start_study again with the same runId and idempotencyKey."
|
|
10060
|
+
}),
|
|
10061
|
+
isError: true
|
|
10062
|
+
};
|
|
10063
|
+
}
|
|
9968
10064
|
let outcome;
|
|
9969
10065
|
try {
|
|
9970
10066
|
outcome = await app.startRun(actor, {
|
|
@@ -10308,7 +10404,7 @@ function createMcpServer(options) {
|
|
|
10308
10404
|
function backendFor(options) {
|
|
10309
10405
|
if ("backend" in options) return options.backend;
|
|
10310
10406
|
return {
|
|
10311
|
-
callTool: (name, input) => handleTool(options.context, name, input, options.execute),
|
|
10407
|
+
callTool: (name, input) => handleTool(options.context, name, input, options.execute, options.prepare),
|
|
10312
10408
|
readResource: (parsed) => readResource(options.context, parsed)
|
|
10313
10409
|
};
|
|
10314
10410
|
}
|
|
@@ -10512,6 +10608,11 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
10512
10608
|
return allow(rest);
|
|
10513
10609
|
case "setup":
|
|
10514
10610
|
return setup();
|
|
10611
|
+
case "version":
|
|
10612
|
+
case "--version":
|
|
10613
|
+
case "-v":
|
|
10614
|
+
process.stdout.write(RUNNER_VERSION + "\n");
|
|
10615
|
+
return 0;
|
|
10515
10616
|
case "mcp":
|
|
10516
10617
|
if (rest[0] === "connect") return connectMcp(rest.slice(1));
|
|
10517
10618
|
if (rest[0] === "disconnect") return disconnectMcp();
|
|
@@ -10538,12 +10639,14 @@ function printHelp() {
|
|
|
10538
10639
|
"",
|
|
10539
10640
|
" aloud setup What to do next, for a person or an agent",
|
|
10540
10641
|
" aloud login [--token <token>] Connect this machine, approving it in your browser",
|
|
10541
|
-
" aloud start [--once] [--quiet]
|
|
10642
|
+
" aloud start [--once] [--quiet] [--no-update]",
|
|
10643
|
+
" Update, then wait for studies and run them here",
|
|
10542
10644
|
" aloud status What is set up, and whether it is running",
|
|
10543
10645
|
" aloud allow <host> Let studies open this host from this machine",
|
|
10544
10646
|
" aloud mcp Serve MCP to an editor, using the saved credential",
|
|
10545
10647
|
" aloud mcp connect Connect an editor, approving it in your browser",
|
|
10546
10648
|
" aloud logout Forget the token on this machine",
|
|
10649
|
+
" aloud --version Print the installed runner version",
|
|
10547
10650
|
"",
|
|
10548
10651
|
`Server: ${DEFAULT_SERVER} (override with ALOUD_SERVER)`,
|
|
10549
10652
|
""
|
|
@@ -10728,7 +10831,7 @@ async function setup() {
|
|
|
10728
10831
|
const running = await readRunning();
|
|
10729
10832
|
const installed = onPath("aloud");
|
|
10730
10833
|
const latest = await latestVersion();
|
|
10731
|
-
const stale = latest !== null && latest
|
|
10834
|
+
const stale = latest !== null && compareRunnerVersions(latest, RUNNER_VERSION) === 1;
|
|
10732
10835
|
const signedIn = await signedInState(credentials);
|
|
10733
10836
|
const out = (line = "") => process.stdout.write(line + "\n");
|
|
10734
10837
|
out();
|
|
@@ -10741,8 +10844,21 @@ async function setup() {
|
|
|
10741
10844
|
);
|
|
10742
10845
|
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
10743
10846
|
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
10847
|
+
out(
|
|
10848
|
+
` Aloud sees ${signedIn.state === "ok" ? signedIn.online ? `yes${signedIn.lastVersion ? ` (v${signedIn.lastVersion})` : ""}${signedIn.lastSeenAt ? `, checked in ${when(signedIn.lastSeenAt)}` : ""}` : running ? "not yet. The local process exists, but no current check-in is confirmed" : "no" : signedIn.state === "unreachable" ? "unknown, the server did not answer" : "no"}`
|
|
10849
|
+
);
|
|
10744
10850
|
out();
|
|
10745
|
-
if (process.stdin.isTTY)
|
|
10851
|
+
if (process.stdin.isTTY) {
|
|
10852
|
+
return interactiveSetup({
|
|
10853
|
+
installed,
|
|
10854
|
+
stale,
|
|
10855
|
+
latest,
|
|
10856
|
+
signedIn,
|
|
10857
|
+
running,
|
|
10858
|
+
credentials,
|
|
10859
|
+
chromiumInstalled: checks.chromiumInstalled
|
|
10860
|
+
});
|
|
10861
|
+
}
|
|
10746
10862
|
const steps = [];
|
|
10747
10863
|
if (!installed) {
|
|
10748
10864
|
steps.push(["npm install -g @aloud/runner"]);
|
|
@@ -10751,11 +10867,11 @@ async function setup() {
|
|
|
10751
10867
|
}
|
|
10752
10868
|
if (signedIn.state === "revoked") {
|
|
10753
10869
|
steps.push([
|
|
10754
|
-
|
|
10755
|
-
"The saved
|
|
10870
|
+
"aloud login",
|
|
10871
|
+
"The saved connection was revoked. This prints a new browser-approval link and code."
|
|
10756
10872
|
]);
|
|
10757
10873
|
}
|
|
10758
|
-
if (signedIn.state === "none"
|
|
10874
|
+
if (signedIn.state === "none") {
|
|
10759
10875
|
steps.push([
|
|
10760
10876
|
"aloud login",
|
|
10761
10877
|
"Prints a link and a short code, then waits. Give both to the person; they approve in",
|
|
@@ -10771,6 +10887,20 @@ async function setup() {
|
|
|
10771
10887
|
out();
|
|
10772
10888
|
return 1;
|
|
10773
10889
|
}
|
|
10890
|
+
if (steps.length === 0 && running && signedIn.state === "ok" && !signedIn.online) {
|
|
10891
|
+
out("The runner process exists locally, but Aloud has not confirmed a current check-in.");
|
|
10892
|
+
out(`Read ${join6(dirname5(credentialsPath()), "runner.log")} and run \`aloud status\` again.`);
|
|
10893
|
+
out("Do not report this machine ready until `Aloud sees` says yes.");
|
|
10894
|
+
out();
|
|
10895
|
+
return 1;
|
|
10896
|
+
}
|
|
10897
|
+
if (steps.length === 0 && running && signedIn.state === "ok" && signedIn.online && signedIn.lastVersion !== RUNNER_VERSION) {
|
|
10898
|
+
out(`The live process is checking in as v${signedIn.lastVersion ?? "unknown"}, not v${RUNNER_VERSION}.`);
|
|
10899
|
+
out(`Restart it with: kill ${running.pid} && aloud start`);
|
|
10900
|
+
out("Do not report the update complete until `aloud status` shows the expected version.");
|
|
10901
|
+
out();
|
|
10902
|
+
return 1;
|
|
10903
|
+
}
|
|
10774
10904
|
if (steps.length === 0) {
|
|
10775
10905
|
out("Nothing to do. This machine is set up and waiting for studies.");
|
|
10776
10906
|
out();
|
|
@@ -10795,7 +10925,7 @@ async function setup() {
|
|
|
10795
10925
|
);
|
|
10796
10926
|
}
|
|
10797
10927
|
out(" - The package is `@aloud/runner`, exactly. If that name does not resolve, stop and say so.");
|
|
10798
|
-
out(" Do not install a similarly named package
|
|
10928
|
+
out(" Do not install a similarly named package.");
|
|
10799
10929
|
out(" - There is nothing secret for you to handle. `aloud login` prints a link and a code,");
|
|
10800
10930
|
out(" and the credential goes straight to this machine. Never ask anyone for a token.");
|
|
10801
10931
|
out(" - `aloud start` does not exit. Run it in a background shell and do not block on it.");
|
|
@@ -10844,12 +10974,28 @@ async function interactiveSetup(state) {
|
|
|
10844
10974
|
out("Start it when you are ready, and leave it running: aloud start");
|
|
10845
10975
|
return 1;
|
|
10846
10976
|
}
|
|
10847
|
-
return startDetached(out);
|
|
10977
|
+
return startDetached(out, { waitForConfirmation: state.chromiumInstalled });
|
|
10978
|
+
}
|
|
10979
|
+
if (state.signedIn.state === "ok" && state.signedIn.online && state.signedIn.lastVersion === RUNNER_VERSION) {
|
|
10980
|
+
out("");
|
|
10981
|
+
out("Set up and confirmed by Aloud. This machine is waiting for eligible studies.");
|
|
10982
|
+
out("");
|
|
10983
|
+
return 0;
|
|
10984
|
+
}
|
|
10985
|
+
if (state.signedIn.state === "ok" && state.signedIn.online) {
|
|
10986
|
+
out("");
|
|
10987
|
+
out(
|
|
10988
|
+
`The live process is checking in as v${state.signedIn.lastVersion ?? "unknown"}, not v${RUNNER_VERSION}.`
|
|
10989
|
+
);
|
|
10990
|
+
out(`Restart it with: kill ${state.running.pid} && aloud start`);
|
|
10991
|
+
out("");
|
|
10992
|
+
return 1;
|
|
10848
10993
|
}
|
|
10849
10994
|
out("");
|
|
10850
|
-
out("
|
|
10995
|
+
out("The runner process exists locally, but Aloud has not confirmed a current check-in.");
|
|
10996
|
+
out(`Read ${join6(dirname5(credentialsPath()), "runner.log")} and run \`aloud status\` again.`);
|
|
10851
10997
|
out("");
|
|
10852
|
-
return
|
|
10998
|
+
return 1;
|
|
10853
10999
|
} finally {
|
|
10854
11000
|
rl.close();
|
|
10855
11001
|
}
|
|
@@ -10869,9 +11015,11 @@ async function run(command, args, out) {
|
|
|
10869
11015
|
child.on("close", (code) => resolve(code === 0));
|
|
10870
11016
|
});
|
|
10871
11017
|
}
|
|
10872
|
-
async function startDetached(out) {
|
|
11018
|
+
async function startDetached(out, options) {
|
|
10873
11019
|
const log = join6(dirname5(credentialsPath()), "runner.log");
|
|
10874
11020
|
await mkdir5(dirname5(log), { recursive: true, mode: 448 });
|
|
11021
|
+
const credentials = await readCredentials();
|
|
11022
|
+
const before = credentials ? await readRunnerPresence({ server: credentials.server, token: credentials.token }) : null;
|
|
10875
11023
|
const handle = openSync(log, "a");
|
|
10876
11024
|
const child = spawn2(process.execPath, [process.argv[1] ?? "", "start"], {
|
|
10877
11025
|
detached: true,
|
|
@@ -10884,23 +11032,50 @@ async function startDetached(out) {
|
|
|
10884
11032
|
out(" Check it aloud status");
|
|
10885
11033
|
out(` Stop it kill ${child.pid}`);
|
|
10886
11034
|
out("");
|
|
10887
|
-
|
|
11035
|
+
if (!options.waitForConfirmation) {
|
|
11036
|
+
out("The runner is preparing Chromium in the background. It is started locally, but not yet");
|
|
11037
|
+
out("confirmed by Aloud. The Machines page updates automatically after its first check-in.");
|
|
11038
|
+
out("");
|
|
11039
|
+
return 0;
|
|
11040
|
+
}
|
|
11041
|
+
if (!credentials || before?.state !== "ok") {
|
|
11042
|
+
out("Started locally, but Aloud could not establish a before-start status to confirm this launch.");
|
|
11043
|
+
out(`Read ${log} and run \`aloud status\`; do not call it ready until \`Aloud sees\` says yes.`);
|
|
11044
|
+
out("");
|
|
11045
|
+
return 1;
|
|
11046
|
+
}
|
|
11047
|
+
out("Waiting for Aloud to confirm the first check-in\u2026");
|
|
11048
|
+
const confirmation = await waitForRunnerCheckIn({
|
|
11049
|
+
server: credentials.server,
|
|
11050
|
+
token: credentials.token,
|
|
11051
|
+
previousLastSeenAt: before.presence.lastSeenAt
|
|
11052
|
+
});
|
|
11053
|
+
if (confirmation === "confirmed") {
|
|
11054
|
+
out("Confirmed by Aloud. This machine is online and waiting for eligible studies.");
|
|
11055
|
+
out("");
|
|
11056
|
+
return 0;
|
|
11057
|
+
}
|
|
11058
|
+
if (confirmation === "revoked") {
|
|
11059
|
+
out("Aloud refused the saved connection. Run `aloud login` to approve this machine again.");
|
|
11060
|
+
} else {
|
|
11061
|
+
out("The process started, but Aloud did not confirm a check-in within 15 seconds.");
|
|
11062
|
+
out(`Read ${log} and run \`aloud status\`; do not call it ready until \`Aloud sees\` says yes.`);
|
|
11063
|
+
}
|
|
10888
11064
|
out("");
|
|
10889
|
-
return
|
|
11065
|
+
return 1;
|
|
10890
11066
|
}
|
|
10891
11067
|
async function signedInState(credentials) {
|
|
10892
11068
|
if (!credentials) return { state: "none" };
|
|
10893
|
-
|
|
10894
|
-
|
|
10895
|
-
|
|
10896
|
-
|
|
10897
|
-
|
|
10898
|
-
|
|
10899
|
-
|
|
10900
|
-
|
|
10901
|
-
|
|
10902
|
-
|
|
10903
|
-
}
|
|
11069
|
+
const result = await readRunnerPresence({ server: credentials.server, token: credentials.token });
|
|
11070
|
+
if (result.state === "revoked") return { state: "revoked" };
|
|
11071
|
+
if (result.state === "unreachable") return { state: "unreachable", server: credentials.server };
|
|
11072
|
+
return {
|
|
11073
|
+
state: "ok",
|
|
11074
|
+
name: credentials.runnerName,
|
|
11075
|
+
online: result.presence.online,
|
|
11076
|
+
lastSeenAt: result.presence.lastSeenAt,
|
|
11077
|
+
lastVersion: result.presence.lastVersion
|
|
11078
|
+
};
|
|
10904
11079
|
}
|
|
10905
11080
|
async function npmPrefix() {
|
|
10906
11081
|
const path = await new Promise((resolve) => {
|
|
@@ -10935,6 +11110,139 @@ async function latestVersion() {
|
|
|
10935
11110
|
return null;
|
|
10936
11111
|
}
|
|
10937
11112
|
}
|
|
11113
|
+
async function serverVersionPolicy(credentials) {
|
|
11114
|
+
try {
|
|
11115
|
+
const client = new RunnerClient({ server: credentials.server, token: credentials.token });
|
|
11116
|
+
const response = await client.request("api/runner/me", {
|
|
11117
|
+
retry: false
|
|
11118
|
+
});
|
|
11119
|
+
return runnerVersionPolicyFrom(response.body?.runnerVersionPolicy);
|
|
11120
|
+
} catch {
|
|
11121
|
+
return null;
|
|
11122
|
+
}
|
|
11123
|
+
}
|
|
11124
|
+
async function updateBeforeStart(credentials, argv, overrides = {}) {
|
|
11125
|
+
const stdout = overrides.stdout ?? ((text2) => process.stdout.write(text2));
|
|
11126
|
+
const stderr = overrides.stderr ?? ((text2) => process.stderr.write(text2));
|
|
11127
|
+
const policy = await (overrides.versionPolicy ?? serverVersionPolicy)(credentials);
|
|
11128
|
+
if (!policy) return null;
|
|
11129
|
+
const update = runnerUpdateFor(policy);
|
|
11130
|
+
if (!update) return null;
|
|
11131
|
+
if (argv.includes("--no-update")) {
|
|
11132
|
+
if (!update.required) return null;
|
|
11133
|
+
stderr(
|
|
11134
|
+
`Runner ${RUNNER_VERSION} cannot start studies on this server; ${policy.minimum} or newer is required.
|
|
11135
|
+
Automatic updates were disabled. Remove --no-update or run npm install -g @aloud/runner@${update.target}.
|
|
11136
|
+
`
|
|
11137
|
+
);
|
|
11138
|
+
return 1;
|
|
11139
|
+
}
|
|
11140
|
+
stdout(`
|
|
11141
|
+
Updating Aloud runner ${RUNNER_VERSION} \u2192 ${update.target} before it starts.
|
|
11142
|
+
`);
|
|
11143
|
+
const prefix = await (overrides.npmPrefix ?? npmPrefix)();
|
|
11144
|
+
if (prefix?.writable === false) {
|
|
11145
|
+
return failedAutomaticUpdate(
|
|
11146
|
+
update,
|
|
11147
|
+
policy,
|
|
11148
|
+
`npm's global install directory (${prefix.path}) is not writable by this user.`,
|
|
11149
|
+
stderr
|
|
11150
|
+
);
|
|
11151
|
+
}
|
|
11152
|
+
const installed = overrides.install ? await overrides.install(update.target, stdout) : await run(
|
|
11153
|
+
"npm",
|
|
11154
|
+
["install", "-g", `@aloud/runner@${update.target}`],
|
|
11155
|
+
(line = "") => stdout(line + "\n")
|
|
11156
|
+
);
|
|
11157
|
+
if (!installed) {
|
|
11158
|
+
return failedAutomaticUpdate(update, policy, "npm did not complete the update.", stderr);
|
|
11159
|
+
}
|
|
11160
|
+
const entry2 = await (overrides.installedEntry ?? installedRunnerEntry)(update.target);
|
|
11161
|
+
if (!entry2) {
|
|
11162
|
+
return failedAutomaticUpdate(
|
|
11163
|
+
update,
|
|
11164
|
+
policy,
|
|
11165
|
+
`npm completed, but the installed @aloud/runner@${update.target} could not be verified.`,
|
|
11166
|
+
stderr
|
|
11167
|
+
);
|
|
11168
|
+
}
|
|
11169
|
+
stdout(`
|
|
11170
|
+
Updated to ${update.target}. Starting it now.
|
|
11171
|
+
|
|
11172
|
+
`);
|
|
11173
|
+
return (overrides.relaunch ?? relaunchUpdatedRunner)(entry2, argv);
|
|
11174
|
+
}
|
|
11175
|
+
function failedAutomaticUpdate(update, policy, reason, stderr) {
|
|
11176
|
+
const command = `npm install -g @aloud/runner@${update.target}`;
|
|
11177
|
+
if (update.required) {
|
|
11178
|
+
stderr(
|
|
11179
|
+
`
|
|
11180
|
+
${reason}
|
|
11181
|
+
Runner ${RUNNER_VERSION} is below this server's minimum ${policy.minimum}, so nothing was started.
|
|
11182
|
+
Do not use sudo. Fix npm's global prefix, then run: ${command}
|
|
11183
|
+
`
|
|
11184
|
+
);
|
|
11185
|
+
return 1;
|
|
11186
|
+
}
|
|
11187
|
+
stderr(
|
|
11188
|
+
`
|
|
11189
|
+
${reason}
|
|
11190
|
+
Runner ${RUNNER_VERSION} is still compatible, so it will start without updating.
|
|
11191
|
+
To update it later, run: ${command}
|
|
11192
|
+
|
|
11193
|
+
`
|
|
11194
|
+
);
|
|
11195
|
+
return null;
|
|
11196
|
+
}
|
|
11197
|
+
async function installedRunnerEntry(target) {
|
|
11198
|
+
const root = await commandOutput("npm", ["root", "--global"]);
|
|
11199
|
+
if (!root) return null;
|
|
11200
|
+
const directory = join6(root, "@aloud", "runner");
|
|
11201
|
+
const entry2 = join6(directory, "dist", "cli.js");
|
|
11202
|
+
try {
|
|
11203
|
+
const manifest = JSON.parse(readFileSync(join6(directory, "package.json"), "utf8"));
|
|
11204
|
+
return manifest.version === target && existsSync2(entry2) ? entry2 : null;
|
|
11205
|
+
} catch {
|
|
11206
|
+
return null;
|
|
11207
|
+
}
|
|
11208
|
+
}
|
|
11209
|
+
async function commandOutput(command, args) {
|
|
11210
|
+
return new Promise((resolve) => {
|
|
11211
|
+
const child = spawn2(command, [...args], { stdio: ["ignore", "pipe", "ignore"] });
|
|
11212
|
+
let output = "";
|
|
11213
|
+
let settled = false;
|
|
11214
|
+
const finish = (value) => {
|
|
11215
|
+
if (settled) return;
|
|
11216
|
+
settled = true;
|
|
11217
|
+
resolve(value);
|
|
11218
|
+
};
|
|
11219
|
+
child.stdout?.on("data", (chunk) => output += chunk.toString("utf8"));
|
|
11220
|
+
child.on("error", () => finish(null));
|
|
11221
|
+
child.on("close", (code) => finish(code === 0 && output.trim() ? output.trim() : null));
|
|
11222
|
+
});
|
|
11223
|
+
}
|
|
11224
|
+
async function relaunchUpdatedRunner(entry2, argv) {
|
|
11225
|
+
return new Promise((resolve) => {
|
|
11226
|
+
let settled = false;
|
|
11227
|
+
const finish = (code) => {
|
|
11228
|
+
if (settled) return;
|
|
11229
|
+
settled = true;
|
|
11230
|
+
resolve(code);
|
|
11231
|
+
};
|
|
11232
|
+
const child = spawn2(process.execPath, [entry2, "start", ...argv, "--no-update"], {
|
|
11233
|
+
stdio: "inherit"
|
|
11234
|
+
});
|
|
11235
|
+
child.on("error", (error) => {
|
|
11236
|
+
process.stderr.write(`The runner updated but could not relaunch: ${error.message}
|
|
11237
|
+
Run \`aloud start\` again.
|
|
11238
|
+
`);
|
|
11239
|
+
finish(1);
|
|
11240
|
+
});
|
|
11241
|
+
child.on("close", (code, signal) => {
|
|
11242
|
+
finish(code ?? (signal ? 130 : 1));
|
|
11243
|
+
});
|
|
11244
|
+
});
|
|
11245
|
+
}
|
|
10938
11246
|
async function status() {
|
|
10939
11247
|
const credentials = await loadCredentials().catch((error) => {
|
|
10940
11248
|
process.stderr.write(error.message + "\n");
|
|
@@ -10942,6 +11250,7 @@ async function status() {
|
|
|
10942
11250
|
});
|
|
10943
11251
|
const checks = await preflight();
|
|
10944
11252
|
const running = await readRunning();
|
|
11253
|
+
const signedIn = await signedInState(credentials);
|
|
10945
11254
|
process.stdout.write("\n");
|
|
10946
11255
|
if (!credentials) {
|
|
10947
11256
|
process.stdout.write("Signed in no. Run `aloud login`.\n");
|
|
@@ -10959,10 +11268,14 @@ async function status() {
|
|
|
10959
11268
|
);
|
|
10960
11269
|
process.stdout.write(
|
|
10961
11270
|
`Running ${running ? `yes, since ${when(running.startedAt)} (pid ${running.pid})` : "no. Run `aloud start`."}
|
|
11271
|
+
`
|
|
11272
|
+
);
|
|
11273
|
+
process.stdout.write(
|
|
11274
|
+
`Aloud sees ${signedIn.state === "ok" ? signedIn.online ? `yes${signedIn.lastVersion ? ` (v${signedIn.lastVersion})` : ""}${signedIn.lastSeenAt ? `, checked in ${when(signedIn.lastSeenAt)}` : ""}` : "no current check-in" : signedIn.state === "unreachable" ? "unknown, server did not answer" : "no"}
|
|
10962
11275
|
`
|
|
10963
11276
|
);
|
|
10964
11277
|
process.stdout.write("\n");
|
|
10965
|
-
return credentials && checks.chromiumInstalled && running ? 0 : 1;
|
|
11278
|
+
return credentials && checks.chromiumInstalled && running && signedIn.state === "ok" && signedIn.online ? 0 : 1;
|
|
10966
11279
|
}
|
|
10967
11280
|
async function allow(argv) {
|
|
10968
11281
|
const host = argv.find((arg) => !arg.startsWith("-"));
|
|
@@ -10992,6 +11305,17 @@ async function start(argv) {
|
|
|
10992
11305
|
process.stderr.write("Not signed in. Run `aloud login` first.\n");
|
|
10993
11306
|
return 1;
|
|
10994
11307
|
}
|
|
11308
|
+
const existing = await readRunning();
|
|
11309
|
+
if (existing && existing.pid !== process.pid) {
|
|
11310
|
+
process.stderr.write(
|
|
11311
|
+
`Aloud runner is already running as pid ${existing.pid}. Stop that process before starting another.
|
|
11312
|
+
To restart it: kill ${existing.pid} && aloud start
|
|
11313
|
+
`
|
|
11314
|
+
);
|
|
11315
|
+
return 1;
|
|
11316
|
+
}
|
|
11317
|
+
const updated = await updateBeforeStart(credentials, argv);
|
|
11318
|
+
if (updated !== null) return updated;
|
|
10995
11319
|
const reporter = new TerminalReporter(process.stdout, credentials.token, !argv.includes("--quiet"));
|
|
10996
11320
|
const checks = await preflight();
|
|
10997
11321
|
if (!checks.chromiumInstalled) {
|
|
@@ -11091,5 +11415,6 @@ if (entry.endsWith("cli.ts") || entry.endsWith("cli.js") || entry.endsWith("/alo
|
|
|
11091
11415
|
}
|
|
11092
11416
|
export {
|
|
11093
11417
|
CredentialsError,
|
|
11094
|
-
main
|
|
11418
|
+
main,
|
|
11419
|
+
updateBeforeStart
|
|
11095
11420
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aloud/runner",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.2",
|
|
4
4
|
"description": "Run Aloud usability studies in a real browser on your own machine, so a study can reach localhost and anything else behind your network.",
|
|
5
5
|
"license": "ISC",
|
|
6
6
|
"repository": {
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
".": "./src/index.ts",
|
|
18
18
|
"./*": "./src/*.ts"
|
|
19
19
|
},
|
|
20
|
-
"bin": { "aloud": "
|
|
20
|
+
"bin": { "aloud": "dist/cli.js" },
|
|
21
21
|
"files": ["dist/cli.js", "src", "README.md"],
|
|
22
22
|
"engines": { "node": ">=20" },
|
|
23
23
|
"scripts": {
|