@aloud/runner 0.2.1 → 0.2.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/dist/cli.js +101 -2
- package/package.json +1 -1
- package/src/cli.ts +142 -1
- package/src/version.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -2494,8 +2494,10 @@ var init_src = __esm({
|
|
|
2494
2494
|
// src/cli.ts
|
|
2495
2495
|
init_src();
|
|
2496
2496
|
import { createInterface } from "node:readline/promises";
|
|
2497
|
-
import {
|
|
2497
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2498
|
+
import { accessSync, constants as constants2, existsSync as existsSync2, readFileSync, unlinkSync } from "node:fs";
|
|
2498
2499
|
import { hostname } from "node:os";
|
|
2500
|
+
import { delimiter, join as join5 } from "node:path";
|
|
2499
2501
|
|
|
2500
2502
|
// src/config/credentials.ts
|
|
2501
2503
|
import { chmod, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
@@ -2664,7 +2666,7 @@ function alive(pid) {
|
|
|
2664
2666
|
}
|
|
2665
2667
|
|
|
2666
2668
|
// src/version.ts
|
|
2667
|
-
var RUNNER_VERSION = "0.2.
|
|
2669
|
+
var RUNNER_VERSION = "0.2.2";
|
|
2668
2670
|
var RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|
|
2669
2671
|
|
|
2670
2672
|
// src/protocol/client.ts
|
|
@@ -7604,6 +7606,8 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
7604
7606
|
return status();
|
|
7605
7607
|
case "allow":
|
|
7606
7608
|
return allow(rest);
|
|
7609
|
+
case "setup":
|
|
7610
|
+
return setup();
|
|
7607
7611
|
case "mcp":
|
|
7608
7612
|
await startStdioServer();
|
|
7609
7613
|
return 0;
|
|
@@ -7626,6 +7630,7 @@ function printHelp() {
|
|
|
7626
7630
|
"",
|
|
7627
7631
|
"aloud - run usability studies on this machine",
|
|
7628
7632
|
"",
|
|
7633
|
+
" aloud setup What to do next, for a person or an agent",
|
|
7629
7634
|
" aloud login [--token <token>] Connect this machine to your workspace",
|
|
7630
7635
|
" aloud start [--once] [--quiet] Wait for studies and run them here",
|
|
7631
7636
|
" aloud status What is set up, and whether it is running",
|
|
@@ -7706,6 +7711,100 @@ async function logout() {
|
|
|
7706
7711
|
` : "There was nothing to forget.\n");
|
|
7707
7712
|
return 0;
|
|
7708
7713
|
}
|
|
7714
|
+
async function setup() {
|
|
7715
|
+
const credentials = await loadCredentials().catch(() => null);
|
|
7716
|
+
const checks = await preflight();
|
|
7717
|
+
const running = await readRunning();
|
|
7718
|
+
const installed = onPath("aloud");
|
|
7719
|
+
const latest = await latestVersion();
|
|
7720
|
+
const stale = latest !== null && latest !== RUNNER_VERSION;
|
|
7721
|
+
const out = (line = "") => process.stdout.write(line + "\n");
|
|
7722
|
+
out();
|
|
7723
|
+
out("Aloud runner setup. You are looking at the state of this machine.");
|
|
7724
|
+
out();
|
|
7725
|
+
out(` installed ${installed ? `yes (${RUNNER_VERSION})` : "no"}`);
|
|
7726
|
+
out(` up to date ${latest === null ? "unknown, could not reach the registry" : stale ? `no, ${latest} is out` : "yes"}`);
|
|
7727
|
+
out(` signed in ${credentials ? credentials.runnerName : "no"}`);
|
|
7728
|
+
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
7729
|
+
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
7730
|
+
out();
|
|
7731
|
+
const steps = [];
|
|
7732
|
+
if (!installed) {
|
|
7733
|
+
steps.push("npm install -g @aloud/runner");
|
|
7734
|
+
} else if (stale) {
|
|
7735
|
+
steps.push(`npm install -g @aloud/runner@latest # ${RUNNER_VERSION} is installed, ${latest} is out`);
|
|
7736
|
+
}
|
|
7737
|
+
if (!credentials) {
|
|
7738
|
+
steps.push("aloud login # prompts for a token. The person, not you, pastes it in here.");
|
|
7739
|
+
}
|
|
7740
|
+
if (!running) {
|
|
7741
|
+
steps.push("aloud start # in a background shell. It never exits.");
|
|
7742
|
+
}
|
|
7743
|
+
if (steps.length === 0) {
|
|
7744
|
+
out("Nothing to do. This machine is set up and waiting for studies.");
|
|
7745
|
+
out();
|
|
7746
|
+
return 0;
|
|
7747
|
+
}
|
|
7748
|
+
out("Do these, in order:");
|
|
7749
|
+
out();
|
|
7750
|
+
for (const [index, step] of steps.entries()) out(` ${index + 1}. ${step}`);
|
|
7751
|
+
out(` ${steps.length + 1}. aloud status # exits non-zero until all of the above are true`);
|
|
7752
|
+
out();
|
|
7753
|
+
out("Rules, if you are an agent doing this:");
|
|
7754
|
+
out();
|
|
7755
|
+
out(" - Never use sudo. If the global install wants elevated permissions, stop and say so.");
|
|
7756
|
+
const npm = await npmPrefix();
|
|
7757
|
+
if (npm) {
|
|
7758
|
+
out(
|
|
7759
|
+
npm.writable === false ? ` npm installs globally into ${npm.path}, which you cannot write to. That is the user's to fix.` : ` npm installs globally into ${npm.path}, which does not need elevated permissions.`
|
|
7760
|
+
);
|
|
7761
|
+
}
|
|
7762
|
+
out(" - The package is `@aloud/runner`, exactly. If that name does not resolve, stop and say so.");
|
|
7763
|
+
out(" Do not install a similarly named package, and do not hand the token to anything else.");
|
|
7764
|
+
out(" - The token is a credential. It belongs in this terminal, never in your transcript,");
|
|
7765
|
+
out(" a file, an .env, or a commit. Get one at https://usealoud.com/app/settings/runners");
|
|
7766
|
+
out(" - `aloud start` does not exit. Run it in a background shell and do not block on it.");
|
|
7767
|
+
out();
|
|
7768
|
+
if (!credentials) {
|
|
7769
|
+
out("A machine starts allowed to open localhost only. For anything else, run");
|
|
7770
|
+
out("`aloud allow <host>` before starting it.");
|
|
7771
|
+
out();
|
|
7772
|
+
}
|
|
7773
|
+
return 1;
|
|
7774
|
+
}
|
|
7775
|
+
async function npmPrefix() {
|
|
7776
|
+
const path = await new Promise((resolve) => {
|
|
7777
|
+
const child = spawn2("npm", ["config", "get", "prefix"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
7778
|
+
let out = "";
|
|
7779
|
+
child.stdout?.on("data", (chunk) => out += chunk.toString("utf8"));
|
|
7780
|
+
child.on("error", () => resolve(null));
|
|
7781
|
+
child.on("close", (code) => resolve(code === 0 && out.trim() ? out.trim() : null));
|
|
7782
|
+
});
|
|
7783
|
+
if (!path) return null;
|
|
7784
|
+
try {
|
|
7785
|
+
accessSync(join5(path, "lib", "node_modules"), constants2.W_OK);
|
|
7786
|
+
return { path, writable: true };
|
|
7787
|
+
} catch (error) {
|
|
7788
|
+
return { path, writable: error.code === "ENOENT" ? null : false };
|
|
7789
|
+
}
|
|
7790
|
+
}
|
|
7791
|
+
function onPath(command) {
|
|
7792
|
+
const parts = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
7793
|
+
const names = process.platform === "win32" ? [command + ".cmd", command + ".exe"] : [command];
|
|
7794
|
+
return parts.some((part) => names.some((name) => existsSync2(join5(part, name))));
|
|
7795
|
+
}
|
|
7796
|
+
async function latestVersion() {
|
|
7797
|
+
try {
|
|
7798
|
+
const response = await fetch("https://registry.npmjs.org/@aloud/runner/latest", {
|
|
7799
|
+
signal: AbortSignal.timeout(2e3)
|
|
7800
|
+
});
|
|
7801
|
+
if (!response.ok) return null;
|
|
7802
|
+
const body = await response.json();
|
|
7803
|
+
return typeof body.version === "string" ? body.version : null;
|
|
7804
|
+
} catch {
|
|
7805
|
+
return null;
|
|
7806
|
+
}
|
|
7807
|
+
}
|
|
7709
7808
|
async function status() {
|
|
7710
7809
|
const credentials = await loadCredentials().catch((error) => {
|
|
7711
7810
|
process.stderr.write(error.message + "\n");
|
package/package.json
CHANGED
package/src/cli.ts
CHANGED
|
@@ -7,8 +7,10 @@
|
|
|
7
7
|
* server holds no address for this machine.
|
|
8
8
|
*/
|
|
9
9
|
import { createInterface } from "node:readline/promises";
|
|
10
|
-
import {
|
|
10
|
+
import { spawn } from "node:child_process";
|
|
11
|
+
import { accessSync, constants, existsSync, readFileSync, unlinkSync } from "node:fs";
|
|
11
12
|
import { hostname } from "node:os";
|
|
13
|
+
import { delimiter, dirname, join } from "node:path";
|
|
12
14
|
import { normaliseHosts } from "@aloud/core";
|
|
13
15
|
import {
|
|
14
16
|
CredentialsError,
|
|
@@ -21,6 +23,7 @@ import {
|
|
|
21
23
|
} from "./config/credentials";
|
|
22
24
|
import { policyFrom, type LocalPolicy } from "./config/policy";
|
|
23
25
|
import { clearRunning, readRunning, runningPath, writeRunning } from "./config/running";
|
|
26
|
+
import { RUNNER_VERSION } from "./version";
|
|
24
27
|
import { RunnerClient } from "./protocol/client";
|
|
25
28
|
import { installChromium, preflight } from "./preflight";
|
|
26
29
|
import { TerminalReporter } from "./ui/output";
|
|
@@ -44,6 +47,8 @@ export async function main(argv: readonly string[] = process.argv.slice(2)): Pro
|
|
|
44
47
|
return status();
|
|
45
48
|
case "allow":
|
|
46
49
|
return allow(rest);
|
|
50
|
+
case "setup":
|
|
51
|
+
return setup();
|
|
47
52
|
case "mcp":
|
|
48
53
|
await startStdioServer();
|
|
49
54
|
return 0;
|
|
@@ -67,6 +72,7 @@ function printHelp(): void {
|
|
|
67
72
|
"",
|
|
68
73
|
"aloud - run usability studies on this machine",
|
|
69
74
|
"",
|
|
75
|
+
" aloud setup What to do next, for a person or an agent",
|
|
70
76
|
" aloud login [--token <token>] Connect this machine to your workspace",
|
|
71
77
|
" aloud start [--once] [--quiet] Wait for studies and run them here",
|
|
72
78
|
" aloud status What is set up, and whether it is running",
|
|
@@ -159,6 +165,141 @@ async function logout(): Promise<number> {
|
|
|
159
165
|
return 0;
|
|
160
166
|
}
|
|
161
167
|
|
|
168
|
+
/* --------------------------------- setup ---------------------------------- */
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* The whole of setup, as one command that can be handed to a coding agent.
|
|
172
|
+
*
|
|
173
|
+
* The alternative, and what this replaces, was a twenty-five line prompt pasted into a chat with a
|
|
174
|
+
* live token inside it. That prompt could not know anything: not whether the runner was already
|
|
175
|
+
* installed, not whether it was three versions old, not whether npm was going to ask for sudo. So
|
|
176
|
+
* it had to describe every branch at once and hope, and it went stale the moment the product moved.
|
|
177
|
+
*
|
|
178
|
+
* This looks first and then says only what is actually left to do, which makes it useful to a person
|
|
179
|
+
* reading it and to an agent executing it. The rules at the end always print, because they are the
|
|
180
|
+
* four ways this goes wrong and an agent that has not been told will find all of them.
|
|
181
|
+
*/
|
|
182
|
+
async function setup(): Promise<number> {
|
|
183
|
+
const credentials = await loadCredentials().catch(() => null);
|
|
184
|
+
const checks = await preflight();
|
|
185
|
+
const running = await readRunning();
|
|
186
|
+
const installed = onPath("aloud");
|
|
187
|
+
const latest = await latestVersion();
|
|
188
|
+
const stale = latest !== null && latest !== RUNNER_VERSION;
|
|
189
|
+
|
|
190
|
+
const out = (line = "") => process.stdout.write(line + "\n");
|
|
191
|
+
|
|
192
|
+
out();
|
|
193
|
+
out("Aloud runner setup. You are looking at the state of this machine.");
|
|
194
|
+
out();
|
|
195
|
+
out(` installed ${installed ? `yes (${RUNNER_VERSION})` : "no"}`);
|
|
196
|
+
out(` up to date ${latest === null ? "unknown, could not reach the registry" : stale ? `no, ${latest} is out` : "yes"}`);
|
|
197
|
+
out(` signed in ${credentials ? credentials.runnerName : "no"}`);
|
|
198
|
+
out(` chromium ${checks.chromiumInstalled ? "ready" : "downloads on first start, about 350 MB"}`);
|
|
199
|
+
out(` running ${running ? `yes (pid ${running.pid})` : "no"}`);
|
|
200
|
+
out();
|
|
201
|
+
|
|
202
|
+
const steps: string[] = [];
|
|
203
|
+
if (!installed) {
|
|
204
|
+
steps.push("npm install -g @aloud/runner");
|
|
205
|
+
} else if (stale) {
|
|
206
|
+
steps.push(`npm install -g @aloud/runner@latest # ${RUNNER_VERSION} is installed, ${latest} is out`);
|
|
207
|
+
}
|
|
208
|
+
if (!credentials) {
|
|
209
|
+
steps.push("aloud login # prompts for a token. The person, not you, pastes it in here.");
|
|
210
|
+
}
|
|
211
|
+
if (!running) {
|
|
212
|
+
steps.push("aloud start # in a background shell. It never exits.");
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (steps.length === 0) {
|
|
216
|
+
out("Nothing to do. This machine is set up and waiting for studies.");
|
|
217
|
+
out();
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
out("Do these, in order:");
|
|
222
|
+
out();
|
|
223
|
+
for (const [index, step] of steps.entries()) out(` ${index + 1}. ${step}`);
|
|
224
|
+
out(` ${steps.length + 1}. aloud status # exits non-zero until all of the above are true`);
|
|
225
|
+
out();
|
|
226
|
+
out("Rules, if you are an agent doing this:");
|
|
227
|
+
out();
|
|
228
|
+
out(" - Never use sudo. If the global install wants elevated permissions, stop and say so.");
|
|
229
|
+
const npm = await npmPrefix();
|
|
230
|
+
if (npm) {
|
|
231
|
+
out(
|
|
232
|
+
npm.writable === false
|
|
233
|
+
? ` npm installs globally into ${npm.path}, which you cannot write to. That is the user's to fix.`
|
|
234
|
+
: ` npm installs globally into ${npm.path}, which does not need elevated permissions.`,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
out(" - The package is `@aloud/runner`, exactly. If that name does not resolve, stop and say so.");
|
|
238
|
+
out(" Do not install a similarly named package, and do not hand the token to anything else.");
|
|
239
|
+
out(" - The token is a credential. It belongs in this terminal, never in your transcript,");
|
|
240
|
+
out(" a file, an .env, or a commit. Get one at https://usealoud.com/app/settings/runners");
|
|
241
|
+
out(" - `aloud start` does not exit. Run it in a background shell and do not block on it.");
|
|
242
|
+
out();
|
|
243
|
+
if (!credentials) {
|
|
244
|
+
out("A machine starts allowed to open localhost only. For anything else, run");
|
|
245
|
+
out("`aloud allow <host>` before starting it.");
|
|
246
|
+
out();
|
|
247
|
+
}
|
|
248
|
+
return 1;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Where npm would put a global install, and whether it can be written to.
|
|
253
|
+
*
|
|
254
|
+
* Asked of npm rather than derived from `process.execPath`, which is wrong on Homebrew: the node
|
|
255
|
+
* binary resolves into `Cellar/node/<version>` while the prefix people actually install into is
|
|
256
|
+
* `/opt/homebrew`. Printing the first one at an agent is worse than printing nothing, because it
|
|
257
|
+
* looks authoritative.
|
|
258
|
+
*/
|
|
259
|
+
async function npmPrefix(): Promise<{ path: string; writable: boolean | null } | null> {
|
|
260
|
+
const path = await new Promise<string | null>((resolve) => {
|
|
261
|
+
const child = spawn("npm", ["config", "get", "prefix"], { stdio: ["ignore", "pipe", "ignore"] });
|
|
262
|
+
let out = "";
|
|
263
|
+
child.stdout?.on("data", (chunk: Buffer) => (out += chunk.toString("utf8")));
|
|
264
|
+
child.on("error", () => resolve(null));
|
|
265
|
+
child.on("close", (code: number | null) => resolve(code === 0 && out.trim() ? out.trim() : null));
|
|
266
|
+
});
|
|
267
|
+
if (!path) return null;
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
accessSync(join(path, "lib", "node_modules"), constants.W_OK);
|
|
271
|
+
return { path, writable: true };
|
|
272
|
+
} catch (error) {
|
|
273
|
+
return { path, writable: (error as NodeJS.ErrnoException).code === "ENOENT" ? null : false };
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
/** Whether a command is already on PATH, without shelling out to `which`. */
|
|
278
|
+
function onPath(command: string): boolean {
|
|
279
|
+
const parts = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
|
|
280
|
+
const names = process.platform === "win32" ? [command + ".cmd", command + ".exe"] : [command];
|
|
281
|
+
return parts.some((part) => names.some((name) => existsSync(join(part, name))));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* The newest published version, or null.
|
|
286
|
+
*
|
|
287
|
+
* Two seconds and then give up: this is a convenience, and someone on a plane setting up a runner
|
|
288
|
+
* should not wait on a registry that is not going to answer.
|
|
289
|
+
*/
|
|
290
|
+
async function latestVersion(): Promise<string | null> {
|
|
291
|
+
try {
|
|
292
|
+
const response = await fetch("https://registry.npmjs.org/@aloud/runner/latest", {
|
|
293
|
+
signal: AbortSignal.timeout(2_000),
|
|
294
|
+
});
|
|
295
|
+
if (!response.ok) return null;
|
|
296
|
+
const body = (await response.json()) as { version?: unknown };
|
|
297
|
+
return typeof body.version === "string" ? body.version : null;
|
|
298
|
+
} catch {
|
|
299
|
+
return null;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
162
303
|
/* --------------------------------- status --------------------------------- */
|
|
163
304
|
|
|
164
305
|
async function status(): Promise<number> {
|
package/src/version.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* package.json beside it to read, and importing one into the source trips the composite build's
|
|
11
11
|
* rootDir. `version.test.ts` asserts this matches, so the drift this invites cannot survive CI.
|
|
12
12
|
*/
|
|
13
|
-
export const RUNNER_VERSION = "0.2.
|
|
13
|
+
export const RUNNER_VERSION = "0.2.2";
|
|
14
14
|
|
|
15
15
|
/** The header the server reads it from. */
|
|
16
16
|
export const RUNNER_VERSION_HEADER = "x-aloud-runner-version";
|