@mostra/cli 0.3.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +1 -1
- package/dist/{flush-BCuWs0Zh.js → flush-CFJkvi2R.js} +140 -22
- package/dist/flush-CFJkvi2R.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/reference/cli.md +3 -3
- package/dist/reference/diagnostics.json +3 -3
- package/dist/reference/errors/MOSTRA_ENV_SERVICE_FAILED.md +2 -2
- package/dist/reference/errors/MOSTRA_HOME_NOT_ALWAYS_ON.md +1 -1
- package/dist/reference/telemetry.md +1 -1
- package/dist/{run-BJaRnz-Q.js → run-BpVu5uQ8.js} +152 -55
- package/dist/run-BpVu5uQ8.js.map +1 -0
- package/dist/run-CC8PNXLU.js +2 -0
- package/dist/telemetry/flush-main.js +1 -1
- package/package.json +5 -5
- package/src/browser.ts +72 -14
- package/src/client.ts +28 -27
- package/src/commands/admin.ts +19 -11
- package/src/commands/authoring.ts +3 -1
- package/src/commands/dev.ts +5 -5
- package/src/commands/interaction.ts +11 -0
- package/src/commands/viewing.ts +5 -2
- package/src/detached.ts +49 -0
- package/src/home.ts +36 -8
- package/src/platform.ts +24 -0
- package/src/run.ts +5 -7
- package/src/telemetry/events.ts +2 -2
- package/src/telemetry/flush.ts +7 -1
- package/src/telemetry/index.ts +4 -1
- package/src/telemetry/store.ts +117 -17
- package/dist/flush-BCuWs0Zh.js.map +0 -1
- package/dist/run-BJaRnz-Q.js.map +0 -1
- package/dist/run-CMyjOm0Q.js +0 -2
package/dist/bin.js
CHANGED
|
@@ -54,7 +54,7 @@ async function start() {
|
|
|
54
54
|
},
|
|
55
55
|
stderrTty: process.stderr.isTTY === true,
|
|
56
56
|
terminal: process.stdin.isTTY === true || process.stdout.isTTY === true || process.stderr.isTTY === true
|
|
57
|
-
}) ?? await (await import("./run-
|
|
57
|
+
}) ?? await (await import("./run-CC8PNXLU.js")).run({
|
|
58
58
|
argv: process.argv.slice(2),
|
|
59
59
|
env: process.env,
|
|
60
60
|
cwd: process.cwd(),
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
-
import { chmodSync, closeSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
2
|
+
import { chmodSync, closeSync, fstatSync, linkSync, mkdirSync, openSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, join, parse, resolve } from "node:path";
|
|
4
4
|
import { DIAGNOSTIC_AREAS } from "@mostra/protocol";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import * as z from "zod";
|
|
@@ -214,7 +214,11 @@ const common = {
|
|
|
214
214
|
at: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00Z$/),
|
|
215
215
|
installId: z.uuid(),
|
|
216
216
|
version: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/),
|
|
217
|
-
os: z.enum([
|
|
217
|
+
os: z.enum([
|
|
218
|
+
"darwin",
|
|
219
|
+
"linux",
|
|
220
|
+
"win32"
|
|
221
|
+
]),
|
|
218
222
|
arch: z.string().regex(/^[a-z0-9]{2,12}$/),
|
|
219
223
|
node: z.int().min(0).max(999),
|
|
220
224
|
harness: z.enum([
|
|
@@ -283,6 +287,48 @@ function catalogCodes(codes, known) {
|
|
|
283
287
|
};
|
|
284
288
|
}
|
|
285
289
|
//#endregion
|
|
290
|
+
//#region src/detached.ts
|
|
291
|
+
/**
|
|
292
|
+
* The directory and environment of the processes the CLI leaves running: its home's daemon and
|
|
293
|
+
* the telemetry flusher. They run in their file system's root, because on Windows nobody can
|
|
294
|
+
* rename or remove a running process's working directory, and they outlive the command (WP-W2,
|
|
295
|
+
* D151 item 3). Every path the environment names relative to the command's directory is made
|
|
296
|
+
* absolute first, so it still names what it named for the command (Opus review of WP-W2,
|
|
297
|
+
* finding 3).
|
|
298
|
+
*/
|
|
299
|
+
/**
|
|
300
|
+
* The variables whose values are paths: Mostra's own, as the runtime, the SDK and the CLI read
|
|
301
|
+
* them. A test keeps the list complete.
|
|
302
|
+
*/
|
|
303
|
+
const PATH_VARIABLES = [
|
|
304
|
+
"MOSTRA_HOME",
|
|
305
|
+
"MOSTRA_RUNTIME_DIR",
|
|
306
|
+
"MOSTRA_CONFIG_DIR",
|
|
307
|
+
"MOSTRA_CACHE_DIR",
|
|
308
|
+
"MOSTRA_BUNDLE",
|
|
309
|
+
"MOSTRA_RUNTIME_BIN",
|
|
310
|
+
"MOSTRA_SHELL_DIR",
|
|
311
|
+
"MOSTRA_WARM_DIR",
|
|
312
|
+
"MOSTRA_WORKER_ENTRY",
|
|
313
|
+
"MOSTRA_VERIFY_ENTRY",
|
|
314
|
+
"MOSTRA_WIN_HELPER",
|
|
315
|
+
"MOSTRA_TEST_WORKER_HOST",
|
|
316
|
+
"MOSTRA_TEST_BUNDLE_PINS"
|
|
317
|
+
];
|
|
318
|
+
/** `env` with every relative path variable resolved against `cwd`. */
|
|
319
|
+
function detachedEnvironment(env, cwd = process.cwd()) {
|
|
320
|
+
const result = { ...env };
|
|
321
|
+
for (const name of PATH_VARIABLES) {
|
|
322
|
+
const value = result[name];
|
|
323
|
+
if (value !== void 0 && value !== "" && !isAbsolute(value)) result[name] = resolve(cwd, value);
|
|
324
|
+
}
|
|
325
|
+
return result;
|
|
326
|
+
}
|
|
327
|
+
/** The working directory of a process the CLI leaves running: the root of `anchor`'s file system. */
|
|
328
|
+
function detachedDirectory(anchor) {
|
|
329
|
+
return parse(resolve(anchor)).root;
|
|
330
|
+
}
|
|
331
|
+
//#endregion
|
|
286
332
|
//#region src/telemetry/store.ts
|
|
287
333
|
/**
|
|
288
334
|
* Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,
|
|
@@ -312,8 +358,11 @@ var TelemetryStore = class {
|
|
|
312
358
|
queueFile;
|
|
313
359
|
idFile;
|
|
314
360
|
#lockFile;
|
|
315
|
-
|
|
361
|
+
#platform;
|
|
362
|
+
/** `platform` is the system whose lock rules apply: this process's, but for tests. */
|
|
363
|
+
constructor(dir, platform = process.platform) {
|
|
316
364
|
this.dir = dir;
|
|
365
|
+
this.#platform = platform;
|
|
317
366
|
this.settingsFile = join(dir, "telemetry.json");
|
|
318
367
|
this.queueFile = join(dir, "telemetry-queue.ndjson");
|
|
319
368
|
this.idFile = join(dir, "telemetry-id");
|
|
@@ -321,6 +370,10 @@ var TelemetryStore = class {
|
|
|
321
370
|
}
|
|
322
371
|
/** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */
|
|
323
372
|
read() {
|
|
373
|
+
if (blocked(this.dir)) return {
|
|
374
|
+
ok: false,
|
|
375
|
+
file: this.dir
|
|
376
|
+
};
|
|
324
377
|
const id = this.#readId();
|
|
325
378
|
if (id === null) return {
|
|
326
379
|
ok: false,
|
|
@@ -490,19 +543,20 @@ var TelemetryStore = class {
|
|
|
490
543
|
}
|
|
491
544
|
const temporary = `${this.queueFile}.${process.pid}.tmp`;
|
|
492
545
|
writeFileSync(temporary, `${lines.join("\n")}\n`, { mode: 384 });
|
|
493
|
-
|
|
546
|
+
replace(temporary, this.queueFile, this.#platform);
|
|
494
547
|
}
|
|
495
548
|
#write(settings) {
|
|
496
549
|
const { installId: _id, ...stored } = settings;
|
|
497
550
|
const temporary = `${this.settingsFile}.${process.pid}.tmp`;
|
|
498
551
|
writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 384 });
|
|
499
|
-
|
|
552
|
+
replace(temporary, this.settingsFile, this.#platform);
|
|
500
553
|
}
|
|
501
554
|
#prepareDir() {
|
|
502
555
|
mkdirSync(this.dir, {
|
|
503
556
|
recursive: true,
|
|
504
557
|
mode: 448
|
|
505
558
|
});
|
|
559
|
+
if (this.#platform === "win32") return;
|
|
506
560
|
try {
|
|
507
561
|
chmodSync(this.dir, 448);
|
|
508
562
|
} catch {}
|
|
@@ -530,9 +584,14 @@ var TelemetryStore = class {
|
|
|
530
584
|
});
|
|
531
585
|
break;
|
|
532
586
|
} catch (error) {
|
|
533
|
-
|
|
534
|
-
if (this.#
|
|
587
|
+
const code = error.code;
|
|
588
|
+
if (!held(code, this.#platform)) return void 0;
|
|
535
589
|
if (Date.now() > deadline) return void 0;
|
|
590
|
+
const next = this.#breakStale();
|
|
591
|
+
if (next === "absent") {
|
|
592
|
+
if (code === "EEXIST") continue;
|
|
593
|
+
if (!writable(this.dir)) return void 0;
|
|
594
|
+
} else if (next === "broken") continue;
|
|
536
595
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
537
596
|
}
|
|
538
597
|
try {
|
|
@@ -550,9 +609,11 @@ var TelemetryStore = class {
|
|
|
550
609
|
} catch {}
|
|
551
610
|
}
|
|
552
611
|
/**
|
|
553
|
-
* Takes a crashed owner's lock away
|
|
554
|
-
*
|
|
555
|
-
*
|
|
612
|
+
* Takes a crashed owner's lock away. The lock is moved aside under a name of this process's own
|
|
613
|
+
* (a rename is atomic, so of two processes breaking it one gets it), and a lock that turns out
|
|
614
|
+
* to be another, live owner's goes back. Answers `broken` when the caller should try again at
|
|
615
|
+
* once, `absent` when no lock file exists, and `held` when the caller should wait: a live
|
|
616
|
+
* owner's lock, or one that can't be read (on Windows, a lock being deleted).
|
|
556
617
|
*/
|
|
557
618
|
#breakStale() {
|
|
558
619
|
let seen;
|
|
@@ -561,28 +622,84 @@ var TelemetryStore = class {
|
|
|
561
622
|
try {
|
|
562
623
|
handle = openSync(this.#lockFile, "r");
|
|
563
624
|
age = Date.now() - fstatSync(handle).mtimeMs;
|
|
564
|
-
if (age <= STALE_LOCK_MS) return
|
|
625
|
+
if (age <= STALE_LOCK_MS) return "held";
|
|
565
626
|
seen = readFileSync(handle, "utf8");
|
|
566
|
-
} catch {
|
|
567
|
-
return
|
|
627
|
+
} catch (error) {
|
|
628
|
+
return error.code === "ENOENT" ? "absent" : "held";
|
|
568
629
|
} finally {
|
|
569
630
|
if (handle !== void 0) closeSync(handle);
|
|
570
631
|
}
|
|
571
|
-
if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return
|
|
632
|
+
if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return "held";
|
|
572
633
|
const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;
|
|
573
634
|
try {
|
|
574
635
|
renameSync(this.#lockFile, aside);
|
|
575
|
-
} catch {
|
|
576
|
-
return
|
|
636
|
+
} catch (error) {
|
|
637
|
+
return error.code === "ENOENT" ? "broken" : "held";
|
|
577
638
|
}
|
|
578
639
|
try {
|
|
579
640
|
if (readFileSync(aside, "utf8") !== seen) linkSync(aside, this.#lockFile);
|
|
580
641
|
} catch {} finally {
|
|
581
642
|
rmSync(aside, { force: true });
|
|
582
643
|
}
|
|
583
|
-
return
|
|
644
|
+
return "broken";
|
|
584
645
|
}
|
|
585
646
|
};
|
|
647
|
+
/**
|
|
648
|
+
* Renames `from` over `to`. Windows refuses to replace a file another process has open, as a
|
|
649
|
+
* process reading the settings outside the lock does, with `EPERM` (MT-01: CI's install-ID race,
|
|
650
|
+
* traced on `windows-latest`); the rename is tried again for up to a second, as the runtime's
|
|
651
|
+
* renames are (D150 item 8), and never turned into a copy.
|
|
652
|
+
*/
|
|
653
|
+
function replace(from, to, platform, rename = renameSync) {
|
|
654
|
+
const deadline = Date.now() + 1e3;
|
|
655
|
+
for (let pause = 5;; pause = Math.min(pause * 2, 100)) try {
|
|
656
|
+
rename(from, to);
|
|
657
|
+
return;
|
|
658
|
+
} catch (error) {
|
|
659
|
+
const code = error.code;
|
|
660
|
+
if (platform !== "win32" || !(code === "EPERM" || code === "EACCES" || code === "EBUSY") || Date.now() > deadline) throw error;
|
|
661
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pause);
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
/** Whether this process can create a file in `dir`: a probe of its own, removed at once. */
|
|
665
|
+
function writable(dir, remove = (path) => rmSync(path, { force: true })) {
|
|
666
|
+
const probe = join(dir, `.probe-${process.pid}-${randomUUID()}`);
|
|
667
|
+
try {
|
|
668
|
+
writeFileSync(probe, "", {
|
|
669
|
+
flag: "wx",
|
|
670
|
+
mode: 384
|
|
671
|
+
});
|
|
672
|
+
} catch {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
675
|
+
try {
|
|
676
|
+
remove(probe);
|
|
677
|
+
} catch {}
|
|
678
|
+
return true;
|
|
679
|
+
}
|
|
680
|
+
/**
|
|
681
|
+
* Whether a lock's exclusive create failed because another process holds it. On Windows a lock
|
|
682
|
+
* its owner just removed stays "delete pending" while anyone still has it open (another process
|
|
683
|
+
* judging its age), and creating it again meanwhile is refused as access denied, not `EEXIST`
|
|
684
|
+
* (WP-W2: twelve first runs at once on an arm64 runner): that is contention too, never a reason
|
|
685
|
+
* to give up.
|
|
686
|
+
*/
|
|
687
|
+
function held(code, platform) {
|
|
688
|
+
return code === "EEXIST" || platform === "win32" && (code === "EPERM" || code === "EACCES");
|
|
689
|
+
}
|
|
690
|
+
/** Whether `dir` can never be created: its nearest existing ancestor isn't a directory. */
|
|
691
|
+
function blocked(dir) {
|
|
692
|
+
for (let path = dir;; path = dirname(path)) {
|
|
693
|
+
try {
|
|
694
|
+
return !statSync(path).isDirectory();
|
|
695
|
+
} catch (error) {
|
|
696
|
+
const code = error.code;
|
|
697
|
+
if (code === "ENOTDIR") return true;
|
|
698
|
+
if (code !== "ENOENT") return false;
|
|
699
|
+
}
|
|
700
|
+
if (dirname(path) === path) return false;
|
|
701
|
+
}
|
|
702
|
+
}
|
|
586
703
|
/** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */
|
|
587
704
|
function ownerAlive(lock) {
|
|
588
705
|
const pid = Number(lock.split(" ")[0]);
|
|
@@ -703,16 +820,17 @@ function startFlusher(store, env, now) {
|
|
|
703
820
|
], {
|
|
704
821
|
detached: true,
|
|
705
822
|
stdio: "ignore",
|
|
706
|
-
|
|
823
|
+
cwd: detachedDirectory(store.dir),
|
|
824
|
+
env: detachedEnvironment({
|
|
707
825
|
...process.env,
|
|
708
826
|
...env
|
|
709
|
-
}
|
|
827
|
+
})
|
|
710
828
|
});
|
|
711
829
|
child.on("error", () => {});
|
|
712
830
|
child.unref();
|
|
713
831
|
} catch {}
|
|
714
832
|
}
|
|
715
833
|
//#endregion
|
|
716
|
-
export {
|
|
834
|
+
export { detachedEnvironment as a, durationBucket as c, buildEndpoint as d, decide as f, packageRoot as g, fromSource as h, detachedDirectory as i, minute as l, cliVersion as m, startFlusher as n, TelemetryEvent as o, truthy as p, TelemetryStore as r, catalogCodes as s, flush as t, detectHarness as u };
|
|
717
835
|
|
|
718
|
-
//# sourceMappingURL=flush-
|
|
836
|
+
//# sourceMappingURL=flush-CFJkvi2R.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flush-CFJkvi2R.js","names":["httpsRequest","httpRequest"],"sources":["../src/version.ts","../src/telemetry/decision.ts","../src/telemetry/harness.ts","../src/telemetry/events.ts","../src/detached.ts","../src/telemetry/store.ts","../src/telemetry/flush.ts"],"sourcesContent":["/** The CLI's own package: its root directory and its release. */\nimport { readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\n\nlet found: { readonly root: string; readonly version: string } | undefined;\n\nfunction own(): { readonly root: string; readonly version: string } {\n if (found) return found;\n let dir = dirname(fileURLToPath(import.meta.url));\n for (;;) {\n try {\n const manifest = JSON.parse(readFileSync(join(dir, \"package.json\"), \"utf8\")) as {\n name?: string;\n version?: string;\n };\n if (manifest.name === \"@mostra/cli\" && manifest.version) {\n found = { root: dir, version: manifest.version };\n return found;\n }\n } catch {\n // Keep looking upwards.\n }\n const parent = dirname(dir);\n if (parent === dir) throw new Error(\"Couldn't find @mostra/cli's package.json.\");\n dir = parent;\n }\n}\n\n/** This CLI's version (`mostra --version`, the client version it reports, telemetry's `version`). */\nexport function cliVersion(): string {\n return own().version;\n}\n\n/** The CLI package's directory: its `dist/`, `src/` and bundled reference live under it. */\nexport function packageRoot(): string {\n return own().root;\n}\n\n/** Whether this CLI runs from its TypeScript sources (tests), not its build. */\nexport function fromSource(): boolean {\n return import.meta.url.endsWith(\".ts\");\n}\n","/**\n * Whether telemetry sends, and why (D140 item 3). The first match decides, so an explicit \"off\"\n * anywhere wins over any \"on\", and withdrawn consent sticks.\n */\nimport type { Environment } from \"../home.ts\";\nimport type { ReadSettings } from \"./store.ts\";\nimport type { TelemetryReason } from \"./words.ts\";\n\n/** The endpoint this build sends to, set only by the release workflow at bundle time (D140 item 7). */\ndeclare const __MOSTRA_TELEMETRY_ENDPOINT__: string | undefined;\n\n/** The build's endpoint: empty in this package and in every development build. */\nexport function buildEndpoint(): string {\n return typeof __MOSTRA_TELEMETRY_ENDPOINT__ === \"string\" ? __MOSTRA_TELEMETRY_ENDPOINT__ : \"\";\n}\n\n/** What the decision reads. */\nexport interface DecisionInputs {\n readonly env: Environment;\n /** The build's endpoint ({@link buildEndpoint}); tests pass their own. */\n readonly buildEndpoint: string;\n /** Whether this use is embedded: a host-spawned instance, or a home a host configures. */\n readonly embedded: boolean;\n readonly settings: ReadSettings;\n /** Whether this machine's time zone asks first. */\n readonly askFirst: boolean;\n}\n\n/** The decision. */\nexport interface TelemetryDecision {\n readonly enabled: boolean;\n readonly reason: TelemetryReason;\n readonly askFirst: boolean;\n /** The endpoint events go to, when there is one. */\n readonly endpoint?: string;\n /** For an enabled decision: whether the person said yes, or it is the default. */\n readonly consent?: \"explicit\" | \"default\";\n}\n\nconst OFF_VALUES = new Set([\"0\", \"false\", \"off\", \"no\"]);\nconst ON_VALUES = new Set([\"1\", \"true\", \"on\", \"yes\"]);\n\n/** `DO_NOT_TRACK` and `MOSTRA_OFFLINE`'s parser: set to anything but empty, `0` or `false`. */\nexport function truthy(value: string | undefined): boolean {\n if (value === undefined) return false;\n const normalized = value.trim().toLowerCase();\n return normalized !== \"\" && normalized !== \"0\" && normalized !== \"false\";\n}\n\n/**\n * The endpoint in use: `MOSTRA_TELEMETRY_ENDPOINT` when it is `https:` or `http:` to a loopback\n * address, else the build's; `undefined` when there is none (D140 item 7).\n */\nexport function endpointOf(env: Environment, build: string): string | undefined {\n const override = env[\"MOSTRA_TELEMETRY_ENDPOINT\"];\n const chosen = override !== undefined && override !== \"\" ? override : build;\n if (chosen === \"\") return undefined;\n let url: URL;\n try {\n url = new URL(chosen);\n } catch {\n return undefined;\n }\n if (url.username !== \"\" || url.password !== \"\") return undefined;\n if (url.protocol === \"https:\") return url.href;\n const loopback =\n url.hostname === \"127.0.0.1\" || url.hostname === \"[::1]\" || url.hostname === \"localhost\";\n return url.protocol === \"http:\" && loopback ? url.href : undefined;\n}\n\n/** Decides, in D140's order. */\nexport function decide(inputs: DecisionInputs): TelemetryDecision {\n const { askFirst } = inputs;\n const off = (reason: TelemetryReason): TelemetryDecision => ({\n enabled: false,\n reason,\n askFirst,\n });\n const endpoint = endpointOf(inputs.env, inputs.buildEndpoint);\n if (endpoint === undefined) return off(\"no-collector\");\n const at = { askFirst, endpoint };\n if (inputs.embedded) return { ...at, enabled: false, reason: \"embedded\" };\n if (truthy(inputs.env[\"DO_NOT_TRACK\"])) return { ...at, enabled: false, reason: \"do-not-track\" };\n const variable = inputs.env[\"MOSTRA_TELEMETRY\"];\n const normalized = variable?.trim().toLowerCase();\n if (normalized !== undefined && OFF_VALUES.has(normalized)) {\n return { ...at, enabled: false, reason: \"variable\" };\n }\n if (!inputs.settings.ok) return { ...at, enabled: false, reason: \"unreadable\" };\n const choice = inputs.settings.settings.choice;\n if (choice === \"off\") return { ...at, enabled: false, reason: \"setting\" };\n if (normalized !== undefined && normalized !== \"\") {\n return ON_VALUES.has(normalized)\n ? { ...at, enabled: true, reason: \"variable\", consent: \"explicit\" }\n : { ...at, enabled: false, reason: \"variable-invalid\" };\n }\n if (choice === \"on\") return { ...at, enabled: true, reason: \"setting\", consent: \"explicit\" };\n return askFirst\n ? { ...at, enabled: false, reason: \"default\" }\n : { ...at, enabled: true, reason: \"default\", consent: \"default\" };\n}\n","/**\n * Which agent ran the CLI (D140 item 9), from environment markers each agent documents or sets in\n * its source (VERIFIED_FACTS, WP-10). Only the agent's ID is ever sent, never a marker's value.\n */\nimport type { Environment } from \"../home.ts\";\n\n/** The agents Mostra recognizes, in the order their markers are checked. */\nexport const HARNESSES = [\"claude-code\", \"codex\", \"gemini-cli\", \"opencode\", \"cursor\"] as const;\n\n/** A recognized agent, `other` for an agent Mostra doesn't know, `none` for no agent at all. */\nexport type Harness = (typeof HARNESSES)[number] | \"other\" | \"none\";\n\n/** Each agent's own marker. */\nconst MARKERS: Readonly<Record<(typeof HARNESSES)[number], string>> = {\n \"claude-code\": \"CLAUDECODE\",\n codex: \"CODEX_THREAD_ID\",\n \"gemini-cli\": \"GEMINI_CLI\",\n opencode: \"OPENCODE\",\n cursor: \"CURSOR_AGENT\",\n};\n\nconst set = (value: string | undefined): boolean => value !== undefined && value !== \"\";\n\n/**\n * The agent that ran this process: `AI_AGENT` when it starts with a known ID (the convention\n * `@vercel/detect-agent` reads), else the first agent whose own marker is set, else `other` when\n * `AI_AGENT` or `AGENT` names something else, else `none`. Cursor's editor terminal sets\n * `CURSOR_TRACE_ID` without `CURSOR_AGENT`: that is a person, so it isn't `cursor`.\n */\nexport function detectHarness(env: Environment): Harness {\n const declared = env[\"AI_AGENT\"];\n if (set(declared)) {\n const known = HARNESSES.find((id) => declared?.startsWith(id));\n if (known) return known;\n }\n for (const id of HARNESSES) if (set(env[MARKERS[id]])) return id;\n if (set(declared) || set(env[\"AGENT\"])) return \"other\";\n return \"none\";\n}\n","/**\n * Telemetry's event schema 1 (D140 item 8), the one published in `docs/reference/telemetry.md`.\n * Every event is checked against it before it is queued and again before it is sent, so nothing\n * that isn't on that page can leave the machine: the objects are strict, every string is from a\n * closed set or a pattern, and no field takes free text.\n */\nimport { DIAGNOSTIC_AREAS } from \"@mostra/protocol\";\nimport * as z from \"zod\";\nimport { HARNESSES, type Harness } from \"./harness.ts\";\n\n/** The schema version every event carries. */\nexport const SCHEMA = 1;\n\n/** The duration buckets of `cli.command`. */\nexport const DURATIONS = [\"<1s\", \"1-5s\", \"5-30s\", \"30s-2m\", \"2-10m\", \">10m\"] as const;\n\n/** A command's duration bucket. */\nexport function durationBucket(ms: number): (typeof DURATIONS)[number] {\n if (ms < 1000) return \"<1s\";\n if (ms < 5000) return \"1-5s\";\n if (ms < 30_000) return \"5-30s\";\n if (ms < 120_000) return \"30s-2m\";\n if (ms < 600_000) return \"2-10m\";\n return \">10m\";\n}\n\n/** A catalog-shaped diagnostic code: `MOSTRA_<AREA>_<NAME>`, never a message. */\nconst code = z\n .string()\n .regex(new RegExp(`^MOSTRA_(${DIAGNOSTIC_AREAS.join(\"|\")})_[A-Z0-9_]{1,64}$`));\n\n/** A command path from the definitions: lowercase words. */\nconst command = z.string().regex(/^[a-z]+( [a-z]+)?$/);\n\nconst common = {\n schema: z.literal(SCHEMA),\n at: z.string().regex(/^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:00Z$/),\n installId: z.uuid(),\n version: z.string().regex(/^\\d+\\.\\d+\\.\\d+(-[0-9A-Za-z.-]+)?$/),\n os: z.enum([\"darwin\", \"linux\", \"win32\"]),\n arch: z.string().regex(/^[a-z0-9]{2,12}$/),\n node: z.int().min(0).max(999),\n harness: z.enum([...HARNESSES, \"other\", \"none\"]),\n ci: z.boolean(),\n consent: z.enum([\"explicit\", \"default\"]),\n};\n\n/** The fields every event carries. */\nexport interface CommonFields {\n readonly schema: typeof SCHEMA;\n readonly at: string;\n readonly installId: string;\n readonly version: string;\n readonly os: \"darwin\" | \"linux\" | \"win32\";\n readonly arch: string;\n readonly node: number;\n readonly harness: Harness;\n readonly ci: boolean;\n readonly consent: \"explicit\" | \"default\";\n}\n\n/** One event, as sent (D140 item 8). */\nexport type TelemetryEvent = CommonFields &\n (\n | { readonly event: \"cli.first_run\" }\n | {\n readonly event: \"cli.command\";\n readonly command: string;\n readonly exit: number;\n readonly duration: (typeof DURATIONS)[number];\n readonly json: boolean;\n readonly error?: string | undefined;\n }\n | {\n readonly event: \"verify.result\";\n readonly status: \"pass\" | \"fail\";\n readonly codes: readonly string[];\n readonly codesTruncated: boolean;\n }\n | { readonly event: \"publish.done\"; readonly reused: boolean; readonly forced: boolean }\n | {\n readonly event: \"share.done\";\n readonly kind: \"tailnet\" | \"funnel\" | \"cloudflare\" | \"remote\";\n }\n | {\n readonly event: \"mcp.session\";\n readonly client: (typeof HARNESSES)[number] | \"other\";\n readonly clientVersion?: string | undefined;\n }\n );\n\n/** Every event Mostra may send, as a strict union on `event`. */\nexport const TelemetryEvent: z.ZodType<TelemetryEvent> = z.discriminatedUnion(\"event\", [\n z.strictObject({ ...common, event: z.literal(\"cli.first_run\") }),\n z.strictObject({\n ...common,\n event: z.literal(\"cli.command\"),\n command,\n exit: z.int().min(0).max(255),\n duration: z.enum(DURATIONS),\n json: z.boolean(),\n error: code.optional(),\n }),\n z.strictObject({\n ...common,\n event: z.literal(\"verify.result\"),\n status: z.enum([\"pass\", \"fail\"]),\n codes: z.array(code).max(50),\n codesTruncated: z.boolean(),\n }),\n z.strictObject({\n ...common,\n event: z.literal(\"publish.done\"),\n reused: z.boolean(),\n forced: z.boolean(),\n }),\n // Emitted by WP-13 and WP-29.\n z.strictObject({\n ...common,\n event: z.literal(\"share.done\"),\n kind: z.enum([\"tailnet\", \"funnel\", \"cloudflare\", \"remote\"]),\n }),\n // Emitted by WP-15: a known agent's ID for the MCP client, or `other`; its version only then.\n z.strictObject({\n ...common,\n event: z.literal(\"mcp.session\"),\n client: z.enum([...HARNESSES, \"other\"]),\n clientVersion: z\n .string()\n .regex(/^[0-9A-Za-z.+-]{1,32}$/)\n .optional(),\n }),\n]);\n\n/** `at`: now in UTC, rounded down to the minute. */\nexport function minute(now: Date): string {\n return `${now.toISOString().slice(0, 16)}:00Z`;\n}\n\n/** The distinct catalog codes of `codes`, sorted, at most 50, and whether some were left out. */\nexport function catalogCodes(\n codes: readonly string[],\n known: ReadonlySet<string>,\n): { codes: string[]; codesTruncated: boolean } {\n const distinct = [...new Set(codes.filter((value) => known.has(value)))].sort();\n return { codes: distinct.slice(0, 50), codesTruncated: distinct.length > 50 };\n}\n","/**\n * The directory and environment of the processes the CLI leaves running: its home's daemon and\n * the telemetry flusher. They run in their file system's root, because on Windows nobody can\n * rename or remove a running process's working directory, and they outlive the command (WP-W2,\n * D151 item 3). Every path the environment names relative to the command's directory is made\n * absolute first, so it still names what it named for the command (Opus review of WP-W2,\n * finding 3).\n */\nimport { isAbsolute, parse, resolve } from \"node:path\";\n\n/**\n * The variables whose values are paths: Mostra's own, as the runtime, the SDK and the CLI read\n * them. A test keeps the list complete.\n */\nexport const PATH_VARIABLES: readonly string[] = [\n \"MOSTRA_HOME\",\n \"MOSTRA_RUNTIME_DIR\",\n \"MOSTRA_CONFIG_DIR\",\n \"MOSTRA_CACHE_DIR\",\n \"MOSTRA_BUNDLE\",\n \"MOSTRA_RUNTIME_BIN\",\n \"MOSTRA_SHELL_DIR\",\n \"MOSTRA_WARM_DIR\",\n \"MOSTRA_WORKER_ENTRY\",\n \"MOSTRA_VERIFY_ENTRY\",\n \"MOSTRA_WIN_HELPER\",\n \"MOSTRA_TEST_WORKER_HOST\",\n \"MOSTRA_TEST_BUNDLE_PINS\",\n];\n\n/** `env` with every relative path variable resolved against `cwd`. */\nexport function detachedEnvironment(\n env: Readonly<Record<string, string | undefined>>,\n cwd: string = process.cwd(),\n): Record<string, string | undefined> {\n const result: Record<string, string | undefined> = { ...env };\n for (const name of PATH_VARIABLES) {\n const value = result[name];\n if (value !== undefined && value !== \"\" && !isAbsolute(value)) {\n result[name] = resolve(cwd, value);\n }\n }\n return result;\n}\n\n/** The working directory of a process the CLI leaves running: the root of `anchor`'s file system. */\nexport function detachedDirectory(anchor: string): string {\n return parse(resolve(anchor)).root;\n}\n","/**\n * Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,\n * when the notice was shown, when a flush last started), `telemetry-id` (the random install ID)\n * and `telemetry-queue.ndjson` (events waiting to be sent). The directory is 0700 and the files\n * 0600; `telemetry.json` and the queue are written by rename. The install ID is made by an\n * exclusive create (a hard link that fails when the file exists), so processes on a first run\n * agree on one ID whatever happens to the lock. Every other change goes through one lock file,\n * owned by a token and taken over only when its holder has died, so a flusher never loses an\n * event appended while it sends.\n */\nimport { randomUUID } from \"node:crypto\";\nimport {\n chmodSync,\n closeSync,\n fstatSync,\n linkSync,\n mkdirSync,\n openSync,\n readFileSync,\n renameSync,\n rmSync,\n statSync,\n writeFileSync,\n} from \"node:fs\";\nimport { dirname, join } from \"node:path\";\n\n/** What `telemetry.json` holds. */\nexport interface TelemetrySettings {\n /** The person's choice: `mostra telemetry on|off`, or their answer to the question. */\n readonly choice?: \"on\" | \"off\";\n /**\n * The random install ID, made on the first run that sends; deleted when telemetry turns off.\n * Read from `telemetry-id`, never written into `telemetry.json`.\n */\n readonly installId?: string;\n /**\n * When the notice of what is sent was shown where sending follows: the default notice, the\n * question, or `mostra telemetry on`'s. A `default`-consent event needs it (Opus review\n * finding 3).\n */\n readonly noticeShownAt?: string;\n /**\n * When the ask-first notice (\"Nothing is sent unless you run: mostra telemetry on\") was\n * shown. It never stands for the default notice: a later run where telemetry is on by default\n * still shows that one before anything is sent.\n */\n readonly askFirstNoticeShownAt?: string;\n /** When a flush last started. */\n readonly lastFlushAt?: string;\n}\n\n/** The settings, or why they can't be read (which means off, D140 item 5). */\nexport type ReadSettings =\n | { readonly ok: true; readonly settings: TelemetrySettings }\n | { readonly ok: false; readonly file: string };\n\n/** A lock older than this whose owner no longer runs is taken over as stale. */\nconst STALE_LOCK_MS = 10_000;\n/**\n * A lock older than this is taken over whoever it names: a held section takes milliseconds, and\n * a PID can name an unrelated live process once the owner died (in a container, PID 1 always\n * runs), which would otherwise hold every later command up forever (Opus review finding 5).\n */\nconst DEAD_LOCK_MS = 60_000;\n/** The queue's bounds (D140 item 6). */\nexport const QUEUE_MAX_EVENTS = 1000;\nexport const QUEUE_MAX_AGE_MS: number = 7 * 24 * 60 * 60 * 1000;\n\nconst UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;\n\n/** Telemetry's files in one config directory. */\nexport class TelemetryStore {\n readonly dir: string;\n readonly settingsFile: string;\n readonly queueFile: string;\n readonly idFile: string;\n readonly #lockFile: string;\n readonly #platform: string;\n\n /** `platform` is the system whose lock rules apply: this process's, but for tests. */\n constructor(dir: string, platform: string = process.platform) {\n this.dir = dir;\n this.#platform = platform;\n this.settingsFile = join(dir, \"telemetry.json\");\n this.queueFile = join(dir, \"telemetry-queue.ndjson\");\n this.idFile = join(dir, \"telemetry-id\");\n this.#lockFile = join(dir, \"telemetry.lock\");\n }\n\n /** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */\n read(): ReadSettings {\n // A directory that can never exist (a part of its path is a file) is unreadable, never\n // \"nothing stored yet\". POSIX says so for every read (ENOTDIR); Windows answers ENOENT, which\n // would read as the default, on, though nothing could ever be recorded (WP-W2).\n if (blocked(this.dir)) return { ok: false, file: this.dir };\n const id = this.#readId();\n if (id === null) return { ok: false, file: this.idFile };\n let text: string;\n try {\n text = readFileSync(this.settingsFile, \"utf8\");\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code === \"ENOENT\")\n return { ok: true, settings: id !== undefined ? { installId: id } : {} };\n return { ok: false, file: this.settingsFile };\n }\n try {\n const data = JSON.parse(text) as Record<string, unknown>;\n if (typeof data !== \"object\" || data === null || Array.isArray(data)) throw new Error();\n const { choice, noticeShownAt, askFirstNoticeShownAt, lastFlushAt } = data;\n const installId = id;\n if (choice !== undefined && choice !== \"on\" && choice !== \"off\") throw new Error();\n // An install ID belongs in `telemetry-id`; one here is a file Mostra didn't write.\n if (data[\"installId\"] !== undefined) throw new Error();\n if (noticeShownAt !== undefined && typeof noticeShownAt !== \"string\") throw new Error();\n if (askFirstNoticeShownAt !== undefined && typeof askFirstNoticeShownAt !== \"string\")\n throw new Error();\n if (lastFlushAt !== undefined && typeof lastFlushAt !== \"string\") throw new Error();\n return {\n ok: true,\n settings: {\n ...(choice !== undefined ? { choice } : {}),\n ...(installId !== undefined ? { installId } : {}),\n ...(noticeShownAt !== undefined ? { noticeShownAt } : {}),\n ...(askFirstNoticeShownAt !== undefined ? { askFirstNoticeShownAt } : {}),\n ...(lastFlushAt !== undefined ? { lastFlushAt } : {}),\n },\n };\n } catch {\n return { ok: false, file: this.settingsFile };\n }\n }\n\n /**\n * Changes the settings under the lock and returns what they became; `undefined` when the file\n * can't be read (nothing is written over it).\n */\n update(change: (current: TelemetrySettings) => TelemetrySettings): TelemetrySettings | undefined {\n return this.locked(() => {\n const current = this.read();\n if (!current.ok) return undefined;\n const next = change(current.settings);\n this.#write(next);\n return next;\n });\n }\n\n /**\n * The install ID, made when there is none yet. A new ID is written to a file of this process's\n * own, then hard-linked to `telemetry-id`, which fails when the file exists: of any processes\n * making one at once exactly one wins, and the others read the winner's. `undefined` when the\n * settings can't be read.\n */\n installId(): { readonly id: string; readonly created: boolean } | undefined {\n const current = this.read();\n if (!current.ok) return undefined;\n if (current.settings.installId !== undefined)\n return { id: current.settings.installId, created: false };\n const candidate = `${this.idFile}.${process.pid}.${randomUUID()}.tmp`;\n const id = randomUUID();\n try {\n this.#prepareDir();\n writeFileSync(candidate, `${id}\\n`, { mode: 0o600 });\n linkSync(candidate, this.idFile);\n return { id, created: true };\n } catch (error) {\n if ((error as NodeJS.ErrnoException).code !== \"EEXIST\") return undefined;\n const winner = this.#readId();\n return winner ? { id: winner, created: false } : undefined;\n } finally {\n rmSync(candidate, { force: true });\n }\n }\n\n /** The install ID in `telemetry-id`: `undefined` when there is none, `null` when it's damaged. */\n #readId(): string | undefined | null {\n let text: string;\n try {\n text = readFileSync(this.idFile, \"utf8\");\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? undefined : null;\n }\n const id = text.trim();\n return UUID.test(id) ? id : null;\n }\n\n /**\n * Replaces a file that can't be read with these settings; only `telemetry on|off` do, as the\n * person's explicit choice. Call it holding the lock.\n */\n reset(settings: TelemetrySettings): TelemetrySettings {\n const { installId: _id, ...rest } = settings;\n this.#write(rest);\n // A damaged install ID goes too; `on` makes a new one on its first send.\n rmSync(this.idFile, { force: true });\n if (settings.choice === \"off\") rmSync(this.queueFile, { force: true });\n return rest;\n }\n\n /** Turns telemetry off: the choice, and the install ID and the queue deleted at once. */\n turnOff(): boolean {\n return (\n this.locked(() => {\n const current = this.read();\n if (!current.ok) return false;\n const { installId: _id, ...rest } = current.settings;\n this.#write({ ...rest, choice: \"off\" });\n rmSync(this.idFile, { force: true });\n rmSync(this.queueFile, { force: true });\n return true;\n }) ?? false\n );\n }\n\n /** Appends events to the queue, keeping its bounds. */\n append(lines: readonly string[], now: number): void {\n if (lines.length === 0) return;\n this.locked(() => {\n const kept = bounded([...this.#queued(), ...lines], now);\n this.#writeQueue(kept);\n });\n }\n\n /** The queued events, within their bounds. */\n queued(now: number): string[] {\n return bounded(this.#queued(), now);\n }\n\n /**\n * Removes the lines a flush sent from the head of the queue, after pruning what expired or\n * fell outside the bounds, as {@link queued} did; how many sent lines were removed (0 when the\n * lock can't be had). A flusher stops when this is 0, so it never sends a batch twice.\n */\n drop(sent: readonly string[], now: number): number {\n return (\n this.locked(() => {\n const queued = bounded(this.#queued(), now);\n let index = 0;\n while (index < sent.length && queued[index] === sent[index]) index += 1;\n this.#writeQueue(queued.slice(index));\n return index;\n }) ?? 0\n );\n }\n\n #queued(): string[] {\n try {\n return readFileSync(this.queueFile, \"utf8\")\n .split(\"\\n\")\n .filter((line) => line.length > 0);\n } catch {\n return [];\n }\n }\n\n #writeQueue(lines: readonly string[]): void {\n if (lines.length === 0) {\n rmSync(this.queueFile, { force: true });\n return;\n }\n const temporary = `${this.queueFile}.${process.pid}.tmp`;\n writeFileSync(temporary, `${lines.join(\"\\n\")}\\n`, { mode: 0o600 });\n replace(temporary, this.queueFile, this.#platform);\n }\n\n #write(settings: TelemetrySettings): void {\n const { installId: _id, ...stored } = settings;\n const temporary = `${this.settingsFile}.${process.pid}.tmp`;\n writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\\n`, { mode: 0o600 });\n replace(temporary, this.settingsFile, this.#platform);\n }\n\n #prepareDir(): void {\n mkdirSync(this.dir, { recursive: true, mode: 0o700 });\n // Windows has no modes: the directory keeps the access list `%APPDATA%` hands down.\n if (this.#platform === \"win32\") return;\n try {\n chmodSync(this.dir, 0o700);\n } catch {\n // A directory someone else owns: the files below fail or succeed on their own terms.\n }\n }\n\n /**\n * Runs `fn` holding the lock file; `undefined` when the lock can't be had within 2 s, or the\n * directory or a file in it can't be written (a full disk, a directory someone else owns):\n * telemetry never stops a command (D140 item 5). The lock\n * holds its owner's PID and a token of its own, is released only by its owner, and is taken\n * over when it is older than 10 s and its owner is no longer running (a crash), or older than\n * 60 s whoever it names.\n */\n locked<T>(fn: () => T): T | undefined {\n try {\n this.#prepareDir();\n } catch {\n return undefined;\n }\n const token = `${process.pid} ${randomUUID()}`;\n const deadline = Date.now() + 2000;\n for (;;) {\n try {\n writeFileSync(this.#lockFile, token, { flag: \"wx\", mode: 0o600 });\n break;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (!held(code, this.#platform)) return undefined;\n // Every path back to the create checks the deadline, so nothing can hold the CLI here\n // (GPT review of WP-W2), and only a lock that just went is tried again without a pause,\n // so nothing spins either (Opus review of WP-W2, finding 2).\n if (Date.now() > deadline) return undefined;\n const next = this.#breakStale();\n if (next === \"absent\") {\n // No lock there, though the create was refused. After `EEXIST` it was removed in\n // between: try again. After access denied, either a lock Windows was deleting went in\n // between (twelve processes at once keep that happening), or this user can't write the\n // directory and waiting changes nothing (Opus review of WP-W2, finding 2). A file of\n // this process's own tells which, whatever the timing (MT-01: a count of such rounds\n // in a row still gave up under contention).\n if (code === \"EEXIST\") continue;\n if (!writable(this.dir)) return undefined;\n } else if (next === \"broken\") continue;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);\n }\n }\n try {\n return fn();\n } catch {\n return undefined;\n } finally {\n this.#release(token);\n }\n }\n\n /** Removes the lock when it is still this owner's. */\n #release(token: string): void {\n try {\n if (readFileSync(this.#lockFile, \"utf8\") === token) rmSync(this.#lockFile, { force: true });\n } catch {\n // Gone already.\n }\n }\n\n /**\n * Takes a crashed owner's lock away. The lock is moved aside under a name of this process's own\n * (a rename is atomic, so of two processes breaking it one gets it), and a lock that turns out\n * to be another, live owner's goes back. Answers `broken` when the caller should try again at\n * once, `absent` when no lock file exists, and `held` when the caller should wait: a live\n * owner's lock, or one that can't be read (on Windows, a lock being deleted).\n */\n #breakStale(): \"broken\" | \"absent\" | \"held\" {\n // One descriptor for the age and the contents, so both describe the same file.\n let seen: string;\n let age: number;\n let handle: number | undefined;\n try {\n handle = openSync(this.#lockFile, \"r\");\n age = Date.now() - fstatSync(handle).mtimeMs;\n if (age <= STALE_LOCK_MS) return \"held\";\n seen = readFileSync(handle, \"utf8\");\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"absent\" : \"held\";\n } finally {\n if (handle !== undefined) closeSync(handle);\n }\n if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return \"held\";\n const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;\n try {\n renameSync(this.#lockFile, aside);\n } catch (error) {\n // Another breaker moved it first: try for it at once; anything else, wait.\n return (error as NodeJS.ErrnoException).code === \"ENOENT\" ? \"broken\" : \"held\";\n }\n try {\n if (readFileSync(aside, \"utf8\") !== seen) linkSync(aside, this.#lockFile);\n } catch {\n // Another process holds a new lock already; ours goes, and the caller waits for theirs.\n } finally {\n rmSync(aside, { force: true });\n }\n return \"broken\";\n }\n}\n\n/**\n * Renames `from` over `to`. Windows refuses to replace a file another process has open, as a\n * process reading the settings outside the lock does, with `EPERM` (MT-01: CI's install-ID race,\n * traced on `windows-latest`); the rename is tried again for up to a second, as the runtime's\n * renames are (D150 item 8), and never turned into a copy.\n */\nexport function replace(\n from: string,\n to: string,\n platform: string,\n rename: (from: string, to: string) => void = renameSync,\n): void {\n const deadline = Date.now() + 1000;\n for (let pause = 5; ; pause = Math.min(pause * 2, 100)) {\n try {\n rename(from, to);\n return;\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n const shared = code === \"EPERM\" || code === \"EACCES\" || code === \"EBUSY\";\n if (platform !== \"win32\" || !shared || Date.now() > deadline) throw error;\n Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, pause);\n }\n }\n}\n\n/** Whether this process can create a file in `dir`: a probe of its own, removed at once. */\nexport function writable(\n dir: string,\n remove: (path: string) => void = (path) => rmSync(path, { force: true }),\n): boolean {\n const probe = join(dir, `.probe-${process.pid}-${randomUUID()}`);\n try {\n writeFileSync(probe, \"\", { flag: \"wx\", mode: 0o600 });\n } catch {\n return false;\n }\n // The probe was made, so the directory is writable whatever its removal meets: on Windows an\n // antivirus or the indexer may hold the new file for a moment (EBUSY, EPERM), and telemetry never\n // stops a command (D140 item 5; Opus review of MT-01, 8). An empty probe left behind is harmless.\n try {\n remove(probe);\n } catch {\n // Left for the next probe's directory to carry; nothing reads it.\n }\n return true;\n}\n\n/**\n * Whether a lock's exclusive create failed because another process holds it. On Windows a lock\n * its owner just removed stays \"delete pending\" while anyone still has it open (another process\n * judging its age), and creating it again meanwhile is refused as access denied, not `EEXIST`\n * (WP-W2: twelve first runs at once on an arm64 runner): that is contention too, never a reason\n * to give up.\n */\nfunction held(code: string | undefined, platform: string): boolean {\n return code === \"EEXIST\" || (platform === \"win32\" && (code === \"EPERM\" || code === \"EACCES\"));\n}\n\n/** Whether `dir` can never be created: its nearest existing ancestor isn't a directory. */\nfunction blocked(dir: string): boolean {\n for (let path = dir; ; path = dirname(path)) {\n try {\n return !statSync(path).isDirectory();\n } catch (error) {\n const code = (error as NodeJS.ErrnoException).code;\n if (code === \"ENOTDIR\") return true;\n if (code !== \"ENOENT\") return false;\n }\n if (dirname(path) === path) return false;\n }\n}\n\n/** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */\nfunction ownerAlive(lock: string): boolean {\n const pid = Number(lock.split(\" \")[0]);\n if (!Number.isSafeInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n return (error as NodeJS.ErrnoException).code === \"EPERM\";\n }\n}\n\n/** The newest events within 1,000, none older than 7 days; lines that aren't JSON go. */\nfunction bounded(lines: readonly string[], now: number): string[] {\n const fresh = lines.filter((line) => {\n try {\n const at = Date.parse((JSON.parse(line) as { at?: string }).at ?? \"\");\n return Number.isFinite(at) && now - at <= QUEUE_MAX_AGE_MS;\n } catch {\n return false;\n }\n });\n return fresh.slice(-QUEUE_MAX_EVENTS);\n}\n","/**\n * Sending the queue (D140 item 6): a detached process the CLI starts after a command, at most one\n * at a time and at most once a minute. It reads the decision again before it sends, so consent\n * withdrawn in between drops the events; it posts at most 100 events per request with no header\n * but `Content-Type`, `User-Agent` and the ones HTTP itself needs, and keeps what a failed\n * attempt couldn't send for the next flush.\n */\n\nimport { spawn } from \"node:child_process\";\nimport { request as httpRequest } from \"node:http\";\nimport { request as httpsRequest } from \"node:https\";\nimport { join } from \"node:path\";\nimport { detachedDirectory, detachedEnvironment } from \"../detached.ts\";\nimport type { Environment } from \"../home.ts\";\nimport { cliVersion, fromSource, packageRoot } from \"../version.ts\";\nimport { buildEndpoint, decide } from \"./decision.ts\";\nimport { TelemetryEvent } from \"./events.ts\";\nimport { QUEUE_MAX_EVENTS, type TelemetryStore } from \"./store.ts\";\n\n/** At most this many events per request. */\nexport const BATCH = 100;\n/** A flush starts at most this often. */\nexport const FLUSH_INTERVAL_MS = 60_000;\n/** The flush interval; `MOSTRA_TEST_FLUSH_INTERVAL_MS` shortens it, for tests only. */\nfunction flushInterval(env: Environment): number {\n const test = Number(env[\"MOSTRA_TEST_FLUSH_INTERVAL_MS\"]);\n return Number.isInteger(test) && test >= 0 && test < FLUSH_INTERVAL_MS ? test : FLUSH_INTERVAL_MS;\n}\n\n/** One request's limit. */\nconst REQUEST_TIMEOUT_MS = 5000;\n\n/** Posts one batch; resolves with whether the collector took it (any 2xx). */\nexport function post(endpoint: string, body: string): Promise<boolean> {\n return new Promise((resolve) => {\n const url = new URL(endpoint);\n const send = url.protocol === \"https:\" ? httpsRequest : httpRequest;\n const req = send(\n url,\n {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n \"User-Agent\": `mostra-cli/${cliVersion()}`,\n \"Content-Length\": Buffer.byteLength(body),\n },\n timeout: REQUEST_TIMEOUT_MS,\n },\n (response) => {\n response.resume();\n response.on(\"end\", () =>\n resolve((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300),\n );\n response.on(\"error\", () => resolve(false));\n },\n );\n req.on(\"timeout\", () => req.destroy());\n req.on(\"error\", () => resolve(false));\n req.end(body);\n });\n}\n\n/**\n * Sends the queue: the decision again, then batches until one fails. Events the schema refuses\n * never leave the machine.\n */\nexport async function flush(\n store: TelemetryStore,\n env: Environment,\n now: () => number,\n): Promise<void> {\n const settings = store.read();\n // The queue holds only events of uses that aren't embedded; the rest of the order applies again.\n const decision = decide({\n env,\n buildEndpoint: buildEndpoint(),\n embedded: false,\n settings,\n askFirst: false,\n });\n if (!decision.enabled || decision.endpoint === undefined) {\n // Consent withdrawn (or never given) since the events were queued: they go unsent.\n if (decision.reason !== \"no-collector\" && settings.ok) store.drop(store.queued(now()), now());\n return;\n }\n // The queue holds at most QUEUE_MAX_EVENTS, so a flush needs at most this many batches.\n for (let round = 0; round <= Math.ceil(QUEUE_MAX_EVENTS / BATCH); round += 1) {\n const lines = store.queued(now()).slice(0, BATCH);\n if (lines.length === 0) return;\n const events: unknown[] = [];\n for (const line of lines) {\n try {\n const parsed = TelemetryEvent.safeParse(JSON.parse(line));\n if (parsed.success) events.push(parsed.data);\n } catch {\n // Not an event: dropped with the batch.\n }\n }\n const sent = events.length === 0 || (await post(decision.endpoint, JSON.stringify({ events })));\n if (!sent) return;\n // Nothing removed (another process changed the queue, or the lock was out of reach): stop\n // rather than send the same batch again.\n if (store.drop(lines, now()) === 0) return;\n }\n}\n\n/** The flusher's entry in the CLI's package. */\nfunction flusherEntry(): { readonly entry: string; readonly nodeArgs: readonly string[] } {\n const root = packageRoot();\n return fromSource()\n ? {\n entry: join(root, \"src\", \"telemetry\", \"flush-main.ts\"),\n nodeArgs: [\"--conditions=@mostra/source\"],\n }\n : { entry: join(root, \"dist\", \"telemetry\", \"flush-main.js\"), nodeArgs: [] };\n}\n\n/**\n * Starts a flusher when the queue has events and no flush started in the last minute, fully\n * detached: no stdio, its own process group, never waited for, so `mostra … | jq` and every\n * harness reading to end of file finish when the command does.\n */\nexport function startFlusher(store: TelemetryStore, env: Environment, now: number): void {\n if (store.queued(now).length === 0) return;\n let due = false;\n store.update((current) => {\n const last = current.lastFlushAt === undefined ? 0 : Date.parse(current.lastFlushAt);\n if (Number.isFinite(last) && now - last < flushInterval(env)) return current;\n due = true;\n return { ...current, lastFlushAt: new Date(now).toISOString() };\n });\n if (!due) return;\n const { entry, nodeArgs } = flusherEntry();\n try {\n const child = spawn(process.execPath, [...nodeArgs, entry, store.dir], {\n detached: true,\n stdio: \"ignore\",\n // Not the command's directory but its file system's root, which always exists: on Windows\n // a process's working directory can't be removed or renamed while it runs, and the flusher\n // outlives the command (WP-W2).\n cwd: detachedDirectory(store.dir),\n env: detachedEnvironment({ ...process.env, ...env }) as NodeJS.ProcessEnv,\n });\n child.on(\"error\", () => {});\n child.unref();\n } catch {\n // Telemetry never changes a command's outcome.\n }\n}\n"],"mappings":"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"}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Diagnostic, MethodDefinition } from "@mostra/protocol";
|
|
2
|
+
import "@mostra/sdk";
|
|
2
3
|
//#region src/home.d.ts
|
|
3
4
|
/** The environment the CLI reads. */
|
|
4
5
|
type Environment = Readonly<Record<string, string | undefined>>;
|
|
@@ -222,6 +223,8 @@ interface RunnerIo {
|
|
|
222
223
|
};
|
|
223
224
|
/** The platform; default process.platform. */
|
|
224
225
|
readonly platform?: string;
|
|
226
|
+
/** The architecture; default process.arch. */
|
|
227
|
+
readonly arch?: string;
|
|
225
228
|
}
|
|
226
229
|
/** Runs one command line and returns its exit code. */
|
|
227
230
|
export declare function run(io: RunnerIo): Promise<number>;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/home.ts","../src/client.ts","../src/command.ts","../src/commands/index.ts","../src/exit.ts","../src/help.ts","../src/run.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../src/home.ts","../src/client.ts","../src/command.ts","../src/commands/index.ts","../src/exit.ts","../src/help.ts","../src/run.ts"],"mappings":";;;;KAWY,cAAc,SAAS;;;;UCoBlB;;WAEN;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,SAAS;;WAET,mBAAmB;;EAE5B,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,GAAG,qBAAqB,QAAQ;;EAEnF,eAAe,gBAAgB,UAAU;;WAEhC,QAAQ;;EAEjB;;;;;KCjDU;;UAGK;;WAEN;;WAEA,MAAM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN,MAAM,SAAS;;WAEf,OAAO,SACd;;WAGO;;WAEA;;WAEA;;EAET,KAAK,GAAG,GAAG,QAAQ,iBAAiB,GAAG,IAAI,QAAQ,IAAI,QAAQ;;EAE/D,aAAa;;EAEb,IAAI;;EAEJ,KAAK,gBAAgB;;EAErB,KAAK,YAAY;;WAER,KAAK,SAAS;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;EAET,OAAO,OAAO;;;;;EAKd,WAAW,sBAAsB,QAAQ;;EAEzC,QACE,yBACA,6BACC;IAAU;IAAc;;;WAElB;;WAEA,SAAS;;WAET,SAAS;;;UAIH;;WAEN;;WAEA,QAAQ,SAAS;;;UAIX;;WAEN;;WAEA;;WAEA;;WAEA;;;UAIM;;WAEN;;WAEA;;WAEA,gBAAgB;;WAEhB,iBAAiB;;WAEjB;aAAmB;aAAuB;;;WAE1C;;WAEA;;WAEA,yBAAyB,OAAO;;WAEhC;;WAEA;;WAEA;;EAET,IAAI,SAAS,iBAAiB,QAAQ;;;;;qBChG3B,mBAAmB;;;;qBC9BnB;;WAEX;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;WAEA;;;KAIU,mBAAmB,mBAAmB;;wBAwDlC,YAAY,eAAe;;;;wBCb3B,YAAY,SAAS;;wBAqDrB,kBAAkB,SAAS,aAAa;;wBA4BxC,kBAAkB,mBAAmB;;;;UC/GpC;;WAEN;;WAEA,KAAK;;WAEL;;WAEA,SAAS;;WAET,SAAS;;WAET,OAAO,OAAO;;WAEd;aAAgB;aAAyB;aAA0B;;;WAEnE;;WAEA;;;wBA+DW,IAAI,IAAI,WAAW"}
|
package/dist/index.js
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
import { a as COMMANDS, i as referenceMarkdown, n as commandDefinition, r as commandHelp, t as run } from "./run-
|
|
1
|
+
import { a as COMMANDS, i as referenceMarkdown, n as commandDefinition, r as commandHelp, t as run } from "./run-BpVu5uQ8.js";
|
|
2
2
|
import { n as exitCodeFor, t as EXIT } from "./exit-CBIf8nAU.js";
|
|
3
3
|
export { COMMANDS, EXIT, commandDefinition, commandHelp, exitCodeFor, referenceMarkdown, run };
|
package/dist/reference/cli.md
CHANGED
|
@@ -715,13 +715,13 @@ Example:
|
|
|
715
715
|
## mostra daemon install
|
|
716
716
|
|
|
717
717
|
```text
|
|
718
|
-
Run the home's daemon as a login service (launchd or
|
|
718
|
+
Run the home's daemon as a login service (launchd, systemd or Task Scheduler), so links answer at any hour.
|
|
719
719
|
|
|
720
720
|
Usage:
|
|
721
721
|
mostra daemon install [flags]
|
|
722
722
|
|
|
723
723
|
Flags:
|
|
724
|
-
--no-start Write or remove the service's
|
|
724
|
+
--no-start Write or remove the service's definition without asking launchd, systemd or Task Scheduler to load it.
|
|
725
725
|
|
|
726
726
|
Global flags:
|
|
727
727
|
--json Print one JSON envelope on stdout (NDJSON for streams).
|
|
@@ -751,7 +751,7 @@ Usage:
|
|
|
751
751
|
mostra daemon uninstall [flags]
|
|
752
752
|
|
|
753
753
|
Flags:
|
|
754
|
-
--no-start Write or remove the service's
|
|
754
|
+
--no-start Write or remove the service's definition without asking launchd, systemd or Task Scheduler to load it.
|
|
755
755
|
|
|
756
756
|
Global flags:
|
|
757
757
|
--json Print one JSON envelope on stdout (NDJSON for streams).
|
|
@@ -420,8 +420,8 @@
|
|
|
420
420
|
"phase": "host",
|
|
421
421
|
"title": "Login service could not be installed",
|
|
422
422
|
"hint": "Read the message, fix what it names, and run the install again.",
|
|
423
|
-
"explanation": "`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent
|
|
424
|
-
"fix": "Check the named file's directory permissions
|
|
423
|
+
"explanation": "`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent, systemd user unit or Task Scheduler task failed, or `launchctl`, `systemctl --user` or `schtasks` refused it.",
|
|
424
|
+
"fix": "Check the named file's directory permissions; on Linux, that a systemd user session is running (`systemctl --user status`); on Windows, that Task Scheduler runs and your account may create tasks (`schtasks /Query`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`."
|
|
425
425
|
},
|
|
426
426
|
{
|
|
427
427
|
"code": "MOSTRA_ENV_SHELL_MISSING",
|
|
@@ -583,7 +583,7 @@
|
|
|
583
583
|
"title": "Long-lived artifacts without an always-on daemon",
|
|
584
584
|
"hint": "Run `mostra daemon install` so the home's daemon runs as a login service and these artifacts answer at any hour.",
|
|
585
585
|
"explanation": "The home has artifacts with submission policy `none`, which are meant to stay open (dashboards, task managers, D63), but no login service runs its daemon. Without one, the daemon exits after 30 idle minutes and their links stop answering until a command starts it again.",
|
|
586
|
-
"fix": "Run `mostra daemon install`. It registers a launchd
|
|
586
|
+
"fix": "Run `mostra daemon install`. It registers a login service (a launchd agent, a systemd user service or a Task Scheduler task) that keeps the home's daemon running; `mostra daemon uninstall` removes it."
|
|
587
587
|
},
|
|
588
588
|
{
|
|
589
589
|
"code": "MOSTRA_HOME_NOT_OWNED",
|
|
@@ -12,11 +12,11 @@ Login service could not be installed. Severity: error. Phase: host.
|
|
|
12
12
|
|
|
13
13
|
## What it means
|
|
14
14
|
|
|
15
|
-
`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent
|
|
15
|
+
`mostra daemon install` couldn't register the home's daemon as a login service: writing the launchd agent, systemd user unit or Task Scheduler task failed, or `launchctl`, `systemctl --user` or `schtasks` refused it.
|
|
16
16
|
|
|
17
17
|
## How to fix it
|
|
18
18
|
|
|
19
|
-
Check the named file's directory permissions
|
|
19
|
+
Check the named file's directory permissions; on Linux, that a systemd user session is running (`systemctl --user status`); on Windows, that Task Scheduler runs and your account may create tasks (`schtasks /Query`). On a machine without one, start the daemon from your own login scripts with `mostra daemon start`.
|
|
20
20
|
|
|
21
21
|
## Hint
|
|
22
22
|
|
|
@@ -16,7 +16,7 @@ The home has artifacts with submission policy `none`, which are meant to stay op
|
|
|
16
16
|
|
|
17
17
|
## How to fix it
|
|
18
18
|
|
|
19
|
-
Run `mostra daemon install`. It registers a launchd
|
|
19
|
+
Run `mostra daemon install`. It registers a login service (a launchd agent, a systemd user service or a Task Scheduler task) that keeps the home's daemon running; `mostra daemon uninstall` removes it.
|
|
20
20
|
|
|
21
21
|
## Hint
|
|
22
22
|
|
|
@@ -51,7 +51,7 @@ one.
|
|
|
51
51
|
| `at` | `2026-11-10T14:05:00Z` | When, to the minute |
|
|
52
52
|
| `installId` | a random UUID | Count installs without knowing who you are |
|
|
53
53
|
| `version` | `1.0.0` | Know which versions fail |
|
|
54
|
-
| `os`, `arch` | `darwin`, `arm64` | Platform support |
|
|
54
|
+
| `os`, `arch` | `darwin`, `linux` or `win32`; `x64` or `arm64` | Platform support |
|
|
55
55
|
| `node` | `22` | Which Node versions to support |
|
|
56
56
|
| `harness` | `claude-code`, `codex`, `gemini-cli`, `opencode`, `cursor`, `other`, `none` | Which agents to work with next |
|
|
57
57
|
| `ci` | `false` | Separate CI from people |
|