@mostra/cli 0.0.0-stage → 0.2.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.
Files changed (196) hide show
  1. package/LICENSE +202 -0
  2. package/README.md +18 -2
  3. package/dist/bin.d.ts +1 -0
  4. package/dist/bin.js +79 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/bootstrap-DfEe_yPr.js +113 -0
  7. package/dist/bootstrap-DfEe_yPr.js.map +1 -0
  8. package/dist/exit-CBIf8nAU.js +79 -0
  9. package/dist/exit-CBIf8nAU.js.map +1 -0
  10. package/dist/flush-BCuWs0Zh.js +718 -0
  11. package/dist/flush-BCuWs0Zh.js.map +1 -0
  12. package/dist/index.d.ts +230 -0
  13. package/dist/index.d.ts.map +1 -0
  14. package/dist/index.js +3 -0
  15. package/dist/reference/cli.md +977 -0
  16. package/dist/reference/diagnostics.json +1264 -0
  17. package/dist/reference/errors/MOSTRA_A11Y_MINOR.md +23 -0
  18. package/dist/reference/errors/MOSTRA_A11Y_SERIOUS.md +23 -0
  19. package/dist/reference/errors/MOSTRA_ACTION_CANCELLED.md +23 -0
  20. package/dist/reference/errors/MOSTRA_ACTION_EVENT_INVALID.md +23 -0
  21. package/dist/reference/errors/MOSTRA_ACTION_EXPORT_INVALID.md +23 -0
  22. package/dist/reference/errors/MOSTRA_ACTION_INTERRUPTED.md +23 -0
  23. package/dist/reference/errors/MOSTRA_ACTION_RUN_NOT_FOUND.md +23 -0
  24. package/dist/reference/errors/MOSTRA_ACTION_THREW.md +23 -0
  25. package/dist/reference/errors/MOSTRA_ACTION_TOO_LARGE.md +23 -0
  26. package/dist/reference/errors/MOSTRA_ACTION_UNKNOWN_FUNCTION.md +23 -0
  27. package/dist/reference/errors/MOSTRA_ACTION_UNSERIALIZABLE.md +23 -0
  28. package/dist/reference/errors/MOSTRA_ARTIFACT_DIR_NOT_EMPTY.md +23 -0
  29. package/dist/reference/errors/MOSTRA_ARTIFACT_ENTRY_AMBIGUOUS.md +23 -0
  30. package/dist/reference/errors/MOSTRA_ARTIFACT_NOT_FOUND.md +23 -0
  31. package/dist/reference/errors/MOSTRA_ARTIFACT_NO_ENTRY.md +23 -0
  32. package/dist/reference/errors/MOSTRA_ARTIFACT_NO_PAGE.md +23 -0
  33. package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_MISSING.md +23 -0
  34. package/dist/reference/errors/MOSTRA_ARTIFACT_SOURCE_UNREADABLE.md +23 -0
  35. package/dist/reference/errors/MOSTRA_ARTIFACT_TEMPLATE_UNKNOWN.md +23 -0
  36. package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_COLLECTED.md +23 -0
  37. package/dist/reference/errors/MOSTRA_ARTIFACT_VERSION_NOT_FOUND.md +23 -0
  38. package/dist/reference/errors/MOSTRA_BUILD_CRASHED.md +23 -0
  39. package/dist/reference/errors/MOSTRA_BUILD_FAILED.md +23 -0
  40. package/dist/reference/errors/MOSTRA_BUILD_TIMEOUT.md +23 -0
  41. package/dist/reference/errors/MOSTRA_BUILD_UNRESOLVED.md +23 -0
  42. package/dist/reference/errors/MOSTRA_CLI_CONFIG_UNWRITABLE.md +23 -0
  43. package/dist/reference/errors/MOSTRA_CLI_UNKNOWN_COMMAND.md +23 -0
  44. package/dist/reference/errors/MOSTRA_CLI_USAGE.md +23 -0
  45. package/dist/reference/errors/MOSTRA_CONSOLE_ERROR.md +23 -0
  46. package/dist/reference/errors/MOSTRA_CONSOLE_WARNING.md +23 -0
  47. package/dist/reference/errors/MOSTRA_DEP_INSTALL_FAILED.md +23 -0
  48. package/dist/reference/errors/MOSTRA_DEP_MISSING.md +23 -0
  49. package/dist/reference/errors/MOSTRA_DEP_NOT_FOUND.md +23 -0
  50. package/dist/reference/errors/MOSTRA_DEP_OFFLINE.md +23 -0
  51. package/dist/reference/errors/MOSTRA_DEP_SPEC_INVALID.md +23 -0
  52. package/dist/reference/errors/MOSTRA_DEP_TIMEOUT.md +23 -0
  53. package/dist/reference/errors/MOSTRA_DEP_WARM_CONFLICT.md +23 -0
  54. package/dist/reference/errors/MOSTRA_ENV_BIND_FAILED.md +23 -0
  55. package/dist/reference/errors/MOSTRA_ENV_BROWSER_MISSING.md +23 -0
  56. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_DOWNLOAD_FAILED.md +23 -0
  57. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_INVALID.md +23 -0
  58. package/dist/reference/errors/MOSTRA_ENV_BUNDLE_UNAVAILABLE.md +23 -0
  59. package/dist/reference/errors/MOSTRA_ENV_NODE_UNSUPPORTED.md +23 -0
  60. package/dist/reference/errors/MOSTRA_ENV_PORT_TAKEN.md +23 -0
  61. package/dist/reference/errors/MOSTRA_ENV_RUNTIME_MISSING.md +23 -0
  62. package/dist/reference/errors/MOSTRA_ENV_SERVICE_FAILED.md +23 -0
  63. package/dist/reference/errors/MOSTRA_ENV_SHELL_MISSING.md +23 -0
  64. package/dist/reference/errors/MOSTRA_ENV_WARM_MISSING.md +23 -0
  65. package/dist/reference/errors/MOSTRA_FIELD_CONFLICT.md +23 -0
  66. package/dist/reference/errors/MOSTRA_FIELD_DUPLICATE_ID.md +23 -0
  67. package/dist/reference/errors/MOSTRA_FIELD_ID_INVALID.md +23 -0
  68. package/dist/reference/errors/MOSTRA_FIELD_ID_UNSTABLE.md +23 -0
  69. package/dist/reference/errors/MOSTRA_FIELD_MUTATION_ID_REUSED.md +23 -0
  70. package/dist/reference/errors/MOSTRA_FIELD_SHARED.md +23 -0
  71. package/dist/reference/errors/MOSTRA_FIELD_TOO_LARGE.md +23 -0
  72. package/dist/reference/errors/MOSTRA_FIELD_UNBOUND.md +23 -0
  73. package/dist/reference/errors/MOSTRA_HOME_DISK_FULL.md +23 -0
  74. package/dist/reference/errors/MOSTRA_HOME_GC_INCOMPLETE.md +23 -0
  75. package/dist/reference/errors/MOSTRA_HOME_GONE.md +23 -0
  76. package/dist/reference/errors/MOSTRA_HOME_INTEGRITY.md +23 -0
  77. package/dist/reference/errors/MOSTRA_HOME_LEDGER_UNREADABLE.md +23 -0
  78. package/dist/reference/errors/MOSTRA_HOME_LOCKED.md +23 -0
  79. package/dist/reference/errors/MOSTRA_HOME_MISMATCH.md +23 -0
  80. package/dist/reference/errors/MOSTRA_HOME_NOT_ALWAYS_ON.md +23 -0
  81. package/dist/reference/errors/MOSTRA_HOME_NOT_OWNED.md +23 -0
  82. package/dist/reference/errors/MOSTRA_HOME_SOCKET_TOO_LONG.md +23 -0
  83. package/dist/reference/errors/MOSTRA_HOME_TOO_NEW.md +23 -0
  84. package/dist/reference/errors/MOSTRA_HOST_CONNECTION_CLOSED.md +23 -0
  85. package/dist/reference/errors/MOSTRA_HOST_CURSOR_COMPACTED.md +23 -0
  86. package/dist/reference/errors/MOSTRA_HOST_IDENTITY_UNAVAILABLE.md +23 -0
  87. package/dist/reference/errors/MOSTRA_HOST_INITIALIZE_ORDER.md +23 -0
  88. package/dist/reference/errors/MOSTRA_HOST_INTERNAL_ERROR.md +23 -0
  89. package/dist/reference/errors/MOSTRA_HOST_INVALID_PARAMS.md +23 -0
  90. package/dist/reference/errors/MOSTRA_HOST_MALFORMED.md +23 -0
  91. package/dist/reference/errors/MOSTRA_HOST_MESSAGE_TOO_LARGE.md +23 -0
  92. package/dist/reference/errors/MOSTRA_HOST_METHOD_UNKNOWN.md +23 -0
  93. package/dist/reference/errors/MOSTRA_HOST_NOTIFICATION_UNSUPPORTED.md +23 -0
  94. package/dist/reference/errors/MOSTRA_HOST_PORT_UNSUPPORTED.md +23 -0
  95. package/dist/reference/errors/MOSTRA_HOST_REQUEST_ID_REUSED.md +23 -0
  96. package/dist/reference/errors/MOSTRA_HOST_REVISION_UNSUPPORTED.md +23 -0
  97. package/dist/reference/errors/MOSTRA_HOST_SHUTDOWN_REFUSED.md +23 -0
  98. package/dist/reference/errors/MOSTRA_HOST_SUBSCRIPTION_UNKNOWN.md +23 -0
  99. package/dist/reference/errors/MOSTRA_HOST_TIMEOUT.md +23 -0
  100. package/dist/reference/errors/MOSTRA_HYGIENE_CONTENT_TYPE.md +23 -0
  101. package/dist/reference/errors/MOSTRA_HYGIENE_CSRF_MISSING.md +23 -0
  102. package/dist/reference/errors/MOSTRA_HYGIENE_HEADERS_TOO_LARGE.md +23 -0
  103. package/dist/reference/errors/MOSTRA_HYGIENE_HOST_REFUSED.md +23 -0
  104. package/dist/reference/errors/MOSTRA_HYGIENE_NOT_SERVED.md +23 -0
  105. package/dist/reference/errors/MOSTRA_HYGIENE_ORIGIN_REFUSED.md +23 -0
  106. package/dist/reference/errors/MOSTRA_HYGIENE_RATE_LIMITED.md +23 -0
  107. package/dist/reference/errors/MOSTRA_HYGIENE_REQUEST_MALFORMED.md +23 -0
  108. package/dist/reference/errors/MOSTRA_HYGIENE_SCOPE_DENIED.md +23 -0
  109. package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_CONFLICT.md +23 -0
  110. package/dist/reference/errors/MOSTRA_HYGIENE_SESSION_MISSING.md +23 -0
  111. package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_INVALID.md +23 -0
  112. package/dist/reference/errors/MOSTRA_HYGIENE_TOKEN_REVOKED.md +23 -0
  113. package/dist/reference/errors/MOSTRA_LIVE_SOURCE_FAILED.md +23 -0
  114. package/dist/reference/errors/MOSTRA_LIVE_TOO_LARGE.md +23 -0
  115. package/dist/reference/errors/MOSTRA_MANIFEST_INVALID.md +23 -0
  116. package/dist/reference/errors/MOSTRA_MANIFEST_NOT_LITERAL.md +23 -0
  117. package/dist/reference/errors/MOSTRA_MANIFEST_UNKNOWN_KEY.md +23 -0
  118. package/dist/reference/errors/MOSTRA_NETWORK_EXTERNAL_FAILED.md +23 -0
  119. package/dist/reference/errors/MOSTRA_NETWORK_REQUEST_FAILED.md +23 -0
  120. package/dist/reference/errors/MOSTRA_PLATFORM_UNSUPPORTED.md +23 -0
  121. package/dist/reference/errors/MOSTRA_PUBLISH_ID_CONFLICT.md +23 -0
  122. package/dist/reference/errors/MOSTRA_PUBLISH_SOURCE_CHANGED.md +23 -0
  123. package/dist/reference/errors/MOSTRA_PUBLISH_VERIFY_FAILED.md +23 -0
  124. package/dist/reference/errors/MOSTRA_REGRESSION_ACTION_REMOVED.md +23 -0
  125. package/dist/reference/errors/MOSTRA_REGRESSION_ARIA.md +23 -0
  126. package/dist/reference/errors/MOSTRA_REGRESSION_FIELD_REMOVED.md +23 -0
  127. package/dist/reference/errors/MOSTRA_REGRESSION_LIVE_REMOVED.md +23 -0
  128. package/dist/reference/errors/MOSTRA_REGRESSION_PAGE_REMOVED.md +23 -0
  129. package/dist/reference/errors/MOSTRA_REGRESSION_PIXELS.md +23 -0
  130. package/dist/reference/errors/MOSTRA_RUNTIME_BLANK.md +23 -0
  131. package/dist/reference/errors/MOSTRA_RUNTIME_NOT_READY.md +23 -0
  132. package/dist/reference/errors/MOSTRA_RUNTIME_PAGE_ERROR.md +23 -0
  133. package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_CRASHED.md +23 -0
  134. package/dist/reference/errors/MOSTRA_RUNTIME_WORKER_PARKED.md +23 -0
  135. package/dist/reference/errors/MOSTRA_SECRET_MISSING.md +23 -0
  136. package/dist/reference/errors/MOSTRA_SMOKE_BUDGET.md +23 -0
  137. package/dist/reference/errors/MOSTRA_SMOKE_NO_EFFECT.md +23 -0
  138. package/dist/reference/errors/MOSTRA_SUBMIT_ALREADY.md +23 -0
  139. package/dist/reference/errors/MOSTRA_SUBMIT_BLOCKED.md +23 -0
  140. package/dist/reference/errors/MOSTRA_SUBMIT_CHANNEL_DENIED.md +23 -0
  141. package/dist/reference/errors/MOSTRA_SUBMIT_NOT_FOUND.md +23 -0
  142. package/dist/reference/errors/MOSTRA_SUBMIT_POLICY_NONE.md +23 -0
  143. package/dist/reference/errors/MOSTRA_SUBMIT_REVISION_MOVED.md +23 -0
  144. package/dist/reference/errors/MOSTRA_SUBMIT_SIGNAL_INVALID.md +23 -0
  145. package/dist/reference/errors/MOSTRA_SUBMIT_VIEW_INVALID.md +23 -0
  146. package/dist/reference/errors/MOSTRA_TARGET_UNAVAILABLE.md +23 -0
  147. package/dist/reference/errors/MOSTRA_TYPE_ERROR.md +23 -0
  148. package/dist/reference/errors/MOSTRA_TYPE_UNTYPED_PACKAGE.md +23 -0
  149. package/dist/reference/errors/MOSTRA_VERIFY_ACTION_SKIPPED.md +23 -0
  150. package/dist/reference/errors/MOSTRA_VERIFY_BASELINE_MISSING.md +23 -0
  151. package/dist/reference/errors/MOSTRA_VERIFY_CRASHED.md +23 -0
  152. package/dist/reference/errors/MOSTRA_VERIFY_LIVE_FAILED.md +23 -0
  153. package/dist/reference/errors/MOSTRA_VERIFY_NO_BASELINE.md +23 -0
  154. package/dist/reference/errors/MOSTRA_VERIFY_SECRET_MISSING.md +23 -0
  155. package/dist/reference/errors/MOSTRA_VERIFY_SKIP_CONSEQUENCE.md +23 -0
  156. package/dist/reference/errors/MOSTRA_VERIFY_TIMEOUT.md +23 -0
  157. package/dist/reference/telemetry.md +82 -0
  158. package/dist/run-BJaRnz-Q.js +3824 -0
  159. package/dist/run-BJaRnz-Q.js.map +1 -0
  160. package/dist/run-CMyjOm0Q.js +2 -0
  161. package/dist/telemetry/flush-main.d.ts +1 -0
  162. package/dist/telemetry/flush-main.js +14 -0
  163. package/dist/telemetry/flush-main.js.map +1 -0
  164. package/package.json +62 -3
  165. package/src/bin.ts +70 -0
  166. package/src/bootstrap.ts +156 -0
  167. package/src/browser.ts +186 -0
  168. package/src/client.ts +313 -0
  169. package/src/command.ts +147 -0
  170. package/src/commands/admin.ts +791 -0
  171. package/src/commands/authoring.ts +244 -0
  172. package/src/commands/dev.ts +106 -0
  173. package/src/commands/index.ts +71 -0
  174. package/src/commands/interaction.ts +288 -0
  175. package/src/commands/shared.ts +71 -0
  176. package/src/commands/viewing.ts +153 -0
  177. package/src/docs.ts +111 -0
  178. package/src/exit.ts +89 -0
  179. package/src/help.ts +170 -0
  180. package/src/home.ts +111 -0
  181. package/src/index.ts +23 -0
  182. package/src/node-gate.ts +15 -0
  183. package/src/parse.ts +246 -0
  184. package/src/run.ts +387 -0
  185. package/src/telemetry/decision.ts +101 -0
  186. package/src/telemetry/events.ts +147 -0
  187. package/src/telemetry/flush-main.ts +13 -0
  188. package/src/telemetry/flush.ts +143 -0
  189. package/src/telemetry/harness.ts +39 -0
  190. package/src/telemetry/index.ts +318 -0
  191. package/src/telemetry/store.ts +379 -0
  192. package/src/telemetry/timezone.ts +72 -0
  193. package/src/telemetry/words.ts +87 -0
  194. package/src/telemetry/zone-sets.ts +182 -0
  195. package/src/telemetry/zones.generated.ts +720 -0
  196. package/src/version.ts +43 -0
@@ -0,0 +1,791 @@
1
+ /**
2
+ * Administration: `secret`, `list`, `status`, `daemon`, `host`, `gc`, `doctor`, `docs` and
3
+ * `telemetry`. Each is a client of the host protocol or the runtime binary, never of the home's
4
+ * files, except `doctor`, which reads what it needs to explain why an instance can't start.
5
+ */
6
+ import { spawn } from "node:child_process";
7
+ import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
8
+ import { join } from "node:path";
9
+ import {
10
+ artifactGetMethod,
11
+ artifactListMethod,
12
+ createDiagnostic,
13
+ type Diagnostic,
14
+ gcRunMethod,
15
+ type InstanceCheck,
16
+ instanceCheckMethod,
17
+ instanceStatusMethod,
18
+ mostraShutdownMethod,
19
+ RpcError,
20
+ secretDeleteMethod,
21
+ secretListMethod,
22
+ secretSetMethod,
23
+ } from "@mostra/protocol";
24
+ import { checkBrowser, checkFiles, enclosingBundle, readBundle, readSetup } from "@mostra/sdk";
25
+ import { type CommandContext, type CommandDef, defineCommand } from "../command.ts";
26
+ import { docsIndex, indexMarkdown, nearestTopics } from "../docs.ts";
27
+ import { EXIT, exitCodeFor } from "../exit.ts";
28
+ import { embeddedHome, locateRuntime, readHomeMarks } from "../home.ts";
29
+ import { usage } from "../parse.ts";
30
+ import { TelemetryStore, TURNED, telemetryStatus } from "../telemetry/index.ts";
31
+ import { NOTICE_DEFAULT } from "../telemetry/words.ts";
32
+ import { cliVersion } from "../version.ts";
33
+ import { arg, artifactRef, flag, pretty } from "./shared.ts";
34
+
35
+ const artifactFlag = {
36
+ name: "artifact",
37
+ kind: "string" as const,
38
+ value: "<path|id>",
39
+ summary: "The artifact's own secret, instead of one the whole home shares.",
40
+ };
41
+
42
+ /** `mostra secret set <name>`. */
43
+ export const secretSetCommand: CommandDef = defineCommand({
44
+ path: ["secret", "set"],
45
+ summary:
46
+ "Store a secret that server code reads with secret(name); the value comes from stdin and is never shown.",
47
+ args: [{ name: "name", summary: "Letters, digits, _, . and -, starting with a letter or _." }],
48
+ flags: [artifactFlag],
49
+ output: { type: "SecretEntry", topic: "cli" },
50
+ exits: [EXIT.failure],
51
+ example: "secret set MAIL_TOKEN --artifact ./inbox < token.txt",
52
+ readsStdin: true,
53
+ async run(context) {
54
+ let value = await context.readStdin();
55
+ // One trailing newline is the terminal's or `echo`'s, never the secret's.
56
+ if (value.endsWith("\n")) value = value.slice(0, value.endsWith("\r\n") ? -2 : -1);
57
+ if (value === "") throw usage("The secret's value comes from stdin, and stdin was empty.");
58
+ const artifact = flag(context, "artifact");
59
+ const entry = await context.call(secretSetMethod, {
60
+ name: arg(context, "name"),
61
+ value,
62
+ ...(artifact !== undefined ? { artifact: artifactRef(artifact, context.cwd) } : {}),
63
+ });
64
+ return { result: entry, human: `Stored ${entry.name} (${entry.scope})\n` };
65
+ },
66
+ });
67
+
68
+ /** `mostra secret list`. */
69
+ export const secretListCommand: CommandDef = defineCommand({
70
+ path: ["secret", "list"],
71
+ summary: "List the secrets' names and scopes; values are never shown.",
72
+ flags: [artifactFlag],
73
+ output: { type: "SecretListResult", topic: "cli" },
74
+ exits: [EXIT.failure],
75
+ example: "secret list --artifact ./inbox",
76
+ async run(context) {
77
+ const artifact = flag(context, "artifact");
78
+ const result = await context.call(secretListMethod, {
79
+ ...(artifact !== undefined ? { artifact: artifactRef(artifact, context.cwd) } : {}),
80
+ });
81
+ return {
82
+ result,
83
+ human: result.secrets
84
+ .map((entry) => `${entry.name} ${entry.scope} ${entry.updatedAt}\n`)
85
+ .join(""),
86
+ };
87
+ },
88
+ });
89
+
90
+ /** `mostra secret rm <name>`. */
91
+ export const secretRmCommand: CommandDef = defineCommand({
92
+ path: ["secret", "rm"],
93
+ summary: "Delete a secret.",
94
+ args: [{ name: "name", summary: "The secret's name." }],
95
+ flags: [artifactFlag],
96
+ output: { type: "SecretDeleteResult", topic: "cli" },
97
+ exits: [EXIT.failure],
98
+ example: "secret rm MAIL_TOKEN --artifact ./inbox",
99
+ async run(context) {
100
+ const artifact = flag(context, "artifact");
101
+ const result = await context.call(secretDeleteMethod, {
102
+ name: arg(context, "name"),
103
+ ...(artifact !== undefined ? { artifact: artifactRef(artifact, context.cwd) } : {}),
104
+ });
105
+ return { result, human: result.deleted ? "Deleted\n" : "No such secret\n" };
106
+ },
107
+ });
108
+
109
+ /** `mostra list`. */
110
+ export const listCommand: CommandDef = defineCommand({
111
+ path: ["list"],
112
+ summary: "List the home's published artifacts.",
113
+ flags: [
114
+ {
115
+ name: "family",
116
+ kind: "string",
117
+ value: "<family>",
118
+ summary: "Only artifacts of this family.",
119
+ },
120
+ ],
121
+ output: { type: "ArtifactListResult", topic: "cli" },
122
+ exits: [],
123
+ example: "list --family dashboard",
124
+ async run(context) {
125
+ const family = flag(context, "family");
126
+ const result = await context.call(artifactListMethod, {
127
+ ...(family !== undefined ? { filter: { family } } : {}),
128
+ });
129
+ return {
130
+ result,
131
+ human: result.artifacts
132
+ .map(
133
+ (artifact) =>
134
+ `${artifact.id} v${artifact.latestVersion?.number ?? "-"} ${artifact.title}${artifact.source ? ` ${artifact.source}` : ""}\n`,
135
+ )
136
+ .join(""),
137
+ };
138
+ },
139
+ });
140
+
141
+ /** `mostra status [artifact]`. */
142
+ export const statusCommand: CommandDef = defineCommand({
143
+ path: ["status"],
144
+ summary:
145
+ "Show the instance (processes, workers, deliveries) or one artifact (versions, policy, links).",
146
+ args: [{ name: "artifact", summary: "An artifact: its source path or its ID.", optional: true }],
147
+ output: { type: "InstanceStatusResult or ArtifactDetail", topic: "cli" },
148
+ exits: [EXIT.failure],
149
+ example: "status ./sidebar-review",
150
+ async run(context) {
151
+ const artifact = context.args["artifact"];
152
+ if (typeof artifact === "string") {
153
+ const detail = await context.call(artifactGetMethod, {
154
+ artifact: artifactRef(artifact, context.cwd),
155
+ });
156
+ const versions = detail.versions
157
+ .map(
158
+ (version) =>
159
+ ` v${version.number} ${version.id} ${version.publishedAt}${version.collected ? " collected" : ""}\n`,
160
+ )
161
+ .join("");
162
+ return { result: detail, human: `${detail.id} ${detail.title}\n${versions}` };
163
+ }
164
+ const status = await context.call(instanceStatusMethod, {});
165
+ return { result: status, human: pretty(status) };
166
+ },
167
+ });
168
+
169
+ /** The runtime's `service` command, as JSON. */
170
+ async function service(
171
+ context: CommandContext,
172
+ action: "install" | "uninstall" | "status",
173
+ ): Promise<{ manager: string; name: string; file: string; installed: boolean }> {
174
+ const { stdout } = await context.runtime([
175
+ "service",
176
+ action,
177
+ "--home",
178
+ context.home,
179
+ ...(context.flags["no-start"] === true ? ["--no-start"] : []),
180
+ ]);
181
+ const line = stdout.split("\n").find((text) => text.trim().startsWith("{"));
182
+ let parsed: { status?: string; service?: unknown; diagnostic?: Diagnostic } = {};
183
+ try {
184
+ parsed = JSON.parse(line ?? "{}") as typeof parsed;
185
+ } catch {
186
+ // Reported below.
187
+ }
188
+ if (parsed.status === "ok" && parsed.service) {
189
+ return parsed.service as { manager: string; name: string; file: string; installed: boolean };
190
+ }
191
+ throw new RpcError(
192
+ parsed.diagnostic ??
193
+ createDiagnostic("MOSTRA_ENV_SERVICE_FAILED", {
194
+ message: `mostra-runtime service ${action} gave no answer.`,
195
+ }),
196
+ );
197
+ }
198
+
199
+ const noStart = {
200
+ name: "no-start",
201
+ kind: "boolean" as const,
202
+ summary: "Write or remove the service's file without asking launchd or systemd to load it.",
203
+ };
204
+
205
+ /** `mostra daemon start`. */
206
+ export const daemonStartCommand: CommandDef = defineCommand({
207
+ path: ["daemon", "start"],
208
+ summary: "Start the home's daemon, or find the one already running.",
209
+ output: { type: "DaemonStatus", topic: "cli" },
210
+ exits: [],
211
+ example: "daemon start",
212
+ async run(context) {
213
+ const connection = await context.connection();
214
+ const summary = connection?.summary;
215
+ return {
216
+ result: { running: true, ...summary },
217
+ human: `${summary?.started ? "Started" : "Running"}: pid ${summary?.pid}, ${summary?.socket}\n`,
218
+ };
219
+ },
220
+ });
221
+
222
+ /** Stops the running daemon; resolves once its socket closed. */
223
+ async function stopDaemon(context: CommandContext): Promise<{ stopped: boolean; pid?: number }> {
224
+ const connection = await context.connection(false);
225
+ if (!connection) return { stopped: false };
226
+ const { pid } = connection.summary;
227
+ await context.call(mostraShutdownMethod, {});
228
+ await Promise.race([connection.closed, new Promise((done) => setTimeout(done, 15_000))]);
229
+ // The record goes last, as the instance releases its home (D87).
230
+ const deadline = Date.now() + 15_000;
231
+ while (existsSync(join(context.home, "instance.json")) && Date.now() < deadline) {
232
+ await new Promise((done) => setTimeout(done, 50));
233
+ }
234
+ return { stopped: true, pid };
235
+ }
236
+
237
+ /** `mostra daemon stop`. */
238
+ export const daemonStopCommand: CommandDef = defineCommand({
239
+ path: ["daemon", "stop"],
240
+ summary: "Stop the home's daemon; open pages reconnect when it starts again.",
241
+ output: { type: "DaemonStop", topic: "cli" },
242
+ exits: [],
243
+ example: "daemon stop",
244
+ async run(context) {
245
+ const result = await stopDaemon(context);
246
+ return {
247
+ result,
248
+ human: result.stopped ? `Stopped pid ${result.pid}\n` : "No daemon was running\n",
249
+ };
250
+ },
251
+ });
252
+
253
+ /** `mostra daemon restart`. */
254
+ export const daemonRestartCommand: CommandDef = defineCommand({
255
+ path: ["daemon", "restart"],
256
+ summary: "Stop the home's daemon and start it again, for instance after an update.",
257
+ output: { type: "DaemonStatus", topic: "cli" },
258
+ exits: [],
259
+ example: "daemon restart",
260
+ async run(context) {
261
+ await stopDaemon(context);
262
+ const connection = await context.connection();
263
+ const summary = connection?.summary;
264
+ return { result: { running: true, ...summary }, human: `Running: pid ${summary?.pid}\n` };
265
+ },
266
+ });
267
+
268
+ /** `mostra daemon status`. */
269
+ export const daemonStatusCommand: CommandDef = defineCommand({
270
+ path: ["daemon", "status"],
271
+ summary:
272
+ "Whether the home's daemon runs, when it would exit for lack of work, and its login service.",
273
+ output: { type: "DaemonStatus", topic: "cli" },
274
+ exits: [],
275
+ example: "daemon status",
276
+ async run(context) {
277
+ const loginService = await service(context, "status");
278
+ const connection = await context.connection(false);
279
+ if (!connection) {
280
+ return {
281
+ result: { running: false, service: loginService },
282
+ human: `Not running. Login service: ${loginService.installed ? "installed" : "not installed"}\n`,
283
+ };
284
+ }
285
+ const status = await context.call(instanceStatusMethod, {});
286
+ return {
287
+ result: { running: true, ...connection.summary, status, service: loginService },
288
+ human: `Running: pid ${connection.summary.pid}${status.idleExitAt ? `, exits when idle at ${status.idleExitAt}` : ""}. Login service: ${loginService.installed ? "installed" : "not installed"}\n`,
289
+ };
290
+ },
291
+ });
292
+
293
+ /** `mostra daemon install`. */
294
+ export const daemonInstallCommand: CommandDef = defineCommand({
295
+ path: ["daemon", "install"],
296
+ summary:
297
+ "Run the home's daemon as a login service (launchd or systemd), so links answer at any hour.",
298
+ flags: [noStart],
299
+ output: { type: "ServiceStatus", topic: "cli" },
300
+ exits: [],
301
+ example: "daemon install",
302
+ async run(context) {
303
+ const result = await service(context, "install");
304
+ return { result, human: `Installed ${result.name} (${result.file})\n` };
305
+ },
306
+ });
307
+
308
+ /** `mostra daemon uninstall`. */
309
+ export const daemonUninstallCommand: CommandDef = defineCommand({
310
+ path: ["daemon", "uninstall"],
311
+ summary: "Remove the home's login service; the daemon then exits after 30 idle minutes.",
312
+ flags: [noStart],
313
+ output: { type: "ServiceStatus", topic: "cli" },
314
+ exits: [],
315
+ example: "daemon uninstall",
316
+ async run(context) {
317
+ const result = await service(context, "uninstall");
318
+ return { result, human: `Removed ${result.name}\n` };
319
+ },
320
+ });
321
+
322
+ /** `mostra host`. */
323
+ export const hostCommand: CommandDef = defineCommand({
324
+ path: ["host"],
325
+ summary:
326
+ "Speak the host protocol on stdin and stdout, for a host that spawns Mostra; it sends no telemetry.",
327
+ flags: [
328
+ {
329
+ name: "bind",
330
+ kind: "string",
331
+ value: "<address>",
332
+ summary: "Serve viewers on this address instead of 127.0.0.1.",
333
+ },
334
+ {
335
+ name: "url-host",
336
+ kind: "string",
337
+ value: "<name>",
338
+ summary: "The host name links use; a wildcard --bind needs it.",
339
+ },
340
+ ],
341
+ output: { type: "the host protocol (INTERFACES §6)", topic: "cli" },
342
+ exits: [],
343
+ example: "host --home ~/.my-host/mostra",
344
+ local: true,
345
+ quiet: true,
346
+ readsStdin: true,
347
+ async run(context) {
348
+ const runtime = await locateRuntime(context.env);
349
+ const bind = flag(context, "bind");
350
+ const urlHost = flag(context, "url-host");
351
+ const code = await new Promise<number>((resolve) => {
352
+ const child = spawn(
353
+ runtime.node,
354
+ [
355
+ runtime.entry,
356
+ "host",
357
+ "--home",
358
+ context.home,
359
+ ...(bind !== undefined ? ["--bind", bind] : []),
360
+ ...(urlHost !== undefined ? ["--url-host", urlHost] : []),
361
+ ],
362
+ { stdio: "inherit", env: context.env as NodeJS.ProcessEnv },
363
+ );
364
+ child.on("exit", (exit) => resolve(exit ?? EXIT.environment));
365
+ child.on("error", () => resolve(EXIT.environment));
366
+ for (const signal of ["SIGINT", "SIGTERM"] as const)
367
+ process.on(signal, () => child.kill(signal));
368
+ });
369
+ return { result: undefined, exit: code, raw: true };
370
+ },
371
+ });
372
+
373
+ /** `mostra gc`. */
374
+ export const gcCommand: CommandDef = defineCommand({
375
+ path: ["gc"],
376
+ summary:
377
+ "Remove what nothing references: unreferenced versions' files, unused packages and caches.",
378
+ flags: [
379
+ {
380
+ name: "dry-run",
381
+ kind: "boolean",
382
+ summary: "List what a run would remove, and remove nothing.",
383
+ },
384
+ {
385
+ name: "compact-events",
386
+ kind: "boolean",
387
+ summary: "Also replace events before --before with one summary per artifact.",
388
+ },
389
+ {
390
+ name: "before",
391
+ kind: "string",
392
+ value: "<cursor>",
393
+ summary: "The cursor --compact-events compacts before.",
394
+ },
395
+ ],
396
+ output: { type: "GcRunResult", topic: "cli" },
397
+ exits: [EXIT.failure],
398
+ example: "gc --dry-run",
399
+ async run(context) {
400
+ const compact = context.flags["compact-events"] === true;
401
+ const before = flag(context, "before");
402
+ if (compact !== (before !== undefined))
403
+ throw usage("--compact-events and --before go together.");
404
+ if (before !== undefined && !/^[0-9]+$/.test(before))
405
+ throw usage(`--before takes an event cursor; "${before}" isn't one.`);
406
+ const result = await context.call(gcRunMethod, {
407
+ ...(context.flags["dry-run"] === true ? { dryRun: true } : {}),
408
+ ...(before !== undefined ? { compactEvents: { before } } : {}),
409
+ });
410
+ // A step gc couldn't finish is a warning beside what it did (MOSTRA_HOME_GC_INCOMPLETE).
411
+ for (const warning of result.warnings ?? []) context.warn(warning);
412
+ const verb = result.dryRun ? "Would remove" : "Removed";
413
+ const lines = [
414
+ `${verb} ${result.versions.length} versions' files, ${result.snapshots.count} snapshots, ${result.lockLayouts.count} lock layouts, ${result.storePackages.count} store packages and ${result.caches.count} cache entries (${result.bytes} bytes).`,
415
+ ...result.versions.map(
416
+ (version) => ` ${version.artifactId} v${version.number} ${version.versionId}`,
417
+ ),
418
+ ...(result.compacted ? [`Compacted ${result.compacted.removed} events.`] : []),
419
+ ];
420
+ return { result, human: `${lines.join("\n")}\n` };
421
+ },
422
+ });
423
+
424
+ /** One line of `doctor`. */
425
+ interface DoctorCheck {
426
+ readonly name: string;
427
+ readonly status: "ok" | "warning" | "error";
428
+ readonly detail?: string;
429
+ readonly diagnostic?: Diagnostic;
430
+ }
431
+
432
+ /** `mostra doctor`. */
433
+ export const doctorCommand: CommandDef = defineCommand({
434
+ path: ["doctor"],
435
+ summary:
436
+ "Check the platform, the runtime, the home, its daemon and ledger, and the login service.",
437
+ flags: [
438
+ {
439
+ name: "deep",
440
+ kind: "boolean",
441
+ summary: "Also hash every version against its recorded output digest.",
442
+ },
443
+ ],
444
+ output: { type: "DoctorResult", topic: "cli" },
445
+ exits: [EXIT.failure],
446
+ example: "doctor",
447
+ async run(context) {
448
+ const checks: DoctorCheck[] = [];
449
+ const node = Number(process.versions.node.split(".")[0]);
450
+ checks.push(
451
+ process.platform === "darwin" || process.platform === "linux"
452
+ ? {
453
+ name: "platform",
454
+ status: "ok",
455
+ detail: `${process.platform} ${process.arch}, Node ${process.versions.node}`,
456
+ }
457
+ : {
458
+ name: "platform",
459
+ status: "error",
460
+ diagnostic: createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", {
461
+ message: `Mostra runs on macOS and Linux; this is ${process.platform}.`,
462
+ }),
463
+ },
464
+ );
465
+ if (node < 24) {
466
+ checks.push({
467
+ name: "node",
468
+ status: "error",
469
+ diagnostic: createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", {
470
+ message: `Mostra needs Node 24 or later; this is ${process.versions.node}.`,
471
+ }),
472
+ });
473
+ }
474
+ try {
475
+ const runtime = await locateRuntime(context.env);
476
+ checks.push({ name: "runtime", status: "ok", detail: runtime.entry });
477
+ } catch (error) {
478
+ checks.push({ name: "runtime", status: "error", diagnostic: (error as RpcError).diagnostic });
479
+ }
480
+ checks.push(await bundleCheck(context.env, context.flags["deep"] === true));
481
+ checks.push(homeCheck(context.home));
482
+ let connection: Awaited<ReturnType<CommandContext["connection"]>>;
483
+ try {
484
+ connection = await context.connection();
485
+ checks.push({
486
+ name: "daemon",
487
+ status: "ok",
488
+ detail: `pid ${connection?.summary.pid}, ${connection?.summary.socket}`,
489
+ });
490
+ } catch (error) {
491
+ const diagnostic = error instanceof RpcError ? error.diagnostic : undefined;
492
+ checks.push({
493
+ name: "daemon",
494
+ status: "error",
495
+ ...(diagnostic ? { diagnostic } : { detail: String(error) }),
496
+ ...(diagnostic?.code === "MOSTRA_HOME_LEDGER_UNREADABLE"
497
+ ? { detail: backups(context.home) }
498
+ : {}),
499
+ });
500
+ }
501
+ if (connection) {
502
+ if (connection.summary.version !== cliVersion()) {
503
+ checks.push({
504
+ name: "versions",
505
+ status: "warning",
506
+ detail: `The CLI is ${cliVersion()} and the daemon ${connection.summary.version}; run "mostra daemon restart" after an update.`,
507
+ });
508
+ }
509
+ const instance = await context.call(
510
+ instanceCheckMethod,
511
+ context.flags["deep"] === true ? { deep: true } : {},
512
+ );
513
+ checks.push(...instance.checks.map((check: InstanceCheck) => ({ ...check })));
514
+ checks.push(await alwaysOnCheck(context));
515
+ }
516
+ const telemetry = telemetryStatus({
517
+ env: context.env,
518
+ configDir: context.configDir,
519
+ embedded:
520
+ readHomeMarks(context.home).hostConfigured !== undefined ||
521
+ connection?.summary.mode === "host",
522
+ });
523
+ checks.push({
524
+ name: "telemetry",
525
+ status: "ok",
526
+ detail: telemetry.text.split("\n")[0] as string,
527
+ });
528
+ const firstError = checks.find((check) => check.status === "error");
529
+ const human = checks
530
+ .map((check) => {
531
+ const mark = check.status === "ok" ? "ok " : check.status === "warning" ? "warn" : "FAIL";
532
+ const what = check.diagnostic
533
+ ? `${check.diagnostic.code}: ${check.diagnostic.message}`
534
+ : (check.detail ?? "");
535
+ const hint = check.diagnostic?.hint ? `\n ${check.diagnostic.hint}` : "";
536
+ return `${mark} ${check.name}${what ? `: ${what}` : ""}${hint}`;
537
+ })
538
+ .join("\n");
539
+ return {
540
+ result: { checks },
541
+ human: `${human}\n`,
542
+ ...(firstError
543
+ ? {
544
+ exit: firstError.diagnostic
545
+ ? exitCodeFor(firstError.diagnostic.code)
546
+ : EXIT.environment,
547
+ }
548
+ : {}),
549
+ };
550
+ },
551
+ });
552
+
553
+ /**
554
+ * The runtime bundle this CLI runs from (RT-01): set up and matching its files. Outside a bundle
555
+ * (a development checkout, `MOSTRA_RUNTIME_BIN`) there is nothing to check. `--deep` hashes every
556
+ * file against the bundle's file list and the browser against its pin, as setup does.
557
+ */
558
+ async function bundleCheck(env: CommandContext["env"], deep: boolean): Promise<DoctorCheck> {
559
+ const root = env["MOSTRA_BUNDLE"] || enclosingBundle();
560
+ if (!root)
561
+ return { name: "bundle", status: "ok", detail: "No runtime bundle: a development runtime." };
562
+ try {
563
+ const bundle = await readBundle(root);
564
+ const label = `Mostra ${bundle.info.version} for ${bundle.info.platform} at ${root}`;
565
+ if (!(await readSetup(bundle))) {
566
+ return {
567
+ name: "bundle",
568
+ status: "error",
569
+ diagnostic: createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", {
570
+ message: `The runtime bundle at ${root} isn't set up; run its bin/mostra-setup.`,
571
+ }),
572
+ };
573
+ }
574
+ if (!deep) return { name: "bundle", status: "ok", detail: label };
575
+ await checkFiles(bundle);
576
+ const browser = await checkBrowser(bundle);
577
+ if (browser.length > 0) {
578
+ return {
579
+ name: "bundle",
580
+ status: "error",
581
+ diagnostic: createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", {
582
+ message: `The verify browser of the bundle at ${root} differs from its pin: ${browser.join("; ")}.`,
583
+ }),
584
+ };
585
+ }
586
+ return { name: "bundle", status: "ok", detail: `${label}; every file matches its SHA-256` };
587
+ } catch (error) {
588
+ return {
589
+ name: "bundle",
590
+ status: "error",
591
+ diagnostic:
592
+ error instanceof RpcError
593
+ ? error.diagnostic
594
+ : createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: (error as Error).message }),
595
+ };
596
+ }
597
+ }
598
+
599
+ /** The home: absent (it is made on first use), a directory the user owns, or not. */
600
+ function homeCheck(home: string): DoctorCheck {
601
+ let info: ReturnType<typeof statSync>;
602
+ try {
603
+ info = statSync(home);
604
+ } catch {
605
+ return { name: "home", status: "ok", detail: `${home} (not created yet)` };
606
+ }
607
+ if (!info.isDirectory() || (process.getuid && info.uid !== process.getuid())) {
608
+ return {
609
+ name: "home",
610
+ status: "error",
611
+ diagnostic: createDiagnostic("MOSTRA_HOME_NOT_OWNED", {
612
+ message: `Mostra can't use ${home}: another user owns it or it isn't a directory.`,
613
+ }),
614
+ };
615
+ }
616
+ return { name: "home", status: "ok", detail: home };
617
+ }
618
+
619
+ /** The ledger's backups beside it, for a ledger that can't be opened (FAILURE_MODEL §3). */
620
+ function backups(home: string): string {
621
+ try {
622
+ const found = readdirSync(join(home, "store")).filter((name) =>
623
+ name.startsWith("ledger.sqlite.bak-"),
624
+ );
625
+ return found.length > 0
626
+ ? `Backups: ${found.map((name) => join(home, "store", name)).join(", ")}`
627
+ : "No ledger backups.";
628
+ } catch {
629
+ return "No ledger backups.";
630
+ }
631
+ }
632
+
633
+ /**
634
+ * LL-05's doctor half (D96, D63): artifacts with submission policy `none` are meant to answer at
635
+ * any hour, which needs the home's login service.
636
+ */
637
+ async function alwaysOnCheck(context: CommandContext): Promise<DoctorCheck> {
638
+ const { artifacts } = await context.call(artifactListMethod, {});
639
+ const longLived = artifacts.filter((artifact) => artifact.policy?.policy === "none");
640
+ if (longLived.length === 0)
641
+ return { name: "always-on", status: "ok", detail: "No artifact needs an always-on daemon." };
642
+ const loginService = await service(context, "status");
643
+ if (loginService.installed) {
644
+ return {
645
+ name: "always-on",
646
+ status: "ok",
647
+ detail: `Login service ${loginService.name} keeps the daemon running.`,
648
+ };
649
+ }
650
+ return {
651
+ name: "always-on",
652
+ status: "warning",
653
+ diagnostic: createDiagnostic("MOSTRA_HOME_NOT_ALWAYS_ON", {
654
+ message: `${longLived.length} ${longLived.length === 1 ? "artifact has" : "artifacts have"} submission policy none (${longLived
655
+ .map((artifact) => artifact.title)
656
+ .slice(0, 3)
657
+ .join(", ")}), and no login service keeps this home's daemon running.`,
658
+ }),
659
+ };
660
+ }
661
+
662
+ /** `mostra docs [topic]`. */
663
+ export const docsCommand: CommandDef = defineCommand({
664
+ path: ["docs"],
665
+ summary: "Print Mostra's reference: the CLI, telemetry, and every diagnostic code's page.",
666
+ args: [
667
+ { name: "topic", summary: "A topic or a diagnostic code. Default: the index.", optional: true },
668
+ ],
669
+ output: { type: "DocsPage", topic: "cli" },
670
+ exits: [],
671
+ example: "docs MOSTRA_DEP_MISSING",
672
+ local: true,
673
+ quiet: true,
674
+ async run(context) {
675
+ const topics = docsIndex();
676
+ const wanted = context.args["topic"];
677
+ if (typeof wanted !== "string") {
678
+ const markdown = indexMarkdown(topics);
679
+ return {
680
+ result: {
681
+ topic: "index",
682
+ markdown,
683
+ url: topics[0]?.url.replace(/\/[^/]+$/, "") ?? "",
684
+ topics: topics.map(({ topic, title, url }) => ({ topic, title, url })),
685
+ },
686
+ human: markdown,
687
+ };
688
+ }
689
+ const found = topics.find(
690
+ (topic) => topic.topic === wanted || topic.topic === wanted.toUpperCase(),
691
+ );
692
+ if (!found) {
693
+ throw usage(
694
+ `No topic "${wanted}". The nearest: ${nearestTopics(wanted, topics).join(", ")}. Run "mostra docs" for the index.`,
695
+ );
696
+ }
697
+ const markdown = readFileSync(found.file, "utf8");
698
+ return { result: { topic: found.topic, markdown, url: found.url }, human: markdown };
699
+ },
700
+ });
701
+
702
+ /** `mostra telemetry status`. */
703
+ export const telemetryStatusCommand: CommandDef = defineCommand({
704
+ path: ["telemetry", "status"],
705
+ summary: "Whether the CLI sends anonymous usage data, and why.",
706
+ output: { type: "TelemetryStatus", topic: "telemetry" },
707
+ exits: [],
708
+ example: "telemetry status",
709
+ local: true,
710
+ quiet: true,
711
+ async run(context) {
712
+ const { status, text } = telemetryStatus({
713
+ env: context.env,
714
+ configDir: context.configDir,
715
+ ...homeTelemetry(context.home),
716
+ });
717
+ return { result: status, human: `${text}\n` };
718
+ },
719
+ });
720
+
721
+ /** How the home bears on telemetry: a host's home, or one whose `config.json` can't be read. */
722
+ function homeTelemetry(home: string): { embedded: boolean; homeConfigUnreadable?: string } {
723
+ const marks = readHomeMarks(home);
724
+ return {
725
+ embedded: embeddedHome(marks) && marks.unreadable !== true,
726
+ ...(marks.unreadable === true ? { homeConfigUnreadable: join(home, "config.json") } : {}),
727
+ };
728
+ }
729
+
730
+ /** The choice couldn't be recorded: nothing changed (Opus review finding 6). */
731
+ function unwritable(store: TelemetryStore): RpcError {
732
+ return new RpcError(
733
+ createDiagnostic("MOSTRA_CLI_CONFIG_UNWRITABLE", {
734
+ message: `${store.settingsFile} can't be written, so the choice wasn't recorded and nothing changed.`,
735
+ }),
736
+ );
737
+ }
738
+
739
+ /** `mostra telemetry on`. */
740
+ export const telemetryOnCommand: CommandDef = defineCommand({
741
+ path: ["telemetry", "on"],
742
+ summary: "Send anonymous usage data from this CLI; the notice says what it contains.",
743
+ output: { type: "TelemetryStatus", topic: "telemetry" },
744
+ exits: [EXIT.environment],
745
+ example: "telemetry on",
746
+ local: true,
747
+ quiet: true,
748
+ async run(context) {
749
+ const store = new TelemetryStore(context.configDir);
750
+ const read = store.read();
751
+ const shownBefore = read.ok && read.settings.noticeShownAt !== undefined;
752
+ if (!shownBefore) context.stderr(NOTICE_DEFAULT);
753
+ const now = new Date().toISOString();
754
+ const written =
755
+ store.update((current) => ({
756
+ ...current,
757
+ choice: "on",
758
+ noticeShownAt: current.noticeShownAt ?? now,
759
+ })) ?? store.locked(() => store.reset({ choice: "on", noticeShownAt: now }));
760
+ if (written === undefined) throw unwritable(store);
761
+ const { status } = telemetryStatus({
762
+ env: context.env,
763
+ configDir: context.configDir,
764
+ ...homeTelemetry(context.home),
765
+ });
766
+ return { result: status, human: `${TURNED.on}\n` };
767
+ },
768
+ });
769
+
770
+ /** `mostra telemetry off`. */
771
+ export const telemetryOffCommand: CommandDef = defineCommand({
772
+ path: ["telemetry", "off"],
773
+ summary: "Stop sending usage data, and delete the install ID and any unsent events.",
774
+ output: { type: "TelemetryStatus", topic: "telemetry" },
775
+ exits: [EXIT.environment],
776
+ example: "telemetry off",
777
+ local: true,
778
+ quiet: true,
779
+ async run(context) {
780
+ const store = new TelemetryStore(context.configDir);
781
+ const done =
782
+ store.turnOff() || store.locked(() => store.reset({ choice: "off" })) !== undefined;
783
+ if (!done) throw unwritable(store);
784
+ const { status } = telemetryStatus({
785
+ env: context.env,
786
+ configDir: context.configDir,
787
+ ...homeTelemetry(context.home),
788
+ });
789
+ return { result: status, human: `${TURNED.off}\n` };
790
+ },
791
+ });