@mostra/cli 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/README.md +18 -2
- package/dist/bin.d.ts +1 -0
- package/dist/bin.js +79 -0
- package/dist/bin.js.map +1 -0
- package/dist/bootstrap-DfEe_yPr.js +113 -0
- package/dist/bootstrap-DfEe_yPr.js.map +1 -0
- package/dist/exit-CBIf8nAU.js +79 -0
- package/dist/exit-CBIf8nAU.js.map +1 -0
- package/dist/flush-BCuWs0Zh.js +718 -0
- package/dist/flush-BCuWs0Zh.js.map +1 -0
- package/dist/index.d.ts +230 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/reference/cli.md +977 -0
- package/dist/reference/diagnostics.json +1255 -0
- package/dist/reference/errors/MOSTRA_A11Y_MINOR.md +23 -0
- package/dist/reference/errors/MOSTRA_A11Y_SERIOUS.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_CANCELLED.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_EVENT_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_EXPORT_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_INTERRUPTED.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_RUN_NOT_FOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_THREW.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_TOO_LARGE.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_UNKNOWN_FUNCTION.md +23 -0
- package/dist/reference/errors/MOSTRA_ACTION_UNSERIALIZABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_DIR_NOT_EMPTY.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_ENTRY_AMBIGUOUS.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_NOT_FOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_NO_ENTRY.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_NO_PAGE.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_UNREADABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_TEMPLATE_UNKNOWN.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_COLLECTED.md +23 -0
- package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_NOT_FOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_BUILD_CRASHED.md +23 -0
- package/dist/reference/errors/MOSTRA_BUILD_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_BUILD_TIMEOUT.md +23 -0
- package/dist/reference/errors/MOSTRA_BUILD_UNRESOLVED.md +23 -0
- package/dist/reference/errors/MOSTRA_CLI_CONFIG_UNWRITABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_CLI_UNKNOWN_COMMAND.md +23 -0
- package/dist/reference/errors/MOSTRA_CLI_USAGE.md +23 -0
- package/dist/reference/errors/MOSTRA_CONSOLE_ERROR.md +23 -0
- package/dist/reference/errors/MOSTRA_CONSOLE_WARNING.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_INSTALL_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_NOT_FOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_OFFLINE.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_SPEC_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_TIMEOUT.md +23 -0
- package/dist/reference/errors/MOSTRA_DEP_WARM_CONFLICT.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_BIND_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_BROWSER_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_BUNDLE_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_BUNDLE_UNAVAILABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_NODE_UNSUPPORTED.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_PORT_TAKEN.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_RUNTIME_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_SERVICE_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_SHELL_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_ENV_WARM_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_CONFLICT.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_DUPLICATE_ID.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_ID_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_ID_UNSTABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_MUTATION_ID_REUSED.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_SHARED.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_TOO_LARGE.md +23 -0
- package/dist/reference/errors/MOSTRA_FIELD_UNBOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_DISK_FULL.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_GC_INCOMPLETE.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_GONE.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_INTEGRITY.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_LEDGER_UNREADABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_LOCKED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_MISMATCH.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_NOT_ALWAYS_ON.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_NOT_OWNED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_SOCKET_TOO_LONG.md +23 -0
- package/dist/reference/errors/MOSTRA_HOME_TOO_NEW.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_CONNECTION_CLOSED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_CURSOR_COMPACTED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_INITIALIZE_ORDER.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_INTERNAL_ERROR.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_INVALID_PARAMS.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_MALFORMED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_MESSAGE_TOO_LARGE.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_METHOD_UNKNOWN.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_NOTIFICATION_UNSUPPORTED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_PORT_UNSUPPORTED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_REQUEST_ID_REUSED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_REVISION_UNSUPPORTED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_SHUTDOWN_REFUSED.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_SUBSCRIPTION_UNKNOWN.md +23 -0
- package/dist/reference/errors/MOSTRA_HOST_TIMEOUT.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_CONTENT_TYPE.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_CSRF_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_HEADERS_TOO_LARGE.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_HOST_REFUSED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_NOT_SERVED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_ORIGIN_REFUSED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_RATE_LIMITED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_REQUEST_MALFORMED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_SCOPE_DENIED.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_CONFLICT.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_REVOKED.md +23 -0
- package/dist/reference/errors/MOSTRA_LIVE_SOURCE_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_LIVE_TOO_LARGE.md +23 -0
- package/dist/reference/errors/MOSTRA_MANIFEST_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_MANIFEST_NOT_LITERAL.md +23 -0
- package/dist/reference/errors/MOSTRA_MANIFEST_UNKNOWN_KEY.md +23 -0
- package/dist/reference/errors/MOSTRA_NETWORK_EXTERNAL_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_NETWORK_REQUEST_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_PLATFORM_UNSUPPORTED.md +23 -0
- package/dist/reference/errors/MOSTRA_PUBLISH_ID_CONFLICT.md +23 -0
- package/dist/reference/errors/MOSTRA_PUBLISH_SOURCE_CHANGED.md +23 -0
- package/dist/reference/errors/MOSTRA_PUBLISH_VERIFY_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_ACTION_REMOVED.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_ARIA.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_FIELD_REMOVED.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_LIVE_REMOVED.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_PAGE_REMOVED.md +23 -0
- package/dist/reference/errors/MOSTRA_REGRESSION_PIXELS.md +23 -0
- package/dist/reference/errors/MOSTRA_RUNTIME_BLANK.md +23 -0
- package/dist/reference/errors/MOSTRA_RUNTIME_NOT_READY.md +23 -0
- package/dist/reference/errors/MOSTRA_RUNTIME_PAGE_ERROR.md +23 -0
- package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_CRASHED.md +23 -0
- package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_PARKED.md +23 -0
- package/dist/reference/errors/MOSTRA_SECRET_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_SMOKE_BUDGET.md +23 -0
- package/dist/reference/errors/MOSTRA_SMOKE_NO_EFFECT.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_ALREADY.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_BLOCKED.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_CHANNEL_DENIED.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_NOT_FOUND.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_POLICY_NONE.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_REVISION_MOVED.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_SIGNAL_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_SUBMIT_VIEW_INVALID.md +23 -0
- package/dist/reference/errors/MOSTRA_TARGET_UNAVAILABLE.md +23 -0
- package/dist/reference/errors/MOSTRA_TYPE_ERROR.md +23 -0
- package/dist/reference/errors/MOSTRA_TYPE_UNTYPED_PACKAGE.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_ACTION_SKIPPED.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_BASELINE_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_CRASHED.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_LIVE_FAILED.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_NO_BASELINE.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_SECRET_MISSING.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_SKIP_CONSEQUENCE.md +23 -0
- package/dist/reference/errors/MOSTRA_VERIFY_TIMEOUT.md +23 -0
- package/dist/reference/telemetry.md +82 -0
- package/dist/run-D1gRM6Bm.js +2 -0
- package/dist/run-Du266XjI.js +3824 -0
- package/dist/run-Du266XjI.js.map +1 -0
- package/dist/telemetry/flush-main.d.ts +1 -0
- package/dist/telemetry/flush-main.js +14 -0
- package/dist/telemetry/flush-main.js.map +1 -0
- package/package.json +62 -3
- package/src/bin.ts +70 -0
- package/src/bootstrap.ts +156 -0
- package/src/browser.ts +186 -0
- package/src/client.ts +313 -0
- package/src/command.ts +147 -0
- package/src/commands/admin.ts +791 -0
- package/src/commands/authoring.ts +244 -0
- package/src/commands/dev.ts +106 -0
- package/src/commands/index.ts +71 -0
- package/src/commands/interaction.ts +288 -0
- package/src/commands/shared.ts +71 -0
- package/src/commands/viewing.ts +153 -0
- package/src/docs.ts +111 -0
- package/src/exit.ts +89 -0
- package/src/help.ts +170 -0
- package/src/home.ts +111 -0
- package/src/index.ts +23 -0
- package/src/node-gate.ts +15 -0
- package/src/parse.ts +246 -0
- package/src/run.ts +387 -0
- package/src/telemetry/decision.ts +101 -0
- package/src/telemetry/events.ts +147 -0
- package/src/telemetry/flush-main.ts +13 -0
- package/src/telemetry/flush.ts +143 -0
- package/src/telemetry/harness.ts +39 -0
- package/src/telemetry/index.ts +318 -0
- package/src/telemetry/store.ts +379 -0
- package/src/telemetry/timezone.ts +72 -0
- package/src/telemetry/words.ts +87 -0
- package/src/telemetry/zone-sets.ts +182 -0
- package/src/telemetry/zones.generated.ts +720 -0
- package/src/version.ts +43 -0
|
@@ -0,0 +1,718 @@
|
|
|
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";
|
|
4
|
+
import { DIAGNOSTIC_AREAS } from "@mostra/protocol";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
import * as z from "zod";
|
|
7
|
+
import { request } from "node:http";
|
|
8
|
+
import { request as request$1 } from "node:https";
|
|
9
|
+
import { randomUUID } from "node:crypto";
|
|
10
|
+
//#region src/version.ts
|
|
11
|
+
/** The CLI's own package: its root directory and its release. */
|
|
12
|
+
let found;
|
|
13
|
+
function own() {
|
|
14
|
+
if (found) return found;
|
|
15
|
+
let dir = dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
for (;;) {
|
|
17
|
+
try {
|
|
18
|
+
const manifest = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
|
|
19
|
+
if (manifest.name === "@mostra/cli" && manifest.version) {
|
|
20
|
+
found = {
|
|
21
|
+
root: dir,
|
|
22
|
+
version: manifest.version
|
|
23
|
+
};
|
|
24
|
+
return found;
|
|
25
|
+
}
|
|
26
|
+
} catch {}
|
|
27
|
+
const parent = dirname(dir);
|
|
28
|
+
if (parent === dir) throw new Error("Couldn't find @mostra/cli's package.json.");
|
|
29
|
+
dir = parent;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** This CLI's version (`mostra --version`, the client version it reports, telemetry's `version`). */
|
|
33
|
+
function cliVersion() {
|
|
34
|
+
return own().version;
|
|
35
|
+
}
|
|
36
|
+
/** The CLI package's directory: its `dist/`, `src/` and bundled reference live under it. */
|
|
37
|
+
function packageRoot() {
|
|
38
|
+
return own().root;
|
|
39
|
+
}
|
|
40
|
+
/** Whether this CLI runs from its TypeScript sources (tests), not its build. */
|
|
41
|
+
function fromSource() {
|
|
42
|
+
return import.meta.url.endsWith(".ts");
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
//#region src/telemetry/decision.ts
|
|
46
|
+
/** The build's endpoint: empty in this package and in every development build. */
|
|
47
|
+
function buildEndpoint() {
|
|
48
|
+
return "";
|
|
49
|
+
}
|
|
50
|
+
const OFF_VALUES = /* @__PURE__ */ new Set([
|
|
51
|
+
"0",
|
|
52
|
+
"false",
|
|
53
|
+
"off",
|
|
54
|
+
"no"
|
|
55
|
+
]);
|
|
56
|
+
const ON_VALUES = /* @__PURE__ */ new Set([
|
|
57
|
+
"1",
|
|
58
|
+
"true",
|
|
59
|
+
"on",
|
|
60
|
+
"yes"
|
|
61
|
+
]);
|
|
62
|
+
/** `DO_NOT_TRACK` and `MOSTRA_OFFLINE`'s parser: set to anything but empty, `0` or `false`. */
|
|
63
|
+
function truthy(value) {
|
|
64
|
+
if (value === void 0) return false;
|
|
65
|
+
const normalized = value.trim().toLowerCase();
|
|
66
|
+
return normalized !== "" && normalized !== "0" && normalized !== "false";
|
|
67
|
+
}
|
|
68
|
+
/**
|
|
69
|
+
* The endpoint in use: `MOSTRA_TELEMETRY_ENDPOINT` when it is `https:` or `http:` to a loopback
|
|
70
|
+
* address, else the build's; `undefined` when there is none (D140 item 7).
|
|
71
|
+
*/
|
|
72
|
+
function endpointOf(env, build) {
|
|
73
|
+
const override = env["MOSTRA_TELEMETRY_ENDPOINT"];
|
|
74
|
+
const chosen = override !== void 0 && override !== "" ? override : build;
|
|
75
|
+
if (chosen === "") return void 0;
|
|
76
|
+
let url;
|
|
77
|
+
try {
|
|
78
|
+
url = new URL(chosen);
|
|
79
|
+
} catch {
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (url.username !== "" || url.password !== "") return void 0;
|
|
83
|
+
if (url.protocol === "https:") return url.href;
|
|
84
|
+
const loopback = url.hostname === "127.0.0.1" || url.hostname === "[::1]" || url.hostname === "localhost";
|
|
85
|
+
return url.protocol === "http:" && loopback ? url.href : void 0;
|
|
86
|
+
}
|
|
87
|
+
/** Decides, in D140's order. */
|
|
88
|
+
function decide(inputs) {
|
|
89
|
+
const { askFirst } = inputs;
|
|
90
|
+
const off = (reason) => ({
|
|
91
|
+
enabled: false,
|
|
92
|
+
reason,
|
|
93
|
+
askFirst
|
|
94
|
+
});
|
|
95
|
+
const endpoint = endpointOf(inputs.env, inputs.buildEndpoint);
|
|
96
|
+
if (endpoint === void 0) return off("no-collector");
|
|
97
|
+
const at = {
|
|
98
|
+
askFirst,
|
|
99
|
+
endpoint
|
|
100
|
+
};
|
|
101
|
+
if (inputs.embedded) return {
|
|
102
|
+
...at,
|
|
103
|
+
enabled: false,
|
|
104
|
+
reason: "embedded"
|
|
105
|
+
};
|
|
106
|
+
if (truthy(inputs.env["DO_NOT_TRACK"])) return {
|
|
107
|
+
...at,
|
|
108
|
+
enabled: false,
|
|
109
|
+
reason: "do-not-track"
|
|
110
|
+
};
|
|
111
|
+
const normalized = inputs.env["MOSTRA_TELEMETRY"]?.trim().toLowerCase();
|
|
112
|
+
if (normalized !== void 0 && OFF_VALUES.has(normalized)) return {
|
|
113
|
+
...at,
|
|
114
|
+
enabled: false,
|
|
115
|
+
reason: "variable"
|
|
116
|
+
};
|
|
117
|
+
if (!inputs.settings.ok) return {
|
|
118
|
+
...at,
|
|
119
|
+
enabled: false,
|
|
120
|
+
reason: "unreadable"
|
|
121
|
+
};
|
|
122
|
+
const choice = inputs.settings.settings.choice;
|
|
123
|
+
if (choice === "off") return {
|
|
124
|
+
...at,
|
|
125
|
+
enabled: false,
|
|
126
|
+
reason: "setting"
|
|
127
|
+
};
|
|
128
|
+
if (normalized !== void 0 && normalized !== "") return ON_VALUES.has(normalized) ? {
|
|
129
|
+
...at,
|
|
130
|
+
enabled: true,
|
|
131
|
+
reason: "variable",
|
|
132
|
+
consent: "explicit"
|
|
133
|
+
} : {
|
|
134
|
+
...at,
|
|
135
|
+
enabled: false,
|
|
136
|
+
reason: "variable-invalid"
|
|
137
|
+
};
|
|
138
|
+
if (choice === "on") return {
|
|
139
|
+
...at,
|
|
140
|
+
enabled: true,
|
|
141
|
+
reason: "setting",
|
|
142
|
+
consent: "explicit"
|
|
143
|
+
};
|
|
144
|
+
return askFirst ? {
|
|
145
|
+
...at,
|
|
146
|
+
enabled: false,
|
|
147
|
+
reason: "default"
|
|
148
|
+
} : {
|
|
149
|
+
...at,
|
|
150
|
+
enabled: true,
|
|
151
|
+
reason: "default",
|
|
152
|
+
consent: "default"
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
//#endregion
|
|
156
|
+
//#region src/telemetry/harness.ts
|
|
157
|
+
/** The agents Mostra recognizes, in the order their markers are checked. */
|
|
158
|
+
const HARNESSES = [
|
|
159
|
+
"claude-code",
|
|
160
|
+
"codex",
|
|
161
|
+
"gemini-cli",
|
|
162
|
+
"opencode",
|
|
163
|
+
"cursor"
|
|
164
|
+
];
|
|
165
|
+
/** Each agent's own marker. */
|
|
166
|
+
const MARKERS = {
|
|
167
|
+
"claude-code": "CLAUDECODE",
|
|
168
|
+
codex: "CODEX_THREAD_ID",
|
|
169
|
+
"gemini-cli": "GEMINI_CLI",
|
|
170
|
+
opencode: "OPENCODE",
|
|
171
|
+
cursor: "CURSOR_AGENT"
|
|
172
|
+
};
|
|
173
|
+
const set = (value) => value !== void 0 && value !== "";
|
|
174
|
+
/**
|
|
175
|
+
* The agent that ran this process: `AI_AGENT` when it starts with a known ID (the convention
|
|
176
|
+
* `@vercel/detect-agent` reads), else the first agent whose own marker is set, else `other` when
|
|
177
|
+
* `AI_AGENT` or `AGENT` names something else, else `none`. Cursor's editor terminal sets
|
|
178
|
+
* `CURSOR_TRACE_ID` without `CURSOR_AGENT`: that is a person, so it isn't `cursor`.
|
|
179
|
+
*/
|
|
180
|
+
function detectHarness(env) {
|
|
181
|
+
const declared = env["AI_AGENT"];
|
|
182
|
+
if (set(declared)) {
|
|
183
|
+
const known = HARNESSES.find((id) => declared?.startsWith(id));
|
|
184
|
+
if (known) return known;
|
|
185
|
+
}
|
|
186
|
+
for (const id of HARNESSES) if (set(env[MARKERS[id]])) return id;
|
|
187
|
+
if (set(declared) || set(env["AGENT"])) return "other";
|
|
188
|
+
return "none";
|
|
189
|
+
}
|
|
190
|
+
/** The duration buckets of `cli.command`. */
|
|
191
|
+
const DURATIONS = [
|
|
192
|
+
"<1s",
|
|
193
|
+
"1-5s",
|
|
194
|
+
"5-30s",
|
|
195
|
+
"30s-2m",
|
|
196
|
+
"2-10m",
|
|
197
|
+
">10m"
|
|
198
|
+
];
|
|
199
|
+
/** A command's duration bucket. */
|
|
200
|
+
function durationBucket(ms) {
|
|
201
|
+
if (ms < 1e3) return "<1s";
|
|
202
|
+
if (ms < 5e3) return "1-5s";
|
|
203
|
+
if (ms < 3e4) return "5-30s";
|
|
204
|
+
if (ms < 12e4) return "30s-2m";
|
|
205
|
+
if (ms < 6e5) return "2-10m";
|
|
206
|
+
return ">10m";
|
|
207
|
+
}
|
|
208
|
+
/** A catalog-shaped diagnostic code: `MOSTRA_<AREA>_<NAME>`, never a message. */
|
|
209
|
+
const code = z.string().regex(new RegExp(`^MOSTRA_(${DIAGNOSTIC_AREAS.join("|")})_[A-Z0-9_]{1,64}$`));
|
|
210
|
+
/** A command path from the definitions: lowercase words. */
|
|
211
|
+
const command = z.string().regex(/^[a-z]+( [a-z]+)?$/);
|
|
212
|
+
const common = {
|
|
213
|
+
schema: z.literal(1),
|
|
214
|
+
at: z.string().regex(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00Z$/),
|
|
215
|
+
installId: z.uuid(),
|
|
216
|
+
version: z.string().regex(/^\d+\.\d+\.\d+(-[0-9A-Za-z.-]+)?$/),
|
|
217
|
+
os: z.enum(["darwin", "linux"]),
|
|
218
|
+
arch: z.string().regex(/^[a-z0-9]{2,12}$/),
|
|
219
|
+
node: z.int().min(0).max(999),
|
|
220
|
+
harness: z.enum([
|
|
221
|
+
...HARNESSES,
|
|
222
|
+
"other",
|
|
223
|
+
"none"
|
|
224
|
+
]),
|
|
225
|
+
ci: z.boolean(),
|
|
226
|
+
consent: z.enum(["explicit", "default"])
|
|
227
|
+
};
|
|
228
|
+
/** Every event Mostra may send, as a strict union on `event`. */
|
|
229
|
+
const TelemetryEvent = z.discriminatedUnion("event", [
|
|
230
|
+
z.strictObject({
|
|
231
|
+
...common,
|
|
232
|
+
event: z.literal("cli.first_run")
|
|
233
|
+
}),
|
|
234
|
+
z.strictObject({
|
|
235
|
+
...common,
|
|
236
|
+
event: z.literal("cli.command"),
|
|
237
|
+
command,
|
|
238
|
+
exit: z.int().min(0).max(255),
|
|
239
|
+
duration: z.enum(DURATIONS),
|
|
240
|
+
json: z.boolean(),
|
|
241
|
+
error: code.optional()
|
|
242
|
+
}),
|
|
243
|
+
z.strictObject({
|
|
244
|
+
...common,
|
|
245
|
+
event: z.literal("verify.result"),
|
|
246
|
+
status: z.enum(["pass", "fail"]),
|
|
247
|
+
codes: z.array(code).max(50),
|
|
248
|
+
codesTruncated: z.boolean()
|
|
249
|
+
}),
|
|
250
|
+
z.strictObject({
|
|
251
|
+
...common,
|
|
252
|
+
event: z.literal("publish.done"),
|
|
253
|
+
reused: z.boolean(),
|
|
254
|
+
forced: z.boolean()
|
|
255
|
+
}),
|
|
256
|
+
z.strictObject({
|
|
257
|
+
...common,
|
|
258
|
+
event: z.literal("share.done"),
|
|
259
|
+
kind: z.enum([
|
|
260
|
+
"tailnet",
|
|
261
|
+
"funnel",
|
|
262
|
+
"cloudflare",
|
|
263
|
+
"remote"
|
|
264
|
+
])
|
|
265
|
+
}),
|
|
266
|
+
z.strictObject({
|
|
267
|
+
...common,
|
|
268
|
+
event: z.literal("mcp.session"),
|
|
269
|
+
client: z.enum([...HARNESSES, "other"]),
|
|
270
|
+
clientVersion: z.string().regex(/^[0-9A-Za-z.+-]{1,32}$/).optional()
|
|
271
|
+
})
|
|
272
|
+
]);
|
|
273
|
+
/** `at`: now in UTC, rounded down to the minute. */
|
|
274
|
+
function minute(now) {
|
|
275
|
+
return `${now.toISOString().slice(0, 16)}:00Z`;
|
|
276
|
+
}
|
|
277
|
+
/** The distinct catalog codes of `codes`, sorted, at most 50, and whether some were left out. */
|
|
278
|
+
function catalogCodes(codes, known) {
|
|
279
|
+
const distinct = [...new Set(codes.filter((value) => known.has(value)))].sort();
|
|
280
|
+
return {
|
|
281
|
+
codes: distinct.slice(0, 50),
|
|
282
|
+
codesTruncated: distinct.length > 50
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
//#endregion
|
|
286
|
+
//#region src/telemetry/store.ts
|
|
287
|
+
/**
|
|
288
|
+
* Telemetry's files in the CLI's config directory (D140 item 5): `telemetry.json` (the choice,
|
|
289
|
+
* when the notice was shown, when a flush last started), `telemetry-id` (the random install ID)
|
|
290
|
+
* and `telemetry-queue.ndjson` (events waiting to be sent). The directory is 0700 and the files
|
|
291
|
+
* 0600; `telemetry.json` and the queue are written by rename. The install ID is made by an
|
|
292
|
+
* exclusive create (a hard link that fails when the file exists), so processes on a first run
|
|
293
|
+
* agree on one ID whatever happens to the lock. Every other change goes through one lock file,
|
|
294
|
+
* owned by a token and taken over only when its holder has died, so a flusher never loses an
|
|
295
|
+
* event appended while it sends.
|
|
296
|
+
*/
|
|
297
|
+
/** A lock older than this whose owner no longer runs is taken over as stale. */
|
|
298
|
+
const STALE_LOCK_MS = 1e4;
|
|
299
|
+
/**
|
|
300
|
+
* A lock older than this is taken over whoever it names: a held section takes milliseconds, and
|
|
301
|
+
* a PID can name an unrelated live process once the owner died (in a container, PID 1 always
|
|
302
|
+
* runs), which would otherwise hold every later command up forever (Opus review finding 5).
|
|
303
|
+
*/
|
|
304
|
+
const DEAD_LOCK_MS = 6e4;
|
|
305
|
+
/** The queue's bounds (D140 item 6). */
|
|
306
|
+
const QUEUE_MAX_EVENTS = 1e3;
|
|
307
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
308
|
+
/** Telemetry's files in one config directory. */
|
|
309
|
+
var TelemetryStore = class {
|
|
310
|
+
dir;
|
|
311
|
+
settingsFile;
|
|
312
|
+
queueFile;
|
|
313
|
+
idFile;
|
|
314
|
+
#lockFile;
|
|
315
|
+
constructor(dir) {
|
|
316
|
+
this.dir = dir;
|
|
317
|
+
this.settingsFile = join(dir, "telemetry.json");
|
|
318
|
+
this.queueFile = join(dir, "telemetry-queue.ndjson");
|
|
319
|
+
this.idFile = join(dir, "telemetry-id");
|
|
320
|
+
this.#lockFile = join(dir, "telemetry.lock");
|
|
321
|
+
}
|
|
322
|
+
/** The settings; a file that isn't telemetry settings is unreadable, never guessed at. */
|
|
323
|
+
read() {
|
|
324
|
+
const id = this.#readId();
|
|
325
|
+
if (id === null) return {
|
|
326
|
+
ok: false,
|
|
327
|
+
file: this.idFile
|
|
328
|
+
};
|
|
329
|
+
let text;
|
|
330
|
+
try {
|
|
331
|
+
text = readFileSync(this.settingsFile, "utf8");
|
|
332
|
+
} catch (error) {
|
|
333
|
+
if (error.code === "ENOENT") return {
|
|
334
|
+
ok: true,
|
|
335
|
+
settings: id !== void 0 ? { installId: id } : {}
|
|
336
|
+
};
|
|
337
|
+
return {
|
|
338
|
+
ok: false,
|
|
339
|
+
file: this.settingsFile
|
|
340
|
+
};
|
|
341
|
+
}
|
|
342
|
+
try {
|
|
343
|
+
const data = JSON.parse(text);
|
|
344
|
+
if (typeof data !== "object" || data === null || Array.isArray(data)) throw new Error();
|
|
345
|
+
const { choice, noticeShownAt, askFirstNoticeShownAt, lastFlushAt } = data;
|
|
346
|
+
const installId = id;
|
|
347
|
+
if (choice !== void 0 && choice !== "on" && choice !== "off") throw new Error();
|
|
348
|
+
if (data["installId"] !== void 0) throw new Error();
|
|
349
|
+
if (noticeShownAt !== void 0 && typeof noticeShownAt !== "string") throw new Error();
|
|
350
|
+
if (askFirstNoticeShownAt !== void 0 && typeof askFirstNoticeShownAt !== "string") throw new Error();
|
|
351
|
+
if (lastFlushAt !== void 0 && typeof lastFlushAt !== "string") throw new Error();
|
|
352
|
+
return {
|
|
353
|
+
ok: true,
|
|
354
|
+
settings: {
|
|
355
|
+
...choice !== void 0 ? { choice } : {},
|
|
356
|
+
...installId !== void 0 ? { installId } : {},
|
|
357
|
+
...noticeShownAt !== void 0 ? { noticeShownAt } : {},
|
|
358
|
+
...askFirstNoticeShownAt !== void 0 ? { askFirstNoticeShownAt } : {},
|
|
359
|
+
...lastFlushAt !== void 0 ? { lastFlushAt } : {}
|
|
360
|
+
}
|
|
361
|
+
};
|
|
362
|
+
} catch {
|
|
363
|
+
return {
|
|
364
|
+
ok: false,
|
|
365
|
+
file: this.settingsFile
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
/**
|
|
370
|
+
* Changes the settings under the lock and returns what they became; `undefined` when the file
|
|
371
|
+
* can't be read (nothing is written over it).
|
|
372
|
+
*/
|
|
373
|
+
update(change) {
|
|
374
|
+
return this.locked(() => {
|
|
375
|
+
const current = this.read();
|
|
376
|
+
if (!current.ok) return void 0;
|
|
377
|
+
const next = change(current.settings);
|
|
378
|
+
this.#write(next);
|
|
379
|
+
return next;
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* The install ID, made when there is none yet. A new ID is written to a file of this process's
|
|
384
|
+
* own, then hard-linked to `telemetry-id`, which fails when the file exists: of any processes
|
|
385
|
+
* making one at once exactly one wins, and the others read the winner's. `undefined` when the
|
|
386
|
+
* settings can't be read.
|
|
387
|
+
*/
|
|
388
|
+
installId() {
|
|
389
|
+
const current = this.read();
|
|
390
|
+
if (!current.ok) return void 0;
|
|
391
|
+
if (current.settings.installId !== void 0) return {
|
|
392
|
+
id: current.settings.installId,
|
|
393
|
+
created: false
|
|
394
|
+
};
|
|
395
|
+
const candidate = `${this.idFile}.${process.pid}.${randomUUID()}.tmp`;
|
|
396
|
+
const id = randomUUID();
|
|
397
|
+
try {
|
|
398
|
+
this.#prepareDir();
|
|
399
|
+
writeFileSync(candidate, `${id}\n`, { mode: 384 });
|
|
400
|
+
linkSync(candidate, this.idFile);
|
|
401
|
+
return {
|
|
402
|
+
id,
|
|
403
|
+
created: true
|
|
404
|
+
};
|
|
405
|
+
} catch (error) {
|
|
406
|
+
if (error.code !== "EEXIST") return void 0;
|
|
407
|
+
const winner = this.#readId();
|
|
408
|
+
return winner ? {
|
|
409
|
+
id: winner,
|
|
410
|
+
created: false
|
|
411
|
+
} : void 0;
|
|
412
|
+
} finally {
|
|
413
|
+
rmSync(candidate, { force: true });
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
/** The install ID in `telemetry-id`: `undefined` when there is none, `null` when it's damaged. */
|
|
417
|
+
#readId() {
|
|
418
|
+
let text;
|
|
419
|
+
try {
|
|
420
|
+
text = readFileSync(this.idFile, "utf8");
|
|
421
|
+
} catch (error) {
|
|
422
|
+
return error.code === "ENOENT" ? void 0 : null;
|
|
423
|
+
}
|
|
424
|
+
const id = text.trim();
|
|
425
|
+
return UUID.test(id) ? id : null;
|
|
426
|
+
}
|
|
427
|
+
/**
|
|
428
|
+
* Replaces a file that can't be read with these settings; only `telemetry on|off` do, as the
|
|
429
|
+
* person's explicit choice. Call it holding the lock.
|
|
430
|
+
*/
|
|
431
|
+
reset(settings) {
|
|
432
|
+
const { installId: _id, ...rest } = settings;
|
|
433
|
+
this.#write(rest);
|
|
434
|
+
rmSync(this.idFile, { force: true });
|
|
435
|
+
if (settings.choice === "off") rmSync(this.queueFile, { force: true });
|
|
436
|
+
return rest;
|
|
437
|
+
}
|
|
438
|
+
/** Turns telemetry off: the choice, and the install ID and the queue deleted at once. */
|
|
439
|
+
turnOff() {
|
|
440
|
+
return this.locked(() => {
|
|
441
|
+
const current = this.read();
|
|
442
|
+
if (!current.ok) return false;
|
|
443
|
+
const { installId: _id, ...rest } = current.settings;
|
|
444
|
+
this.#write({
|
|
445
|
+
...rest,
|
|
446
|
+
choice: "off"
|
|
447
|
+
});
|
|
448
|
+
rmSync(this.idFile, { force: true });
|
|
449
|
+
rmSync(this.queueFile, { force: true });
|
|
450
|
+
return true;
|
|
451
|
+
}) ?? false;
|
|
452
|
+
}
|
|
453
|
+
/** Appends events to the queue, keeping its bounds. */
|
|
454
|
+
append(lines, now) {
|
|
455
|
+
if (lines.length === 0) return;
|
|
456
|
+
this.locked(() => {
|
|
457
|
+
const kept = bounded([...this.#queued(), ...lines], now);
|
|
458
|
+
this.#writeQueue(kept);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
/** The queued events, within their bounds. */
|
|
462
|
+
queued(now) {
|
|
463
|
+
return bounded(this.#queued(), now);
|
|
464
|
+
}
|
|
465
|
+
/**
|
|
466
|
+
* Removes the lines a flush sent from the head of the queue, after pruning what expired or
|
|
467
|
+
* fell outside the bounds, as {@link queued} did; how many sent lines were removed (0 when the
|
|
468
|
+
* lock can't be had). A flusher stops when this is 0, so it never sends a batch twice.
|
|
469
|
+
*/
|
|
470
|
+
drop(sent, now) {
|
|
471
|
+
return this.locked(() => {
|
|
472
|
+
const queued = bounded(this.#queued(), now);
|
|
473
|
+
let index = 0;
|
|
474
|
+
while (index < sent.length && queued[index] === sent[index]) index += 1;
|
|
475
|
+
this.#writeQueue(queued.slice(index));
|
|
476
|
+
return index;
|
|
477
|
+
}) ?? 0;
|
|
478
|
+
}
|
|
479
|
+
#queued() {
|
|
480
|
+
try {
|
|
481
|
+
return readFileSync(this.queueFile, "utf8").split("\n").filter((line) => line.length > 0);
|
|
482
|
+
} catch {
|
|
483
|
+
return [];
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
#writeQueue(lines) {
|
|
487
|
+
if (lines.length === 0) {
|
|
488
|
+
rmSync(this.queueFile, { force: true });
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
const temporary = `${this.queueFile}.${process.pid}.tmp`;
|
|
492
|
+
writeFileSync(temporary, `${lines.join("\n")}\n`, { mode: 384 });
|
|
493
|
+
renameSync(temporary, this.queueFile);
|
|
494
|
+
}
|
|
495
|
+
#write(settings) {
|
|
496
|
+
const { installId: _id, ...stored } = settings;
|
|
497
|
+
const temporary = `${this.settingsFile}.${process.pid}.tmp`;
|
|
498
|
+
writeFileSync(temporary, `${JSON.stringify(stored, null, 2)}\n`, { mode: 384 });
|
|
499
|
+
renameSync(temporary, this.settingsFile);
|
|
500
|
+
}
|
|
501
|
+
#prepareDir() {
|
|
502
|
+
mkdirSync(this.dir, {
|
|
503
|
+
recursive: true,
|
|
504
|
+
mode: 448
|
|
505
|
+
});
|
|
506
|
+
try {
|
|
507
|
+
chmodSync(this.dir, 448);
|
|
508
|
+
} catch {}
|
|
509
|
+
}
|
|
510
|
+
/**
|
|
511
|
+
* Runs `fn` holding the lock file; `undefined` when the lock can't be had within 2 s, or the
|
|
512
|
+
* directory or a file in it can't be written (a full disk, a directory someone else owns):
|
|
513
|
+
* telemetry never stops a command (D140 item 5). The lock
|
|
514
|
+
* holds its owner's PID and a token of its own, is released only by its owner, and is taken
|
|
515
|
+
* over when it is older than 10 s and its owner is no longer running (a crash), or older than
|
|
516
|
+
* 60 s whoever it names.
|
|
517
|
+
*/
|
|
518
|
+
locked(fn) {
|
|
519
|
+
try {
|
|
520
|
+
this.#prepareDir();
|
|
521
|
+
} catch {
|
|
522
|
+
return;
|
|
523
|
+
}
|
|
524
|
+
const token = `${process.pid} ${randomUUID()}`;
|
|
525
|
+
const deadline = Date.now() + 2e3;
|
|
526
|
+
for (;;) try {
|
|
527
|
+
writeFileSync(this.#lockFile, token, {
|
|
528
|
+
flag: "wx",
|
|
529
|
+
mode: 384
|
|
530
|
+
});
|
|
531
|
+
break;
|
|
532
|
+
} catch (error) {
|
|
533
|
+
if (error.code !== "EEXIST") return void 0;
|
|
534
|
+
if (this.#breakStale()) continue;
|
|
535
|
+
if (Date.now() > deadline) return void 0;
|
|
536
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 20);
|
|
537
|
+
}
|
|
538
|
+
try {
|
|
539
|
+
return fn();
|
|
540
|
+
} catch {
|
|
541
|
+
return;
|
|
542
|
+
} finally {
|
|
543
|
+
this.#release(token);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/** Removes the lock when it is still this owner's. */
|
|
547
|
+
#release(token) {
|
|
548
|
+
try {
|
|
549
|
+
if (readFileSync(this.#lockFile, "utf8") === token) rmSync(this.#lockFile, { force: true });
|
|
550
|
+
} catch {}
|
|
551
|
+
}
|
|
552
|
+
/**
|
|
553
|
+
* Takes a crashed owner's lock away; `true` when the caller should try again at once. The lock
|
|
554
|
+
* is moved aside under a name of this process's own (a rename is atomic, so of two processes
|
|
555
|
+
* breaking it one gets it), and a lock that turns out to be another, live owner's goes back.
|
|
556
|
+
*/
|
|
557
|
+
#breakStale() {
|
|
558
|
+
let seen;
|
|
559
|
+
let age;
|
|
560
|
+
let handle;
|
|
561
|
+
try {
|
|
562
|
+
handle = openSync(this.#lockFile, "r");
|
|
563
|
+
age = Date.now() - fstatSync(handle).mtimeMs;
|
|
564
|
+
if (age <= STALE_LOCK_MS) return false;
|
|
565
|
+
seen = readFileSync(handle, "utf8");
|
|
566
|
+
} catch {
|
|
567
|
+
return true;
|
|
568
|
+
} finally {
|
|
569
|
+
if (handle !== void 0) closeSync(handle);
|
|
570
|
+
}
|
|
571
|
+
if (age <= DEAD_LOCK_MS && ownerAlive(seen)) return false;
|
|
572
|
+
const aside = `${this.#lockFile}.${process.pid}.${randomUUID()}.stale`;
|
|
573
|
+
try {
|
|
574
|
+
renameSync(this.#lockFile, aside);
|
|
575
|
+
} catch {
|
|
576
|
+
return true;
|
|
577
|
+
}
|
|
578
|
+
try {
|
|
579
|
+
if (readFileSync(aside, "utf8") !== seen) linkSync(aside, this.#lockFile);
|
|
580
|
+
} catch {} finally {
|
|
581
|
+
rmSync(aside, { force: true });
|
|
582
|
+
}
|
|
583
|
+
return true;
|
|
584
|
+
}
|
|
585
|
+
};
|
|
586
|
+
/** Whether the process a lock names still runs; a lock whose owner can't be read counts as live. */
|
|
587
|
+
function ownerAlive(lock) {
|
|
588
|
+
const pid = Number(lock.split(" ")[0]);
|
|
589
|
+
if (!Number.isSafeInteger(pid) || pid <= 0) return false;
|
|
590
|
+
try {
|
|
591
|
+
process.kill(pid, 0);
|
|
592
|
+
return true;
|
|
593
|
+
} catch (error) {
|
|
594
|
+
return error.code === "EPERM";
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
/** The newest events within 1,000, none older than 7 days; lines that aren't JSON go. */
|
|
598
|
+
function bounded(lines, now) {
|
|
599
|
+
return lines.filter((line) => {
|
|
600
|
+
try {
|
|
601
|
+
const at = Date.parse(JSON.parse(line).at ?? "");
|
|
602
|
+
return Number.isFinite(at) && now - at <= 6048e5;
|
|
603
|
+
} catch {
|
|
604
|
+
return false;
|
|
605
|
+
}
|
|
606
|
+
}).slice(-1e3);
|
|
607
|
+
}
|
|
608
|
+
/** A flush starts at most this often. */
|
|
609
|
+
const FLUSH_INTERVAL_MS = 6e4;
|
|
610
|
+
/** The flush interval; `MOSTRA_TEST_FLUSH_INTERVAL_MS` shortens it, for tests only. */
|
|
611
|
+
function flushInterval(env) {
|
|
612
|
+
const test = Number(env["MOSTRA_TEST_FLUSH_INTERVAL_MS"]);
|
|
613
|
+
return Number.isInteger(test) && test >= 0 && test < 6e4 ? test : FLUSH_INTERVAL_MS;
|
|
614
|
+
}
|
|
615
|
+
/** One request's limit. */
|
|
616
|
+
const REQUEST_TIMEOUT_MS = 5e3;
|
|
617
|
+
/** Posts one batch; resolves with whether the collector took it (any 2xx). */
|
|
618
|
+
function post(endpoint, body) {
|
|
619
|
+
return new Promise((resolve) => {
|
|
620
|
+
const url = new URL(endpoint);
|
|
621
|
+
const req = (url.protocol === "https:" ? request$1 : request)(url, {
|
|
622
|
+
method: "POST",
|
|
623
|
+
headers: {
|
|
624
|
+
"Content-Type": "application/json",
|
|
625
|
+
"User-Agent": `mostra-cli/${cliVersion()}`,
|
|
626
|
+
"Content-Length": Buffer.byteLength(body)
|
|
627
|
+
},
|
|
628
|
+
timeout: REQUEST_TIMEOUT_MS
|
|
629
|
+
}, (response) => {
|
|
630
|
+
response.resume();
|
|
631
|
+
response.on("end", () => resolve((response.statusCode ?? 0) >= 200 && (response.statusCode ?? 0) < 300));
|
|
632
|
+
response.on("error", () => resolve(false));
|
|
633
|
+
});
|
|
634
|
+
req.on("timeout", () => req.destroy());
|
|
635
|
+
req.on("error", () => resolve(false));
|
|
636
|
+
req.end(body);
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* Sends the queue: the decision again, then batches until one fails. Events the schema refuses
|
|
641
|
+
* never leave the machine.
|
|
642
|
+
*/
|
|
643
|
+
async function flush(store, env, now) {
|
|
644
|
+
const settings = store.read();
|
|
645
|
+
const decision = decide({
|
|
646
|
+
env,
|
|
647
|
+
buildEndpoint: buildEndpoint(),
|
|
648
|
+
embedded: false,
|
|
649
|
+
settings,
|
|
650
|
+
askFirst: false
|
|
651
|
+
});
|
|
652
|
+
if (!decision.enabled || decision.endpoint === void 0) {
|
|
653
|
+
if (decision.reason !== "no-collector" && settings.ok) store.drop(store.queued(now()), now());
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
for (let round = 0; round <= Math.ceil(QUEUE_MAX_EVENTS / 100); round += 1) {
|
|
657
|
+
const lines = store.queued(now()).slice(0, 100);
|
|
658
|
+
if (lines.length === 0) return;
|
|
659
|
+
const events = [];
|
|
660
|
+
for (const line of lines) try {
|
|
661
|
+
const parsed = TelemetryEvent.safeParse(JSON.parse(line));
|
|
662
|
+
if (parsed.success) events.push(parsed.data);
|
|
663
|
+
} catch {}
|
|
664
|
+
if (!(events.length === 0 || await post(decision.endpoint, JSON.stringify({ events })))) return;
|
|
665
|
+
if (store.drop(lines, now()) === 0) return;
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
/** The flusher's entry in the CLI's package. */
|
|
669
|
+
function flusherEntry() {
|
|
670
|
+
const root = packageRoot();
|
|
671
|
+
return fromSource() ? {
|
|
672
|
+
entry: join(root, "src", "telemetry", "flush-main.ts"),
|
|
673
|
+
nodeArgs: ["--conditions=@mostra/source"]
|
|
674
|
+
} : {
|
|
675
|
+
entry: join(root, "dist", "telemetry", "flush-main.js"),
|
|
676
|
+
nodeArgs: []
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Starts a flusher when the queue has events and no flush started in the last minute, fully
|
|
681
|
+
* detached: no stdio, its own process group, never waited for, so `mostra … | jq` and every
|
|
682
|
+
* harness reading to end of file finish when the command does.
|
|
683
|
+
*/
|
|
684
|
+
function startFlusher(store, env, now) {
|
|
685
|
+
if (store.queued(now).length === 0) return;
|
|
686
|
+
let due = false;
|
|
687
|
+
store.update((current) => {
|
|
688
|
+
const last = current.lastFlushAt === void 0 ? 0 : Date.parse(current.lastFlushAt);
|
|
689
|
+
if (Number.isFinite(last) && now - last < flushInterval(env)) return current;
|
|
690
|
+
due = true;
|
|
691
|
+
return {
|
|
692
|
+
...current,
|
|
693
|
+
lastFlushAt: new Date(now).toISOString()
|
|
694
|
+
};
|
|
695
|
+
});
|
|
696
|
+
if (!due) return;
|
|
697
|
+
const { entry, nodeArgs } = flusherEntry();
|
|
698
|
+
try {
|
|
699
|
+
const child = spawn(process.execPath, [
|
|
700
|
+
...nodeArgs,
|
|
701
|
+
entry,
|
|
702
|
+
store.dir
|
|
703
|
+
], {
|
|
704
|
+
detached: true,
|
|
705
|
+
stdio: "ignore",
|
|
706
|
+
env: {
|
|
707
|
+
...process.env,
|
|
708
|
+
...env
|
|
709
|
+
}
|
|
710
|
+
});
|
|
711
|
+
child.on("error", () => {});
|
|
712
|
+
child.unref();
|
|
713
|
+
} catch {}
|
|
714
|
+
}
|
|
715
|
+
//#endregion
|
|
716
|
+
export { catalogCodes as a, detectHarness as c, truthy as d, cliVersion as f, TelemetryEvent as i, buildEndpoint as l, packageRoot as m, startFlusher as n, durationBucket as o, fromSource as p, TelemetryStore as r, minute as s, flush as t, decide as u };
|
|
717
|
+
|
|
718
|
+
//# sourceMappingURL=flush-BCuWs0Zh.js.map
|