@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.
- 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 +1264 -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_IDENTITY_UNAVAILABLE.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-BJaRnz-Q.js +3824 -0
- package/dist/run-BJaRnz-Q.js.map +1 -0
- package/dist/run-CMyjOm0Q.js +2 -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,3824 @@
|
|
|
1
|
+
import { a as catalogCodes, c as detectHarness, d as truthy, f as cliVersion, i as TelemetryEvent, l as buildEndpoint, m as packageRoot, n as startFlusher, o as durationBucket, p as fromSource, r as TelemetryStore, s as minute, u as decide } from "./flush-BCuWs0Zh.js";
|
|
2
|
+
import { n as exitCodeFor, t as EXIT } from "./exit-CBIf8nAU.js";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { isAbsolute, join, resolve } from "node:path";
|
|
6
|
+
import { ARTIFACT_PROGRESS, CLI_CLIENT_NAME, DEFAULT_DOCS_BASE, DEV_NOTIFY, FieldOp, HOST_PROTOCOL_REVISIONS, LineSplitter, MutationId, RpcError, RpcPeer, artifactCreateMethod, artifactGetMethod, artifactListMethod, artifactPublishMethod, artifactVerifyMethod, createDiagnostic, depsAddMethod, devStartMethod, devStopMethod, diagnosticCatalog, encodeLine, gcRunMethod, instanceCheckMethod, instanceStatusMethod, livePushMethod, mostraInitializeMethod, mostraShutdownMethod, resultsGetMethod, secretDeleteMethod, secretListMethod, secretSetMethod, stateGetMethod, statePatchMethod, viewDefaultMethod, viewLaunchMethod, viewRevokeMethod, viewRotateMethod, viewUrlMethod, waitMethod } from "@mostra/protocol";
|
|
7
|
+
import { checkBrowser, checkFiles, enclosingBundle, readBundle, readSetup, resolveRuntime } from "@mostra/sdk";
|
|
8
|
+
import { homedir, tmpdir } from "node:os";
|
|
9
|
+
import { parseArgs } from "node:util";
|
|
10
|
+
import { randomUUID } from "node:crypto";
|
|
11
|
+
import { createInterface } from "node:readline";
|
|
12
|
+
import { createConnection } from "node:net";
|
|
13
|
+
//#region src/command.ts
|
|
14
|
+
/** Defines a command; the identity function, for types. */
|
|
15
|
+
function defineCommand(command) {
|
|
16
|
+
return command;
|
|
17
|
+
}
|
|
18
|
+
/** The command path as one string: `daemon install`. */
|
|
19
|
+
function commandName(command) {
|
|
20
|
+
return command.path.join(" ");
|
|
21
|
+
}
|
|
22
|
+
//#endregion
|
|
23
|
+
//#region src/docs.ts
|
|
24
|
+
/**
|
|
25
|
+
* `mostra docs` (D140 item 17): the reference that ships with the CLI, by stable slug. The
|
|
26
|
+
* diagnostic pages are keyed by code (`mostra docs MOSTRA_DEP_MISSING`, as hints say), beside the
|
|
27
|
+
* CLI reference and the telemetry page. This index is the one source WP-15's `docs_search` and
|
|
28
|
+
* WP-26's site read.
|
|
29
|
+
*/
|
|
30
|
+
/** Where the reference is: bundled in the build (`dist/reference`), or the repository's. */
|
|
31
|
+
function referenceDir() {
|
|
32
|
+
const bundled = join(packageRoot(), "dist", "reference");
|
|
33
|
+
return !fromSource() && existsSync(bundled) ? bundled : join(packageRoot(), "..", "..", "docs", "reference");
|
|
34
|
+
}
|
|
35
|
+
function titleOf(file) {
|
|
36
|
+
const first = readFileSync(file, "utf8").split("\n").find((line) => line.startsWith("# "));
|
|
37
|
+
return first ? first.slice(2).trim() : "";
|
|
38
|
+
}
|
|
39
|
+
/** Every topic, the pages first and then the codes, sorted. */
|
|
40
|
+
function docsIndex(dir = referenceDir()) {
|
|
41
|
+
const base = DEFAULT_DOCS_BASE.replace(/\/+$/, "");
|
|
42
|
+
const pages = [];
|
|
43
|
+
for (const topic of ["cli", "telemetry"]) {
|
|
44
|
+
const file = join(dir, `${topic}.md`);
|
|
45
|
+
if (existsSync(file)) pages.push({
|
|
46
|
+
topic,
|
|
47
|
+
title: titleOf(file),
|
|
48
|
+
url: `${base}/${topic}`,
|
|
49
|
+
file
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
const errorsDir = join(dir, "errors");
|
|
53
|
+
const errors = (existsSync(errorsDir) ? readdirSync(errorsDir).filter((name) => /^MOSTRA_[A-Z0-9_]+\.md$/.test(name)).sort() : []).map((name) => {
|
|
54
|
+
const code = name.slice(0, -3);
|
|
55
|
+
const file = join(errorsDir, name);
|
|
56
|
+
return {
|
|
57
|
+
topic: code,
|
|
58
|
+
title: titleOf(file),
|
|
59
|
+
url: `${base}/errors/${code}`,
|
|
60
|
+
file
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
return [...pages, ...errors];
|
|
64
|
+
}
|
|
65
|
+
/** The index as Markdown: what `mostra docs` prints without a topic. */
|
|
66
|
+
function indexMarkdown(topics) {
|
|
67
|
+
const pages = topics.filter((topic) => !topic.topic.startsWith("MOSTRA_"));
|
|
68
|
+
const codes = topics.filter((topic) => topic.topic.startsWith("MOSTRA_"));
|
|
69
|
+
return [
|
|
70
|
+
"# Mostra reference",
|
|
71
|
+
"",
|
|
72
|
+
"Run `mostra docs <topic>` to read one. Diagnostic codes are topics too.",
|
|
73
|
+
"",
|
|
74
|
+
...pages.map((topic) => `- \`${topic.topic}\`: ${topic.title}`),
|
|
75
|
+
"",
|
|
76
|
+
"## Diagnostic codes",
|
|
77
|
+
"",
|
|
78
|
+
...codes.map((topic) => `- \`${topic.topic}\`: ${topic.title}`),
|
|
79
|
+
""
|
|
80
|
+
].join("\n");
|
|
81
|
+
}
|
|
82
|
+
/** The topics nearest an unknown one, for its usage error. */
|
|
83
|
+
function nearestTopics(wanted, topics, count = 3) {
|
|
84
|
+
const target = wanted.toUpperCase();
|
|
85
|
+
return topics.map((topic) => {
|
|
86
|
+
const name = topic.topic.toUpperCase();
|
|
87
|
+
const contains = name.includes(target) || target.includes(name) ? 0 : 1;
|
|
88
|
+
return {
|
|
89
|
+
topic: topic.topic,
|
|
90
|
+
score: contains * 100 + distance(name, target)
|
|
91
|
+
};
|
|
92
|
+
}).sort((a, b) => a.score - b.score || (a.topic < b.topic ? -1 : 1)).slice(0, count).map((item) => item.topic);
|
|
93
|
+
}
|
|
94
|
+
/** Levenshtein distance. */
|
|
95
|
+
function distance(a, b) {
|
|
96
|
+
const row = Array.from({ length: b.length + 1 }, (_, index) => index);
|
|
97
|
+
for (let i = 1; i <= a.length; i += 1) {
|
|
98
|
+
let previous = row[0];
|
|
99
|
+
row[0] = i;
|
|
100
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
101
|
+
const current = row[j];
|
|
102
|
+
row[j] = Math.min(row[j] + 1, row[j - 1] + 1, previous + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
103
|
+
previous = current;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return row[b.length];
|
|
107
|
+
}
|
|
108
|
+
//#endregion
|
|
109
|
+
//#region src/home.ts
|
|
110
|
+
/**
|
|
111
|
+
* Where the CLI's things are: the home (D84's order), the runtime it starts, the home's settings
|
|
112
|
+
* it reads, and the CLI's own config directory (D140 item 5). Resolving a path never touches the
|
|
113
|
+
* file system; the runtime owns everything inside the home.
|
|
114
|
+
*/
|
|
115
|
+
const nonEmpty = (value) => value === void 0 || value === "" ? void 0 : value;
|
|
116
|
+
/**
|
|
117
|
+
* The home, in D84's order: `--home`, then `MOSTRA_HOME`, then the platform default (`~/.mostra`
|
|
118
|
+
* on macOS, `${XDG_DATA_HOME:-~/.local/share}/mostra` on Linux), the same order the runtime uses.
|
|
119
|
+
*/
|
|
120
|
+
function resolveHome(options) {
|
|
121
|
+
const chosen = nonEmpty(options.home) ?? nonEmpty(options.env["MOSTRA_HOME"]);
|
|
122
|
+
if (chosen !== void 0) return resolve(options.cwd, chosen);
|
|
123
|
+
const user = nonEmpty(options.env["HOME"]) ?? homedir();
|
|
124
|
+
if ((options.platform ?? process.platform) === "darwin") return join(user, ".mostra");
|
|
125
|
+
const data = nonEmpty(options.env["XDG_DATA_HOME"]);
|
|
126
|
+
return join(data !== void 0 && isAbsolute(data) ? data : join(user, ".local", "share"), "mostra");
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* The CLI's config directory (D140 item 5): `MOSTRA_CONFIG_DIR`, else
|
|
130
|
+
* `${XDG_CONFIG_HOME:-~/.config}/mostra`, on both platforms. It holds telemetry's choice and
|
|
131
|
+
* queue, per operating-system user, never per home.
|
|
132
|
+
*/
|
|
133
|
+
function configDir(env, cwd) {
|
|
134
|
+
const override = nonEmpty(env["MOSTRA_CONFIG_DIR"]);
|
|
135
|
+
if (override !== void 0) return resolve(cwd, override);
|
|
136
|
+
const xdg = nonEmpty(env["XDG_CONFIG_HOME"]);
|
|
137
|
+
const base = xdg !== void 0 && isAbsolute(xdg) ? xdg : join(nonEmpty(env["HOME"]) ?? homedir(), ".config");
|
|
138
|
+
return join(base, "mostra");
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* The runtime the CLI starts (D85, RT-01), found as the SDK finds it: `MOSTRA_BUNDLE`,
|
|
142
|
+
* `MOSTRA_RUNTIME_BIN`, the runtime bundle this CLI sits in, or a development checkout's built
|
|
143
|
+
* runtime beside it. The bootstrap (`bootstrap.ts`) has already moved a CLI installed from npm
|
|
144
|
+
* into its pinned bundle, so this never downloads. Throws the SDK's diagnostic when there is none.
|
|
145
|
+
*/
|
|
146
|
+
async function locateRuntime(env) {
|
|
147
|
+
const runtime = await resolveRuntime({ env });
|
|
148
|
+
return {
|
|
149
|
+
node: runtime.node,
|
|
150
|
+
entry: runtime.runtimeEntry
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Whether the CLI treats the home as a host's, which sends no telemetry (D140 item 4): a host
|
|
155
|
+
* marked it, or its `config.json` can't be read as settings (fail closed). One rule for the
|
|
156
|
+
* runner and for `mostra telemetry` (Opus review finding 6).
|
|
157
|
+
*/
|
|
158
|
+
function embeddedHome(marks) {
|
|
159
|
+
return marks.hostConfigured !== void 0 || marks.unreadable === true;
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* The home's host mark, read from `config.json` without the instance, for commands that never
|
|
163
|
+
* reach one. A file that can't be read as JSON counts as a host's home, so telemetry fails closed.
|
|
164
|
+
*/
|
|
165
|
+
function readHomeMarks(home) {
|
|
166
|
+
let text;
|
|
167
|
+
try {
|
|
168
|
+
text = readFileSync(join(home, "config.json"), "utf8");
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return error.code === "ENOENT" ? {} : { unreadable: true };
|
|
171
|
+
}
|
|
172
|
+
try {
|
|
173
|
+
const mark = JSON.parse(text).hostConfigured;
|
|
174
|
+
if (mark === void 0) return {};
|
|
175
|
+
return typeof mark.client === "string" && typeof mark.at === "string" ? { hostConfigured: {
|
|
176
|
+
client: mark.client,
|
|
177
|
+
at: mark.at
|
|
178
|
+
} } : { unreadable: true };
|
|
179
|
+
} catch {
|
|
180
|
+
return { unreadable: true };
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
//#endregion
|
|
184
|
+
//#region src/parse.ts
|
|
185
|
+
/**
|
|
186
|
+
* The command line, read against the command definitions (D140 item 13): the global flags, the
|
|
187
|
+
* command path, its arguments and flags. Every failure is `MOSTRA_CLI_USAGE` or
|
|
188
|
+
* `MOSTRA_CLI_UNKNOWN_COMMAND`, so `--json` mode reports it like any other error.
|
|
189
|
+
*/
|
|
190
|
+
/** The flags every command takes (INTERFACES §9). */
|
|
191
|
+
const GLOBAL_FLAGS = [
|
|
192
|
+
{
|
|
193
|
+
name: "json",
|
|
194
|
+
kind: "boolean",
|
|
195
|
+
summary: "Print one JSON envelope on stdout (NDJSON for streams)."
|
|
196
|
+
},
|
|
197
|
+
{
|
|
198
|
+
name: "home",
|
|
199
|
+
kind: "string",
|
|
200
|
+
value: "<dir>",
|
|
201
|
+
summary: "Use this Mostra home instead of MOSTRA_HOME or the default."
|
|
202
|
+
},
|
|
203
|
+
{
|
|
204
|
+
name: "cwd",
|
|
205
|
+
kind: "string",
|
|
206
|
+
value: "<dir>",
|
|
207
|
+
summary: "Resolve relative paths from this directory."
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
name: "timeout",
|
|
211
|
+
kind: "duration",
|
|
212
|
+
value: "<d>",
|
|
213
|
+
summary: "Give up on the instance after this long (exit 4), such as 30s or 5m."
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
name: "help",
|
|
217
|
+
kind: "boolean",
|
|
218
|
+
summary: "Show this command's help; with --json, its definition."
|
|
219
|
+
}
|
|
220
|
+
];
|
|
221
|
+
/** A usage error. */
|
|
222
|
+
function usage(message) {
|
|
223
|
+
return new RpcError(createDiagnostic("MOSTRA_CLI_USAGE", { message }));
|
|
224
|
+
}
|
|
225
|
+
const DURATION = /^([1-9][0-9]*)(ms|s|m|h|d)$/;
|
|
226
|
+
const UNIT_MS = {
|
|
227
|
+
ms: 1,
|
|
228
|
+
s: 1e3,
|
|
229
|
+
m: 6e4,
|
|
230
|
+
h: 36e5,
|
|
231
|
+
d: 864e5
|
|
232
|
+
};
|
|
233
|
+
/** A duration flag's milliseconds: `30s`, `5m`, `2h`, `7d`, `500ms`. */
|
|
234
|
+
function durationMs(value) {
|
|
235
|
+
const match = DURATION.exec(value);
|
|
236
|
+
if (!match) throw usage(`"${value}" isn't a duration; use a number with ms, s, m, h or d, such as 30s.`);
|
|
237
|
+
return Number(match[1]) * UNIT_MS[match[2]];
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Reads the global flags wherever they stand, before the command is known: `--json` anywhere
|
|
241
|
+
* before `--` makes even a usage error a JSON envelope (D140 item 16).
|
|
242
|
+
*/
|
|
243
|
+
function globalFlags(argv) {
|
|
244
|
+
const end = argv.indexOf("--");
|
|
245
|
+
const words = end === -1 ? argv : argv.slice(0, end);
|
|
246
|
+
const value = (name) => {
|
|
247
|
+
for (let index = 0; index < words.length; index += 1) {
|
|
248
|
+
const word = words[index];
|
|
249
|
+
if (word === `--${name}`) return words[index + 1];
|
|
250
|
+
if (word.startsWith(`--${name}=`)) return word.slice(name.length + 3);
|
|
251
|
+
}
|
|
252
|
+
};
|
|
253
|
+
const timeout = value("timeout");
|
|
254
|
+
let timeoutMs;
|
|
255
|
+
try {
|
|
256
|
+
timeoutMs = timeout === void 0 ? void 0 : durationMs(timeout);
|
|
257
|
+
} catch {
|
|
258
|
+
timeoutMs = void 0;
|
|
259
|
+
}
|
|
260
|
+
const home = value("home");
|
|
261
|
+
const cwd = value("cwd");
|
|
262
|
+
return {
|
|
263
|
+
json: words.includes("--json"),
|
|
264
|
+
...home !== void 0 ? { home } : {},
|
|
265
|
+
...cwd !== void 0 ? { cwd } : {},
|
|
266
|
+
...timeoutMs !== void 0 ? { timeoutMs } : {}
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
/** The command that names `words`' longest prefix. */
|
|
270
|
+
function findCommand(commands, words) {
|
|
271
|
+
let best;
|
|
272
|
+
for (const command of commands) {
|
|
273
|
+
const { path } = command;
|
|
274
|
+
if (path.length <= words.length && path.every((word, index) => words[index] === word)) {
|
|
275
|
+
if (!best || path.length > best.used) best = {
|
|
276
|
+
command,
|
|
277
|
+
used: path.length
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return best;
|
|
282
|
+
}
|
|
283
|
+
/** The first words of every command: the groups and the single-word commands. */
|
|
284
|
+
function topWords(commands) {
|
|
285
|
+
return [...new Set(commands.map((command) => command.path[0]))].sort();
|
|
286
|
+
}
|
|
287
|
+
/** Parses the command line against the definitions. Throws a usage diagnostic. */
|
|
288
|
+
function parseCommandLine(commands, argv) {
|
|
289
|
+
const bare = argv.filter((word) => word !== "--json");
|
|
290
|
+
if (bare.length === 1 && (bare[0] === "--version" || bare[0] === "-v")) return { kind: "version" };
|
|
291
|
+
const words = [];
|
|
292
|
+
const pathAt = [];
|
|
293
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
294
|
+
const word = argv[index];
|
|
295
|
+
if (word === "--json" || word === "--help" || word === "-h") continue;
|
|
296
|
+
if (word === "--home" || word === "--cwd" || word === "--timeout") {
|
|
297
|
+
index += 1;
|
|
298
|
+
continue;
|
|
299
|
+
}
|
|
300
|
+
if (/^--(home|cwd|timeout)=/.test(word)) continue;
|
|
301
|
+
if (word.startsWith("-")) break;
|
|
302
|
+
words.push(word);
|
|
303
|
+
pathAt.push(index);
|
|
304
|
+
}
|
|
305
|
+
const helpAsked = argv.includes("--help") || argv.includes("-h");
|
|
306
|
+
if (words.length === 0) {
|
|
307
|
+
if (helpAsked || argv.every((word) => word === "--json")) return { kind: "help" };
|
|
308
|
+
throw usage(`Name a command first; ${argv[0]} isn't one.`);
|
|
309
|
+
}
|
|
310
|
+
const found = findCommand(commands, words);
|
|
311
|
+
if (!found) {
|
|
312
|
+
const group = commands.filter((command) => command.path[0] === words[0]);
|
|
313
|
+
if (group.length > 0) {
|
|
314
|
+
if (helpAsked) return {
|
|
315
|
+
kind: "help",
|
|
316
|
+
group: words[0]
|
|
317
|
+
};
|
|
318
|
+
throw usage(`"mostra ${words[0]}" needs one of: ${group.map((command) => command.path.slice(1).join(" ")).join(", ")}.`);
|
|
319
|
+
}
|
|
320
|
+
throw new RpcError(createDiagnostic("MOSTRA_CLI_UNKNOWN_COMMAND", { message: `"${words[0]}" isn't a mostra command. The commands are: ${topWords(commands).join(", ")}.` }));
|
|
321
|
+
}
|
|
322
|
+
const { command } = found;
|
|
323
|
+
if (helpAsked) return {
|
|
324
|
+
kind: "help",
|
|
325
|
+
command
|
|
326
|
+
};
|
|
327
|
+
const used = new Set(pathAt.slice(0, found.used));
|
|
328
|
+
const rest = argv.filter((_, index) => !used.has(index));
|
|
329
|
+
const flagDefs = [...GLOBAL_FLAGS, ...command.flags ?? []];
|
|
330
|
+
let parsed;
|
|
331
|
+
try {
|
|
332
|
+
parsed = parseArgs({
|
|
333
|
+
args: [...rest],
|
|
334
|
+
allowPositionals: true,
|
|
335
|
+
strict: true,
|
|
336
|
+
options: Object.fromEntries(flagDefs.map((flag) => [flag.name, { type: flag.kind === "boolean" ? "boolean" : "string" }]))
|
|
337
|
+
});
|
|
338
|
+
} catch (error) {
|
|
339
|
+
throw usage(`${error.message.split(". To specify")[0]}. Run "mostra ${commandName(command)} --help".`);
|
|
340
|
+
}
|
|
341
|
+
const flags = {};
|
|
342
|
+
for (const flag of command.flags ?? []) {
|
|
343
|
+
const raw = parsed.values[flag.name];
|
|
344
|
+
if (raw === void 0) continue;
|
|
345
|
+
if (flag.kind === "boolean") {
|
|
346
|
+
flags[flag.name] = raw === true;
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
const text = String(raw);
|
|
350
|
+
if (flag.choices && !flag.choices.includes(text)) throw usage(`--${flag.name} takes one of ${flag.choices.join(", ")}; "${text}" isn't one.`);
|
|
351
|
+
if (flag.kind === "number") {
|
|
352
|
+
const number = Number(text);
|
|
353
|
+
if (!Number.isInteger(number) || number < 0) throw usage(`--${flag.name} takes a whole number; "${text}" isn't one.`);
|
|
354
|
+
flags[flag.name] = number;
|
|
355
|
+
} else if (flag.kind === "duration") {
|
|
356
|
+
durationMs(text);
|
|
357
|
+
flags[flag.name] = text;
|
|
358
|
+
} else if (flag.kind === "list") flags[flag.name] = text.split(",").map((item) => item.trim()).filter((item) => item.length > 0);
|
|
359
|
+
else flags[flag.name] = text;
|
|
360
|
+
}
|
|
361
|
+
const positionals = [...parsed.positionals];
|
|
362
|
+
const args = {};
|
|
363
|
+
for (const arg of command.args ?? []) {
|
|
364
|
+
if (arg.variadic) {
|
|
365
|
+
if (positionals.length === 0 && !arg.optional) throw usage(`"mostra ${commandName(command)}" needs <${arg.name}…>.`);
|
|
366
|
+
args[arg.name] = positionals.splice(0);
|
|
367
|
+
continue;
|
|
368
|
+
}
|
|
369
|
+
const value = positionals.shift();
|
|
370
|
+
if (value === void 0 && !arg.optional) throw usage(`"mostra ${commandName(command)}" needs <${arg.name}>.`);
|
|
371
|
+
args[arg.name] = value;
|
|
372
|
+
}
|
|
373
|
+
if (positionals.length > 0) throw usage(`"mostra ${commandName(command)}" takes no argument "${positionals[0]}". Run "mostra ${commandName(command)} --help".`);
|
|
374
|
+
if (typeof parsed.values["timeout"] === "string") durationMs(parsed.values["timeout"]);
|
|
375
|
+
return {
|
|
376
|
+
kind: "run",
|
|
377
|
+
command,
|
|
378
|
+
args,
|
|
379
|
+
flags
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
//#endregion
|
|
383
|
+
//#region src/telemetry/zones.generated.ts
|
|
384
|
+
/** The time zones where Mostra asks before sending telemetry (D140 item 10), matched as given. */
|
|
385
|
+
const ASK_FIRST_ZONES = [
|
|
386
|
+
"Africa/Ceuta",
|
|
387
|
+
"America/Cayenne",
|
|
388
|
+
"America/Guadeloupe",
|
|
389
|
+
"America/Marigot",
|
|
390
|
+
"America/Martinique",
|
|
391
|
+
"Arctic/Longyearbyen",
|
|
392
|
+
"Asia/Famagusta",
|
|
393
|
+
"Asia/Nicosia",
|
|
394
|
+
"Atlantic/Azores",
|
|
395
|
+
"Atlantic/Canary",
|
|
396
|
+
"Atlantic/Faeroe",
|
|
397
|
+
"Atlantic/Faroe",
|
|
398
|
+
"Atlantic/Jan_Mayen",
|
|
399
|
+
"Atlantic/Madeira",
|
|
400
|
+
"Atlantic/Reykjavik",
|
|
401
|
+
"CET",
|
|
402
|
+
"EET",
|
|
403
|
+
"Eire",
|
|
404
|
+
"Etc/GMT",
|
|
405
|
+
"Etc/GMT+0",
|
|
406
|
+
"Etc/GMT+1",
|
|
407
|
+
"Etc/GMT+10",
|
|
408
|
+
"Etc/GMT+11",
|
|
409
|
+
"Etc/GMT+12",
|
|
410
|
+
"Etc/GMT+2",
|
|
411
|
+
"Etc/GMT+3",
|
|
412
|
+
"Etc/GMT+4",
|
|
413
|
+
"Etc/GMT+5",
|
|
414
|
+
"Etc/GMT+6",
|
|
415
|
+
"Etc/GMT+7",
|
|
416
|
+
"Etc/GMT+8",
|
|
417
|
+
"Etc/GMT+9",
|
|
418
|
+
"Etc/GMT-0",
|
|
419
|
+
"Etc/GMT-1",
|
|
420
|
+
"Etc/GMT-10",
|
|
421
|
+
"Etc/GMT-11",
|
|
422
|
+
"Etc/GMT-12",
|
|
423
|
+
"Etc/GMT-13",
|
|
424
|
+
"Etc/GMT-14",
|
|
425
|
+
"Etc/GMT-2",
|
|
426
|
+
"Etc/GMT-3",
|
|
427
|
+
"Etc/GMT-4",
|
|
428
|
+
"Etc/GMT-5",
|
|
429
|
+
"Etc/GMT-6",
|
|
430
|
+
"Etc/GMT-7",
|
|
431
|
+
"Etc/GMT-8",
|
|
432
|
+
"Etc/GMT-9",
|
|
433
|
+
"Etc/GMT0",
|
|
434
|
+
"Etc/Greenwich",
|
|
435
|
+
"Etc/UCT",
|
|
436
|
+
"Etc/UTC",
|
|
437
|
+
"Etc/Universal",
|
|
438
|
+
"Etc/Zulu",
|
|
439
|
+
"Europe/Amsterdam",
|
|
440
|
+
"Europe/Andorra",
|
|
441
|
+
"Europe/Athens",
|
|
442
|
+
"Europe/Belfast",
|
|
443
|
+
"Europe/Berlin",
|
|
444
|
+
"Europe/Bratislava",
|
|
445
|
+
"Europe/Brussels",
|
|
446
|
+
"Europe/Bucharest",
|
|
447
|
+
"Europe/Budapest",
|
|
448
|
+
"Europe/Busingen",
|
|
449
|
+
"Europe/Copenhagen",
|
|
450
|
+
"Europe/Dublin",
|
|
451
|
+
"Europe/Gibraltar",
|
|
452
|
+
"Europe/Guernsey",
|
|
453
|
+
"Europe/Helsinki",
|
|
454
|
+
"Europe/Isle_of_Man",
|
|
455
|
+
"Europe/Jersey",
|
|
456
|
+
"Europe/Lisbon",
|
|
457
|
+
"Europe/Ljubljana",
|
|
458
|
+
"Europe/London",
|
|
459
|
+
"Europe/Luxembourg",
|
|
460
|
+
"Europe/Madrid",
|
|
461
|
+
"Europe/Malta",
|
|
462
|
+
"Europe/Mariehamn",
|
|
463
|
+
"Europe/Monaco",
|
|
464
|
+
"Europe/Nicosia",
|
|
465
|
+
"Europe/Oslo",
|
|
466
|
+
"Europe/Paris",
|
|
467
|
+
"Europe/Prague",
|
|
468
|
+
"Europe/Riga",
|
|
469
|
+
"Europe/Rome",
|
|
470
|
+
"Europe/San_Marino",
|
|
471
|
+
"Europe/Sofia",
|
|
472
|
+
"Europe/Stockholm",
|
|
473
|
+
"Europe/Tallinn",
|
|
474
|
+
"Europe/Vaduz",
|
|
475
|
+
"Europe/Vatican",
|
|
476
|
+
"Europe/Vienna",
|
|
477
|
+
"Europe/Vilnius",
|
|
478
|
+
"Europe/Warsaw",
|
|
479
|
+
"Europe/Zagreb",
|
|
480
|
+
"Europe/Zurich",
|
|
481
|
+
"Factory",
|
|
482
|
+
"GB",
|
|
483
|
+
"GB-Eire",
|
|
484
|
+
"GMT",
|
|
485
|
+
"GMT+0",
|
|
486
|
+
"GMT-0",
|
|
487
|
+
"GMT0",
|
|
488
|
+
"Greenwich",
|
|
489
|
+
"Iceland",
|
|
490
|
+
"Indian/Mayotte",
|
|
491
|
+
"Indian/Reunion",
|
|
492
|
+
"MET",
|
|
493
|
+
"Poland",
|
|
494
|
+
"Portugal",
|
|
495
|
+
"UCT",
|
|
496
|
+
"UTC",
|
|
497
|
+
"Universal",
|
|
498
|
+
"WET",
|
|
499
|
+
"Zulu"
|
|
500
|
+
];
|
|
501
|
+
/** Every zone name tzdata 2026e knows; anything else is an unknown zone. */
|
|
502
|
+
const KNOWN_ZONES = [
|
|
503
|
+
"Africa/Abidjan",
|
|
504
|
+
"Africa/Accra",
|
|
505
|
+
"Africa/Addis_Ababa",
|
|
506
|
+
"Africa/Algiers",
|
|
507
|
+
"Africa/Asmara",
|
|
508
|
+
"Africa/Asmera",
|
|
509
|
+
"Africa/Bamako",
|
|
510
|
+
"Africa/Bangui",
|
|
511
|
+
"Africa/Banjul",
|
|
512
|
+
"Africa/Bissau",
|
|
513
|
+
"Africa/Blantyre",
|
|
514
|
+
"Africa/Brazzaville",
|
|
515
|
+
"Africa/Bujumbura",
|
|
516
|
+
"Africa/Cairo",
|
|
517
|
+
"Africa/Casablanca",
|
|
518
|
+
"Africa/Ceuta",
|
|
519
|
+
"Africa/Conakry",
|
|
520
|
+
"Africa/Dakar",
|
|
521
|
+
"Africa/Dar_es_Salaam",
|
|
522
|
+
"Africa/Djibouti",
|
|
523
|
+
"Africa/Douala",
|
|
524
|
+
"Africa/El_Aaiun",
|
|
525
|
+
"Africa/Freetown",
|
|
526
|
+
"Africa/Gaborone",
|
|
527
|
+
"Africa/Harare",
|
|
528
|
+
"Africa/Johannesburg",
|
|
529
|
+
"Africa/Juba",
|
|
530
|
+
"Africa/Kampala",
|
|
531
|
+
"Africa/Khartoum",
|
|
532
|
+
"Africa/Kigali",
|
|
533
|
+
"Africa/Kinshasa",
|
|
534
|
+
"Africa/Lagos",
|
|
535
|
+
"Africa/Libreville",
|
|
536
|
+
"Africa/Lome",
|
|
537
|
+
"Africa/Luanda",
|
|
538
|
+
"Africa/Lubumbashi",
|
|
539
|
+
"Africa/Lusaka",
|
|
540
|
+
"Africa/Malabo",
|
|
541
|
+
"Africa/Maputo",
|
|
542
|
+
"Africa/Maseru",
|
|
543
|
+
"Africa/Mbabane",
|
|
544
|
+
"Africa/Mogadishu",
|
|
545
|
+
"Africa/Monrovia",
|
|
546
|
+
"Africa/Nairobi",
|
|
547
|
+
"Africa/Ndjamena",
|
|
548
|
+
"Africa/Niamey",
|
|
549
|
+
"Africa/Nouakchott",
|
|
550
|
+
"Africa/Ouagadougou",
|
|
551
|
+
"Africa/Porto-Novo",
|
|
552
|
+
"Africa/Sao_Tome",
|
|
553
|
+
"Africa/Timbuktu",
|
|
554
|
+
"Africa/Tripoli",
|
|
555
|
+
"Africa/Tunis",
|
|
556
|
+
"Africa/Windhoek",
|
|
557
|
+
"America/Adak",
|
|
558
|
+
"America/Anchorage",
|
|
559
|
+
"America/Anguilla",
|
|
560
|
+
"America/Antigua",
|
|
561
|
+
"America/Araguaina",
|
|
562
|
+
"America/Argentina/Buenos_Aires",
|
|
563
|
+
"America/Argentina/Catamarca",
|
|
564
|
+
"America/Argentina/ComodRivadavia",
|
|
565
|
+
"America/Argentina/Cordoba",
|
|
566
|
+
"America/Argentina/Jujuy",
|
|
567
|
+
"America/Argentina/La_Rioja",
|
|
568
|
+
"America/Argentina/Mendoza",
|
|
569
|
+
"America/Argentina/Rio_Gallegos",
|
|
570
|
+
"America/Argentina/Salta",
|
|
571
|
+
"America/Argentina/San_Juan",
|
|
572
|
+
"America/Argentina/San_Luis",
|
|
573
|
+
"America/Argentina/Tucuman",
|
|
574
|
+
"America/Argentina/Ushuaia",
|
|
575
|
+
"America/Aruba",
|
|
576
|
+
"America/Asuncion",
|
|
577
|
+
"America/Atikokan",
|
|
578
|
+
"America/Atka",
|
|
579
|
+
"America/Bahia",
|
|
580
|
+
"America/Bahia_Banderas",
|
|
581
|
+
"America/Barbados",
|
|
582
|
+
"America/Belem",
|
|
583
|
+
"America/Belize",
|
|
584
|
+
"America/Blanc-Sablon",
|
|
585
|
+
"America/Boa_Vista",
|
|
586
|
+
"America/Bogota",
|
|
587
|
+
"America/Boise",
|
|
588
|
+
"America/Buenos_Aires",
|
|
589
|
+
"America/Cambridge_Bay",
|
|
590
|
+
"America/Campo_Grande",
|
|
591
|
+
"America/Cancun",
|
|
592
|
+
"America/Caracas",
|
|
593
|
+
"America/Catamarca",
|
|
594
|
+
"America/Cayenne",
|
|
595
|
+
"America/Cayman",
|
|
596
|
+
"America/Chicago",
|
|
597
|
+
"America/Chihuahua",
|
|
598
|
+
"America/Ciudad_Juarez",
|
|
599
|
+
"America/Coral_Harbour",
|
|
600
|
+
"America/Cordoba",
|
|
601
|
+
"America/Costa_Rica",
|
|
602
|
+
"America/Coyhaique",
|
|
603
|
+
"America/Creston",
|
|
604
|
+
"America/Cuiaba",
|
|
605
|
+
"America/Curacao",
|
|
606
|
+
"America/Danmarkshavn",
|
|
607
|
+
"America/Dawson",
|
|
608
|
+
"America/Dawson_Creek",
|
|
609
|
+
"America/Denver",
|
|
610
|
+
"America/Detroit",
|
|
611
|
+
"America/Dominica",
|
|
612
|
+
"America/Edmonton",
|
|
613
|
+
"America/Eirunepe",
|
|
614
|
+
"America/El_Salvador",
|
|
615
|
+
"America/Ensenada",
|
|
616
|
+
"America/Fort_Nelson",
|
|
617
|
+
"America/Fort_Wayne",
|
|
618
|
+
"America/Fortaleza",
|
|
619
|
+
"America/Glace_Bay",
|
|
620
|
+
"America/Godthab",
|
|
621
|
+
"America/Goose_Bay",
|
|
622
|
+
"America/Grand_Turk",
|
|
623
|
+
"America/Grenada",
|
|
624
|
+
"America/Guadeloupe",
|
|
625
|
+
"America/Guatemala",
|
|
626
|
+
"America/Guayaquil",
|
|
627
|
+
"America/Guyana",
|
|
628
|
+
"America/Halifax",
|
|
629
|
+
"America/Havana",
|
|
630
|
+
"America/Hermosillo",
|
|
631
|
+
"America/Indiana/Indianapolis",
|
|
632
|
+
"America/Indiana/Knox",
|
|
633
|
+
"America/Indiana/Marengo",
|
|
634
|
+
"America/Indiana/Petersburg",
|
|
635
|
+
"America/Indiana/Tell_City",
|
|
636
|
+
"America/Indiana/Vevay",
|
|
637
|
+
"America/Indiana/Vincennes",
|
|
638
|
+
"America/Indiana/Winamac",
|
|
639
|
+
"America/Indianapolis",
|
|
640
|
+
"America/Inuvik",
|
|
641
|
+
"America/Iqaluit",
|
|
642
|
+
"America/Jamaica",
|
|
643
|
+
"America/Jujuy",
|
|
644
|
+
"America/Juneau",
|
|
645
|
+
"America/Kentucky/Louisville",
|
|
646
|
+
"America/Kentucky/Monticello",
|
|
647
|
+
"America/Knox_IN",
|
|
648
|
+
"America/Kralendijk",
|
|
649
|
+
"America/La_Paz",
|
|
650
|
+
"America/Lima",
|
|
651
|
+
"America/Los_Angeles",
|
|
652
|
+
"America/Louisville",
|
|
653
|
+
"America/Lower_Princes",
|
|
654
|
+
"America/Maceio",
|
|
655
|
+
"America/Managua",
|
|
656
|
+
"America/Manaus",
|
|
657
|
+
"America/Marigot",
|
|
658
|
+
"America/Martinique",
|
|
659
|
+
"America/Matamoros",
|
|
660
|
+
"America/Mazatlan",
|
|
661
|
+
"America/Mendoza",
|
|
662
|
+
"America/Menominee",
|
|
663
|
+
"America/Merida",
|
|
664
|
+
"America/Metlakatla",
|
|
665
|
+
"America/Mexico_City",
|
|
666
|
+
"America/Miquelon",
|
|
667
|
+
"America/Moncton",
|
|
668
|
+
"America/Monterrey",
|
|
669
|
+
"America/Montevideo",
|
|
670
|
+
"America/Montreal",
|
|
671
|
+
"America/Montserrat",
|
|
672
|
+
"America/Nassau",
|
|
673
|
+
"America/New_York",
|
|
674
|
+
"America/Nipigon",
|
|
675
|
+
"America/Nome",
|
|
676
|
+
"America/Noronha",
|
|
677
|
+
"America/North_Dakota/Beulah",
|
|
678
|
+
"America/North_Dakota/Center",
|
|
679
|
+
"America/North_Dakota/New_Salem",
|
|
680
|
+
"America/Nuuk",
|
|
681
|
+
"America/Ojinaga",
|
|
682
|
+
"America/Panama",
|
|
683
|
+
"America/Pangnirtung",
|
|
684
|
+
"America/Paramaribo",
|
|
685
|
+
"America/Phoenix",
|
|
686
|
+
"America/Port-au-Prince",
|
|
687
|
+
"America/Port_of_Spain",
|
|
688
|
+
"America/Porto_Acre",
|
|
689
|
+
"America/Porto_Velho",
|
|
690
|
+
"America/Puerto_Rico",
|
|
691
|
+
"America/Punta_Arenas",
|
|
692
|
+
"America/Rainy_River",
|
|
693
|
+
"America/Rankin_Inlet",
|
|
694
|
+
"America/Recife",
|
|
695
|
+
"America/Regina",
|
|
696
|
+
"America/Resolute",
|
|
697
|
+
"America/Rio_Branco",
|
|
698
|
+
"America/Rosario",
|
|
699
|
+
"America/Santa_Isabel",
|
|
700
|
+
"America/Santarem",
|
|
701
|
+
"America/Santiago",
|
|
702
|
+
"America/Santo_Domingo",
|
|
703
|
+
"America/Sao_Paulo",
|
|
704
|
+
"America/Scoresbysund",
|
|
705
|
+
"America/Shiprock",
|
|
706
|
+
"America/Sitka",
|
|
707
|
+
"America/St_Barthelemy",
|
|
708
|
+
"America/St_Johns",
|
|
709
|
+
"America/St_Kitts",
|
|
710
|
+
"America/St_Lucia",
|
|
711
|
+
"America/St_Thomas",
|
|
712
|
+
"America/St_Vincent",
|
|
713
|
+
"America/Swift_Current",
|
|
714
|
+
"America/Tegucigalpa",
|
|
715
|
+
"America/Thule",
|
|
716
|
+
"America/Thunder_Bay",
|
|
717
|
+
"America/Tijuana",
|
|
718
|
+
"America/Toronto",
|
|
719
|
+
"America/Tortola",
|
|
720
|
+
"America/Vancouver",
|
|
721
|
+
"America/Virgin",
|
|
722
|
+
"America/Whitehorse",
|
|
723
|
+
"America/Winnipeg",
|
|
724
|
+
"America/Yakutat",
|
|
725
|
+
"America/Yellowknife",
|
|
726
|
+
"Antarctica/Casey",
|
|
727
|
+
"Antarctica/Davis",
|
|
728
|
+
"Antarctica/DumontDUrville",
|
|
729
|
+
"Antarctica/Macquarie",
|
|
730
|
+
"Antarctica/Mawson",
|
|
731
|
+
"Antarctica/McMurdo",
|
|
732
|
+
"Antarctica/Palmer",
|
|
733
|
+
"Antarctica/Rothera",
|
|
734
|
+
"Antarctica/South_Pole",
|
|
735
|
+
"Antarctica/Syowa",
|
|
736
|
+
"Antarctica/Troll",
|
|
737
|
+
"Antarctica/Vostok",
|
|
738
|
+
"Arctic/Longyearbyen",
|
|
739
|
+
"Asia/Aden",
|
|
740
|
+
"Asia/Almaty",
|
|
741
|
+
"Asia/Amman",
|
|
742
|
+
"Asia/Anadyr",
|
|
743
|
+
"Asia/Aqtau",
|
|
744
|
+
"Asia/Aqtobe",
|
|
745
|
+
"Asia/Ashgabat",
|
|
746
|
+
"Asia/Ashkhabad",
|
|
747
|
+
"Asia/Atyrau",
|
|
748
|
+
"Asia/Baghdad",
|
|
749
|
+
"Asia/Bahrain",
|
|
750
|
+
"Asia/Baku",
|
|
751
|
+
"Asia/Bangkok",
|
|
752
|
+
"Asia/Barnaul",
|
|
753
|
+
"Asia/Beirut",
|
|
754
|
+
"Asia/Bishkek",
|
|
755
|
+
"Asia/Brunei",
|
|
756
|
+
"Asia/Calcutta",
|
|
757
|
+
"Asia/Chita",
|
|
758
|
+
"Asia/Choibalsan",
|
|
759
|
+
"Asia/Chongqing",
|
|
760
|
+
"Asia/Chungking",
|
|
761
|
+
"Asia/Colombo",
|
|
762
|
+
"Asia/Dacca",
|
|
763
|
+
"Asia/Damascus",
|
|
764
|
+
"Asia/Dhaka",
|
|
765
|
+
"Asia/Dili",
|
|
766
|
+
"Asia/Dubai",
|
|
767
|
+
"Asia/Dushanbe",
|
|
768
|
+
"Asia/Famagusta",
|
|
769
|
+
"Asia/Gaza",
|
|
770
|
+
"Asia/Harbin",
|
|
771
|
+
"Asia/Hebron",
|
|
772
|
+
"Asia/Ho_Chi_Minh",
|
|
773
|
+
"Asia/Hong_Kong",
|
|
774
|
+
"Asia/Hovd",
|
|
775
|
+
"Asia/Irkutsk",
|
|
776
|
+
"Asia/Istanbul",
|
|
777
|
+
"Asia/Jakarta",
|
|
778
|
+
"Asia/Jayapura",
|
|
779
|
+
"Asia/Jerusalem",
|
|
780
|
+
"Asia/Kabul",
|
|
781
|
+
"Asia/Kamchatka",
|
|
782
|
+
"Asia/Karachi",
|
|
783
|
+
"Asia/Kashgar",
|
|
784
|
+
"Asia/Kathmandu",
|
|
785
|
+
"Asia/Katmandu",
|
|
786
|
+
"Asia/Khandyga",
|
|
787
|
+
"Asia/Kolkata",
|
|
788
|
+
"Asia/Krasnoyarsk",
|
|
789
|
+
"Asia/Kuala_Lumpur",
|
|
790
|
+
"Asia/Kuching",
|
|
791
|
+
"Asia/Kuwait",
|
|
792
|
+
"Asia/Macao",
|
|
793
|
+
"Asia/Macau",
|
|
794
|
+
"Asia/Magadan",
|
|
795
|
+
"Asia/Makassar",
|
|
796
|
+
"Asia/Manila",
|
|
797
|
+
"Asia/Muscat",
|
|
798
|
+
"Asia/Nicosia",
|
|
799
|
+
"Asia/Novokuznetsk",
|
|
800
|
+
"Asia/Novosibirsk",
|
|
801
|
+
"Asia/Omsk",
|
|
802
|
+
"Asia/Oral",
|
|
803
|
+
"Asia/Phnom_Penh",
|
|
804
|
+
"Asia/Pontianak",
|
|
805
|
+
"Asia/Pyongyang",
|
|
806
|
+
"Asia/Qatar",
|
|
807
|
+
"Asia/Qostanay",
|
|
808
|
+
"Asia/Qyzylorda",
|
|
809
|
+
"Asia/Rangoon",
|
|
810
|
+
"Asia/Riyadh",
|
|
811
|
+
"Asia/Saigon",
|
|
812
|
+
"Asia/Sakhalin",
|
|
813
|
+
"Asia/Samarkand",
|
|
814
|
+
"Asia/Seoul",
|
|
815
|
+
"Asia/Shanghai",
|
|
816
|
+
"Asia/Singapore",
|
|
817
|
+
"Asia/Srednekolymsk",
|
|
818
|
+
"Asia/Taipei",
|
|
819
|
+
"Asia/Tashkent",
|
|
820
|
+
"Asia/Tbilisi",
|
|
821
|
+
"Asia/Tehran",
|
|
822
|
+
"Asia/Tel_Aviv",
|
|
823
|
+
"Asia/Thimbu",
|
|
824
|
+
"Asia/Thimphu",
|
|
825
|
+
"Asia/Tokyo",
|
|
826
|
+
"Asia/Tomsk",
|
|
827
|
+
"Asia/Ujung_Pandang",
|
|
828
|
+
"Asia/Ulaanbaatar",
|
|
829
|
+
"Asia/Ulan_Bator",
|
|
830
|
+
"Asia/Urumqi",
|
|
831
|
+
"Asia/Ust-Nera",
|
|
832
|
+
"Asia/Vientiane",
|
|
833
|
+
"Asia/Vladivostok",
|
|
834
|
+
"Asia/Yakutsk",
|
|
835
|
+
"Asia/Yangon",
|
|
836
|
+
"Asia/Yekaterinburg",
|
|
837
|
+
"Asia/Yerevan",
|
|
838
|
+
"Atlantic/Azores",
|
|
839
|
+
"Atlantic/Bermuda",
|
|
840
|
+
"Atlantic/Canary",
|
|
841
|
+
"Atlantic/Cape_Verde",
|
|
842
|
+
"Atlantic/Faeroe",
|
|
843
|
+
"Atlantic/Faroe",
|
|
844
|
+
"Atlantic/Jan_Mayen",
|
|
845
|
+
"Atlantic/Madeira",
|
|
846
|
+
"Atlantic/Reykjavik",
|
|
847
|
+
"Atlantic/South_Georgia",
|
|
848
|
+
"Atlantic/St_Helena",
|
|
849
|
+
"Atlantic/Stanley",
|
|
850
|
+
"Australia/ACT",
|
|
851
|
+
"Australia/Adelaide",
|
|
852
|
+
"Australia/Brisbane",
|
|
853
|
+
"Australia/Broken_Hill",
|
|
854
|
+
"Australia/Canberra",
|
|
855
|
+
"Australia/Currie",
|
|
856
|
+
"Australia/Darwin",
|
|
857
|
+
"Australia/Eucla",
|
|
858
|
+
"Australia/Hobart",
|
|
859
|
+
"Australia/LHI",
|
|
860
|
+
"Australia/Lindeman",
|
|
861
|
+
"Australia/Lord_Howe",
|
|
862
|
+
"Australia/Melbourne",
|
|
863
|
+
"Australia/NSW",
|
|
864
|
+
"Australia/North",
|
|
865
|
+
"Australia/Perth",
|
|
866
|
+
"Australia/Queensland",
|
|
867
|
+
"Australia/South",
|
|
868
|
+
"Australia/Sydney",
|
|
869
|
+
"Australia/Tasmania",
|
|
870
|
+
"Australia/Victoria",
|
|
871
|
+
"Australia/West",
|
|
872
|
+
"Australia/Yancowinna",
|
|
873
|
+
"Brazil/Acre",
|
|
874
|
+
"Brazil/DeNoronha",
|
|
875
|
+
"Brazil/East",
|
|
876
|
+
"Brazil/West",
|
|
877
|
+
"CET",
|
|
878
|
+
"Canada/Atlantic",
|
|
879
|
+
"Canada/Central",
|
|
880
|
+
"Canada/Eastern",
|
|
881
|
+
"Canada/Mountain",
|
|
882
|
+
"Canada/Newfoundland",
|
|
883
|
+
"Canada/Pacific",
|
|
884
|
+
"Canada/Saskatchewan",
|
|
885
|
+
"Canada/Yukon",
|
|
886
|
+
"Chile/Continental",
|
|
887
|
+
"Chile/EasterIsland",
|
|
888
|
+
"Cuba",
|
|
889
|
+
"EET",
|
|
890
|
+
"EST",
|
|
891
|
+
"Egypt",
|
|
892
|
+
"Eire",
|
|
893
|
+
"Etc/GMT",
|
|
894
|
+
"Etc/GMT+0",
|
|
895
|
+
"Etc/GMT+1",
|
|
896
|
+
"Etc/GMT+10",
|
|
897
|
+
"Etc/GMT+11",
|
|
898
|
+
"Etc/GMT+12",
|
|
899
|
+
"Etc/GMT+2",
|
|
900
|
+
"Etc/GMT+3",
|
|
901
|
+
"Etc/GMT+4",
|
|
902
|
+
"Etc/GMT+5",
|
|
903
|
+
"Etc/GMT+6",
|
|
904
|
+
"Etc/GMT+7",
|
|
905
|
+
"Etc/GMT+8",
|
|
906
|
+
"Etc/GMT+9",
|
|
907
|
+
"Etc/GMT-0",
|
|
908
|
+
"Etc/GMT-1",
|
|
909
|
+
"Etc/GMT-10",
|
|
910
|
+
"Etc/GMT-11",
|
|
911
|
+
"Etc/GMT-12",
|
|
912
|
+
"Etc/GMT-13",
|
|
913
|
+
"Etc/GMT-14",
|
|
914
|
+
"Etc/GMT-2",
|
|
915
|
+
"Etc/GMT-3",
|
|
916
|
+
"Etc/GMT-4",
|
|
917
|
+
"Etc/GMT-5",
|
|
918
|
+
"Etc/GMT-6",
|
|
919
|
+
"Etc/GMT-7",
|
|
920
|
+
"Etc/GMT-8",
|
|
921
|
+
"Etc/GMT-9",
|
|
922
|
+
"Etc/GMT0",
|
|
923
|
+
"Etc/Greenwich",
|
|
924
|
+
"Etc/UCT",
|
|
925
|
+
"Etc/UTC",
|
|
926
|
+
"Etc/Universal",
|
|
927
|
+
"Etc/Zulu",
|
|
928
|
+
"Europe/Amsterdam",
|
|
929
|
+
"Europe/Andorra",
|
|
930
|
+
"Europe/Astrakhan",
|
|
931
|
+
"Europe/Athens",
|
|
932
|
+
"Europe/Belfast",
|
|
933
|
+
"Europe/Belgrade",
|
|
934
|
+
"Europe/Berlin",
|
|
935
|
+
"Europe/Bratislava",
|
|
936
|
+
"Europe/Brussels",
|
|
937
|
+
"Europe/Bucharest",
|
|
938
|
+
"Europe/Budapest",
|
|
939
|
+
"Europe/Busingen",
|
|
940
|
+
"Europe/Chisinau",
|
|
941
|
+
"Europe/Copenhagen",
|
|
942
|
+
"Europe/Dublin",
|
|
943
|
+
"Europe/Gibraltar",
|
|
944
|
+
"Europe/Guernsey",
|
|
945
|
+
"Europe/Helsinki",
|
|
946
|
+
"Europe/Isle_of_Man",
|
|
947
|
+
"Europe/Istanbul",
|
|
948
|
+
"Europe/Jersey",
|
|
949
|
+
"Europe/Kaliningrad",
|
|
950
|
+
"Europe/Kiev",
|
|
951
|
+
"Europe/Kirov",
|
|
952
|
+
"Europe/Kyiv",
|
|
953
|
+
"Europe/Lisbon",
|
|
954
|
+
"Europe/Ljubljana",
|
|
955
|
+
"Europe/London",
|
|
956
|
+
"Europe/Luxembourg",
|
|
957
|
+
"Europe/Madrid",
|
|
958
|
+
"Europe/Malta",
|
|
959
|
+
"Europe/Mariehamn",
|
|
960
|
+
"Europe/Minsk",
|
|
961
|
+
"Europe/Monaco",
|
|
962
|
+
"Europe/Moscow",
|
|
963
|
+
"Europe/Nicosia",
|
|
964
|
+
"Europe/Oslo",
|
|
965
|
+
"Europe/Paris",
|
|
966
|
+
"Europe/Podgorica",
|
|
967
|
+
"Europe/Prague",
|
|
968
|
+
"Europe/Riga",
|
|
969
|
+
"Europe/Rome",
|
|
970
|
+
"Europe/Samara",
|
|
971
|
+
"Europe/San_Marino",
|
|
972
|
+
"Europe/Sarajevo",
|
|
973
|
+
"Europe/Saratov",
|
|
974
|
+
"Europe/Simferopol",
|
|
975
|
+
"Europe/Skopje",
|
|
976
|
+
"Europe/Sofia",
|
|
977
|
+
"Europe/Stockholm",
|
|
978
|
+
"Europe/Tallinn",
|
|
979
|
+
"Europe/Tirane",
|
|
980
|
+
"Europe/Tiraspol",
|
|
981
|
+
"Europe/Ulyanovsk",
|
|
982
|
+
"Europe/Uzhgorod",
|
|
983
|
+
"Europe/Vaduz",
|
|
984
|
+
"Europe/Vatican",
|
|
985
|
+
"Europe/Vienna",
|
|
986
|
+
"Europe/Vilnius",
|
|
987
|
+
"Europe/Volgograd",
|
|
988
|
+
"Europe/Warsaw",
|
|
989
|
+
"Europe/Zagreb",
|
|
990
|
+
"Europe/Zaporozhye",
|
|
991
|
+
"Europe/Zurich",
|
|
992
|
+
"Factory",
|
|
993
|
+
"GB",
|
|
994
|
+
"GB-Eire",
|
|
995
|
+
"GMT",
|
|
996
|
+
"GMT+0",
|
|
997
|
+
"GMT-0",
|
|
998
|
+
"GMT0",
|
|
999
|
+
"Greenwich",
|
|
1000
|
+
"HST",
|
|
1001
|
+
"Hongkong",
|
|
1002
|
+
"Iceland",
|
|
1003
|
+
"Indian/Antananarivo",
|
|
1004
|
+
"Indian/Chagos",
|
|
1005
|
+
"Indian/Christmas",
|
|
1006
|
+
"Indian/Cocos",
|
|
1007
|
+
"Indian/Comoro",
|
|
1008
|
+
"Indian/Kerguelen",
|
|
1009
|
+
"Indian/Mahe",
|
|
1010
|
+
"Indian/Maldives",
|
|
1011
|
+
"Indian/Mauritius",
|
|
1012
|
+
"Indian/Mayotte",
|
|
1013
|
+
"Indian/Reunion",
|
|
1014
|
+
"Iran",
|
|
1015
|
+
"Israel",
|
|
1016
|
+
"Jamaica",
|
|
1017
|
+
"Japan",
|
|
1018
|
+
"Kwajalein",
|
|
1019
|
+
"Libya",
|
|
1020
|
+
"MET",
|
|
1021
|
+
"MST",
|
|
1022
|
+
"Mexico/BajaNorte",
|
|
1023
|
+
"Mexico/BajaSur",
|
|
1024
|
+
"Mexico/General",
|
|
1025
|
+
"NZ",
|
|
1026
|
+
"NZ-CHAT",
|
|
1027
|
+
"Navajo",
|
|
1028
|
+
"PRC",
|
|
1029
|
+
"Pacific/Apia",
|
|
1030
|
+
"Pacific/Auckland",
|
|
1031
|
+
"Pacific/Bougainville",
|
|
1032
|
+
"Pacific/Chatham",
|
|
1033
|
+
"Pacific/Chuuk",
|
|
1034
|
+
"Pacific/Easter",
|
|
1035
|
+
"Pacific/Efate",
|
|
1036
|
+
"Pacific/Enderbury",
|
|
1037
|
+
"Pacific/Fakaofo",
|
|
1038
|
+
"Pacific/Fiji",
|
|
1039
|
+
"Pacific/Funafuti",
|
|
1040
|
+
"Pacific/Galapagos",
|
|
1041
|
+
"Pacific/Gambier",
|
|
1042
|
+
"Pacific/Guadalcanal",
|
|
1043
|
+
"Pacific/Guam",
|
|
1044
|
+
"Pacific/Honolulu",
|
|
1045
|
+
"Pacific/Johnston",
|
|
1046
|
+
"Pacific/Kanton",
|
|
1047
|
+
"Pacific/Kiritimati",
|
|
1048
|
+
"Pacific/Kosrae",
|
|
1049
|
+
"Pacific/Kwajalein",
|
|
1050
|
+
"Pacific/Majuro",
|
|
1051
|
+
"Pacific/Marquesas",
|
|
1052
|
+
"Pacific/Midway",
|
|
1053
|
+
"Pacific/Nauru",
|
|
1054
|
+
"Pacific/Niue",
|
|
1055
|
+
"Pacific/Norfolk",
|
|
1056
|
+
"Pacific/Noumea",
|
|
1057
|
+
"Pacific/Pago_Pago",
|
|
1058
|
+
"Pacific/Palau",
|
|
1059
|
+
"Pacific/Pitcairn",
|
|
1060
|
+
"Pacific/Pohnpei",
|
|
1061
|
+
"Pacific/Ponape",
|
|
1062
|
+
"Pacific/Port_Moresby",
|
|
1063
|
+
"Pacific/Rarotonga",
|
|
1064
|
+
"Pacific/Saipan",
|
|
1065
|
+
"Pacific/Samoa",
|
|
1066
|
+
"Pacific/Tahiti",
|
|
1067
|
+
"Pacific/Tarawa",
|
|
1068
|
+
"Pacific/Tongatapu",
|
|
1069
|
+
"Pacific/Truk",
|
|
1070
|
+
"Pacific/Wake",
|
|
1071
|
+
"Pacific/Wallis",
|
|
1072
|
+
"Pacific/Yap",
|
|
1073
|
+
"Poland",
|
|
1074
|
+
"Portugal",
|
|
1075
|
+
"ROC",
|
|
1076
|
+
"ROK",
|
|
1077
|
+
"Singapore",
|
|
1078
|
+
"Turkey",
|
|
1079
|
+
"UCT",
|
|
1080
|
+
"US/Alaska",
|
|
1081
|
+
"US/Aleutian",
|
|
1082
|
+
"US/Arizona",
|
|
1083
|
+
"US/Central",
|
|
1084
|
+
"US/East-Indiana",
|
|
1085
|
+
"US/Eastern",
|
|
1086
|
+
"US/Hawaii",
|
|
1087
|
+
"US/Indiana-Starke",
|
|
1088
|
+
"US/Michigan",
|
|
1089
|
+
"US/Mountain",
|
|
1090
|
+
"US/Pacific",
|
|
1091
|
+
"US/Samoa",
|
|
1092
|
+
"UTC",
|
|
1093
|
+
"Universal",
|
|
1094
|
+
"W-SU",
|
|
1095
|
+
"WET",
|
|
1096
|
+
"Zulu"
|
|
1097
|
+
];
|
|
1098
|
+
//#endregion
|
|
1099
|
+
//#region src/telemetry/timezone.ts
|
|
1100
|
+
/**
|
|
1101
|
+
* Whether this machine's time zone asks first (D140 items 2 and 10). The zone is read here, on
|
|
1102
|
+
* this machine, and never sent. Every candidate name counts, as given: `TZ` (a leading `:`
|
|
1103
|
+
* removed), the target of `/etc/localtime`, and `Intl`'s zone. A machine asks first when any of
|
|
1104
|
+
* them is an ask-first zone, or none is a zone tzdata knows (missing, empty, `Etc/Unknown`, a
|
|
1105
|
+
* POSIX rule, a typo). Names are never canonicalized: tzdata links `Europe/Zagreb` to
|
|
1106
|
+
* `Europe/Belgrade` and `Indian/Reunion` to `Asia/Dubai`, and ICU builds differ in what they
|
|
1107
|
+
* resolve.
|
|
1108
|
+
*/
|
|
1109
|
+
const ASK_FIRST = new Set(ASK_FIRST_ZONES);
|
|
1110
|
+
const KNOWN = new Set(KNOWN_ZONES);
|
|
1111
|
+
/** The target of `/etc/localtime` after `zoneinfo/`, when it is a symbolic link. */
|
|
1112
|
+
function localtimeLink() {
|
|
1113
|
+
try {
|
|
1114
|
+
const target = readlinkSync("/etc/localtime");
|
|
1115
|
+
const at = target.lastIndexOf("zoneinfo/");
|
|
1116
|
+
return at === -1 ? void 0 : target.slice(at + 9);
|
|
1117
|
+
} catch {
|
|
1118
|
+
return;
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
function intlZone() {
|
|
1122
|
+
try {
|
|
1123
|
+
const zone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
|
1124
|
+
return typeof zone === "string" ? zone : void 0;
|
|
1125
|
+
} catch {
|
|
1126
|
+
return;
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
/** The candidate zone names, as given (empty and missing ones dropped). */
|
|
1130
|
+
function zoneCandidates(sources) {
|
|
1131
|
+
const tz = sources.env["TZ"];
|
|
1132
|
+
const fromTz = tz === void 0 ? void 0 : tz.startsWith(":") ? tz.slice(1) : tz;
|
|
1133
|
+
const testLocaltime = sources.env["MOSTRA_TEST_LOCALTIME"];
|
|
1134
|
+
return [
|
|
1135
|
+
fromTz,
|
|
1136
|
+
(sources.localtime ?? (testLocaltime !== void 0 ? () => testLocaltime === "" ? void 0 : testLocaltime : localtimeLink))(),
|
|
1137
|
+
(sources.intl ?? intlZone)()
|
|
1138
|
+
].filter((name) => typeof name === "string" && name.length > 0);
|
|
1139
|
+
}
|
|
1140
|
+
/** Whether telemetry asks first on this machine. */
|
|
1141
|
+
function asksFirst(sources) {
|
|
1142
|
+
const candidates = zoneCandidates(sources);
|
|
1143
|
+
if (candidates.some((name) => ASK_FIRST.has(name))) return true;
|
|
1144
|
+
const tz = sources.env["TZ"];
|
|
1145
|
+
if (tz !== void 0 && !KNOWN.has(tz.startsWith(":") ? tz.slice(1) : tz)) return true;
|
|
1146
|
+
return !candidates.some((name) => KNOWN.has(name) && name !== "Etc/Unknown");
|
|
1147
|
+
}
|
|
1148
|
+
//#endregion
|
|
1149
|
+
//#region src/telemetry/words.ts
|
|
1150
|
+
/**
|
|
1151
|
+
* Telemetry's words to people (D140 item 11): the notice, the question, its answers and the
|
|
1152
|
+
* `status` lines, exactly as Mirza approved them (M28). `<docs>` is `DEFAULT_DOCS_BASE` (D37).
|
|
1153
|
+
*/
|
|
1154
|
+
const DOCS = `${DEFAULT_DOCS_BASE}/telemetry`;
|
|
1155
|
+
/** The notice where telemetry is on by default: stderr, once, before the first event. */
|
|
1156
|
+
const NOTICE_DEFAULT = `Mostra sends anonymous usage data to improve the CLI: the command, its
|
|
1157
|
+
outcome, the version, the platform, which agent ran it, and a random
|
|
1158
|
+
install ID. Never content, paths, tokens or personal data.
|
|
1159
|
+
Details: ${DOCS}
|
|
1160
|
+
Turn it off: mostra telemetry off, or set DO_NOT_TRACK=1.
|
|
1161
|
+
`;
|
|
1162
|
+
/** The question in the ask-first zones, when it may be asked. */
|
|
1163
|
+
const QUESTION = `Mostra can send anonymous usage data to improve the CLI: the command,
|
|
1164
|
+
its outcome, the version, the platform, which agent ran it, and a random
|
|
1165
|
+
install ID. Never content, paths, tokens or personal data.
|
|
1166
|
+
Details: ${DOCS}
|
|
1167
|
+
Send anonymous usage data? [y/N] `;
|
|
1168
|
+
/** The answers to the question. */
|
|
1169
|
+
const ANSWER = {
|
|
1170
|
+
yes: "Telemetry is on. Turn it off any time: mostra telemetry off\n",
|
|
1171
|
+
no: "Telemetry stays off. Turn it on any time: mostra telemetry on\n",
|
|
1172
|
+
timeout: "No answer, so telemetry stays off. Turn it on any time: mostra telemetry on\n"
|
|
1173
|
+
};
|
|
1174
|
+
/** The notice in the ask-first zones when the question can't be asked: stderr, once. */
|
|
1175
|
+
const NOTICE_ASK_FIRST = `Mostra can send anonymous usage data to improve the CLI: the command,
|
|
1176
|
+
its outcome, the version, the platform, which agent ran it, and a random
|
|
1177
|
+
install ID. Never content, paths, tokens or personal data.
|
|
1178
|
+
Details: ${DOCS}
|
|
1179
|
+
Nothing is sent unless you run: mostra telemetry on
|
|
1180
|
+
An agent should ask its user before turning it on.
|
|
1181
|
+
`;
|
|
1182
|
+
/** `mostra telemetry on` and `off`. */
|
|
1183
|
+
const TURNED = {
|
|
1184
|
+
on: "Telemetry is on. Turn it off any time: mostra telemetry off",
|
|
1185
|
+
off: "Telemetry is off. Your install ID and any unsent events were deleted."
|
|
1186
|
+
};
|
|
1187
|
+
/** The first line of `mostra telemetry status`. */
|
|
1188
|
+
function statusLine(input) {
|
|
1189
|
+
if (input.enabled) {
|
|
1190
|
+
if (input.reason === "setting") return "Telemetry is on: you turned it on.";
|
|
1191
|
+
if (input.reason === "variable") return "Telemetry is on: MOSTRA_TELEMETRY=1 is set.";
|
|
1192
|
+
return "Telemetry is on by default here. Turn it off: mostra telemetry off";
|
|
1193
|
+
}
|
|
1194
|
+
switch (input.reason) {
|
|
1195
|
+
case "setting": return "Telemetry is off: you turned it off.";
|
|
1196
|
+
case "do-not-track": return "Telemetry is off: DO_NOT_TRACK is set.";
|
|
1197
|
+
case "variable": return "Telemetry is off: MOSTRA_TELEMETRY=0 is set.";
|
|
1198
|
+
case "variable-invalid": return "Telemetry is off: MOSTRA_TELEMETRY has a value Mostra doesn't recognize, so nothing is sent.";
|
|
1199
|
+
case "embedded": return "Telemetry is off: a host configures this home, and hosts never send telemetry.";
|
|
1200
|
+
case "no-collector": return "Telemetry is off: this build has no telemetry endpoint.";
|
|
1201
|
+
case "unreadable": return `Telemetry is off: ${input.settingsFile} can't be read, so nothing is sent.`;
|
|
1202
|
+
case "default": return "Telemetry is off until you turn it on: mostra telemetry on";
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
//#endregion
|
|
1206
|
+
//#region src/telemetry/index.ts
|
|
1207
|
+
/**
|
|
1208
|
+
* The standalone CLI's telemetry (D119, D140): the only code in Mostra that can send an event, and
|
|
1209
|
+
* only `@mostra/cli` may import it (a dependency-cruiser rule). The runner calls {@link begin}
|
|
1210
|
+
* before a command's work and {@link end} after it; everything else here is the decision, the
|
|
1211
|
+
* words, the schema, the store and the flusher.
|
|
1212
|
+
*/
|
|
1213
|
+
/** The catalog's codes: only these leave the machine in `error` and `codes`. */
|
|
1214
|
+
const CATALOG = new Set(Object.keys(diagnosticCatalog));
|
|
1215
|
+
/** Variables CI services set; any of them means `ci: true`, and the question is never asked. */
|
|
1216
|
+
const CI_MARKERS = [
|
|
1217
|
+
"CI",
|
|
1218
|
+
"CONTINUOUS_INTEGRATION",
|
|
1219
|
+
"GITHUB_ACTIONS",
|
|
1220
|
+
"GITLAB_CI",
|
|
1221
|
+
"BUILDKITE",
|
|
1222
|
+
"CIRCLECI",
|
|
1223
|
+
"TRAVIS",
|
|
1224
|
+
"TF_BUILD",
|
|
1225
|
+
"JENKINS_URL",
|
|
1226
|
+
"TEAMCITY_VERSION",
|
|
1227
|
+
"BITBUCKET_BUILD_NUMBER",
|
|
1228
|
+
"CODEBUILD_BUILD_ID"
|
|
1229
|
+
];
|
|
1230
|
+
/** Whether this process runs in CI. */
|
|
1231
|
+
function inCi(env) {
|
|
1232
|
+
return CI_MARKERS.some((name) => truthy(env[name]));
|
|
1233
|
+
}
|
|
1234
|
+
/** How long the question waits for an answer. */
|
|
1235
|
+
const QUESTION_TIMEOUT_MS = 6e4;
|
|
1236
|
+
/** The current decision, for `telemetry status` and the session. */
|
|
1237
|
+
function currentDecision(options) {
|
|
1238
|
+
return decide({
|
|
1239
|
+
env: options.env,
|
|
1240
|
+
buildEndpoint: options.buildEndpoint ?? buildEndpoint(),
|
|
1241
|
+
embedded: options.embedded,
|
|
1242
|
+
settings: options.store.read(),
|
|
1243
|
+
askFirst: asksFirst({
|
|
1244
|
+
...options.zones,
|
|
1245
|
+
env: options.env
|
|
1246
|
+
})
|
|
1247
|
+
});
|
|
1248
|
+
}
|
|
1249
|
+
/** Starts one command's telemetry. */
|
|
1250
|
+
function telemetrySession(options) {
|
|
1251
|
+
const now = options.now ?? Date.now;
|
|
1252
|
+
const store = new TelemetryStore(options.configDir);
|
|
1253
|
+
const pending = [];
|
|
1254
|
+
const decision = (embedded) => currentDecision({
|
|
1255
|
+
env: options.env,
|
|
1256
|
+
store,
|
|
1257
|
+
embedded,
|
|
1258
|
+
...options.buildEndpoint !== void 0 ? { buildEndpoint: options.buildEndpoint } : {},
|
|
1259
|
+
...options.zones !== void 0 ? { zones: options.zones } : {}
|
|
1260
|
+
});
|
|
1261
|
+
const shown = () => {
|
|
1262
|
+
const read = store.read();
|
|
1263
|
+
return read.ok && read.settings.noticeShownAt !== void 0;
|
|
1264
|
+
};
|
|
1265
|
+
/** Records that the notice was shown; `false` when the store can't be written. */
|
|
1266
|
+
const markShown = () => store.update((current) => ({
|
|
1267
|
+
...current,
|
|
1268
|
+
noticeShownAt: new Date(now()).toISOString()
|
|
1269
|
+
})) !== void 0;
|
|
1270
|
+
const harness = detectHarness(options.env);
|
|
1271
|
+
const ci = inCi(options.env);
|
|
1272
|
+
return {
|
|
1273
|
+
async begin() {
|
|
1274
|
+
if (options.quiet) return true;
|
|
1275
|
+
const current = decision(options.hostHome);
|
|
1276
|
+
if (current.reason === "no-collector" || current.reason === "embedded") return true;
|
|
1277
|
+
if (current.enabled) {
|
|
1278
|
+
if (!shown() && markShown()) options.stderr(NOTICE_DEFAULT);
|
|
1279
|
+
return true;
|
|
1280
|
+
}
|
|
1281
|
+
if (current.reason !== "default" || !current.askFirst) return true;
|
|
1282
|
+
if (!(options.tty.stdin && options.tty.stderr && !options.json && !ci && harness === "none" && !options.readsStdin)) {
|
|
1283
|
+
const read = store.read();
|
|
1284
|
+
if (read.ok && read.settings.askFirstNoticeShownAt === void 0) {
|
|
1285
|
+
if (store.update((settings) => ({
|
|
1286
|
+
...settings,
|
|
1287
|
+
askFirstNoticeShownAt: new Date(now()).toISOString()
|
|
1288
|
+
})) !== void 0) options.stderr(NOTICE_ASK_FIRST);
|
|
1289
|
+
}
|
|
1290
|
+
return true;
|
|
1291
|
+
}
|
|
1292
|
+
const answer = await options.ask(QUESTION, QUESTION_TIMEOUT_MS);
|
|
1293
|
+
const yes = typeof answer === "string" && /^\s*y(es)?\s*$/i.test(answer);
|
|
1294
|
+
const stored = store.update((settings) => ({
|
|
1295
|
+
...settings,
|
|
1296
|
+
choice: yes ? "on" : "off",
|
|
1297
|
+
noticeShownAt: new Date(now()).toISOString()
|
|
1298
|
+
}));
|
|
1299
|
+
options.stderr(stored === void 0 ? `${statusLine({
|
|
1300
|
+
enabled: false,
|
|
1301
|
+
reason: "unreadable",
|
|
1302
|
+
askFirst: true,
|
|
1303
|
+
settingsFile: store.settingsFile
|
|
1304
|
+
})}\n` : yes ? ANSWER.yes : answer === void 0 ? ANSWER.timeout : ANSWER.no);
|
|
1305
|
+
return answer !== "interrupt";
|
|
1306
|
+
},
|
|
1307
|
+
record(event) {
|
|
1308
|
+
pending.push({
|
|
1309
|
+
event: event.event,
|
|
1310
|
+
fields: event.fields
|
|
1311
|
+
});
|
|
1312
|
+
},
|
|
1313
|
+
end(outcome) {
|
|
1314
|
+
if (options.quiet) return;
|
|
1315
|
+
const current = decision(options.hostHome || outcome.hostInstance);
|
|
1316
|
+
if (!current.enabled || current.consent === void 0 || !shown()) return;
|
|
1317
|
+
const id = store.installId();
|
|
1318
|
+
if (!id) return;
|
|
1319
|
+
const common = {
|
|
1320
|
+
schema: 1,
|
|
1321
|
+
at: minute(new Date(now())),
|
|
1322
|
+
installId: id.id,
|
|
1323
|
+
version: cliVersion(),
|
|
1324
|
+
os: process.platform === "darwin" ? "darwin" : "linux",
|
|
1325
|
+
arch: process.arch,
|
|
1326
|
+
node: Number(process.versions.node.split(".")[0]),
|
|
1327
|
+
harness,
|
|
1328
|
+
ci,
|
|
1329
|
+
consent: current.consent
|
|
1330
|
+
};
|
|
1331
|
+
const events = [];
|
|
1332
|
+
if (id.created) events.push({
|
|
1333
|
+
...common,
|
|
1334
|
+
event: "cli.first_run"
|
|
1335
|
+
});
|
|
1336
|
+
for (const item of pending) {
|
|
1337
|
+
const fields = item.event === "verify.result" ? {
|
|
1338
|
+
status: item.fields["status"],
|
|
1339
|
+
...catalogCodes(item.fields["codes"] ?? [], CATALOG)
|
|
1340
|
+
} : item.fields;
|
|
1341
|
+
events.push({
|
|
1342
|
+
...common,
|
|
1343
|
+
...fields,
|
|
1344
|
+
event: item.event
|
|
1345
|
+
});
|
|
1346
|
+
}
|
|
1347
|
+
events.push({
|
|
1348
|
+
...common,
|
|
1349
|
+
event: "cli.command",
|
|
1350
|
+
command: options.command,
|
|
1351
|
+
exit: outcome.exit,
|
|
1352
|
+
duration: durationBucket(outcome.durationMs),
|
|
1353
|
+
json: options.json,
|
|
1354
|
+
...outcome.error !== void 0 && CATALOG.has(outcome.error) ? { error: outcome.error } : {}
|
|
1355
|
+
});
|
|
1356
|
+
const lines = [];
|
|
1357
|
+
for (const event of events) {
|
|
1358
|
+
const parsed = TelemetryEvent.safeParse(event);
|
|
1359
|
+
if (parsed.success) lines.push(JSON.stringify(parsed.data));
|
|
1360
|
+
}
|
|
1361
|
+
if (truthy(options.env["MOSTRA_TELEMETRY_DEBUG"])) {
|
|
1362
|
+
for (const line of lines) options.stderr(`mostra telemetry (debug, not sent): ${line}\n`);
|
|
1363
|
+
return;
|
|
1364
|
+
}
|
|
1365
|
+
store.append(lines, now());
|
|
1366
|
+
if (options.flush !== false) startFlusher(store, options.env, now());
|
|
1367
|
+
}
|
|
1368
|
+
};
|
|
1369
|
+
}
|
|
1370
|
+
/** `mostra telemetry status`: the decision, the install ID and the queue. */
|
|
1371
|
+
function telemetryStatus(options) {
|
|
1372
|
+
const store = new TelemetryStore(options.configDir);
|
|
1373
|
+
const decided = currentDecision({
|
|
1374
|
+
env: options.env,
|
|
1375
|
+
store,
|
|
1376
|
+
embedded: options.embedded
|
|
1377
|
+
});
|
|
1378
|
+
const homeBlocks = options.homeConfigUnreadable !== void 0 && decided.reason !== "no-collector";
|
|
1379
|
+
const decision = homeBlocks ? {
|
|
1380
|
+
...decided,
|
|
1381
|
+
enabled: false,
|
|
1382
|
+
reason: "unreadable"
|
|
1383
|
+
} : decided;
|
|
1384
|
+
const unreadableFile = homeBlocks ? options.homeConfigUnreadable : decision.reason === "unreadable" ? store.settingsFile : void 0;
|
|
1385
|
+
const read = store.read();
|
|
1386
|
+
const settings = read.ok ? read.settings : {};
|
|
1387
|
+
const queued = read.ok ? store.queued(options.now ?? Date.now()).length : 0;
|
|
1388
|
+
const status = {
|
|
1389
|
+
enabled: decision.enabled,
|
|
1390
|
+
reason: decision.reason,
|
|
1391
|
+
askFirst: decision.askFirst,
|
|
1392
|
+
...settings.installId !== void 0 ? { installId: settings.installId } : {},
|
|
1393
|
+
...decision.endpoint !== void 0 ? { endpoint: decision.endpoint } : {},
|
|
1394
|
+
queued,
|
|
1395
|
+
...settings.noticeShownAt !== void 0 ? { noticeShownAt: settings.noticeShownAt } : {},
|
|
1396
|
+
settingsFile: store.settingsFile,
|
|
1397
|
+
...unreadableFile !== void 0 ? { unreadableFile } : {}
|
|
1398
|
+
};
|
|
1399
|
+
return {
|
|
1400
|
+
status,
|
|
1401
|
+
text: [
|
|
1402
|
+
statusLine({
|
|
1403
|
+
...decision,
|
|
1404
|
+
settingsFile: unreadableFile ?? store.settingsFile
|
|
1405
|
+
}),
|
|
1406
|
+
...status.installId !== void 0 ? [`Install ID: ${status.installId}`] : [],
|
|
1407
|
+
`Queued events: ${queued}`,
|
|
1408
|
+
`Settings: ${store.settingsFile}`
|
|
1409
|
+
].join("\n")
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
//#endregion
|
|
1413
|
+
//#region src/commands/shared.ts
|
|
1414
|
+
/**
|
|
1415
|
+
* What commands share: naming artifacts and sources on the command line, and reading JSON values.
|
|
1416
|
+
*/
|
|
1417
|
+
const ARTIFACT_ID = /^art_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
1418
|
+
/** An artifact named by its ID (`art_…`) or by a path, relative ones from `--cwd`. */
|
|
1419
|
+
function artifactRef(value, cwd) {
|
|
1420
|
+
return ARTIFACT_ID.test(value) ? { artifactId: value } : { path: resolve(cwd, value) };
|
|
1421
|
+
}
|
|
1422
|
+
/** A source named the same way: a path, or the ID of an artifact (the path it was published from). */
|
|
1423
|
+
function sourceRef(value, cwd) {
|
|
1424
|
+
return ARTIFACT_ID.test(value) ? { artifactId: value } : { path: resolve(cwd, value) };
|
|
1425
|
+
}
|
|
1426
|
+
/** A string argument the parser guaranteed. */
|
|
1427
|
+
function arg(context, name) {
|
|
1428
|
+
const value = context.args[name];
|
|
1429
|
+
if (typeof value !== "string") throw usage(`<${name}> is missing.`);
|
|
1430
|
+
return value;
|
|
1431
|
+
}
|
|
1432
|
+
/** An optional string flag. */
|
|
1433
|
+
function flag(context, name) {
|
|
1434
|
+
const value = context.flags[name];
|
|
1435
|
+
return typeof value === "string" ? value : void 0;
|
|
1436
|
+
}
|
|
1437
|
+
/** Parses a JSON value given on the command line, in a file or on stdin. */
|
|
1438
|
+
function parseJson(text, where) {
|
|
1439
|
+
try {
|
|
1440
|
+
return JSON.parse(text);
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
throw usage(`${where} isn't JSON: ${error.message}`);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
/** Reads a JSON value from `--value`, `--file` or `--stdin`, exactly one of them. */
|
|
1446
|
+
async function jsonInput(context, valueFlag = "value") {
|
|
1447
|
+
if ([
|
|
1448
|
+
valueFlag,
|
|
1449
|
+
"file",
|
|
1450
|
+
"stdin"
|
|
1451
|
+
].filter((name) => context.flags[name] !== void 0 && context.flags[name] !== false).length !== 1) throw usage(`Give the value with exactly one of --${valueFlag}, --file or --stdin.`);
|
|
1452
|
+
if (context.flags["stdin"] === true) return parseJson(await context.readStdin(), "stdin");
|
|
1453
|
+
const file = flag(context, "file");
|
|
1454
|
+
if (file !== void 0) {
|
|
1455
|
+
let text;
|
|
1456
|
+
try {
|
|
1457
|
+
text = readFileSync(resolve(context.cwd, file), "utf8");
|
|
1458
|
+
} catch (error) {
|
|
1459
|
+
throw usage(`--file ${file} can't be read (${error.code ?? "error"}).`);
|
|
1460
|
+
}
|
|
1461
|
+
return parseJson(text, file);
|
|
1462
|
+
}
|
|
1463
|
+
return parseJson(flag(context, valueFlag) ?? "", `--${valueFlag}`);
|
|
1464
|
+
}
|
|
1465
|
+
/** Indented JSON, for human output of structured results. */
|
|
1466
|
+
function pretty(value) {
|
|
1467
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
1468
|
+
}
|
|
1469
|
+
//#endregion
|
|
1470
|
+
//#region src/commands/admin.ts
|
|
1471
|
+
/**
|
|
1472
|
+
* Administration: `secret`, `list`, `status`, `daemon`, `host`, `gc`, `doctor`, `docs` and
|
|
1473
|
+
* `telemetry`. Each is a client of the host protocol or the runtime binary, never of the home's
|
|
1474
|
+
* files, except `doctor`, which reads what it needs to explain why an instance can't start.
|
|
1475
|
+
*/
|
|
1476
|
+
const artifactFlag = {
|
|
1477
|
+
name: "artifact",
|
|
1478
|
+
kind: "string",
|
|
1479
|
+
value: "<path|id>",
|
|
1480
|
+
summary: "The artifact's own secret, instead of one the whole home shares."
|
|
1481
|
+
};
|
|
1482
|
+
/** `mostra secret set <name>`. */
|
|
1483
|
+
const secretSetCommand = defineCommand({
|
|
1484
|
+
path: ["secret", "set"],
|
|
1485
|
+
summary: "Store a secret that server code reads with secret(name); the value comes from stdin and is never shown.",
|
|
1486
|
+
args: [{
|
|
1487
|
+
name: "name",
|
|
1488
|
+
summary: "Letters, digits, _, . and -, starting with a letter or _."
|
|
1489
|
+
}],
|
|
1490
|
+
flags: [artifactFlag],
|
|
1491
|
+
output: {
|
|
1492
|
+
type: "SecretEntry",
|
|
1493
|
+
topic: "cli"
|
|
1494
|
+
},
|
|
1495
|
+
exits: [EXIT.failure],
|
|
1496
|
+
example: "secret set MAIL_TOKEN --artifact ./inbox < token.txt",
|
|
1497
|
+
readsStdin: true,
|
|
1498
|
+
async run(context) {
|
|
1499
|
+
let value = await context.readStdin();
|
|
1500
|
+
if (value.endsWith("\n")) value = value.slice(0, value.endsWith("\r\n") ? -2 : -1);
|
|
1501
|
+
if (value === "") throw usage("The secret's value comes from stdin, and stdin was empty.");
|
|
1502
|
+
const artifact = flag(context, "artifact");
|
|
1503
|
+
const entry = await context.call(secretSetMethod, {
|
|
1504
|
+
name: arg(context, "name"),
|
|
1505
|
+
value,
|
|
1506
|
+
...artifact !== void 0 ? { artifact: artifactRef(artifact, context.cwd) } : {}
|
|
1507
|
+
});
|
|
1508
|
+
return {
|
|
1509
|
+
result: entry,
|
|
1510
|
+
human: `Stored ${entry.name} (${entry.scope})\n`
|
|
1511
|
+
};
|
|
1512
|
+
}
|
|
1513
|
+
});
|
|
1514
|
+
/** `mostra secret list`. */
|
|
1515
|
+
const secretListCommand = defineCommand({
|
|
1516
|
+
path: ["secret", "list"],
|
|
1517
|
+
summary: "List the secrets' names and scopes; values are never shown.",
|
|
1518
|
+
flags: [artifactFlag],
|
|
1519
|
+
output: {
|
|
1520
|
+
type: "SecretListResult",
|
|
1521
|
+
topic: "cli"
|
|
1522
|
+
},
|
|
1523
|
+
exits: [EXIT.failure],
|
|
1524
|
+
example: "secret list --artifact ./inbox",
|
|
1525
|
+
async run(context) {
|
|
1526
|
+
const artifact = flag(context, "artifact");
|
|
1527
|
+
const result = await context.call(secretListMethod, { ...artifact !== void 0 ? { artifact: artifactRef(artifact, context.cwd) } : {} });
|
|
1528
|
+
return {
|
|
1529
|
+
result,
|
|
1530
|
+
human: result.secrets.map((entry) => `${entry.name} ${entry.scope} ${entry.updatedAt}\n`).join("")
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
});
|
|
1534
|
+
/** `mostra secret rm <name>`. */
|
|
1535
|
+
const secretRmCommand = defineCommand({
|
|
1536
|
+
path: ["secret", "rm"],
|
|
1537
|
+
summary: "Delete a secret.",
|
|
1538
|
+
args: [{
|
|
1539
|
+
name: "name",
|
|
1540
|
+
summary: "The secret's name."
|
|
1541
|
+
}],
|
|
1542
|
+
flags: [artifactFlag],
|
|
1543
|
+
output: {
|
|
1544
|
+
type: "SecretDeleteResult",
|
|
1545
|
+
topic: "cli"
|
|
1546
|
+
},
|
|
1547
|
+
exits: [EXIT.failure],
|
|
1548
|
+
example: "secret rm MAIL_TOKEN --artifact ./inbox",
|
|
1549
|
+
async run(context) {
|
|
1550
|
+
const artifact = flag(context, "artifact");
|
|
1551
|
+
const result = await context.call(secretDeleteMethod, {
|
|
1552
|
+
name: arg(context, "name"),
|
|
1553
|
+
...artifact !== void 0 ? { artifact: artifactRef(artifact, context.cwd) } : {}
|
|
1554
|
+
});
|
|
1555
|
+
return {
|
|
1556
|
+
result,
|
|
1557
|
+
human: result.deleted ? "Deleted\n" : "No such secret\n"
|
|
1558
|
+
};
|
|
1559
|
+
}
|
|
1560
|
+
});
|
|
1561
|
+
/** `mostra list`. */
|
|
1562
|
+
const listCommand = defineCommand({
|
|
1563
|
+
path: ["list"],
|
|
1564
|
+
summary: "List the home's published artifacts.",
|
|
1565
|
+
flags: [{
|
|
1566
|
+
name: "family",
|
|
1567
|
+
kind: "string",
|
|
1568
|
+
value: "<family>",
|
|
1569
|
+
summary: "Only artifacts of this family."
|
|
1570
|
+
}],
|
|
1571
|
+
output: {
|
|
1572
|
+
type: "ArtifactListResult",
|
|
1573
|
+
topic: "cli"
|
|
1574
|
+
},
|
|
1575
|
+
exits: [],
|
|
1576
|
+
example: "list --family dashboard",
|
|
1577
|
+
async run(context) {
|
|
1578
|
+
const family = flag(context, "family");
|
|
1579
|
+
const result = await context.call(artifactListMethod, { ...family !== void 0 ? { filter: { family } } : {} });
|
|
1580
|
+
return {
|
|
1581
|
+
result,
|
|
1582
|
+
human: result.artifacts.map((artifact) => `${artifact.id} v${artifact.latestVersion?.number ?? "-"} ${artifact.title}${artifact.source ? ` ${artifact.source}` : ""}\n`).join("")
|
|
1583
|
+
};
|
|
1584
|
+
}
|
|
1585
|
+
});
|
|
1586
|
+
/** `mostra status [artifact]`. */
|
|
1587
|
+
const statusCommand = defineCommand({
|
|
1588
|
+
path: ["status"],
|
|
1589
|
+
summary: "Show the instance (processes, workers, deliveries) or one artifact (versions, policy, links).",
|
|
1590
|
+
args: [{
|
|
1591
|
+
name: "artifact",
|
|
1592
|
+
summary: "An artifact: its source path or its ID.",
|
|
1593
|
+
optional: true
|
|
1594
|
+
}],
|
|
1595
|
+
output: {
|
|
1596
|
+
type: "InstanceStatusResult or ArtifactDetail",
|
|
1597
|
+
topic: "cli"
|
|
1598
|
+
},
|
|
1599
|
+
exits: [EXIT.failure],
|
|
1600
|
+
example: "status ./sidebar-review",
|
|
1601
|
+
async run(context) {
|
|
1602
|
+
const artifact = context.args["artifact"];
|
|
1603
|
+
if (typeof artifact === "string") {
|
|
1604
|
+
const detail = await context.call(artifactGetMethod, { artifact: artifactRef(artifact, context.cwd) });
|
|
1605
|
+
const versions = detail.versions.map((version) => ` v${version.number} ${version.id} ${version.publishedAt}${version.collected ? " collected" : ""}\n`).join("");
|
|
1606
|
+
return {
|
|
1607
|
+
result: detail,
|
|
1608
|
+
human: `${detail.id} ${detail.title}\n${versions}`
|
|
1609
|
+
};
|
|
1610
|
+
}
|
|
1611
|
+
const status = await context.call(instanceStatusMethod, {});
|
|
1612
|
+
return {
|
|
1613
|
+
result: status,
|
|
1614
|
+
human: pretty(status)
|
|
1615
|
+
};
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
/** The runtime's `service` command, as JSON. */
|
|
1619
|
+
async function service(context, action) {
|
|
1620
|
+
const { stdout } = await context.runtime([
|
|
1621
|
+
"service",
|
|
1622
|
+
action,
|
|
1623
|
+
"--home",
|
|
1624
|
+
context.home,
|
|
1625
|
+
...context.flags["no-start"] === true ? ["--no-start"] : []
|
|
1626
|
+
]);
|
|
1627
|
+
const line = stdout.split("\n").find((text) => text.trim().startsWith("{"));
|
|
1628
|
+
let parsed = {};
|
|
1629
|
+
try {
|
|
1630
|
+
parsed = JSON.parse(line ?? "{}");
|
|
1631
|
+
} catch {}
|
|
1632
|
+
if (parsed.status === "ok" && parsed.service) return parsed.service;
|
|
1633
|
+
throw new RpcError(parsed.diagnostic ?? createDiagnostic("MOSTRA_ENV_SERVICE_FAILED", { message: `mostra-runtime service ${action} gave no answer.` }));
|
|
1634
|
+
}
|
|
1635
|
+
const noStart = {
|
|
1636
|
+
name: "no-start",
|
|
1637
|
+
kind: "boolean",
|
|
1638
|
+
summary: "Write or remove the service's file without asking launchd or systemd to load it."
|
|
1639
|
+
};
|
|
1640
|
+
/** `mostra daemon start`. */
|
|
1641
|
+
const daemonStartCommand = defineCommand({
|
|
1642
|
+
path: ["daemon", "start"],
|
|
1643
|
+
summary: "Start the home's daemon, or find the one already running.",
|
|
1644
|
+
output: {
|
|
1645
|
+
type: "DaemonStatus",
|
|
1646
|
+
topic: "cli"
|
|
1647
|
+
},
|
|
1648
|
+
exits: [],
|
|
1649
|
+
example: "daemon start",
|
|
1650
|
+
async run(context) {
|
|
1651
|
+
const summary = (await context.connection())?.summary;
|
|
1652
|
+
return {
|
|
1653
|
+
result: {
|
|
1654
|
+
running: true,
|
|
1655
|
+
...summary
|
|
1656
|
+
},
|
|
1657
|
+
human: `${summary?.started ? "Started" : "Running"}: pid ${summary?.pid}, ${summary?.socket}\n`
|
|
1658
|
+
};
|
|
1659
|
+
}
|
|
1660
|
+
});
|
|
1661
|
+
/** Stops the running daemon; resolves once its socket closed. */
|
|
1662
|
+
async function stopDaemon(context) {
|
|
1663
|
+
const connection = await context.connection(false);
|
|
1664
|
+
if (!connection) return { stopped: false };
|
|
1665
|
+
const { pid } = connection.summary;
|
|
1666
|
+
await context.call(mostraShutdownMethod, {});
|
|
1667
|
+
await Promise.race([connection.closed, new Promise((done) => setTimeout(done, 15e3))]);
|
|
1668
|
+
const deadline = Date.now() + 15e3;
|
|
1669
|
+
while (existsSync(join(context.home, "instance.json")) && Date.now() < deadline) await new Promise((done) => setTimeout(done, 50));
|
|
1670
|
+
return {
|
|
1671
|
+
stopped: true,
|
|
1672
|
+
pid
|
|
1673
|
+
};
|
|
1674
|
+
}
|
|
1675
|
+
/** `mostra daemon stop`. */
|
|
1676
|
+
const daemonStopCommand = defineCommand({
|
|
1677
|
+
path: ["daemon", "stop"],
|
|
1678
|
+
summary: "Stop the home's daemon; open pages reconnect when it starts again.",
|
|
1679
|
+
output: {
|
|
1680
|
+
type: "DaemonStop",
|
|
1681
|
+
topic: "cli"
|
|
1682
|
+
},
|
|
1683
|
+
exits: [],
|
|
1684
|
+
example: "daemon stop",
|
|
1685
|
+
async run(context) {
|
|
1686
|
+
const result = await stopDaemon(context);
|
|
1687
|
+
return {
|
|
1688
|
+
result,
|
|
1689
|
+
human: result.stopped ? `Stopped pid ${result.pid}\n` : "No daemon was running\n"
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1692
|
+
});
|
|
1693
|
+
/** `mostra daemon restart`. */
|
|
1694
|
+
const daemonRestartCommand = defineCommand({
|
|
1695
|
+
path: ["daemon", "restart"],
|
|
1696
|
+
summary: "Stop the home's daemon and start it again, for instance after an update.",
|
|
1697
|
+
output: {
|
|
1698
|
+
type: "DaemonStatus",
|
|
1699
|
+
topic: "cli"
|
|
1700
|
+
},
|
|
1701
|
+
exits: [],
|
|
1702
|
+
example: "daemon restart",
|
|
1703
|
+
async run(context) {
|
|
1704
|
+
await stopDaemon(context);
|
|
1705
|
+
const summary = (await context.connection())?.summary;
|
|
1706
|
+
return {
|
|
1707
|
+
result: {
|
|
1708
|
+
running: true,
|
|
1709
|
+
...summary
|
|
1710
|
+
},
|
|
1711
|
+
human: `Running: pid ${summary?.pid}\n`
|
|
1712
|
+
};
|
|
1713
|
+
}
|
|
1714
|
+
});
|
|
1715
|
+
/** `mostra daemon status`. */
|
|
1716
|
+
const daemonStatusCommand = defineCommand({
|
|
1717
|
+
path: ["daemon", "status"],
|
|
1718
|
+
summary: "Whether the home's daemon runs, when it would exit for lack of work, and its login service.",
|
|
1719
|
+
output: {
|
|
1720
|
+
type: "DaemonStatus",
|
|
1721
|
+
topic: "cli"
|
|
1722
|
+
},
|
|
1723
|
+
exits: [],
|
|
1724
|
+
example: "daemon status",
|
|
1725
|
+
async run(context) {
|
|
1726
|
+
const loginService = await service(context, "status");
|
|
1727
|
+
const connection = await context.connection(false);
|
|
1728
|
+
if (!connection) return {
|
|
1729
|
+
result: {
|
|
1730
|
+
running: false,
|
|
1731
|
+
service: loginService
|
|
1732
|
+
},
|
|
1733
|
+
human: `Not running. Login service: ${loginService.installed ? "installed" : "not installed"}\n`
|
|
1734
|
+
};
|
|
1735
|
+
const status = await context.call(instanceStatusMethod, {});
|
|
1736
|
+
return {
|
|
1737
|
+
result: {
|
|
1738
|
+
running: true,
|
|
1739
|
+
...connection.summary,
|
|
1740
|
+
status,
|
|
1741
|
+
service: loginService
|
|
1742
|
+
},
|
|
1743
|
+
human: `Running: pid ${connection.summary.pid}${status.idleExitAt ? `, exits when idle at ${status.idleExitAt}` : ""}. Login service: ${loginService.installed ? "installed" : "not installed"}\n`
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
});
|
|
1747
|
+
/** `mostra daemon install`. */
|
|
1748
|
+
const daemonInstallCommand = defineCommand({
|
|
1749
|
+
path: ["daemon", "install"],
|
|
1750
|
+
summary: "Run the home's daemon as a login service (launchd or systemd), so links answer at any hour.",
|
|
1751
|
+
flags: [noStart],
|
|
1752
|
+
output: {
|
|
1753
|
+
type: "ServiceStatus",
|
|
1754
|
+
topic: "cli"
|
|
1755
|
+
},
|
|
1756
|
+
exits: [],
|
|
1757
|
+
example: "daemon install",
|
|
1758
|
+
async run(context) {
|
|
1759
|
+
const result = await service(context, "install");
|
|
1760
|
+
return {
|
|
1761
|
+
result,
|
|
1762
|
+
human: `Installed ${result.name} (${result.file})\n`
|
|
1763
|
+
};
|
|
1764
|
+
}
|
|
1765
|
+
});
|
|
1766
|
+
/** `mostra daemon uninstall`. */
|
|
1767
|
+
const daemonUninstallCommand = defineCommand({
|
|
1768
|
+
path: ["daemon", "uninstall"],
|
|
1769
|
+
summary: "Remove the home's login service; the daemon then exits after 30 idle minutes.",
|
|
1770
|
+
flags: [noStart],
|
|
1771
|
+
output: {
|
|
1772
|
+
type: "ServiceStatus",
|
|
1773
|
+
topic: "cli"
|
|
1774
|
+
},
|
|
1775
|
+
exits: [],
|
|
1776
|
+
example: "daemon uninstall",
|
|
1777
|
+
async run(context) {
|
|
1778
|
+
const result = await service(context, "uninstall");
|
|
1779
|
+
return {
|
|
1780
|
+
result,
|
|
1781
|
+
human: `Removed ${result.name}\n`
|
|
1782
|
+
};
|
|
1783
|
+
}
|
|
1784
|
+
});
|
|
1785
|
+
/** `mostra host`. */
|
|
1786
|
+
const hostCommand = defineCommand({
|
|
1787
|
+
path: ["host"],
|
|
1788
|
+
summary: "Speak the host protocol on stdin and stdout, for a host that spawns Mostra; it sends no telemetry.",
|
|
1789
|
+
flags: [{
|
|
1790
|
+
name: "bind",
|
|
1791
|
+
kind: "string",
|
|
1792
|
+
value: "<address>",
|
|
1793
|
+
summary: "Serve viewers on this address instead of 127.0.0.1."
|
|
1794
|
+
}, {
|
|
1795
|
+
name: "url-host",
|
|
1796
|
+
kind: "string",
|
|
1797
|
+
value: "<name>",
|
|
1798
|
+
summary: "The host name links use; a wildcard --bind needs it."
|
|
1799
|
+
}],
|
|
1800
|
+
output: {
|
|
1801
|
+
type: "the host protocol (INTERFACES §6)",
|
|
1802
|
+
topic: "cli"
|
|
1803
|
+
},
|
|
1804
|
+
exits: [],
|
|
1805
|
+
example: "host --home ~/.my-host/mostra",
|
|
1806
|
+
local: true,
|
|
1807
|
+
quiet: true,
|
|
1808
|
+
readsStdin: true,
|
|
1809
|
+
async run(context) {
|
|
1810
|
+
const runtime = await locateRuntime(context.env);
|
|
1811
|
+
const bind = flag(context, "bind");
|
|
1812
|
+
const urlHost = flag(context, "url-host");
|
|
1813
|
+
return {
|
|
1814
|
+
result: void 0,
|
|
1815
|
+
exit: await new Promise((resolve) => {
|
|
1816
|
+
const child = spawn(runtime.node, [
|
|
1817
|
+
runtime.entry,
|
|
1818
|
+
"host",
|
|
1819
|
+
"--home",
|
|
1820
|
+
context.home,
|
|
1821
|
+
...bind !== void 0 ? ["--bind", bind] : [],
|
|
1822
|
+
...urlHost !== void 0 ? ["--url-host", urlHost] : []
|
|
1823
|
+
], {
|
|
1824
|
+
stdio: "inherit",
|
|
1825
|
+
env: context.env
|
|
1826
|
+
});
|
|
1827
|
+
child.on("exit", (exit) => resolve(exit ?? EXIT.environment));
|
|
1828
|
+
child.on("error", () => resolve(EXIT.environment));
|
|
1829
|
+
for (const signal of ["SIGINT", "SIGTERM"]) process.on(signal, () => child.kill(signal));
|
|
1830
|
+
}),
|
|
1831
|
+
raw: true
|
|
1832
|
+
};
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
/** `mostra gc`. */
|
|
1836
|
+
const gcCommand = defineCommand({
|
|
1837
|
+
path: ["gc"],
|
|
1838
|
+
summary: "Remove what nothing references: unreferenced versions' files, unused packages and caches.",
|
|
1839
|
+
flags: [
|
|
1840
|
+
{
|
|
1841
|
+
name: "dry-run",
|
|
1842
|
+
kind: "boolean",
|
|
1843
|
+
summary: "List what a run would remove, and remove nothing."
|
|
1844
|
+
},
|
|
1845
|
+
{
|
|
1846
|
+
name: "compact-events",
|
|
1847
|
+
kind: "boolean",
|
|
1848
|
+
summary: "Also replace events before --before with one summary per artifact."
|
|
1849
|
+
},
|
|
1850
|
+
{
|
|
1851
|
+
name: "before",
|
|
1852
|
+
kind: "string",
|
|
1853
|
+
value: "<cursor>",
|
|
1854
|
+
summary: "The cursor --compact-events compacts before."
|
|
1855
|
+
}
|
|
1856
|
+
],
|
|
1857
|
+
output: {
|
|
1858
|
+
type: "GcRunResult",
|
|
1859
|
+
topic: "cli"
|
|
1860
|
+
},
|
|
1861
|
+
exits: [EXIT.failure],
|
|
1862
|
+
example: "gc --dry-run",
|
|
1863
|
+
async run(context) {
|
|
1864
|
+
const compact = context.flags["compact-events"] === true;
|
|
1865
|
+
const before = flag(context, "before");
|
|
1866
|
+
if (compact !== (before !== void 0)) throw usage("--compact-events and --before go together.");
|
|
1867
|
+
if (before !== void 0 && !/^[0-9]+$/.test(before)) throw usage(`--before takes an event cursor; "${before}" isn't one.`);
|
|
1868
|
+
const result = await context.call(gcRunMethod, {
|
|
1869
|
+
...context.flags["dry-run"] === true ? { dryRun: true } : {},
|
|
1870
|
+
...before !== void 0 ? { compactEvents: { before } } : {}
|
|
1871
|
+
});
|
|
1872
|
+
for (const warning of result.warnings ?? []) context.warn(warning);
|
|
1873
|
+
return {
|
|
1874
|
+
result,
|
|
1875
|
+
human: `${[
|
|
1876
|
+
`${result.dryRun ? "Would remove" : "Removed"} ${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).`,
|
|
1877
|
+
...result.versions.map((version) => ` ${version.artifactId} v${version.number} ${version.versionId}`),
|
|
1878
|
+
...result.compacted ? [`Compacted ${result.compacted.removed} events.`] : []
|
|
1879
|
+
].join("\n")}\n`
|
|
1880
|
+
};
|
|
1881
|
+
}
|
|
1882
|
+
});
|
|
1883
|
+
/** `mostra doctor`. */
|
|
1884
|
+
const doctorCommand = defineCommand({
|
|
1885
|
+
path: ["doctor"],
|
|
1886
|
+
summary: "Check the platform, the runtime, the home, its daemon and ledger, and the login service.",
|
|
1887
|
+
flags: [{
|
|
1888
|
+
name: "deep",
|
|
1889
|
+
kind: "boolean",
|
|
1890
|
+
summary: "Also hash every version against its recorded output digest."
|
|
1891
|
+
}],
|
|
1892
|
+
output: {
|
|
1893
|
+
type: "DoctorResult",
|
|
1894
|
+
topic: "cli"
|
|
1895
|
+
},
|
|
1896
|
+
exits: [EXIT.failure],
|
|
1897
|
+
example: "doctor",
|
|
1898
|
+
async run(context) {
|
|
1899
|
+
const checks = [];
|
|
1900
|
+
const node = Number(process.versions.node.split(".")[0]);
|
|
1901
|
+
checks.push(process.platform === "darwin" || process.platform === "linux" ? {
|
|
1902
|
+
name: "platform",
|
|
1903
|
+
status: "ok",
|
|
1904
|
+
detail: `${process.platform} ${process.arch}, Node ${process.versions.node}`
|
|
1905
|
+
} : {
|
|
1906
|
+
name: "platform",
|
|
1907
|
+
status: "error",
|
|
1908
|
+
diagnostic: createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message: `Mostra runs on macOS and Linux; this is ${process.platform}.` })
|
|
1909
|
+
});
|
|
1910
|
+
if (node < 24) checks.push({
|
|
1911
|
+
name: "node",
|
|
1912
|
+
status: "error",
|
|
1913
|
+
diagnostic: createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", { message: `Mostra needs Node 24 or later; this is ${process.versions.node}.` })
|
|
1914
|
+
});
|
|
1915
|
+
try {
|
|
1916
|
+
const runtime = await locateRuntime(context.env);
|
|
1917
|
+
checks.push({
|
|
1918
|
+
name: "runtime",
|
|
1919
|
+
status: "ok",
|
|
1920
|
+
detail: runtime.entry
|
|
1921
|
+
});
|
|
1922
|
+
} catch (error) {
|
|
1923
|
+
checks.push({
|
|
1924
|
+
name: "runtime",
|
|
1925
|
+
status: "error",
|
|
1926
|
+
diagnostic: error.diagnostic
|
|
1927
|
+
});
|
|
1928
|
+
}
|
|
1929
|
+
checks.push(await bundleCheck(context.env, context.flags["deep"] === true));
|
|
1930
|
+
checks.push(homeCheck(context.home));
|
|
1931
|
+
let connection;
|
|
1932
|
+
try {
|
|
1933
|
+
connection = await context.connection();
|
|
1934
|
+
checks.push({
|
|
1935
|
+
name: "daemon",
|
|
1936
|
+
status: "ok",
|
|
1937
|
+
detail: `pid ${connection?.summary.pid}, ${connection?.summary.socket}`
|
|
1938
|
+
});
|
|
1939
|
+
} catch (error) {
|
|
1940
|
+
const diagnostic = error instanceof RpcError ? error.diagnostic : void 0;
|
|
1941
|
+
checks.push({
|
|
1942
|
+
name: "daemon",
|
|
1943
|
+
status: "error",
|
|
1944
|
+
...diagnostic ? { diagnostic } : { detail: String(error) },
|
|
1945
|
+
...diagnostic?.code === "MOSTRA_HOME_LEDGER_UNREADABLE" ? { detail: backups(context.home) } : {}
|
|
1946
|
+
});
|
|
1947
|
+
}
|
|
1948
|
+
if (connection) {
|
|
1949
|
+
if (connection.summary.version !== cliVersion()) checks.push({
|
|
1950
|
+
name: "versions",
|
|
1951
|
+
status: "warning",
|
|
1952
|
+
detail: `The CLI is ${cliVersion()} and the daemon ${connection.summary.version}; run "mostra daemon restart" after an update.`
|
|
1953
|
+
});
|
|
1954
|
+
const instance = await context.call(instanceCheckMethod, context.flags["deep"] === true ? { deep: true } : {});
|
|
1955
|
+
checks.push(...instance.checks.map((check) => ({ ...check })));
|
|
1956
|
+
checks.push(await alwaysOnCheck(context));
|
|
1957
|
+
}
|
|
1958
|
+
const telemetry = telemetryStatus({
|
|
1959
|
+
env: context.env,
|
|
1960
|
+
configDir: context.configDir,
|
|
1961
|
+
embedded: readHomeMarks(context.home).hostConfigured !== void 0 || connection?.summary.mode === "host"
|
|
1962
|
+
});
|
|
1963
|
+
checks.push({
|
|
1964
|
+
name: "telemetry",
|
|
1965
|
+
status: "ok",
|
|
1966
|
+
detail: telemetry.text.split("\n")[0]
|
|
1967
|
+
});
|
|
1968
|
+
const firstError = checks.find((check) => check.status === "error");
|
|
1969
|
+
const human = checks.map((check) => {
|
|
1970
|
+
const mark = check.status === "ok" ? "ok " : check.status === "warning" ? "warn" : "FAIL";
|
|
1971
|
+
const what = check.diagnostic ? `${check.diagnostic.code}: ${check.diagnostic.message}` : check.detail ?? "";
|
|
1972
|
+
const hint = check.diagnostic?.hint ? `\n ${check.diagnostic.hint}` : "";
|
|
1973
|
+
return `${mark} ${check.name}${what ? `: ${what}` : ""}${hint}`;
|
|
1974
|
+
}).join("\n");
|
|
1975
|
+
return {
|
|
1976
|
+
result: { checks },
|
|
1977
|
+
human: `${human}\n`,
|
|
1978
|
+
...firstError ? { exit: firstError.diagnostic ? exitCodeFor(firstError.diagnostic.code) : EXIT.environment } : {}
|
|
1979
|
+
};
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
/**
|
|
1983
|
+
* The runtime bundle this CLI runs from (RT-01): set up and matching its files. Outside a bundle
|
|
1984
|
+
* (a development checkout, `MOSTRA_RUNTIME_BIN`) there is nothing to check. `--deep` hashes every
|
|
1985
|
+
* file against the bundle's file list and the browser against its pin, as setup does.
|
|
1986
|
+
*/
|
|
1987
|
+
async function bundleCheck(env, deep) {
|
|
1988
|
+
const root = env["MOSTRA_BUNDLE"] || enclosingBundle();
|
|
1989
|
+
if (!root) return {
|
|
1990
|
+
name: "bundle",
|
|
1991
|
+
status: "ok",
|
|
1992
|
+
detail: "No runtime bundle: a development runtime."
|
|
1993
|
+
};
|
|
1994
|
+
try {
|
|
1995
|
+
const bundle = await readBundle(root);
|
|
1996
|
+
const label = `Mostra ${bundle.info.version} for ${bundle.info.platform} at ${root}`;
|
|
1997
|
+
if (!await readSetup(bundle)) return {
|
|
1998
|
+
name: "bundle",
|
|
1999
|
+
status: "error",
|
|
2000
|
+
diagnostic: createDiagnostic("MOSTRA_ENV_BUNDLE_UNAVAILABLE", { message: `The runtime bundle at ${root} isn't set up; run its bin/mostra-setup.` })
|
|
2001
|
+
};
|
|
2002
|
+
if (!deep) return {
|
|
2003
|
+
name: "bundle",
|
|
2004
|
+
status: "ok",
|
|
2005
|
+
detail: label
|
|
2006
|
+
};
|
|
2007
|
+
await checkFiles(bundle);
|
|
2008
|
+
const browser = await checkBrowser(bundle);
|
|
2009
|
+
if (browser.length > 0) return {
|
|
2010
|
+
name: "bundle",
|
|
2011
|
+
status: "error",
|
|
2012
|
+
diagnostic: createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: `The verify browser of the bundle at ${root} differs from its pin: ${browser.join("; ")}.` })
|
|
2013
|
+
};
|
|
2014
|
+
return {
|
|
2015
|
+
name: "bundle",
|
|
2016
|
+
status: "ok",
|
|
2017
|
+
detail: `${label}; every file matches its SHA-256`
|
|
2018
|
+
};
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
return {
|
|
2021
|
+
name: "bundle",
|
|
2022
|
+
status: "error",
|
|
2023
|
+
diagnostic: error instanceof RpcError ? error.diagnostic : createDiagnostic("MOSTRA_ENV_BUNDLE_INVALID", { message: error.message })
|
|
2024
|
+
};
|
|
2025
|
+
}
|
|
2026
|
+
}
|
|
2027
|
+
/** The home: absent (it is made on first use), a directory the user owns, or not. */
|
|
2028
|
+
function homeCheck(home) {
|
|
2029
|
+
let info;
|
|
2030
|
+
try {
|
|
2031
|
+
info = statSync(home);
|
|
2032
|
+
} catch {
|
|
2033
|
+
return {
|
|
2034
|
+
name: "home",
|
|
2035
|
+
status: "ok",
|
|
2036
|
+
detail: `${home} (not created yet)`
|
|
2037
|
+
};
|
|
2038
|
+
}
|
|
2039
|
+
if (!info.isDirectory() || process.getuid && info.uid !== process.getuid()) return {
|
|
2040
|
+
name: "home",
|
|
2041
|
+
status: "error",
|
|
2042
|
+
diagnostic: createDiagnostic("MOSTRA_HOME_NOT_OWNED", { message: `Mostra can't use ${home}: another user owns it or it isn't a directory.` })
|
|
2043
|
+
};
|
|
2044
|
+
return {
|
|
2045
|
+
name: "home",
|
|
2046
|
+
status: "ok",
|
|
2047
|
+
detail: home
|
|
2048
|
+
};
|
|
2049
|
+
}
|
|
2050
|
+
/** The ledger's backups beside it, for a ledger that can't be opened (FAILURE_MODEL §3). */
|
|
2051
|
+
function backups(home) {
|
|
2052
|
+
try {
|
|
2053
|
+
const found = readdirSync(join(home, "store")).filter((name) => name.startsWith("ledger.sqlite.bak-"));
|
|
2054
|
+
return found.length > 0 ? `Backups: ${found.map((name) => join(home, "store", name)).join(", ")}` : "No ledger backups.";
|
|
2055
|
+
} catch {
|
|
2056
|
+
return "No ledger backups.";
|
|
2057
|
+
}
|
|
2058
|
+
}
|
|
2059
|
+
/**
|
|
2060
|
+
* LL-05's doctor half (D96, D63): artifacts with submission policy `none` are meant to answer at
|
|
2061
|
+
* any hour, which needs the home's login service.
|
|
2062
|
+
*/
|
|
2063
|
+
async function alwaysOnCheck(context) {
|
|
2064
|
+
const { artifacts } = await context.call(artifactListMethod, {});
|
|
2065
|
+
const longLived = artifacts.filter((artifact) => artifact.policy?.policy === "none");
|
|
2066
|
+
if (longLived.length === 0) return {
|
|
2067
|
+
name: "always-on",
|
|
2068
|
+
status: "ok",
|
|
2069
|
+
detail: "No artifact needs an always-on daemon."
|
|
2070
|
+
};
|
|
2071
|
+
const loginService = await service(context, "status");
|
|
2072
|
+
if (loginService.installed) return {
|
|
2073
|
+
name: "always-on",
|
|
2074
|
+
status: "ok",
|
|
2075
|
+
detail: `Login service ${loginService.name} keeps the daemon running.`
|
|
2076
|
+
};
|
|
2077
|
+
return {
|
|
2078
|
+
name: "always-on",
|
|
2079
|
+
status: "warning",
|
|
2080
|
+
diagnostic: createDiagnostic("MOSTRA_HOME_NOT_ALWAYS_ON", { message: `${longLived.length} ${longLived.length === 1 ? "artifact has" : "artifacts have"} submission policy none (${longLived.map((artifact) => artifact.title).slice(0, 3).join(", ")}), and no login service keeps this home's daemon running.` })
|
|
2081
|
+
};
|
|
2082
|
+
}
|
|
2083
|
+
/** `mostra docs [topic]`. */
|
|
2084
|
+
const docsCommand = defineCommand({
|
|
2085
|
+
path: ["docs"],
|
|
2086
|
+
summary: "Print Mostra's reference: the CLI, telemetry, and every diagnostic code's page.",
|
|
2087
|
+
args: [{
|
|
2088
|
+
name: "topic",
|
|
2089
|
+
summary: "A topic or a diagnostic code. Default: the index.",
|
|
2090
|
+
optional: true
|
|
2091
|
+
}],
|
|
2092
|
+
output: {
|
|
2093
|
+
type: "DocsPage",
|
|
2094
|
+
topic: "cli"
|
|
2095
|
+
},
|
|
2096
|
+
exits: [],
|
|
2097
|
+
example: "docs MOSTRA_DEP_MISSING",
|
|
2098
|
+
local: true,
|
|
2099
|
+
quiet: true,
|
|
2100
|
+
async run(context) {
|
|
2101
|
+
const topics = docsIndex();
|
|
2102
|
+
const wanted = context.args["topic"];
|
|
2103
|
+
if (typeof wanted !== "string") {
|
|
2104
|
+
const markdown = indexMarkdown(topics);
|
|
2105
|
+
return {
|
|
2106
|
+
result: {
|
|
2107
|
+
topic: "index",
|
|
2108
|
+
markdown,
|
|
2109
|
+
url: topics[0]?.url.replace(/\/[^/]+$/, "") ?? "",
|
|
2110
|
+
topics: topics.map(({ topic, title, url }) => ({
|
|
2111
|
+
topic,
|
|
2112
|
+
title,
|
|
2113
|
+
url
|
|
2114
|
+
}))
|
|
2115
|
+
},
|
|
2116
|
+
human: markdown
|
|
2117
|
+
};
|
|
2118
|
+
}
|
|
2119
|
+
const found = topics.find((topic) => topic.topic === wanted || topic.topic === wanted.toUpperCase());
|
|
2120
|
+
if (!found) throw usage(`No topic "${wanted}". The nearest: ${nearestTopics(wanted, topics).join(", ")}. Run "mostra docs" for the index.`);
|
|
2121
|
+
const markdown = readFileSync(found.file, "utf8");
|
|
2122
|
+
return {
|
|
2123
|
+
result: {
|
|
2124
|
+
topic: found.topic,
|
|
2125
|
+
markdown,
|
|
2126
|
+
url: found.url
|
|
2127
|
+
},
|
|
2128
|
+
human: markdown
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2131
|
+
});
|
|
2132
|
+
/** `mostra telemetry status`. */
|
|
2133
|
+
const telemetryStatusCommand = defineCommand({
|
|
2134
|
+
path: ["telemetry", "status"],
|
|
2135
|
+
summary: "Whether the CLI sends anonymous usage data, and why.",
|
|
2136
|
+
output: {
|
|
2137
|
+
type: "TelemetryStatus",
|
|
2138
|
+
topic: "telemetry"
|
|
2139
|
+
},
|
|
2140
|
+
exits: [],
|
|
2141
|
+
example: "telemetry status",
|
|
2142
|
+
local: true,
|
|
2143
|
+
quiet: true,
|
|
2144
|
+
async run(context) {
|
|
2145
|
+
const { status, text } = telemetryStatus({
|
|
2146
|
+
env: context.env,
|
|
2147
|
+
configDir: context.configDir,
|
|
2148
|
+
...homeTelemetry(context.home)
|
|
2149
|
+
});
|
|
2150
|
+
return {
|
|
2151
|
+
result: status,
|
|
2152
|
+
human: `${text}\n`
|
|
2153
|
+
};
|
|
2154
|
+
}
|
|
2155
|
+
});
|
|
2156
|
+
/** How the home bears on telemetry: a host's home, or one whose `config.json` can't be read. */
|
|
2157
|
+
function homeTelemetry(home) {
|
|
2158
|
+
const marks = readHomeMarks(home);
|
|
2159
|
+
return {
|
|
2160
|
+
embedded: embeddedHome(marks) && marks.unreadable !== true,
|
|
2161
|
+
...marks.unreadable === true ? { homeConfigUnreadable: join(home, "config.json") } : {}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
/** The choice couldn't be recorded: nothing changed (Opus review finding 6). */
|
|
2165
|
+
function unwritable(store) {
|
|
2166
|
+
return new RpcError(createDiagnostic("MOSTRA_CLI_CONFIG_UNWRITABLE", { message: `${store.settingsFile} can't be written, so the choice wasn't recorded and nothing changed.` }));
|
|
2167
|
+
}
|
|
2168
|
+
/** `mostra telemetry on`. */
|
|
2169
|
+
const telemetryOnCommand = defineCommand({
|
|
2170
|
+
path: ["telemetry", "on"],
|
|
2171
|
+
summary: "Send anonymous usage data from this CLI; the notice says what it contains.",
|
|
2172
|
+
output: {
|
|
2173
|
+
type: "TelemetryStatus",
|
|
2174
|
+
topic: "telemetry"
|
|
2175
|
+
},
|
|
2176
|
+
exits: [EXIT.environment],
|
|
2177
|
+
example: "telemetry on",
|
|
2178
|
+
local: true,
|
|
2179
|
+
quiet: true,
|
|
2180
|
+
async run(context) {
|
|
2181
|
+
const store = new TelemetryStore(context.configDir);
|
|
2182
|
+
const read = store.read();
|
|
2183
|
+
if (!(read.ok && read.settings.noticeShownAt !== void 0)) context.stderr(NOTICE_DEFAULT);
|
|
2184
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2185
|
+
if ((store.update((current) => ({
|
|
2186
|
+
...current,
|
|
2187
|
+
choice: "on",
|
|
2188
|
+
noticeShownAt: current.noticeShownAt ?? now
|
|
2189
|
+
})) ?? store.locked(() => store.reset({
|
|
2190
|
+
choice: "on",
|
|
2191
|
+
noticeShownAt: now
|
|
2192
|
+
}))) === void 0) throw unwritable(store);
|
|
2193
|
+
const { status } = telemetryStatus({
|
|
2194
|
+
env: context.env,
|
|
2195
|
+
configDir: context.configDir,
|
|
2196
|
+
...homeTelemetry(context.home)
|
|
2197
|
+
});
|
|
2198
|
+
return {
|
|
2199
|
+
result: status,
|
|
2200
|
+
human: `${TURNED.on}\n`
|
|
2201
|
+
};
|
|
2202
|
+
}
|
|
2203
|
+
});
|
|
2204
|
+
/** `mostra telemetry off`. */
|
|
2205
|
+
const telemetryOffCommand = defineCommand({
|
|
2206
|
+
path: ["telemetry", "off"],
|
|
2207
|
+
summary: "Stop sending usage data, and delete the install ID and any unsent events.",
|
|
2208
|
+
output: {
|
|
2209
|
+
type: "TelemetryStatus",
|
|
2210
|
+
topic: "telemetry"
|
|
2211
|
+
},
|
|
2212
|
+
exits: [EXIT.environment],
|
|
2213
|
+
example: "telemetry off",
|
|
2214
|
+
local: true,
|
|
2215
|
+
quiet: true,
|
|
2216
|
+
async run(context) {
|
|
2217
|
+
const store = new TelemetryStore(context.configDir);
|
|
2218
|
+
if (!(store.turnOff() || store.locked(() => store.reset({ choice: "off" })) !== void 0)) throw unwritable(store);
|
|
2219
|
+
const { status } = telemetryStatus({
|
|
2220
|
+
env: context.env,
|
|
2221
|
+
configDir: context.configDir,
|
|
2222
|
+
...homeTelemetry(context.home)
|
|
2223
|
+
});
|
|
2224
|
+
return {
|
|
2225
|
+
result: status,
|
|
2226
|
+
human: `${TURNED.off}\n`
|
|
2227
|
+
};
|
|
2228
|
+
}
|
|
2229
|
+
});
|
|
2230
|
+
//#endregion
|
|
2231
|
+
//#region src/commands/authoring.ts
|
|
2232
|
+
/**
|
|
2233
|
+
* Authoring: `new`, `verify`, `publish` and `add`, thin clients of `artifact/create`,
|
|
2234
|
+
* `artifact/verify`, `artifact/publish` and `deps/add` (INTERFACES §6).
|
|
2235
|
+
*/
|
|
2236
|
+
/** `mostra new <dir>`. */
|
|
2237
|
+
const newCommand = defineCommand({
|
|
2238
|
+
path: ["new"],
|
|
2239
|
+
summary: "Create an artifact in an empty directory, with a generated ID.",
|
|
2240
|
+
args: [{
|
|
2241
|
+
name: "dir",
|
|
2242
|
+
summary: "The directory to create it in; it must be empty or not exist."
|
|
2243
|
+
}],
|
|
2244
|
+
flags: [
|
|
2245
|
+
{
|
|
2246
|
+
name: "title",
|
|
2247
|
+
kind: "string",
|
|
2248
|
+
value: "<text>",
|
|
2249
|
+
summary: "The artifact's title. Default: the directory's name."
|
|
2250
|
+
},
|
|
2251
|
+
{
|
|
2252
|
+
name: "template",
|
|
2253
|
+
kind: "string",
|
|
2254
|
+
value: "<family>",
|
|
2255
|
+
summary: "Start from a template (templates arrive in a later release)."
|
|
2256
|
+
},
|
|
2257
|
+
{
|
|
2258
|
+
name: "request-id",
|
|
2259
|
+
kind: "string",
|
|
2260
|
+
value: "<id>",
|
|
2261
|
+
summary: "Make a retry return the first attempt's result."
|
|
2262
|
+
}
|
|
2263
|
+
],
|
|
2264
|
+
output: {
|
|
2265
|
+
type: "ArtifactCreateResult",
|
|
2266
|
+
topic: "cli"
|
|
2267
|
+
},
|
|
2268
|
+
exits: [EXIT.failure],
|
|
2269
|
+
example: "new ./sidebar-review --title 'Sidebar review'",
|
|
2270
|
+
async run(context) {
|
|
2271
|
+
const title = flag(context, "title");
|
|
2272
|
+
const template = flag(context, "template");
|
|
2273
|
+
const requestId = flag(context, "request-id");
|
|
2274
|
+
const result = await context.call(artifactCreateMethod, {
|
|
2275
|
+
dir: resolve(context.cwd, arg(context, "dir")),
|
|
2276
|
+
...title !== void 0 ? { title } : {},
|
|
2277
|
+
...template !== void 0 ? { template } : {},
|
|
2278
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
2279
|
+
});
|
|
2280
|
+
return {
|
|
2281
|
+
result,
|
|
2282
|
+
human: `Created ${result.artifactId} in ${result.dir}\n${result.files.map((file) => ` ${file}\n`).join("")}`
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
/** The verdict for people: the status, each diagnostic, the screenshots worth looking at. */
|
|
2287
|
+
function verdictText(verdict) {
|
|
2288
|
+
const lines = [`verify: ${verdict.status}`];
|
|
2289
|
+
for (const diagnostic of verdict.diagnostics) {
|
|
2290
|
+
const where = diagnostic.location && "file" in diagnostic.location ? ` ${diagnostic.location.file}:${diagnostic.location.line}:${diagnostic.location.column}` : "";
|
|
2291
|
+
const count = diagnostic.count !== void 0 && diagnostic.count > 1 ? ` (×${diagnostic.count})` : "";
|
|
2292
|
+
lines.push(` ${diagnostic.severity} ${diagnostic.code}${where}${count}: ${diagnostic.message}`);
|
|
2293
|
+
if (diagnostic.hint) lines.push(` ${diagnostic.hint}`);
|
|
2294
|
+
}
|
|
2295
|
+
for (const shot of verdict.screenshots.filter((screenshot) => screenshot.returned)) lines.push(` screenshot ${shot.viewport}: ${shot.path}`);
|
|
2296
|
+
return `${lines.join("\n")}\n`;
|
|
2297
|
+
}
|
|
2298
|
+
/** `mostra verify <path>`. */
|
|
2299
|
+
const verifyCommand = defineCommand({
|
|
2300
|
+
path: ["verify"],
|
|
2301
|
+
summary: "Build, render and check an artifact's source; one verdict, nothing published.",
|
|
2302
|
+
args: [{
|
|
2303
|
+
name: "path",
|
|
2304
|
+
summary: "The artifact's source: a file, a directory, or its ID."
|
|
2305
|
+
}],
|
|
2306
|
+
flags: [{
|
|
2307
|
+
name: "screenshots",
|
|
2308
|
+
kind: "string",
|
|
2309
|
+
value: "<when>",
|
|
2310
|
+
choices: [
|
|
2311
|
+
"on-failure",
|
|
2312
|
+
"always",
|
|
2313
|
+
"never"
|
|
2314
|
+
],
|
|
2315
|
+
default: "on-failure",
|
|
2316
|
+
summary: "Which screenshots the verdict marks as worth looking at."
|
|
2317
|
+
}],
|
|
2318
|
+
output: {
|
|
2319
|
+
type: "VerifyVerdict",
|
|
2320
|
+
topic: "cli"
|
|
2321
|
+
},
|
|
2322
|
+
exits: [EXIT.failure],
|
|
2323
|
+
example: "verify ./sidebar-review",
|
|
2324
|
+
async run(context) {
|
|
2325
|
+
const connection = await context.connection();
|
|
2326
|
+
const progressToken = randomUUID();
|
|
2327
|
+
connection?.onNotification(ARTIFACT_PROGRESS, (params) => {
|
|
2328
|
+
const { progressToken: token, stage } = params ?? {};
|
|
2329
|
+
if (token === progressToken && typeof stage === "string") context.log(`verify: ${stage}`);
|
|
2330
|
+
});
|
|
2331
|
+
const screenshots = flag(context, "screenshots");
|
|
2332
|
+
const verdict = await context.call(artifactVerifyMethod, {
|
|
2333
|
+
source: sourceRef(arg(context, "path"), context.cwd),
|
|
2334
|
+
progressToken,
|
|
2335
|
+
...screenshots !== void 0 ? { screenshots } : {}
|
|
2336
|
+
});
|
|
2337
|
+
context.record({
|
|
2338
|
+
event: "verify.result",
|
|
2339
|
+
fields: {
|
|
2340
|
+
status: verdict.status,
|
|
2341
|
+
codes: verdict.diagnostics.map((diagnostic) => diagnostic.code)
|
|
2342
|
+
}
|
|
2343
|
+
});
|
|
2344
|
+
return {
|
|
2345
|
+
result: verdict,
|
|
2346
|
+
human: verdictText(verdict),
|
|
2347
|
+
...verdict.status === "fail" ? { exit: EXIT.failure } : {}
|
|
2348
|
+
};
|
|
2349
|
+
}
|
|
2350
|
+
});
|
|
2351
|
+
const POLICIES = {
|
|
2352
|
+
once: {
|
|
2353
|
+
policy: "once",
|
|
2354
|
+
scope: "per-artifact"
|
|
2355
|
+
},
|
|
2356
|
+
"once-per-viewer": {
|
|
2357
|
+
policy: "once",
|
|
2358
|
+
scope: "per-viewer"
|
|
2359
|
+
},
|
|
2360
|
+
epochs: { policy: "epochs" },
|
|
2361
|
+
none: { policy: "none" }
|
|
2362
|
+
};
|
|
2363
|
+
/** `mostra publish <path>`. */
|
|
2364
|
+
const publishCommand = defineCommand({
|
|
2365
|
+
path: ["publish"],
|
|
2366
|
+
summary: "Publish an artifact's source as a new immutable version, gated by verify, and print its link.",
|
|
2367
|
+
args: [{
|
|
2368
|
+
name: "path",
|
|
2369
|
+
summary: "The artifact's source: a file, a directory, or its ID."
|
|
2370
|
+
}],
|
|
2371
|
+
flags: [
|
|
2372
|
+
{
|
|
2373
|
+
name: "force",
|
|
2374
|
+
kind: "boolean",
|
|
2375
|
+
summary: "Publish despite verify's errors and record their codes; never what didn't build."
|
|
2376
|
+
},
|
|
2377
|
+
{
|
|
2378
|
+
name: "policy",
|
|
2379
|
+
kind: "string",
|
|
2380
|
+
value: "<policy>",
|
|
2381
|
+
choices: Object.keys(POLICIES),
|
|
2382
|
+
summary: "The submission policy to use, instead of the manifest's."
|
|
2383
|
+
},
|
|
2384
|
+
{
|
|
2385
|
+
name: "request-id",
|
|
2386
|
+
kind: "string",
|
|
2387
|
+
value: "<id>",
|
|
2388
|
+
summary: "Make a retry return the first attempt's receipt."
|
|
2389
|
+
}
|
|
2390
|
+
],
|
|
2391
|
+
output: {
|
|
2392
|
+
type: "PublishReceipt",
|
|
2393
|
+
topic: "cli"
|
|
2394
|
+
},
|
|
2395
|
+
exits: [EXIT.failure, EXIT.conflict],
|
|
2396
|
+
example: "publish ./sidebar-review",
|
|
2397
|
+
async run(context) {
|
|
2398
|
+
const policyName = flag(context, "policy");
|
|
2399
|
+
const requestId = flag(context, "request-id");
|
|
2400
|
+
const force = context.flags["force"] === true;
|
|
2401
|
+
const receipt = await context.call(artifactPublishMethod, {
|
|
2402
|
+
source: sourceRef(arg(context, "path"), context.cwd),
|
|
2403
|
+
...force ? { force: true } : {},
|
|
2404
|
+
...policyName !== void 0 ? { policy: POLICIES[policyName] } : {},
|
|
2405
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
2406
|
+
});
|
|
2407
|
+
context.record({
|
|
2408
|
+
event: "publish.done",
|
|
2409
|
+
fields: {
|
|
2410
|
+
reused: receipt.reused,
|
|
2411
|
+
forced: force
|
|
2412
|
+
}
|
|
2413
|
+
});
|
|
2414
|
+
const forced = receipt.forcedDiagnostics !== void 0 && receipt.forcedDiagnostics.length > 0 ? `Forced past: ${receipt.forcedDiagnostics.join(", ")}\n` : "";
|
|
2415
|
+
return {
|
|
2416
|
+
result: receipt,
|
|
2417
|
+
human: `${receipt.reused ? "Already published" : "Published"} version ${receipt.version} (${receipt.versionId}) of ${receipt.artifactId}\n${forced}${receipt.url ?? ""}\n`
|
|
2418
|
+
};
|
|
2419
|
+
}
|
|
2420
|
+
});
|
|
2421
|
+
/** `mostra add <package…>`. */
|
|
2422
|
+
const addCommand = defineCommand({
|
|
2423
|
+
path: ["add"],
|
|
2424
|
+
summary: "Add npm packages to an artifact's lockfile at exact versions, installing them into the home's store.",
|
|
2425
|
+
args: [{
|
|
2426
|
+
name: "package",
|
|
2427
|
+
summary: "npm specs: name, name@version, name@range or name@tag.",
|
|
2428
|
+
variadic: true
|
|
2429
|
+
}],
|
|
2430
|
+
flags: [{
|
|
2431
|
+
name: "artifact",
|
|
2432
|
+
kind: "string",
|
|
2433
|
+
value: "<path|id>",
|
|
2434
|
+
summary: "The artifact. Default: the one in the current directory."
|
|
2435
|
+
}, {
|
|
2436
|
+
name: "request-id",
|
|
2437
|
+
kind: "string",
|
|
2438
|
+
value: "<id>",
|
|
2439
|
+
summary: "Make a retry return the first attempt's result."
|
|
2440
|
+
}],
|
|
2441
|
+
output: {
|
|
2442
|
+
type: "DepsAddResult",
|
|
2443
|
+
topic: "cli"
|
|
2444
|
+
},
|
|
2445
|
+
exits: [EXIT.failure],
|
|
2446
|
+
example: "add recharts@3 --artifact ./dashboard",
|
|
2447
|
+
async run(context) {
|
|
2448
|
+
const packages = context.args["package"];
|
|
2449
|
+
if (!Array.isArray(packages) || packages.length === 0) throw usage("\"mostra add\" needs <package…>.");
|
|
2450
|
+
const requestId = flag(context, "request-id");
|
|
2451
|
+
const result = await context.call(depsAddMethod, {
|
|
2452
|
+
artifact: sourceRef(flag(context, "artifact") ?? ".", context.cwd),
|
|
2453
|
+
packages,
|
|
2454
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
2455
|
+
});
|
|
2456
|
+
return {
|
|
2457
|
+
result,
|
|
2458
|
+
human: result.packages.map((added) => `${added.name}@${added.version}${added.warm ? " (warm set)" : ""}\n`).join("")
|
|
2459
|
+
};
|
|
2460
|
+
}
|
|
2461
|
+
});
|
|
2462
|
+
//#endregion
|
|
2463
|
+
//#region src/commands/dev.ts
|
|
2464
|
+
/**
|
|
2465
|
+
* `mostra dev` (VR-08, D140 item 12): a development preview of unpublished source with hot
|
|
2466
|
+
* reload, until Ctrl-C. It prints the preview's link and then what happens in the session (built,
|
|
2467
|
+
* reloaded, server restarted, submissions with their Results, diagnostics) as a stream: NDJSON
|
|
2468
|
+
* envelopes with `--json`, lines otherwise. Viewers never reach a preview; it isn't a version.
|
|
2469
|
+
*/
|
|
2470
|
+
/** One event's line without `--json`. */
|
|
2471
|
+
function line(event) {
|
|
2472
|
+
const files = event.files && event.files.length > 0 ? `: ${event.files.join(", ")}` : "";
|
|
2473
|
+
switch (event.kind) {
|
|
2474
|
+
case "built": return `Updated${files}\n`;
|
|
2475
|
+
case "reloaded": return `Reloaded${files}\n`;
|
|
2476
|
+
case "server-restarted": return `Server code restarted${files}\n`;
|
|
2477
|
+
case "submission": return `Submission recorded (not delivered):\n${JSON.stringify(event.results, null, 2)}\n`;
|
|
2478
|
+
case "diagnostics": return `${(event.diagnostics ?? []).map((diagnostic) => `${diagnostic.code}: ${diagnostic.message}`).join("\n")}\n`;
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
const devCommand = defineCommand({
|
|
2482
|
+
path: ["dev"],
|
|
2483
|
+
summary: "Preview an artifact's unpublished source with hot reload, until Ctrl-C; viewers never see it.",
|
|
2484
|
+
args: [{
|
|
2485
|
+
name: "path",
|
|
2486
|
+
summary: "The artifact's source: a file, a directory, or its ID."
|
|
2487
|
+
}],
|
|
2488
|
+
flags: [{
|
|
2489
|
+
name: "port",
|
|
2490
|
+
kind: "number",
|
|
2491
|
+
value: "<port>",
|
|
2492
|
+
summary: "The preview's port on 127.0.0.1; default one the system picks."
|
|
2493
|
+
}],
|
|
2494
|
+
output: {
|
|
2495
|
+
type: "DevStartResult",
|
|
2496
|
+
topic: "cli"
|
|
2497
|
+
},
|
|
2498
|
+
exits: [EXIT.failure, EXIT.environment],
|
|
2499
|
+
example: "dev ./sidebar-review",
|
|
2500
|
+
streams: true,
|
|
2501
|
+
async run(context) {
|
|
2502
|
+
const connection = await context.connection();
|
|
2503
|
+
let sessionId;
|
|
2504
|
+
const early = [];
|
|
2505
|
+
const show = (event) => {
|
|
2506
|
+
if (event.kind === "diagnostics") for (const diagnostic of event.diagnostics ?? []) context.warn(diagnostic);
|
|
2507
|
+
context.emit(event, line(event));
|
|
2508
|
+
};
|
|
2509
|
+
connection?.onNotification(DEV_NOTIFY, (params) => {
|
|
2510
|
+
const event = params;
|
|
2511
|
+
if (sessionId === void 0) early.push(event);
|
|
2512
|
+
else if (event.sessionId === sessionId) show(event);
|
|
2513
|
+
});
|
|
2514
|
+
const portFlag = context.flags["port"];
|
|
2515
|
+
const started = await context.call(devStartMethod, {
|
|
2516
|
+
source: sourceRef(arg(context, "path"), context.cwd),
|
|
2517
|
+
...typeof portFlag === "number" ? { port: portFlag } : {}
|
|
2518
|
+
});
|
|
2519
|
+
sessionId = started.sessionId;
|
|
2520
|
+
context.emit({
|
|
2521
|
+
kind: "started",
|
|
2522
|
+
...started
|
|
2523
|
+
}, `Development preview, not published: ${started.url}\nPress Ctrl-C to stop.\n`);
|
|
2524
|
+
for (const event of early.splice(0)) if (event.sessionId === sessionId) show(event);
|
|
2525
|
+
let stop = () => {};
|
|
2526
|
+
const ended = await new Promise((done) => {
|
|
2527
|
+
stop = () => done("signal");
|
|
2528
|
+
process.once("SIGINT", stop);
|
|
2529
|
+
process.once("SIGTERM", stop);
|
|
2530
|
+
connection?.closed.then(() => done("closed"));
|
|
2531
|
+
});
|
|
2532
|
+
process.off("SIGINT", stop);
|
|
2533
|
+
process.off("SIGTERM", stop);
|
|
2534
|
+
if (ended === "closed") throw new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", {
|
|
2535
|
+
message: "The instance ended the development preview (it stopped or restarted).",
|
|
2536
|
+
hint: "Run mostra dev again."
|
|
2537
|
+
}));
|
|
2538
|
+
await context.call(devStopMethod, { sessionId }).catch(() => void 0);
|
|
2539
|
+
return {
|
|
2540
|
+
result: {
|
|
2541
|
+
sessionId,
|
|
2542
|
+
stopped: "signal"
|
|
2543
|
+
},
|
|
2544
|
+
human: "Stopped.\n"
|
|
2545
|
+
};
|
|
2546
|
+
}
|
|
2547
|
+
});
|
|
2548
|
+
//#endregion
|
|
2549
|
+
//#region src/commands/interaction.ts
|
|
2550
|
+
/**
|
|
2551
|
+
* Interaction: `wait`, `results`, `push` and `state get|set|patch`, thin clients of `wait`,
|
|
2552
|
+
* `results/get`, `live/push` and `state/*` (INTERFACES §6, D101). `wait` ends with exit 5 when it
|
|
2553
|
+
* times out (`still_waiting`), and a write that meets a conflict with exit 6.
|
|
2554
|
+
*/
|
|
2555
|
+
/** The longest one `wait` request may ask for (INTERFACES §6: 2^31 − 1 ms). */
|
|
2556
|
+
const MAX_WAIT_MS = 2 ** 31 - 1;
|
|
2557
|
+
/** `mostra wait <artifact>`. */
|
|
2558
|
+
const waitCommand = defineCommand({
|
|
2559
|
+
path: ["wait"],
|
|
2560
|
+
summary: "Block until an artifact's submission, signal, state change or event; still_waiting on timeout.",
|
|
2561
|
+
args: [{
|
|
2562
|
+
name: "artifact",
|
|
2563
|
+
summary: "The artifact: its source path or its ID."
|
|
2564
|
+
}],
|
|
2565
|
+
flags: [
|
|
2566
|
+
{
|
|
2567
|
+
name: "until",
|
|
2568
|
+
kind: "string",
|
|
2569
|
+
value: "<condition>",
|
|
2570
|
+
summary: "submitted, signal:<name>, state:<fieldId> or event:<type>."
|
|
2571
|
+
},
|
|
2572
|
+
{
|
|
2573
|
+
name: "after",
|
|
2574
|
+
kind: "string",
|
|
2575
|
+
value: "<cursor>",
|
|
2576
|
+
summary: "Wait for what comes after this cursor. Default: the latest publish."
|
|
2577
|
+
},
|
|
2578
|
+
{
|
|
2579
|
+
name: "timeout",
|
|
2580
|
+
kind: "duration",
|
|
2581
|
+
value: "<d>",
|
|
2582
|
+
summary: "Stop waiting after this long, with still_waiting and exit 5. Default: no limit."
|
|
2583
|
+
}
|
|
2584
|
+
],
|
|
2585
|
+
output: {
|
|
2586
|
+
type: "WaitResult",
|
|
2587
|
+
topic: "cli"
|
|
2588
|
+
},
|
|
2589
|
+
exits: [EXIT.failure, EXIT.stillWaiting],
|
|
2590
|
+
example: "wait ./sidebar-review --until submitted --timeout 30m",
|
|
2591
|
+
async run(context) {
|
|
2592
|
+
const until = flag(context, "until");
|
|
2593
|
+
if (until === void 0) throw usage("\"mostra wait\" needs --until, such as --until submitted.");
|
|
2594
|
+
const timeout = flag(context, "timeout");
|
|
2595
|
+
const deadline = timeout === void 0 ? void 0 : Date.now() + durationMs(timeout);
|
|
2596
|
+
const artifact = artifactRef(arg(context, "artifact"), context.cwd);
|
|
2597
|
+
let after = flag(context, "after");
|
|
2598
|
+
for (;;) {
|
|
2599
|
+
const remaining = deadline === void 0 ? MAX_WAIT_MS : Math.max(0, deadline - Date.now());
|
|
2600
|
+
const result = await context.call(waitMethod, {
|
|
2601
|
+
artifact,
|
|
2602
|
+
until,
|
|
2603
|
+
timeoutMs: Math.min(remaining, MAX_WAIT_MS),
|
|
2604
|
+
...after !== void 0 ? { after } : {}
|
|
2605
|
+
});
|
|
2606
|
+
if (result.status === "satisfied") return {
|
|
2607
|
+
result,
|
|
2608
|
+
human: `${result.event.type} at cursor ${result.cursor}\n`
|
|
2609
|
+
};
|
|
2610
|
+
if (deadline === void 0) {
|
|
2611
|
+
after = result.cursor;
|
|
2612
|
+
continue;
|
|
2613
|
+
}
|
|
2614
|
+
return {
|
|
2615
|
+
result,
|
|
2616
|
+
human: `still waiting; resume with --after ${result.cursor}\n`,
|
|
2617
|
+
exit: EXIT.stillWaiting
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
});
|
|
2622
|
+
/** `mostra results <artifact>`. */
|
|
2623
|
+
const resultsCommand = defineCommand({
|
|
2624
|
+
path: ["results"],
|
|
2625
|
+
summary: "Print an artifact's submission in full: answers, state, choices, comments, actions and signals.",
|
|
2626
|
+
args: [{
|
|
2627
|
+
name: "artifact",
|
|
2628
|
+
summary: "The artifact: its source path or its ID."
|
|
2629
|
+
}],
|
|
2630
|
+
flags: [{
|
|
2631
|
+
name: "submission",
|
|
2632
|
+
kind: "string",
|
|
2633
|
+
value: "<id>",
|
|
2634
|
+
summary: "This submission instead of the latest."
|
|
2635
|
+
}, {
|
|
2636
|
+
name: "format",
|
|
2637
|
+
kind: "string",
|
|
2638
|
+
value: "<format>",
|
|
2639
|
+
choices: ["md", "json"],
|
|
2640
|
+
summary: "Markdown or JSON. Default: Markdown, or JSON with --json."
|
|
2641
|
+
}],
|
|
2642
|
+
output: {
|
|
2643
|
+
type: "Results",
|
|
2644
|
+
topic: "cli"
|
|
2645
|
+
},
|
|
2646
|
+
exits: [EXIT.failure],
|
|
2647
|
+
example: "results ./sidebar-review",
|
|
2648
|
+
async run(context) {
|
|
2649
|
+
const format = flag(context, "format") ?? (context.json ? "json" : "md");
|
|
2650
|
+
const submission = flag(context, "submission");
|
|
2651
|
+
const result = await context.call(resultsGetMethod, {
|
|
2652
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
2653
|
+
format,
|
|
2654
|
+
...submission !== void 0 ? { submission } : {}
|
|
2655
|
+
});
|
|
2656
|
+
const markdown = typeof result === "object" && result !== null && "markdown" in result ? result.markdown : void 0;
|
|
2657
|
+
return {
|
|
2658
|
+
result,
|
|
2659
|
+
human: markdown !== void 0 ? `${markdown.trimEnd()}\n` : pretty(result)
|
|
2660
|
+
};
|
|
2661
|
+
}
|
|
2662
|
+
});
|
|
2663
|
+
/** `mostra push <artifact> <key>`. */
|
|
2664
|
+
const pushCommand = defineCommand({
|
|
2665
|
+
path: ["push"],
|
|
2666
|
+
summary: "Push a value into an artifact's live source; pages showing it update at once.",
|
|
2667
|
+
args: [{
|
|
2668
|
+
name: "artifact",
|
|
2669
|
+
summary: "The artifact: its source path or its ID."
|
|
2670
|
+
}, {
|
|
2671
|
+
name: "key",
|
|
2672
|
+
summary: "The live source's key: its export name in live.ts, or a new one."
|
|
2673
|
+
}],
|
|
2674
|
+
flags: [
|
|
2675
|
+
{
|
|
2676
|
+
name: "value",
|
|
2677
|
+
kind: "string",
|
|
2678
|
+
value: "<json>",
|
|
2679
|
+
summary: "The value, as JSON."
|
|
2680
|
+
},
|
|
2681
|
+
{
|
|
2682
|
+
name: "file",
|
|
2683
|
+
kind: "string",
|
|
2684
|
+
value: "<path>",
|
|
2685
|
+
summary: "Read the value from a JSON file."
|
|
2686
|
+
},
|
|
2687
|
+
{
|
|
2688
|
+
name: "stdin",
|
|
2689
|
+
kind: "boolean",
|
|
2690
|
+
summary: "Read the value from stdin."
|
|
2691
|
+
},
|
|
2692
|
+
{
|
|
2693
|
+
name: "append",
|
|
2694
|
+
kind: "boolean",
|
|
2695
|
+
summary: "Add the value to the source's ring instead of replacing it."
|
|
2696
|
+
},
|
|
2697
|
+
{
|
|
2698
|
+
name: "request-id",
|
|
2699
|
+
kind: "string",
|
|
2700
|
+
value: "<id>",
|
|
2701
|
+
summary: "Make a retry return the first attempt's revision."
|
|
2702
|
+
}
|
|
2703
|
+
],
|
|
2704
|
+
output: {
|
|
2705
|
+
type: "LivePushResult",
|
|
2706
|
+
topic: "cli"
|
|
2707
|
+
},
|
|
2708
|
+
exits: [EXIT.failure],
|
|
2709
|
+
example: `push ./dashboard prices --value '{"btc": 64000}'`,
|
|
2710
|
+
readsStdin: (flags) => flags["stdin"] === true,
|
|
2711
|
+
async run(context) {
|
|
2712
|
+
const value = await jsonInput(context);
|
|
2713
|
+
const requestId = flag(context, "request-id");
|
|
2714
|
+
const result = await context.call(livePushMethod, {
|
|
2715
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
2716
|
+
key: arg(context, "key"),
|
|
2717
|
+
value,
|
|
2718
|
+
...context.flags["append"] === true ? { mode: "append" } : {},
|
|
2719
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
2720
|
+
});
|
|
2721
|
+
return {
|
|
2722
|
+
result,
|
|
2723
|
+
human: `revision ${result.revision}\n`
|
|
2724
|
+
};
|
|
2725
|
+
}
|
|
2726
|
+
});
|
|
2727
|
+
const writeFlags = [
|
|
2728
|
+
{
|
|
2729
|
+
name: "base",
|
|
2730
|
+
kind: "string",
|
|
2731
|
+
value: "<revision>",
|
|
2732
|
+
summary: "Write only if the fields are unchanged since this revision; else a conflict (exit 6)."
|
|
2733
|
+
},
|
|
2734
|
+
{
|
|
2735
|
+
name: "as",
|
|
2736
|
+
kind: "string",
|
|
2737
|
+
value: "<actor>",
|
|
2738
|
+
choices: ["agent", "host"],
|
|
2739
|
+
default: "agent",
|
|
2740
|
+
summary: "Who the change is attributed to."
|
|
2741
|
+
},
|
|
2742
|
+
{
|
|
2743
|
+
name: "mutation-id",
|
|
2744
|
+
kind: "string",
|
|
2745
|
+
value: "<mut_…>",
|
|
2746
|
+
summary: "Make a retry return the first attempt's result."
|
|
2747
|
+
}
|
|
2748
|
+
];
|
|
2749
|
+
/** `mostra state get <artifact> [ids…]`. */
|
|
2750
|
+
const stateGetCommand = defineCommand({
|
|
2751
|
+
path: ["state", "get"],
|
|
2752
|
+
summary: "Read an artifact's state: the fields every viewer shares, with their revisions.",
|
|
2753
|
+
args: [{
|
|
2754
|
+
name: "artifact",
|
|
2755
|
+
summary: "The artifact: its source path or its ID."
|
|
2756
|
+
}, {
|
|
2757
|
+
name: "id",
|
|
2758
|
+
summary: "Only these field IDs.",
|
|
2759
|
+
optional: true,
|
|
2760
|
+
variadic: true
|
|
2761
|
+
}],
|
|
2762
|
+
output: {
|
|
2763
|
+
type: "StateGetResult",
|
|
2764
|
+
topic: "cli"
|
|
2765
|
+
},
|
|
2766
|
+
exits: [EXIT.failure],
|
|
2767
|
+
example: "state get ./tasks priorities",
|
|
2768
|
+
async run(context) {
|
|
2769
|
+
const ids = context.args["id"];
|
|
2770
|
+
const result = await context.call(stateGetMethod, {
|
|
2771
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
2772
|
+
...ids !== void 0 && ids.length > 0 ? { ids: [...ids] } : {}
|
|
2773
|
+
});
|
|
2774
|
+
return {
|
|
2775
|
+
result,
|
|
2776
|
+
human: pretty(result)
|
|
2777
|
+
};
|
|
2778
|
+
}
|
|
2779
|
+
});
|
|
2780
|
+
/** Sends one state mutation; a conflict ends with exit 6. */
|
|
2781
|
+
async function patch(context, ops) {
|
|
2782
|
+
const base = flag(context, "base");
|
|
2783
|
+
const as = flag(context, "as") ?? "agent";
|
|
2784
|
+
const result = await context.call(statePatchMethod, {
|
|
2785
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
2786
|
+
ops,
|
|
2787
|
+
mutationId: flag(context, "mutation-id") ?? MutationId.create(),
|
|
2788
|
+
as,
|
|
2789
|
+
...base !== void 0 ? { baseRevision: base } : {}
|
|
2790
|
+
});
|
|
2791
|
+
const conflicted = result.conflicts.length > 0;
|
|
2792
|
+
return {
|
|
2793
|
+
result,
|
|
2794
|
+
human: conflicted ? `conflict on ${result.conflicts.map((conflict) => conflict.id).join(", ")}; revision ${result.revision}\n` : `revision ${result.revision}\n`,
|
|
2795
|
+
...conflicted ? { exit: EXIT.conflict } : {}
|
|
2796
|
+
};
|
|
2797
|
+
}
|
|
2798
|
+
/** `mostra state set <artifact> <id> <json>`. */
|
|
2799
|
+
const stateSetCommand = defineCommand({
|
|
2800
|
+
path: ["state", "set"],
|
|
2801
|
+
summary: "Set one field of an artifact's state; viewers see it at once.",
|
|
2802
|
+
args: [
|
|
2803
|
+
{
|
|
2804
|
+
name: "artifact",
|
|
2805
|
+
summary: "The artifact: its source path or its ID."
|
|
2806
|
+
},
|
|
2807
|
+
{
|
|
2808
|
+
name: "id",
|
|
2809
|
+
summary: "The field ID."
|
|
2810
|
+
},
|
|
2811
|
+
{
|
|
2812
|
+
name: "value",
|
|
2813
|
+
summary: "The value, as JSON."
|
|
2814
|
+
}
|
|
2815
|
+
],
|
|
2816
|
+
flags: writeFlags,
|
|
2817
|
+
output: {
|
|
2818
|
+
type: "StatePatchResult",
|
|
2819
|
+
topic: "cli"
|
|
2820
|
+
},
|
|
2821
|
+
exits: [EXIT.failure, EXIT.conflict],
|
|
2822
|
+
example: `state set ./tasks priorities '["Ship the CLI"]'`,
|
|
2823
|
+
async run(context) {
|
|
2824
|
+
return patch(context, [{
|
|
2825
|
+
id: arg(context, "id"),
|
|
2826
|
+
value: parseJson(arg(context, "value"), "<value>")
|
|
2827
|
+
}]);
|
|
2828
|
+
}
|
|
2829
|
+
});
|
|
2830
|
+
/** `mostra state patch <artifact>`. */
|
|
2831
|
+
const statePatchCommand = defineCommand({
|
|
2832
|
+
path: ["state", "patch"],
|
|
2833
|
+
summary: "Change several fields of an artifact's state in one revision: [{ id, value } | { id, delete: true }].",
|
|
2834
|
+
args: [{
|
|
2835
|
+
name: "artifact",
|
|
2836
|
+
summary: "The artifact: its source path or its ID."
|
|
2837
|
+
}],
|
|
2838
|
+
flags: [
|
|
2839
|
+
{
|
|
2840
|
+
name: "ops",
|
|
2841
|
+
kind: "string",
|
|
2842
|
+
value: "<json>",
|
|
2843
|
+
summary: "The operations, as JSON."
|
|
2844
|
+
},
|
|
2845
|
+
{
|
|
2846
|
+
name: "file",
|
|
2847
|
+
kind: "string",
|
|
2848
|
+
value: "<path>",
|
|
2849
|
+
summary: "Read the operations from a JSON file."
|
|
2850
|
+
},
|
|
2851
|
+
{
|
|
2852
|
+
name: "stdin",
|
|
2853
|
+
kind: "boolean",
|
|
2854
|
+
summary: "Read the operations from stdin."
|
|
2855
|
+
},
|
|
2856
|
+
...writeFlags
|
|
2857
|
+
],
|
|
2858
|
+
output: {
|
|
2859
|
+
type: "StatePatchResult",
|
|
2860
|
+
topic: "cli"
|
|
2861
|
+
},
|
|
2862
|
+
exits: [EXIT.failure, EXIT.conflict],
|
|
2863
|
+
example: `state patch ./tasks --file ops.json`,
|
|
2864
|
+
readsStdin: (flags) => flags["stdin"] === true,
|
|
2865
|
+
async run(context) {
|
|
2866
|
+
const ops = await jsonInput(context, "ops");
|
|
2867
|
+
const parsed = FieldOp.array().safeParse(ops);
|
|
2868
|
+
if (!parsed.success || parsed.data.length === 0) throw usage("The operations must be a non-empty JSON array of { id, value } or { id, delete: true }.");
|
|
2869
|
+
return patch(context, parsed.data);
|
|
2870
|
+
}
|
|
2871
|
+
});
|
|
2872
|
+
//#endregion
|
|
2873
|
+
//#region src/browser.ts
|
|
2874
|
+
/**
|
|
2875
|
+
* Opening a browser on a launch URL without putting it on a command line (D140 item 15, Opus
|
|
2876
|
+
* review finding 4). A program's arguments are readable by every local user (`ps`,
|
|
2877
|
+
* `/proc/<pid>/cmdline`), so the opener gets the path of a page that redirects to the launch URL,
|
|
2878
|
+
* written mode 0600 in a fresh directory of mode 0700, as Jupyter's server does for its token.
|
|
2879
|
+
* The launch ticket in the page is single-use and lives a minute, so a page left behind is
|
|
2880
|
+
* harmless; each launch removes the ones older than that.
|
|
2881
|
+
*/
|
|
2882
|
+
/** The prefix of the directories launch pages live in. */
|
|
2883
|
+
const PREFIX = "mostra-open-";
|
|
2884
|
+
/** A launch page older than this holds a ticket that expired: it goes on the next launch. */
|
|
2885
|
+
const STALE_MS = 12e4;
|
|
2886
|
+
/** Escapes text for an HTML attribute or element. */
|
|
2887
|
+
const html = (text) => text.replaceAll("&", "&").replaceAll("\"", """).replaceAll("<", "<").replaceAll(">", ">");
|
|
2888
|
+
/** The page that sends the browser on to `url`. */
|
|
2889
|
+
function launchPage(url) {
|
|
2890
|
+
const script = JSON.stringify(url).replaceAll("<", "\\u003c");
|
|
2891
|
+
return `<!DOCTYPE html>
|
|
2892
|
+
<html lang="en">
|
|
2893
|
+
<head>
|
|
2894
|
+
<meta charset="utf-8">
|
|
2895
|
+
<meta name="referrer" content="no-referrer">
|
|
2896
|
+
<meta http-equiv="refresh" content="0;url=${html(url)}">
|
|
2897
|
+
<title>Opening Mostra</title>
|
|
2898
|
+
<script>location.replace(${script});<\/script>
|
|
2899
|
+
</head>
|
|
2900
|
+
<body>
|
|
2901
|
+
<p>Opening the artifact. If nothing happens, <a href="${html(url)}">open it here</a>.</p>
|
|
2902
|
+
</body>
|
|
2903
|
+
</html>
|
|
2904
|
+
`;
|
|
2905
|
+
}
|
|
2906
|
+
/** Removes launch pages of earlier launches that are this user's and older than a minute or two. */
|
|
2907
|
+
function sweep(base, now) {
|
|
2908
|
+
const uid = process.getuid?.();
|
|
2909
|
+
let names;
|
|
2910
|
+
try {
|
|
2911
|
+
names = readdirSync(base);
|
|
2912
|
+
} catch {
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
for (const name of names) {
|
|
2916
|
+
if (!name.startsWith(PREFIX)) continue;
|
|
2917
|
+
const dir = join(base, name);
|
|
2918
|
+
try {
|
|
2919
|
+
const stats = lstatSync(dir);
|
|
2920
|
+
if (!stats.isDirectory() || uid !== void 0 && stats.uid !== uid) continue;
|
|
2921
|
+
if (now - stats.mtimeMs < STALE_MS) continue;
|
|
2922
|
+
rmSync(dir, {
|
|
2923
|
+
recursive: true,
|
|
2924
|
+
force: true
|
|
2925
|
+
});
|
|
2926
|
+
} catch {}
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
/** Writes the launch page for `url`; its path. */
|
|
2930
|
+
function writeLaunchPage(url, base = tmpdir(), now = Date.now()) {
|
|
2931
|
+
sweep(base, now);
|
|
2932
|
+
const dir = mkdtempSync(join(base, PREFIX));
|
|
2933
|
+
const file = join(dir, "open.html");
|
|
2934
|
+
writeFileSync(file, launchPage(url), {
|
|
2935
|
+
mode: 384,
|
|
2936
|
+
flag: "wx"
|
|
2937
|
+
});
|
|
2938
|
+
return file;
|
|
2939
|
+
}
|
|
2940
|
+
/**
|
|
2941
|
+
* Splits a command the way a shell would split plain words and quotes, without running one:
|
|
2942
|
+
* `firefox --new-window %s` or `"/Applications/My Browser" %s`.
|
|
2943
|
+
*/
|
|
2944
|
+
function splitCommand(command) {
|
|
2945
|
+
const words = [];
|
|
2946
|
+
let word = "";
|
|
2947
|
+
let inWord = false;
|
|
2948
|
+
let quote;
|
|
2949
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
2950
|
+
const char = command[index];
|
|
2951
|
+
if (quote) {
|
|
2952
|
+
if (char === quote) quote = void 0;
|
|
2953
|
+
else if (char === "\\" && quote === "\"" && index + 1 < command.length) {
|
|
2954
|
+
index += 1;
|
|
2955
|
+
word += command[index];
|
|
2956
|
+
} else word += char;
|
|
2957
|
+
continue;
|
|
2958
|
+
}
|
|
2959
|
+
if (char === "\"" || char === "'") {
|
|
2960
|
+
quote = char;
|
|
2961
|
+
inWord = true;
|
|
2962
|
+
} else if (char === "\\" && index + 1 < command.length) {
|
|
2963
|
+
index += 1;
|
|
2964
|
+
word += command[index];
|
|
2965
|
+
inWord = true;
|
|
2966
|
+
} else if (/\s/.test(char)) {
|
|
2967
|
+
if (inWord) words.push(word);
|
|
2968
|
+
word = "";
|
|
2969
|
+
inWord = false;
|
|
2970
|
+
} else {
|
|
2971
|
+
word += char;
|
|
2972
|
+
inWord = true;
|
|
2973
|
+
}
|
|
2974
|
+
}
|
|
2975
|
+
if (inWord) words.push(word);
|
|
2976
|
+
return words;
|
|
2977
|
+
}
|
|
2978
|
+
/**
|
|
2979
|
+
* The commands that may open `target`, in order: each entry of `$BROWSER` (separated by `:`, its
|
|
2980
|
+
* `%s` replaced by the target, else the target appended, `%%` a literal `%`, as `xdg-open` and
|
|
2981
|
+
* Python's `webbrowser` read it), else the platform's opener.
|
|
2982
|
+
*/
|
|
2983
|
+
function browserCommands(target, env, platform = process.platform) {
|
|
2984
|
+
const chosen = env["BROWSER"];
|
|
2985
|
+
const entries = chosen !== void 0 && chosen.trim() !== "" ? chosen.split(":").filter((entry) => entry.trim() !== "") : [platform === "darwin" ? "open" : "xdg-open"];
|
|
2986
|
+
const commands = [];
|
|
2987
|
+
for (const entry of entries) {
|
|
2988
|
+
const words = splitCommand(entry);
|
|
2989
|
+
if (words.length === 0) continue;
|
|
2990
|
+
let substituted = false;
|
|
2991
|
+
const argv = words.map((word) => word.replaceAll(/%%|%s/g, (match) => {
|
|
2992
|
+
if (match === "%%") return "%";
|
|
2993
|
+
substituted = true;
|
|
2994
|
+
return target;
|
|
2995
|
+
}));
|
|
2996
|
+
commands.push(substituted ? argv : [...argv, target]);
|
|
2997
|
+
}
|
|
2998
|
+
return commands;
|
|
2999
|
+
}
|
|
3000
|
+
/** Starts one command, detached; whether it started (its program was found and could run). */
|
|
3001
|
+
function start(argv) {
|
|
3002
|
+
const [program, ...args] = argv;
|
|
3003
|
+
if (program === void 0) return Promise.resolve(false);
|
|
3004
|
+
return new Promise((resolve) => {
|
|
3005
|
+
try {
|
|
3006
|
+
const child = spawn(program, args, {
|
|
3007
|
+
detached: true,
|
|
3008
|
+
stdio: "ignore"
|
|
3009
|
+
});
|
|
3010
|
+
child.once("spawn", () => {
|
|
3011
|
+
child.unref();
|
|
3012
|
+
resolve(true);
|
|
3013
|
+
});
|
|
3014
|
+
child.once("error", () => resolve(false));
|
|
3015
|
+
} catch {
|
|
3016
|
+
resolve(false);
|
|
3017
|
+
}
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
/** Opens `url` in a browser through a launch page; whether a browser command started. */
|
|
3021
|
+
async function openInBrowser(url, env) {
|
|
3022
|
+
let page;
|
|
3023
|
+
try {
|
|
3024
|
+
page = writeLaunchPage(url);
|
|
3025
|
+
} catch {
|
|
3026
|
+
return false;
|
|
3027
|
+
}
|
|
3028
|
+
for (const command of browserCommands(page, env)) if (await start(command)) return true;
|
|
3029
|
+
return false;
|
|
3030
|
+
}
|
|
3031
|
+
//#endregion
|
|
3032
|
+
//#region src/commands/viewing.ts
|
|
3033
|
+
/**
|
|
3034
|
+
* `mostra open`: the artifact's link on stdout, and a browser opened on a one-time launch URL that
|
|
3035
|
+
* never carries the link's secret (D140 item 15), handed over in a page, never on a command line
|
|
3036
|
+
* (`browser.ts`). A client of `view/default`, `view/url`,
|
|
3037
|
+
* `view/rotate`, `view/revoke` and `view/launch`.
|
|
3038
|
+
*/
|
|
3039
|
+
/** Whether this session can show a browser: not over SSH without a display. */
|
|
3040
|
+
function hasDisplay(env, platform = process.platform) {
|
|
3041
|
+
const ssh = env["SSH_CONNECTION"] !== void 0 || env["SSH_TTY"] !== void 0;
|
|
3042
|
+
if (platform === "darwin") return !ssh;
|
|
3043
|
+
return env["DISPLAY"] !== void 0 || env["WAYLAND_DISPLAY"] !== void 0;
|
|
3044
|
+
}
|
|
3045
|
+
/** Whether `open` should launch a browser: a terminal without `--json`, or `--browser`. */
|
|
3046
|
+
function wantsBrowser(context) {
|
|
3047
|
+
if (context.flags["no-browser"] === true) return false;
|
|
3048
|
+
if (context.flags["browser"] === true) return true;
|
|
3049
|
+
return context.tty.stdout && !context.json && hasDisplay(context.env);
|
|
3050
|
+
}
|
|
3051
|
+
/** The link, opened in a browser when wanted. */
|
|
3052
|
+
async function show(context, link) {
|
|
3053
|
+
let opened = false;
|
|
3054
|
+
if (wantsBrowser(context)) opened = await openInBrowser((await context.call(viewLaunchMethod, { tokenId: link.tokenId })).url, context.env);
|
|
3055
|
+
return {
|
|
3056
|
+
result: {
|
|
3057
|
+
...link,
|
|
3058
|
+
opened
|
|
3059
|
+
},
|
|
3060
|
+
human: `${link.url}\n`
|
|
3061
|
+
};
|
|
3062
|
+
}
|
|
3063
|
+
//#endregion
|
|
3064
|
+
//#region src/commands/index.ts
|
|
3065
|
+
/** The commands, in help's order: the authoring loop first. */
|
|
3066
|
+
const COMMANDS = [
|
|
3067
|
+
newCommand,
|
|
3068
|
+
devCommand,
|
|
3069
|
+
verifyCommand,
|
|
3070
|
+
publishCommand,
|
|
3071
|
+
defineCommand({
|
|
3072
|
+
path: ["open"],
|
|
3073
|
+
summary: "Print an artifact's capability link and open it in a browser; or mint, rotate or revoke a link.",
|
|
3074
|
+
args: [{
|
|
3075
|
+
name: "artifact",
|
|
3076
|
+
summary: "The artifact: its source path or its ID."
|
|
3077
|
+
}],
|
|
3078
|
+
flags: [
|
|
3079
|
+
{
|
|
3080
|
+
name: "new",
|
|
3081
|
+
kind: "boolean",
|
|
3082
|
+
summary: "Mint a separate link instead of the artifact's default one."
|
|
3083
|
+
},
|
|
3084
|
+
{
|
|
3085
|
+
name: "version",
|
|
3086
|
+
kind: "string",
|
|
3087
|
+
value: "<n|id>",
|
|
3088
|
+
summary: "A new link that opens only this version."
|
|
3089
|
+
},
|
|
3090
|
+
{
|
|
3091
|
+
name: "scope",
|
|
3092
|
+
kind: "list",
|
|
3093
|
+
value: "<a,b>",
|
|
3094
|
+
summary: "A new link's scopes: view, draft, submit, review, signal, action:*. Default: all."
|
|
3095
|
+
},
|
|
3096
|
+
{
|
|
3097
|
+
name: "ttl",
|
|
3098
|
+
kind: "duration",
|
|
3099
|
+
value: "<d>",
|
|
3100
|
+
summary: "A new link that expires after this long, such as 24h."
|
|
3101
|
+
},
|
|
3102
|
+
{
|
|
3103
|
+
name: "viewer",
|
|
3104
|
+
kind: "string",
|
|
3105
|
+
value: "<ref>",
|
|
3106
|
+
summary: "A new link that acts for this person, by your own stable key for them."
|
|
3107
|
+
},
|
|
3108
|
+
{
|
|
3109
|
+
name: "viewer-name",
|
|
3110
|
+
kind: "string",
|
|
3111
|
+
value: "<name>",
|
|
3112
|
+
summary: "The display name of --viewer."
|
|
3113
|
+
},
|
|
3114
|
+
{
|
|
3115
|
+
name: "rotate",
|
|
3116
|
+
kind: "string",
|
|
3117
|
+
value: "<tokenId>",
|
|
3118
|
+
summary: "Replace this link with a new one; the old one stops working."
|
|
3119
|
+
},
|
|
3120
|
+
{
|
|
3121
|
+
name: "revoke",
|
|
3122
|
+
kind: "string",
|
|
3123
|
+
value: "<tokenId>",
|
|
3124
|
+
summary: "Stop this link from working."
|
|
3125
|
+
},
|
|
3126
|
+
{
|
|
3127
|
+
name: "browser",
|
|
3128
|
+
kind: "boolean",
|
|
3129
|
+
summary: "Open a browser even without a terminal."
|
|
3130
|
+
},
|
|
3131
|
+
{
|
|
3132
|
+
name: "no-browser",
|
|
3133
|
+
kind: "boolean",
|
|
3134
|
+
summary: "Only print the link."
|
|
3135
|
+
},
|
|
3136
|
+
{
|
|
3137
|
+
name: "request-id",
|
|
3138
|
+
kind: "string",
|
|
3139
|
+
value: "<id>",
|
|
3140
|
+
summary: "Make a retried --new return the same link."
|
|
3141
|
+
}
|
|
3142
|
+
],
|
|
3143
|
+
output: {
|
|
3144
|
+
type: "CapabilityLink",
|
|
3145
|
+
topic: "cli"
|
|
3146
|
+
},
|
|
3147
|
+
exits: [EXIT.failure],
|
|
3148
|
+
example: "open ./sidebar-review",
|
|
3149
|
+
async run(context) {
|
|
3150
|
+
const artifact = artifactRef(arg(context, "artifact"), context.cwd);
|
|
3151
|
+
const revoke = flag(context, "revoke");
|
|
3152
|
+
const rotate = flag(context, "rotate");
|
|
3153
|
+
if (revoke !== void 0 && rotate !== void 0) throw usage("Give --rotate or --revoke, not both.");
|
|
3154
|
+
if (revoke !== void 0) {
|
|
3155
|
+
await context.call(viewRevokeMethod, { tokenId: revoke });
|
|
3156
|
+
return {
|
|
3157
|
+
result: {
|
|
3158
|
+
tokenId: revoke,
|
|
3159
|
+
revoked: true
|
|
3160
|
+
},
|
|
3161
|
+
human: `Revoked ${revoke}\n`
|
|
3162
|
+
};
|
|
3163
|
+
}
|
|
3164
|
+
if (rotate !== void 0) return show(context, await context.call(viewRotateMethod, { tokenId: rotate }));
|
|
3165
|
+
const version = flag(context, "version");
|
|
3166
|
+
const scopes = context.flags["scope"];
|
|
3167
|
+
const ttl = flag(context, "ttl");
|
|
3168
|
+
const viewerRef = flag(context, "viewer");
|
|
3169
|
+
const viewerName = flag(context, "viewer-name");
|
|
3170
|
+
const requestId = flag(context, "request-id");
|
|
3171
|
+
if (viewerName !== void 0 && viewerRef === void 0) throw usage("--viewer-name needs --viewer.");
|
|
3172
|
+
if (!(context.flags["new"] === true || version !== void 0 || scopes !== void 0 || ttl !== void 0 || viewerRef !== void 0)) return show(context, await context.call(viewDefaultMethod, { artifact }));
|
|
3173
|
+
return show(context, await context.call(viewUrlMethod, {
|
|
3174
|
+
artifact,
|
|
3175
|
+
...version !== void 0 ? { version: /^[0-9]+$/.test(version) ? Number(version) : version } : {},
|
|
3176
|
+
...scopes !== void 0 ? { scopes } : {},
|
|
3177
|
+
...ttl !== void 0 ? { ttl } : {},
|
|
3178
|
+
...viewerRef !== void 0 ? { viewer: {
|
|
3179
|
+
ref: viewerRef,
|
|
3180
|
+
...viewerName !== void 0 ? { name: viewerName } : {}
|
|
3181
|
+
} } : {},
|
|
3182
|
+
...requestId !== void 0 ? { requestId } : {}
|
|
3183
|
+
}));
|
|
3184
|
+
}
|
|
3185
|
+
}),
|
|
3186
|
+
waitCommand,
|
|
3187
|
+
resultsCommand,
|
|
3188
|
+
stateGetCommand,
|
|
3189
|
+
stateSetCommand,
|
|
3190
|
+
statePatchCommand,
|
|
3191
|
+
pushCommand,
|
|
3192
|
+
addCommand,
|
|
3193
|
+
secretSetCommand,
|
|
3194
|
+
secretListCommand,
|
|
3195
|
+
secretRmCommand,
|
|
3196
|
+
listCommand,
|
|
3197
|
+
statusCommand,
|
|
3198
|
+
daemonStartCommand,
|
|
3199
|
+
daemonStopCommand,
|
|
3200
|
+
daemonRestartCommand,
|
|
3201
|
+
daemonStatusCommand,
|
|
3202
|
+
daemonInstallCommand,
|
|
3203
|
+
daemonUninstallCommand,
|
|
3204
|
+
hostCommand,
|
|
3205
|
+
docsCommand,
|
|
3206
|
+
doctorCommand,
|
|
3207
|
+
gcCommand,
|
|
3208
|
+
telemetryStatusCommand,
|
|
3209
|
+
telemetryOnCommand,
|
|
3210
|
+
telemetryOffCommand
|
|
3211
|
+
];
|
|
3212
|
+
//#endregion
|
|
3213
|
+
//#region src/help.ts
|
|
3214
|
+
/**
|
|
3215
|
+
* Help, generated from the command definitions (D140 items 8 and 16): `--help` for people,
|
|
3216
|
+
* `--help --json` for agents (the definition itself), and `docs/reference/cli.md`, which CI
|
|
3217
|
+
* checks is current. The wording lives in the definitions; this module only lays it out.
|
|
3218
|
+
*/
|
|
3219
|
+
const EXIT_MEANING = {
|
|
3220
|
+
[EXIT.ok]: "success",
|
|
3221
|
+
[EXIT.failure]: "artifact failure (verify failed, publish refused, the artifact's state refused the request)",
|
|
3222
|
+
[EXIT.usage]: "usage error",
|
|
3223
|
+
[EXIT.environment]: "environment error (no runtime, browser or socket; the home; an unsupported platform)",
|
|
3224
|
+
[EXIT.timeout]: "timeout (--timeout passed)",
|
|
3225
|
+
[EXIT.stillWaiting]: "still waiting (the wait timed out; the result says where to resume)",
|
|
3226
|
+
[EXIT.conflict]: "conflict (already submitted, a compare-and-set mismatch, a reused request ID)"
|
|
3227
|
+
};
|
|
3228
|
+
/** The usage line: `mostra open <artifact> [flags]`. */
|
|
3229
|
+
function usageLine(command) {
|
|
3230
|
+
const args = (command.args ?? []).map((arg) => {
|
|
3231
|
+
const name = arg.variadic ? `<${arg.name}…>` : `<${arg.name}>`;
|
|
3232
|
+
return arg.optional ? `[${name}]` : name;
|
|
3233
|
+
});
|
|
3234
|
+
return [
|
|
3235
|
+
"mostra",
|
|
3236
|
+
...command.path,
|
|
3237
|
+
...args,
|
|
3238
|
+
...(command.flags ?? []).length > 0 ? ["[flags]"] : []
|
|
3239
|
+
].join(" ");
|
|
3240
|
+
}
|
|
3241
|
+
function flagLine(flag) {
|
|
3242
|
+
const name = `--${flag.name}${flag.kind === "boolean" ? "" : ` ${flag.value ?? `<${flag.kind}>`}`}`;
|
|
3243
|
+
const choices = flag.choices ? ` (${flag.choices.join(", ")})` : "";
|
|
3244
|
+
const fallback = flag.default !== void 0 ? ` Default: ${flag.default}.` : "";
|
|
3245
|
+
return ` ${name.padEnd(28)} ${flag.summary}${choices}${fallback}`;
|
|
3246
|
+
}
|
|
3247
|
+
/** Commands that run until their connection or Ctrl-C ends them: `--timeout` doesn't bound them. */
|
|
3248
|
+
const UNBOUNDED = /* @__PURE__ */ new Set(["host", "dev"]);
|
|
3249
|
+
/**
|
|
3250
|
+
* The global flags a command doesn't define again (`wait --timeout` is the wait's own); a command
|
|
3251
|
+
* `--timeout` doesn't bound has no `--timeout`.
|
|
3252
|
+
*/
|
|
3253
|
+
function globalFlagsOf(command) {
|
|
3254
|
+
const own = new Set((command.flags ?? []).map((flag) => flag.name));
|
|
3255
|
+
if (UNBOUNDED.has(command.path[0] ?? "")) own.add("timeout");
|
|
3256
|
+
return GLOBAL_FLAGS.filter((flag) => !own.has(flag.name));
|
|
3257
|
+
}
|
|
3258
|
+
/** Every exit code a command can end with, sorted: `--timeout` can end any it bounds with 4. */
|
|
3259
|
+
function exitCodes(command) {
|
|
3260
|
+
const bounded = !UNBOUNDED.has(command.path[0] ?? "") && command.path[0] !== "wait";
|
|
3261
|
+
return [.../* @__PURE__ */ new Set([
|
|
3262
|
+
EXIT.ok,
|
|
3263
|
+
...command.exits,
|
|
3264
|
+
EXIT.usage,
|
|
3265
|
+
EXIT.environment,
|
|
3266
|
+
...bounded ? [EXIT.timeout] : []
|
|
3267
|
+
])].sort((a, b) => a - b);
|
|
3268
|
+
}
|
|
3269
|
+
/** One command's help, for people. */
|
|
3270
|
+
function commandHelp(command) {
|
|
3271
|
+
const lines = [
|
|
3272
|
+
command.summary,
|
|
3273
|
+
"",
|
|
3274
|
+
"Usage:",
|
|
3275
|
+
` ${usageLine(command)}`
|
|
3276
|
+
];
|
|
3277
|
+
if ((command.args ?? []).length > 0) {
|
|
3278
|
+
lines.push("", "Arguments:");
|
|
3279
|
+
for (const arg of command.args ?? []) lines.push(` ${`<${arg.name}>`.padEnd(28)} ${arg.summary}`);
|
|
3280
|
+
}
|
|
3281
|
+
if ((command.flags ?? []).length > 0) {
|
|
3282
|
+
lines.push("", "Flags:");
|
|
3283
|
+
for (const flag of command.flags ?? []) lines.push(flagLine(flag));
|
|
3284
|
+
}
|
|
3285
|
+
lines.push("", "Global flags:");
|
|
3286
|
+
for (const flag of globalFlagsOf(command)) lines.push(flagLine(flag));
|
|
3287
|
+
lines.push("", `Output: ${command.output.type} (mostra docs ${command.output.topic})`);
|
|
3288
|
+
lines.push("", "Exit codes:");
|
|
3289
|
+
for (const code of exitCodes(command)) lines.push(` ${code} ${EXIT_MEANING[code] ?? ""}`);
|
|
3290
|
+
lines.push("", "Example:", ` mostra ${command.example}`, "");
|
|
3291
|
+
return lines.join("\n");
|
|
3292
|
+
}
|
|
3293
|
+
/** A group's help (`mostra daemon --help`): its commands. */
|
|
3294
|
+
function groupHelp(commands, group) {
|
|
3295
|
+
const members = commands.filter((command) => command.path[0] === group);
|
|
3296
|
+
return [
|
|
3297
|
+
`mostra ${group}: ${members.length} commands.`,
|
|
3298
|
+
"",
|
|
3299
|
+
...members.map((command) => ` ${commandName(command).padEnd(22)} ${command.summary}`),
|
|
3300
|
+
"",
|
|
3301
|
+
`Run "mostra ${group} <command> --help" for one of them.`,
|
|
3302
|
+
""
|
|
3303
|
+
].join("\n");
|
|
3304
|
+
}
|
|
3305
|
+
/** The top-level help: every command. */
|
|
3306
|
+
function topHelp(commands) {
|
|
3307
|
+
return [
|
|
3308
|
+
"Mostra, the agent artifact runtime: write ordinary React, verify it, publish it, share the link, wait for answers.",
|
|
3309
|
+
"",
|
|
3310
|
+
"Usage:",
|
|
3311
|
+
" mostra <command> [arguments] [flags]",
|
|
3312
|
+
"",
|
|
3313
|
+
"Commands:",
|
|
3314
|
+
...commands.map((command) => ` ${commandName(command).padEnd(22)} ${command.summary}`),
|
|
3315
|
+
"",
|
|
3316
|
+
"Global flags:",
|
|
3317
|
+
...GLOBAL_FLAGS.map(flagLine),
|
|
3318
|
+
"",
|
|
3319
|
+
"Run \"mostra <command> --help\" for a command, and \"mostra docs\" for the reference.",
|
|
3320
|
+
""
|
|
3321
|
+
].join("\n");
|
|
3322
|
+
}
|
|
3323
|
+
/** One command's machine-readable definition (`--help --json`). */
|
|
3324
|
+
function commandDefinition(command) {
|
|
3325
|
+
return {
|
|
3326
|
+
command: commandName(command),
|
|
3327
|
+
summary: command.summary,
|
|
3328
|
+
usage: usageLine(command),
|
|
3329
|
+
args: (command.args ?? []).map((arg) => ({
|
|
3330
|
+
name: arg.name,
|
|
3331
|
+
summary: arg.summary,
|
|
3332
|
+
optional: arg.optional === true,
|
|
3333
|
+
variadic: arg.variadic === true
|
|
3334
|
+
})),
|
|
3335
|
+
flags: [...command.flags ?? [], ...globalFlagsOf(command)].map((flag) => ({
|
|
3336
|
+
name: flag.name,
|
|
3337
|
+
type: flag.kind,
|
|
3338
|
+
summary: flag.summary,
|
|
3339
|
+
...flag.choices ? { choices: flag.choices } : {},
|
|
3340
|
+
...flag.default !== void 0 ? { default: flag.default } : {},
|
|
3341
|
+
global: GLOBAL_FLAGS.includes(flag)
|
|
3342
|
+
})),
|
|
3343
|
+
output: command.output,
|
|
3344
|
+
exitCodes: exitCodes(command).map((code) => ({
|
|
3345
|
+
code,
|
|
3346
|
+
meaning: EXIT_MEANING[code] ?? ""
|
|
3347
|
+
})),
|
|
3348
|
+
stdin: typeof command.readsStdin === "function" ? "sometimes" : command.readsStdin === true,
|
|
3349
|
+
streams: command.streams === true,
|
|
3350
|
+
example: `mostra ${command.example}`
|
|
3351
|
+
};
|
|
3352
|
+
}
|
|
3353
|
+
/** `docs/reference/cli.md`: every command's help as one page. */
|
|
3354
|
+
function referenceMarkdown(commands) {
|
|
3355
|
+
const sections = commands.map((command) => `## mostra ${commandName(command)}\n\n\`\`\`text\n${commandHelp(command).trimEnd()}\n\`\`\`\n`);
|
|
3356
|
+
return [
|
|
3357
|
+
"# The mostra CLI",
|
|
3358
|
+
"",
|
|
3359
|
+
"<!-- Generated from the command definitions in packages/cli/src/commands/. Don't edit; run `pnpm fix`. -->",
|
|
3360
|
+
"",
|
|
3361
|
+
"Every command takes `--json`, which prints one envelope on stdout: `{ \"ok\": true, \"result\", \"instance\", \"warnings\" }` or `{ \"ok\": false, \"error\", \"warnings\" }`. Streaming commands print one envelope per line, the last one carrying `result` or `error`. Logs and progress go to stderr. Without a terminal, or with `--json`, no command asks a question.",
|
|
3362
|
+
"",
|
|
3363
|
+
`Commands: ${topWords(commands).map((word) => `\`${word}\``).join(", ")}.`,
|
|
3364
|
+
"",
|
|
3365
|
+
...sections
|
|
3366
|
+
].join("\n");
|
|
3367
|
+
}
|
|
3368
|
+
//#endregion
|
|
3369
|
+
//#region src/client.ts
|
|
3370
|
+
/**
|
|
3371
|
+
* The CLI's connection to its home's instance (D45, D81, INTERFACES §9): it attaches to the
|
|
3372
|
+
* daemon recorded in `instance.json`, or starts one (`mostra-runtime daemon`, which itself
|
|
3373
|
+
* attaches when another starter won), and opens a session as `mostra-cli`. The SDK (WP-11) does
|
|
3374
|
+
* the same few steps over the same protocol.
|
|
3375
|
+
*/
|
|
3376
|
+
/** How long one attempt to reach a recorded socket waits. */
|
|
3377
|
+
const ATTACH_TIMEOUT_MS = 5e3;
|
|
3378
|
+
/** How long a daemon this command starts may take to say where it listens. */
|
|
3379
|
+
const START_TIMEOUT_MS = 3e4;
|
|
3380
|
+
/**
|
|
3381
|
+
* Opens a session with the home's instance, starting its daemon when none runs. Returns
|
|
3382
|
+
* `undefined` when `autostart` is false and no instance runs. Throws the start's diagnostic when
|
|
3383
|
+
* the daemon can't start (`MOSTRA_HOME_LOCKED`, `MOSTRA_PLATFORM_UNSUPPORTED` …).
|
|
3384
|
+
*/
|
|
3385
|
+
async function connect(options) {
|
|
3386
|
+
const recorded = readSocket(options.home);
|
|
3387
|
+
if (recorded !== void 0) {
|
|
3388
|
+
const session = await open(recorded, options, false).catch(() => void 0);
|
|
3389
|
+
if (session) return session;
|
|
3390
|
+
}
|
|
3391
|
+
if (options.autostart === false) return void 0;
|
|
3392
|
+
options.onStarting?.(true);
|
|
3393
|
+
let started;
|
|
3394
|
+
try {
|
|
3395
|
+
started = await startDaemon(options);
|
|
3396
|
+
} finally {
|
|
3397
|
+
options.onStarting?.(false);
|
|
3398
|
+
}
|
|
3399
|
+
return open(started.socket, options, started.status === "started");
|
|
3400
|
+
}
|
|
3401
|
+
/** The socket `instance.json` records (D81), if the file exists and reads. */
|
|
3402
|
+
function readSocket(home) {
|
|
3403
|
+
try {
|
|
3404
|
+
const record = JSON.parse(readFileSync(join(home, "instance.json"), "utf8"));
|
|
3405
|
+
return typeof record.socket === "string" ? record.socket : void 0;
|
|
3406
|
+
} catch {
|
|
3407
|
+
return;
|
|
3408
|
+
}
|
|
3409
|
+
}
|
|
3410
|
+
/**
|
|
3411
|
+
* Starts the home's daemon, detached from this process, and reads the one line it prints: where
|
|
3412
|
+
* it listens, or why it couldn't start. A daemon another starter won prints `attached` and exits.
|
|
3413
|
+
*/
|
|
3414
|
+
async function startDaemon(options) {
|
|
3415
|
+
const deadlineAt = Date.now() + START_TIMEOUT_MS;
|
|
3416
|
+
const timedOut = () => new RpcError(createDiagnostic("MOSTRA_HOST_TIMEOUT", { message: `The home's daemon didn't start within ${formatDuration(START_TIMEOUT_MS)}.` }));
|
|
3417
|
+
const runtime = await locateRuntime(options.env);
|
|
3418
|
+
if (Date.now() >= deadlineAt) throw timedOut();
|
|
3419
|
+
const args = [
|
|
3420
|
+
runtime.entry,
|
|
3421
|
+
"daemon",
|
|
3422
|
+
"--home",
|
|
3423
|
+
options.home
|
|
3424
|
+
];
|
|
3425
|
+
return new Promise((resolve, reject) => {
|
|
3426
|
+
const child = spawn(runtime.node, args, {
|
|
3427
|
+
detached: true,
|
|
3428
|
+
stdio: [
|
|
3429
|
+
"ignore",
|
|
3430
|
+
"pipe",
|
|
3431
|
+
"ignore"
|
|
3432
|
+
],
|
|
3433
|
+
env: {
|
|
3434
|
+
...process.env,
|
|
3435
|
+
...options.env
|
|
3436
|
+
}
|
|
3437
|
+
});
|
|
3438
|
+
const splitter = new LineSplitter();
|
|
3439
|
+
let settled = false;
|
|
3440
|
+
const finish = (outcome) => {
|
|
3441
|
+
if (settled) return;
|
|
3442
|
+
settled = true;
|
|
3443
|
+
clearTimeout(timer);
|
|
3444
|
+
child.stdout?.removeAllListeners("data");
|
|
3445
|
+
child.stdout?.destroy();
|
|
3446
|
+
child.unref();
|
|
3447
|
+
outcome();
|
|
3448
|
+
};
|
|
3449
|
+
const timer = setTimeout(() => finish(() => reject(timedOut())), Math.max(0, deadlineAt - Date.now()));
|
|
3450
|
+
child.on("error", (error) => finish(() => reject(new RpcError(createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", { message: `The runtime couldn't be started: ${error.message}` })))));
|
|
3451
|
+
child.on("close", (code) => finish(() => reject(new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", { message: `The home's daemon exited (${code ?? "signal"}) before it said where it listens.` })))));
|
|
3452
|
+
child.stdout?.setEncoding("utf8");
|
|
3453
|
+
child.stdout?.on("data", (chunk) => {
|
|
3454
|
+
for (const line of splitter.push(chunk)) {
|
|
3455
|
+
let parsed;
|
|
3456
|
+
try {
|
|
3457
|
+
parsed = JSON.parse(line);
|
|
3458
|
+
} catch {
|
|
3459
|
+
continue;
|
|
3460
|
+
}
|
|
3461
|
+
finish(() => {
|
|
3462
|
+
if (parsed.status === "failed") reject(new RpcError(parsed.diagnostic ?? createDiagnostic("MOSTRA_HOST_INTERNAL_ERROR", { message: "The home's daemon failed to start." })));
|
|
3463
|
+
else resolve(parsed);
|
|
3464
|
+
});
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
});
|
|
3468
|
+
});
|
|
3469
|
+
}
|
|
3470
|
+
async function open(socketPath, options, started) {
|
|
3471
|
+
const socket = await new Promise((resolve, reject) => {
|
|
3472
|
+
const connection = createConnection(socketPath);
|
|
3473
|
+
const timer = setTimeout(() => {
|
|
3474
|
+
connection.destroy();
|
|
3475
|
+
reject(closed(socketPath, "timed out"));
|
|
3476
|
+
}, ATTACH_TIMEOUT_MS);
|
|
3477
|
+
connection.once("error", (error) => {
|
|
3478
|
+
clearTimeout(timer);
|
|
3479
|
+
reject(closed(socketPath, error.code ?? "failed"));
|
|
3480
|
+
});
|
|
3481
|
+
connection.once("connect", () => {
|
|
3482
|
+
clearTimeout(timer);
|
|
3483
|
+
resolve(connection);
|
|
3484
|
+
});
|
|
3485
|
+
});
|
|
3486
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
3487
|
+
const peer = new RpcPeer({
|
|
3488
|
+
send: (message) => {
|
|
3489
|
+
if (!socket.destroyed) socket.write(encodeLine(message));
|
|
3490
|
+
},
|
|
3491
|
+
onNotification: (method, params) => handlers.get(method)?.(params)
|
|
3492
|
+
});
|
|
3493
|
+
let markClosed = () => {};
|
|
3494
|
+
const closedPromise = new Promise((done) => {
|
|
3495
|
+
markClosed = done;
|
|
3496
|
+
});
|
|
3497
|
+
const splitter = new LineSplitter();
|
|
3498
|
+
socket.setEncoding("utf8");
|
|
3499
|
+
socket.on("data", (chunk) => {
|
|
3500
|
+
for (const line of splitter.push(chunk)) peer.receiveLine(line);
|
|
3501
|
+
});
|
|
3502
|
+
const end = () => {
|
|
3503
|
+
peer.close();
|
|
3504
|
+
markClosed();
|
|
3505
|
+
};
|
|
3506
|
+
socket.on("close", end);
|
|
3507
|
+
socket.on("error", () => socket.destroy());
|
|
3508
|
+
let result;
|
|
3509
|
+
try {
|
|
3510
|
+
result = await peer.call(mostraInitializeMethod, {
|
|
3511
|
+
client: {
|
|
3512
|
+
name: CLI_CLIENT_NAME,
|
|
3513
|
+
version: cliVersion()
|
|
3514
|
+
},
|
|
3515
|
+
revisions: [...HOST_PROTOCOL_REVISIONS],
|
|
3516
|
+
capabilities: { notifications: [ARTIFACT_PROGRESS, DEV_NOTIFY] },
|
|
3517
|
+
ports: [],
|
|
3518
|
+
home: { path: options.home }
|
|
3519
|
+
}, { timeoutMs: ATTACH_TIMEOUT_MS });
|
|
3520
|
+
} catch (error) {
|
|
3521
|
+
socket.destroy();
|
|
3522
|
+
throw error;
|
|
3523
|
+
}
|
|
3524
|
+
return {
|
|
3525
|
+
summary: {
|
|
3526
|
+
home: result.instance.home,
|
|
3527
|
+
socket: socketPath,
|
|
3528
|
+
pid: result.instance.pid,
|
|
3529
|
+
mode: result.instance.mode,
|
|
3530
|
+
version: result.instance.version,
|
|
3531
|
+
started
|
|
3532
|
+
},
|
|
3533
|
+
warnings: result.warnings,
|
|
3534
|
+
call: (method, params, timeoutMs) => peer.call(method, params, timeoutMs !== void 0 ? { timeoutMs } : {}),
|
|
3535
|
+
onNotification: (method, handler) => {
|
|
3536
|
+
handlers.set(method, handler);
|
|
3537
|
+
},
|
|
3538
|
+
closed: closedPromise,
|
|
3539
|
+
close: () => {
|
|
3540
|
+
peer.close();
|
|
3541
|
+
socket.end();
|
|
3542
|
+
socket.destroy();
|
|
3543
|
+
}
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
function closed(path, why) {
|
|
3547
|
+
return new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", { message: `Couldn't reach the instance at ${path} (${why}).` }));
|
|
3548
|
+
}
|
|
3549
|
+
/** A limit as people write it: `250 ms`, `5 s`, `1.5 s`. */
|
|
3550
|
+
function formatDuration(ms) {
|
|
3551
|
+
if (ms < 1e3) return `${ms} ms`;
|
|
3552
|
+
const seconds = ms / 1e3;
|
|
3553
|
+
return `${Number.isInteger(seconds) ? seconds : seconds.toFixed(1)} s`;
|
|
3554
|
+
}
|
|
3555
|
+
//#endregion
|
|
3556
|
+
//#region src/run.ts
|
|
3557
|
+
/**
|
|
3558
|
+
* The runner (INTERFACES §9, D140 item 16): reads the command line, runs one command, and owns
|
|
3559
|
+
* everything commands share. stdout carries exactly one JSON value in `--json` mode (one per line
|
|
3560
|
+
* for a stream), whatever happens, usage errors and crashes included; logs, progress and warnings
|
|
3561
|
+
* go to stderr; the exit code comes from one table; telemetry's notice or question comes before
|
|
3562
|
+
* the command's work and its events after it.
|
|
3563
|
+
*/
|
|
3564
|
+
/** The diagnostic an exception is, for the envelope: its own, or an internal error. */
|
|
3565
|
+
function diagnosticOf(error) {
|
|
3566
|
+
if (error instanceof RpcError) return error.diagnostic;
|
|
3567
|
+
return createDiagnostic("MOSTRA_HOST_INTERNAL_ERROR", { message: `mostra failed unexpectedly: ${error instanceof Error ? error.message : String(error)}` });
|
|
3568
|
+
}
|
|
3569
|
+
/** A diagnostic for people, on stderr. */
|
|
3570
|
+
function describe(diagnostic) {
|
|
3571
|
+
const hint = diagnostic.hint ? `\n ${diagnostic.hint}` : "";
|
|
3572
|
+
const fix = diagnostic.fix?.kind === "command" && diagnostic.fix.command ? `\n Run: ${diagnostic.fix.command}` : "";
|
|
3573
|
+
return `${diagnostic.severity} ${diagnostic.code}: ${diagnostic.message}${hint}${fix}\n ${diagnostic.docs}\n`;
|
|
3574
|
+
}
|
|
3575
|
+
/** Reads the whole of stdin. */
|
|
3576
|
+
function readAll(stream) {
|
|
3577
|
+
return new Promise((resolve, reject) => {
|
|
3578
|
+
const chunks = [];
|
|
3579
|
+
stream.on("data", (chunk) => chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk)));
|
|
3580
|
+
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf8")));
|
|
3581
|
+
stream.on("error", reject);
|
|
3582
|
+
});
|
|
3583
|
+
}
|
|
3584
|
+
/** Asks one question on stderr, read from stdin: the answer, `undefined` after the timeout, or Ctrl-C. */
|
|
3585
|
+
function ask(io, question, timeoutMs) {
|
|
3586
|
+
return new Promise((resolve) => {
|
|
3587
|
+
const rl = createInterface({
|
|
3588
|
+
input: io.stdin,
|
|
3589
|
+
output: process.stderr,
|
|
3590
|
+
terminal: true
|
|
3591
|
+
});
|
|
3592
|
+
let done = false;
|
|
3593
|
+
const finish = (answer) => {
|
|
3594
|
+
if (done) return;
|
|
3595
|
+
done = true;
|
|
3596
|
+
clearTimeout(timer);
|
|
3597
|
+
rl.close();
|
|
3598
|
+
resolve(answer);
|
|
3599
|
+
};
|
|
3600
|
+
const timer = setTimeout(() => {
|
|
3601
|
+
io.stderr("\n");
|
|
3602
|
+
finish(void 0);
|
|
3603
|
+
}, timeoutMs);
|
|
3604
|
+
rl.on("SIGINT", () => {
|
|
3605
|
+
io.stderr("\n");
|
|
3606
|
+
finish("interrupt");
|
|
3607
|
+
});
|
|
3608
|
+
rl.on("close", () => finish(""));
|
|
3609
|
+
rl.question(question, (answer) => finish(answer));
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
/** Runs one command line and returns its exit code. */
|
|
3613
|
+
async function run(io) {
|
|
3614
|
+
const started = Date.now();
|
|
3615
|
+
const global = globalFlags(io.argv);
|
|
3616
|
+
const json = global.json;
|
|
3617
|
+
const warnings = [];
|
|
3618
|
+
let summary;
|
|
3619
|
+
let connection;
|
|
3620
|
+
let streamed = false;
|
|
3621
|
+
const writeJson = (value) => io.stdout(`${JSON.stringify(value)}\n`);
|
|
3622
|
+
const fail = (diagnostic) => {
|
|
3623
|
+
if (json) writeJson({
|
|
3624
|
+
ok: false,
|
|
3625
|
+
error: diagnostic,
|
|
3626
|
+
...summary && !streamed ? { instance: summary } : {},
|
|
3627
|
+
warnings
|
|
3628
|
+
});
|
|
3629
|
+
else io.stderr(describe(diagnostic));
|
|
3630
|
+
return exitCodeFor(diagnostic.code);
|
|
3631
|
+
};
|
|
3632
|
+
const platform = io.platform ?? process.platform;
|
|
3633
|
+
if (platform !== "darwin" && platform !== "linux") return fail(createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message: `Mostra runs on macOS and Linux; this is ${platform}.` }));
|
|
3634
|
+
let parsed;
|
|
3635
|
+
try {
|
|
3636
|
+
parsed = parseCommandLine(COMMANDS, io.argv);
|
|
3637
|
+
} catch (error) {
|
|
3638
|
+
return fail(diagnosticOf(error));
|
|
3639
|
+
}
|
|
3640
|
+
if (parsed.kind === "version") {
|
|
3641
|
+
if (json) writeJson({
|
|
3642
|
+
ok: true,
|
|
3643
|
+
result: { version: cliVersion() },
|
|
3644
|
+
warnings
|
|
3645
|
+
});
|
|
3646
|
+
else io.stdout(`${cliVersion()}\n`);
|
|
3647
|
+
return EXIT.ok;
|
|
3648
|
+
}
|
|
3649
|
+
if (parsed.kind === "help") {
|
|
3650
|
+
if (json) writeJson({
|
|
3651
|
+
ok: true,
|
|
3652
|
+
result: parsed.command ? commandDefinition(parsed.command) : { commands: COMMANDS.filter((command) => !parsed.group || command.path[0] === parsed.group).map(commandDefinition) },
|
|
3653
|
+
warnings
|
|
3654
|
+
});
|
|
3655
|
+
else io.stdout(parsed.command ? commandHelp(parsed.command) : parsed.group ? groupHelp(COMMANDS, parsed.group) : topHelp(COMMANDS));
|
|
3656
|
+
return EXIT.ok;
|
|
3657
|
+
}
|
|
3658
|
+
const { command } = parsed;
|
|
3659
|
+
if (global.timeoutMs !== void 0 && UNBOUNDED.has(command.path[0] ?? "")) return fail(createDiagnostic("MOSTRA_CLI_USAGE", { message: `mostra ${commandName(command)} runs until Ctrl-C or its connection ends, so --timeout doesn't apply to it.` }));
|
|
3660
|
+
const timeoutMs = command.path[0] === "wait" ? void 0 : global.timeoutMs;
|
|
3661
|
+
const cwd = global.cwd !== void 0 ? resolve(io.cwd, global.cwd) : io.cwd;
|
|
3662
|
+
const home = resolveHome({
|
|
3663
|
+
home: global.home,
|
|
3664
|
+
env: io.env,
|
|
3665
|
+
cwd,
|
|
3666
|
+
platform
|
|
3667
|
+
});
|
|
3668
|
+
const config = configDir(io.env, cwd);
|
|
3669
|
+
const marks = readHomeMarks(home);
|
|
3670
|
+
const readsStdin = typeof command.readsStdin === "function" ? command.readsStdin(parsed.flags) : command.readsStdin === true;
|
|
3671
|
+
const telemetry = telemetrySession({
|
|
3672
|
+
env: io.env,
|
|
3673
|
+
configDir: config,
|
|
3674
|
+
command: commandName(command),
|
|
3675
|
+
json,
|
|
3676
|
+
tty: io.tty,
|
|
3677
|
+
readsStdin,
|
|
3678
|
+
quiet: command.quiet === true,
|
|
3679
|
+
hostHome: embeddedHome(marks),
|
|
3680
|
+
stderr: io.stderr,
|
|
3681
|
+
ask: (question, timeoutMs) => ask(io, question, timeoutMs)
|
|
3682
|
+
});
|
|
3683
|
+
let connectionClosed = false;
|
|
3684
|
+
let startingDaemon = false;
|
|
3685
|
+
const getConnection = async (autostart = true) => {
|
|
3686
|
+
if (connection && !connectionClosed) return connection;
|
|
3687
|
+
connectionClosed = false;
|
|
3688
|
+
const opened = await connect({
|
|
3689
|
+
home,
|
|
3690
|
+
env: io.env,
|
|
3691
|
+
autostart,
|
|
3692
|
+
onStarting: (starting) => {
|
|
3693
|
+
startingDaemon = starting;
|
|
3694
|
+
}
|
|
3695
|
+
});
|
|
3696
|
+
if (!opened) return void 0;
|
|
3697
|
+
connection = opened;
|
|
3698
|
+
opened.closed.then(() => {
|
|
3699
|
+
if (connection === opened) connectionClosed = true;
|
|
3700
|
+
});
|
|
3701
|
+
summary = opened.summary;
|
|
3702
|
+
for (const warning of opened.warnings) {
|
|
3703
|
+
warnings.push(warning);
|
|
3704
|
+
io.stderr(describe(warning));
|
|
3705
|
+
}
|
|
3706
|
+
return opened;
|
|
3707
|
+
};
|
|
3708
|
+
const context = {
|
|
3709
|
+
args: parsed.args,
|
|
3710
|
+
flags: parsed.flags,
|
|
3711
|
+
cwd,
|
|
3712
|
+
json,
|
|
3713
|
+
timeoutMs,
|
|
3714
|
+
env: io.env,
|
|
3715
|
+
tty: io.tty,
|
|
3716
|
+
home,
|
|
3717
|
+
configDir: config,
|
|
3718
|
+
stdout: io.stdout,
|
|
3719
|
+
stderr: io.stderr,
|
|
3720
|
+
async call(method, params) {
|
|
3721
|
+
const session = await getConnection();
|
|
3722
|
+
if (!session) throw new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", { message: "No instance runs for this home." }));
|
|
3723
|
+
const remaining = timeoutMs === void 0 ? void 0 : Math.max(1, timeoutMs - (Date.now() - started));
|
|
3724
|
+
return session.call(method, params, remaining);
|
|
3725
|
+
},
|
|
3726
|
+
connection: getConnection,
|
|
3727
|
+
readStdin: () => readAll(io.stdin),
|
|
3728
|
+
log: (line) => io.stderr(`${line}\n`),
|
|
3729
|
+
emit(event, human) {
|
|
3730
|
+
if (json) {
|
|
3731
|
+
writeJson({
|
|
3732
|
+
ok: true,
|
|
3733
|
+
event,
|
|
3734
|
+
...summary && !streamed ? { instance: summary } : {}
|
|
3735
|
+
});
|
|
3736
|
+
streamed = true;
|
|
3737
|
+
} else if (human !== void 0) io.stdout(human);
|
|
3738
|
+
},
|
|
3739
|
+
warn(diagnostic) {
|
|
3740
|
+
warnings.push(diagnostic);
|
|
3741
|
+
io.stderr(describe(diagnostic));
|
|
3742
|
+
},
|
|
3743
|
+
record: (event) => telemetry.record(event),
|
|
3744
|
+
runtime: (args, stdio = "pipe") => runRuntime(io, args, stdio)
|
|
3745
|
+
};
|
|
3746
|
+
let exit;
|
|
3747
|
+
let error;
|
|
3748
|
+
try {
|
|
3749
|
+
if (!await telemetry.begin().catch(() => true)) return 130;
|
|
3750
|
+
const outcome = await withTimeout(command, context, timeoutMs, () => startingDaemon);
|
|
3751
|
+
exit = outcome.exit ?? EXIT.ok;
|
|
3752
|
+
if (outcome.raw) return exit;
|
|
3753
|
+
if (json) writeJson({
|
|
3754
|
+
ok: true,
|
|
3755
|
+
result: outcome.result ?? null,
|
|
3756
|
+
...summary && !streamed && command.local !== true ? { instance: summary } : {},
|
|
3757
|
+
warnings
|
|
3758
|
+
});
|
|
3759
|
+
else io.stdout(outcome.human ?? `${JSON.stringify(outcome.result, null, 2)}\n`);
|
|
3760
|
+
} catch (caught) {
|
|
3761
|
+
const diagnostic = diagnosticOf(caught);
|
|
3762
|
+
error = diagnostic.code;
|
|
3763
|
+
exit = fail(diagnostic);
|
|
3764
|
+
} finally {
|
|
3765
|
+
connection?.close();
|
|
3766
|
+
}
|
|
3767
|
+
try {
|
|
3768
|
+
telemetry.end({
|
|
3769
|
+
exit,
|
|
3770
|
+
durationMs: Date.now() - started,
|
|
3771
|
+
hostInstance: summary?.mode === "host",
|
|
3772
|
+
...error !== void 0 ? { error } : {}
|
|
3773
|
+
});
|
|
3774
|
+
} catch {}
|
|
3775
|
+
return exit;
|
|
3776
|
+
}
|
|
3777
|
+
/**
|
|
3778
|
+
* Runs a command, bounded by `--timeout` (exit 4 with `MOSTRA_HOST_TIMEOUT`). `host` and `dev` run
|
|
3779
|
+
* until their connection or Ctrl-C ends them, so `--timeout` bounds neither.
|
|
3780
|
+
*/
|
|
3781
|
+
async function withTimeout(command, context, timeoutMs, startingDaemon) {
|
|
3782
|
+
if (timeoutMs === void 0 || command.path[0] === "host" || command.path[0] === "dev") return command.run(context);
|
|
3783
|
+
let timer;
|
|
3784
|
+
try {
|
|
3785
|
+
return await Promise.race([command.run(context), new Promise((_, reject) => {
|
|
3786
|
+
timer = setTimeout(() => reject(new RpcError(createDiagnostic("MOSTRA_HOST_TIMEOUT", { message: startingDaemon() ? `The home's daemon didn't start within ${timeoutMs} ms (--timeout).` : `mostra ${commandName(command)} didn't finish within --timeout (${timeoutMs} ms); what it asked for may still take effect.` }))), timeoutMs);
|
|
3787
|
+
})]);
|
|
3788
|
+
} finally {
|
|
3789
|
+
clearTimeout(timer);
|
|
3790
|
+
}
|
|
3791
|
+
}
|
|
3792
|
+
/** Runs `mostra-runtime …` and collects its stdout. */
|
|
3793
|
+
async function runRuntime(io, args, stdio) {
|
|
3794
|
+
const runtime = await locateRuntime(io.env);
|
|
3795
|
+
return new Promise((resolve) => {
|
|
3796
|
+
const child = spawn(runtime.node, [runtime.entry, ...args], {
|
|
3797
|
+
stdio: stdio === "inherit" ? "inherit" : [
|
|
3798
|
+
"ignore",
|
|
3799
|
+
"pipe",
|
|
3800
|
+
"pipe"
|
|
3801
|
+
],
|
|
3802
|
+
env: io.env
|
|
3803
|
+
});
|
|
3804
|
+
let stdout = "";
|
|
3805
|
+
child.stdout?.setEncoding("utf8");
|
|
3806
|
+
child.stdout?.on("data", (chunk) => {
|
|
3807
|
+
stdout += chunk;
|
|
3808
|
+
});
|
|
3809
|
+
child.stderr?.setEncoding("utf8");
|
|
3810
|
+
child.stderr?.on("data", (chunk) => io.stderr(chunk));
|
|
3811
|
+
child.on("error", () => resolve({
|
|
3812
|
+
code: EXIT.environment,
|
|
3813
|
+
stdout
|
|
3814
|
+
}));
|
|
3815
|
+
child.on("close", (code) => resolve({
|
|
3816
|
+
code: code ?? EXIT.environment,
|
|
3817
|
+
stdout
|
|
3818
|
+
}));
|
|
3819
|
+
});
|
|
3820
|
+
}
|
|
3821
|
+
//#endregion
|
|
3822
|
+
export { COMMANDS as a, referenceMarkdown as i, commandDefinition as n, commandHelp as r, run as t };
|
|
3823
|
+
|
|
3824
|
+
//# sourceMappingURL=run-BJaRnz-Q.js.map
|