@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,288 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction: `wait`, `results`, `push` and `state get|set|patch`, thin clients of `wait`,
|
|
3
|
+
* `results/get`, `live/push` and `state/*` (INTERFACES §6, D101). `wait` ends with exit 5 when it
|
|
4
|
+
* times out (`still_waiting`), and a write that meets a conflict with exit 6.
|
|
5
|
+
*/
|
|
6
|
+
import {
|
|
7
|
+
FieldOp,
|
|
8
|
+
livePushMethod,
|
|
9
|
+
MutationId,
|
|
10
|
+
resultsGetMethod,
|
|
11
|
+
stateGetMethod,
|
|
12
|
+
statePatchMethod,
|
|
13
|
+
type WaitCondition,
|
|
14
|
+
type WaitResult,
|
|
15
|
+
waitMethod,
|
|
16
|
+
} from "@mostra/protocol";
|
|
17
|
+
import { type CommandDef, defineCommand } from "../command.ts";
|
|
18
|
+
import { EXIT } from "../exit.ts";
|
|
19
|
+
import { durationMs, usage } from "../parse.ts";
|
|
20
|
+
import { arg, artifactRef, flag, jsonInput, parseJson, pretty } from "./shared.ts";
|
|
21
|
+
|
|
22
|
+
/** The longest one `wait` request may ask for (INTERFACES §6: 2^31 − 1 ms). */
|
|
23
|
+
const MAX_WAIT_MS = 2 ** 31 - 1;
|
|
24
|
+
|
|
25
|
+
/** `mostra wait <artifact>`. */
|
|
26
|
+
export const waitCommand: CommandDef = defineCommand({
|
|
27
|
+
path: ["wait"],
|
|
28
|
+
summary:
|
|
29
|
+
"Block until an artifact's submission, signal, state change or event; still_waiting on timeout.",
|
|
30
|
+
args: [{ name: "artifact", summary: "The artifact: its source path or its ID." }],
|
|
31
|
+
flags: [
|
|
32
|
+
{
|
|
33
|
+
name: "until",
|
|
34
|
+
kind: "string",
|
|
35
|
+
value: "<condition>",
|
|
36
|
+
summary: "submitted, signal:<name>, state:<fieldId> or event:<type>.",
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: "after",
|
|
40
|
+
kind: "string",
|
|
41
|
+
value: "<cursor>",
|
|
42
|
+
summary: "Wait for what comes after this cursor. Default: the latest publish.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "timeout",
|
|
46
|
+
kind: "duration",
|
|
47
|
+
value: "<d>",
|
|
48
|
+
summary: "Stop waiting after this long, with still_waiting and exit 5. Default: no limit.",
|
|
49
|
+
},
|
|
50
|
+
],
|
|
51
|
+
output: { type: "WaitResult", topic: "cli" },
|
|
52
|
+
exits: [EXIT.failure, EXIT.stillWaiting],
|
|
53
|
+
example: "wait ./sidebar-review --until submitted --timeout 30m",
|
|
54
|
+
async run(context) {
|
|
55
|
+
const until = flag(context, "until");
|
|
56
|
+
if (until === undefined) throw usage('"mostra wait" needs --until, such as --until submitted.');
|
|
57
|
+
const timeout = flag(context, "timeout");
|
|
58
|
+
const deadline = timeout === undefined ? undefined : Date.now() + durationMs(timeout);
|
|
59
|
+
const artifact = artifactRef(arg(context, "artifact"), context.cwd);
|
|
60
|
+
let after = flag(context, "after");
|
|
61
|
+
for (;;) {
|
|
62
|
+
const remaining = deadline === undefined ? MAX_WAIT_MS : Math.max(0, deadline - Date.now());
|
|
63
|
+
const result: WaitResult = await context.call(waitMethod, {
|
|
64
|
+
artifact,
|
|
65
|
+
until: until as WaitCondition,
|
|
66
|
+
timeoutMs: Math.min(remaining, MAX_WAIT_MS),
|
|
67
|
+
...(after !== undefined ? { after } : {}),
|
|
68
|
+
});
|
|
69
|
+
if (result.status === "satisfied") {
|
|
70
|
+
return { result, human: `${result.event.type} at cursor ${result.cursor}\n` };
|
|
71
|
+
}
|
|
72
|
+
// No limit: wait again from the newest cursor examined, so nothing is missed.
|
|
73
|
+
if (deadline === undefined) {
|
|
74
|
+
after = result.cursor;
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
result,
|
|
79
|
+
human: `still waiting; resume with --after ${result.cursor}\n`,
|
|
80
|
+
exit: EXIT.stillWaiting,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
/** `mostra results <artifact>`. */
|
|
87
|
+
export const resultsCommand: CommandDef = defineCommand({
|
|
88
|
+
path: ["results"],
|
|
89
|
+
summary:
|
|
90
|
+
"Print an artifact's submission in full: answers, state, choices, comments, actions and signals.",
|
|
91
|
+
args: [{ name: "artifact", summary: "The artifact: its source path or its ID." }],
|
|
92
|
+
flags: [
|
|
93
|
+
{
|
|
94
|
+
name: "submission",
|
|
95
|
+
kind: "string",
|
|
96
|
+
value: "<id>",
|
|
97
|
+
summary: "This submission instead of the latest.",
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: "format",
|
|
101
|
+
kind: "string",
|
|
102
|
+
value: "<format>",
|
|
103
|
+
choices: ["md", "json"],
|
|
104
|
+
summary: "Markdown or JSON. Default: Markdown, or JSON with --json.",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
output: { type: "Results", topic: "cli" },
|
|
108
|
+
exits: [EXIT.failure],
|
|
109
|
+
example: "results ./sidebar-review",
|
|
110
|
+
async run(context) {
|
|
111
|
+
const format = (flag(context, "format") ?? (context.json ? "json" : "md")) as "md" | "json";
|
|
112
|
+
const submission = flag(context, "submission");
|
|
113
|
+
const result = await context.call(resultsGetMethod, {
|
|
114
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
115
|
+
format,
|
|
116
|
+
...(submission !== undefined ? { submission } : {}),
|
|
117
|
+
});
|
|
118
|
+
const markdown =
|
|
119
|
+
typeof result === "object" && result !== null && "markdown" in result
|
|
120
|
+
? (result as { markdown: string }).markdown
|
|
121
|
+
: undefined;
|
|
122
|
+
return { result, human: markdown !== undefined ? `${markdown.trimEnd()}\n` : pretty(result) };
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
/** `mostra push <artifact> <key>`. */
|
|
127
|
+
export const pushCommand: CommandDef = defineCommand({
|
|
128
|
+
path: ["push"],
|
|
129
|
+
summary: "Push a value into an artifact's live source; pages showing it update at once.",
|
|
130
|
+
args: [
|
|
131
|
+
{ name: "artifact", summary: "The artifact: its source path or its ID." },
|
|
132
|
+
{ name: "key", summary: "The live source's key: its export name in live.ts, or a new one." },
|
|
133
|
+
],
|
|
134
|
+
flags: [
|
|
135
|
+
{ name: "value", kind: "string", value: "<json>", summary: "The value, as JSON." },
|
|
136
|
+
{ name: "file", kind: "string", value: "<path>", summary: "Read the value from a JSON file." },
|
|
137
|
+
{ name: "stdin", kind: "boolean", summary: "Read the value from stdin." },
|
|
138
|
+
{
|
|
139
|
+
name: "append",
|
|
140
|
+
kind: "boolean",
|
|
141
|
+
summary: "Add the value to the source's ring instead of replacing it.",
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: "request-id",
|
|
145
|
+
kind: "string",
|
|
146
|
+
value: "<id>",
|
|
147
|
+
summary: "Make a retry return the first attempt's revision.",
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
output: { type: "LivePushResult", topic: "cli" },
|
|
151
|
+
exits: [EXIT.failure],
|
|
152
|
+
example: `push ./dashboard prices --value '{"btc": 64000}'`,
|
|
153
|
+
readsStdin: (flags) => flags["stdin"] === true,
|
|
154
|
+
async run(context) {
|
|
155
|
+
const value = await jsonInput(context);
|
|
156
|
+
const requestId = flag(context, "request-id");
|
|
157
|
+
const result = await context.call(livePushMethod, {
|
|
158
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
159
|
+
key: arg(context, "key"),
|
|
160
|
+
value,
|
|
161
|
+
...(context.flags["append"] === true ? { mode: "append" as const } : {}),
|
|
162
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
163
|
+
});
|
|
164
|
+
return { result, human: `revision ${result.revision}\n` };
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
const writeFlags = [
|
|
169
|
+
{
|
|
170
|
+
name: "base",
|
|
171
|
+
kind: "string" as const,
|
|
172
|
+
value: "<revision>",
|
|
173
|
+
summary:
|
|
174
|
+
"Write only if the fields are unchanged since this revision; else a conflict (exit 6).",
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
name: "as",
|
|
178
|
+
kind: "string" as const,
|
|
179
|
+
value: "<actor>",
|
|
180
|
+
choices: ["agent", "host"],
|
|
181
|
+
default: "agent",
|
|
182
|
+
summary: "Who the change is attributed to.",
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
name: "mutation-id",
|
|
186
|
+
kind: "string" as const,
|
|
187
|
+
value: "<mut_…>",
|
|
188
|
+
summary: "Make a retry return the first attempt's result.",
|
|
189
|
+
},
|
|
190
|
+
];
|
|
191
|
+
|
|
192
|
+
/** `mostra state get <artifact> [ids…]`. */
|
|
193
|
+
export const stateGetCommand: CommandDef = defineCommand({
|
|
194
|
+
path: ["state", "get"],
|
|
195
|
+
summary: "Read an artifact's state: the fields every viewer shares, with their revisions.",
|
|
196
|
+
args: [
|
|
197
|
+
{ name: "artifact", summary: "The artifact: its source path or its ID." },
|
|
198
|
+
{ name: "id", summary: "Only these field IDs.", optional: true, variadic: true },
|
|
199
|
+
],
|
|
200
|
+
output: { type: "StateGetResult", topic: "cli" },
|
|
201
|
+
exits: [EXIT.failure],
|
|
202
|
+
example: "state get ./tasks priorities",
|
|
203
|
+
async run(context) {
|
|
204
|
+
const ids = context.args["id"] as readonly string[] | undefined;
|
|
205
|
+
const result = await context.call(stateGetMethod, {
|
|
206
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
207
|
+
...(ids !== undefined && ids.length > 0 ? { ids: [...ids] } : {}),
|
|
208
|
+
});
|
|
209
|
+
return { result, human: pretty(result) };
|
|
210
|
+
},
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
/** Sends one state mutation; a conflict ends with exit 6. */
|
|
214
|
+
async function patch(
|
|
215
|
+
context: Parameters<(typeof stateGetCommand)["run"]>[0],
|
|
216
|
+
ops: readonly FieldOp[],
|
|
217
|
+
): Promise<{ result: unknown; human: string; exit?: number }> {
|
|
218
|
+
const base = flag(context, "base");
|
|
219
|
+
const as = (flag(context, "as") ?? "agent") as "agent" | "host";
|
|
220
|
+
const result = await context.call(statePatchMethod, {
|
|
221
|
+
artifact: artifactRef(arg(context, "artifact"), context.cwd),
|
|
222
|
+
ops,
|
|
223
|
+
mutationId: flag(context, "mutation-id") ?? MutationId.create(),
|
|
224
|
+
as,
|
|
225
|
+
...(base !== undefined ? { baseRevision: base } : {}),
|
|
226
|
+
});
|
|
227
|
+
const conflicted = result.conflicts.length > 0;
|
|
228
|
+
return {
|
|
229
|
+
result,
|
|
230
|
+
human: conflicted
|
|
231
|
+
? `conflict on ${result.conflicts.map((conflict) => conflict.id).join(", ")}; revision ${result.revision}\n`
|
|
232
|
+
: `revision ${result.revision}\n`,
|
|
233
|
+
...(conflicted ? { exit: EXIT.conflict } : {}),
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/** `mostra state set <artifact> <id> <json>`. */
|
|
238
|
+
export const stateSetCommand: CommandDef = defineCommand({
|
|
239
|
+
path: ["state", "set"],
|
|
240
|
+
summary: "Set one field of an artifact's state; viewers see it at once.",
|
|
241
|
+
args: [
|
|
242
|
+
{ name: "artifact", summary: "The artifact: its source path or its ID." },
|
|
243
|
+
{ name: "id", summary: "The field ID." },
|
|
244
|
+
{ name: "value", summary: "The value, as JSON." },
|
|
245
|
+
],
|
|
246
|
+
flags: writeFlags,
|
|
247
|
+
output: { type: "StatePatchResult", topic: "cli" },
|
|
248
|
+
exits: [EXIT.failure, EXIT.conflict],
|
|
249
|
+
example: `state set ./tasks priorities '["Ship the CLI"]'`,
|
|
250
|
+
async run(context) {
|
|
251
|
+
return patch(context, [
|
|
252
|
+
{ id: arg(context, "id"), value: parseJson(arg(context, "value"), "<value>") },
|
|
253
|
+
]);
|
|
254
|
+
},
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
/** `mostra state patch <artifact>`. */
|
|
258
|
+
export const statePatchCommand: CommandDef = defineCommand({
|
|
259
|
+
path: ["state", "patch"],
|
|
260
|
+
summary:
|
|
261
|
+
"Change several fields of an artifact's state in one revision: [{ id, value } | { id, delete: true }].",
|
|
262
|
+
args: [{ name: "artifact", summary: "The artifact: its source path or its ID." }],
|
|
263
|
+
flags: [
|
|
264
|
+
{ name: "ops", kind: "string", value: "<json>", summary: "The operations, as JSON." },
|
|
265
|
+
{
|
|
266
|
+
name: "file",
|
|
267
|
+
kind: "string",
|
|
268
|
+
value: "<path>",
|
|
269
|
+
summary: "Read the operations from a JSON file.",
|
|
270
|
+
},
|
|
271
|
+
{ name: "stdin", kind: "boolean", summary: "Read the operations from stdin." },
|
|
272
|
+
...writeFlags,
|
|
273
|
+
],
|
|
274
|
+
output: { type: "StatePatchResult", topic: "cli" },
|
|
275
|
+
exits: [EXIT.failure, EXIT.conflict],
|
|
276
|
+
example: `state patch ./tasks --file ops.json`,
|
|
277
|
+
readsStdin: (flags) => flags["stdin"] === true,
|
|
278
|
+
async run(context) {
|
|
279
|
+
const ops = await jsonInput(context, "ops");
|
|
280
|
+
const parsed = FieldOp.array().safeParse(ops);
|
|
281
|
+
if (!parsed.success || parsed.data.length === 0) {
|
|
282
|
+
throw usage(
|
|
283
|
+
"The operations must be a non-empty JSON array of { id, value } or { id, delete: true }.",
|
|
284
|
+
);
|
|
285
|
+
}
|
|
286
|
+
return patch(context, parsed.data);
|
|
287
|
+
},
|
|
288
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What commands share: naming artifacts and sources on the command line, and reading JSON values.
|
|
3
|
+
*/
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import { resolve } from "node:path";
|
|
6
|
+
import type { ArtifactRef, SourceRef } from "@mostra/protocol";
|
|
7
|
+
import type { CommandContext } from "../command.ts";
|
|
8
|
+
import { usage } from "../parse.ts";
|
|
9
|
+
|
|
10
|
+
const ARTIFACT_ID = /^art_[0-9A-HJKMNP-TV-Z]{26}$/;
|
|
11
|
+
|
|
12
|
+
/** An artifact named by its ID (`art_…`) or by a path, relative ones from `--cwd`. */
|
|
13
|
+
export function artifactRef(value: string, cwd: string): ArtifactRef {
|
|
14
|
+
return ARTIFACT_ID.test(value) ? { artifactId: value } : { path: resolve(cwd, value) };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** A source named the same way: a path, or the ID of an artifact (the path it was published from). */
|
|
18
|
+
export function sourceRef(value: string, cwd: string): SourceRef {
|
|
19
|
+
return ARTIFACT_ID.test(value) ? { artifactId: value } : { path: resolve(cwd, value) };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** A string argument the parser guaranteed. */
|
|
23
|
+
export function arg(context: CommandContext, name: string): string {
|
|
24
|
+
const value = context.args[name];
|
|
25
|
+
if (typeof value !== "string") throw usage(`<${name}> is missing.`);
|
|
26
|
+
return value;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** An optional string flag. */
|
|
30
|
+
export function flag(context: CommandContext, name: string): string | undefined {
|
|
31
|
+
const value = context.flags[name];
|
|
32
|
+
return typeof value === "string" ? value : undefined;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Parses a JSON value given on the command line, in a file or on stdin. */
|
|
36
|
+
export function parseJson(text: string, where: string): unknown {
|
|
37
|
+
try {
|
|
38
|
+
return JSON.parse(text);
|
|
39
|
+
} catch (error) {
|
|
40
|
+
throw usage(`${where} isn't JSON: ${(error as Error).message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Reads a JSON value from `--value`, `--file` or `--stdin`, exactly one of them. */
|
|
45
|
+
export async function jsonInput(context: CommandContext, valueFlag = "value"): Promise<unknown> {
|
|
46
|
+
const given = [valueFlag, "file", "stdin"].filter(
|
|
47
|
+
(name) => context.flags[name] !== undefined && context.flags[name] !== false,
|
|
48
|
+
);
|
|
49
|
+
if (given.length !== 1) {
|
|
50
|
+
throw usage(`Give the value with exactly one of --${valueFlag}, --file or --stdin.`);
|
|
51
|
+
}
|
|
52
|
+
if (context.flags["stdin"] === true) return parseJson(await context.readStdin(), "stdin");
|
|
53
|
+
const file = flag(context, "file");
|
|
54
|
+
if (file !== undefined) {
|
|
55
|
+
let text: string;
|
|
56
|
+
try {
|
|
57
|
+
text = readFileSync(resolve(context.cwd, file), "utf8");
|
|
58
|
+
} catch (error) {
|
|
59
|
+
throw usage(
|
|
60
|
+
`--file ${file} can't be read (${(error as NodeJS.ErrnoException).code ?? "error"}).`,
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return parseJson(text, file);
|
|
64
|
+
}
|
|
65
|
+
return parseJson(flag(context, valueFlag) ?? "", `--${valueFlag}`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Indented JSON, for human output of structured results. */
|
|
69
|
+
export function pretty(value: unknown): string {
|
|
70
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
71
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mostra open`: the artifact's link on stdout, and a browser opened on a one-time launch URL that
|
|
3
|
+
* never carries the link's secret (D140 item 15), handed over in a page, never on a command line
|
|
4
|
+
* (`browser.ts`). A client of `view/default`, `view/url`,
|
|
5
|
+
* `view/rotate`, `view/revoke` and `view/launch`.
|
|
6
|
+
*/
|
|
7
|
+
import {
|
|
8
|
+
type CapabilityLink,
|
|
9
|
+
type CapabilityScope,
|
|
10
|
+
viewDefaultMethod,
|
|
11
|
+
viewLaunchMethod,
|
|
12
|
+
viewRevokeMethod,
|
|
13
|
+
viewRotateMethod,
|
|
14
|
+
viewUrlMethod,
|
|
15
|
+
} from "@mostra/protocol";
|
|
16
|
+
import { openInBrowser } from "../browser.ts";
|
|
17
|
+
import { type CommandContext, type CommandDef, defineCommand } from "../command.ts";
|
|
18
|
+
import { EXIT } from "../exit.ts";
|
|
19
|
+
import type { Environment } from "../home.ts";
|
|
20
|
+
import { usage } from "../parse.ts";
|
|
21
|
+
import { arg, artifactRef, flag } from "./shared.ts";
|
|
22
|
+
|
|
23
|
+
/** Whether this session can show a browser: not over SSH without a display. */
|
|
24
|
+
export function hasDisplay(env: Environment, platform: string = process.platform): boolean {
|
|
25
|
+
const ssh = env["SSH_CONNECTION"] !== undefined || env["SSH_TTY"] !== undefined;
|
|
26
|
+
if (platform === "darwin") return !ssh;
|
|
27
|
+
return env["DISPLAY"] !== undefined || env["WAYLAND_DISPLAY"] !== undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Whether `open` should launch a browser: a terminal without `--json`, or `--browser`. */
|
|
31
|
+
function wantsBrowser(context: CommandContext): boolean {
|
|
32
|
+
if (context.flags["no-browser"] === true) return false;
|
|
33
|
+
if (context.flags["browser"] === true) return true;
|
|
34
|
+
return context.tty.stdout && !context.json && hasDisplay(context.env);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The link, opened in a browser when wanted. */
|
|
38
|
+
async function show(
|
|
39
|
+
context: CommandContext,
|
|
40
|
+
link: CapabilityLink,
|
|
41
|
+
): Promise<{ result: CapabilityLink & { readonly opened: boolean }; human: string }> {
|
|
42
|
+
let opened = false;
|
|
43
|
+
if (wantsBrowser(context)) {
|
|
44
|
+
const launch = await context.call(viewLaunchMethod, { tokenId: link.tokenId });
|
|
45
|
+
// The launch URL reaches the browser through a 0600 page, never its arguments.
|
|
46
|
+
opened = await openInBrowser(launch.url, context.env);
|
|
47
|
+
}
|
|
48
|
+
return { result: { ...link, opened }, human: `${link.url}\n` };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** `mostra open <artifact>`. */
|
|
52
|
+
export const openCommand: CommandDef = defineCommand({
|
|
53
|
+
path: ["open"],
|
|
54
|
+
summary:
|
|
55
|
+
"Print an artifact's capability link and open it in a browser; or mint, rotate or revoke a link.",
|
|
56
|
+
args: [{ name: "artifact", summary: "The artifact: its source path or its ID." }],
|
|
57
|
+
flags: [
|
|
58
|
+
{
|
|
59
|
+
name: "new",
|
|
60
|
+
kind: "boolean",
|
|
61
|
+
summary: "Mint a separate link instead of the artifact's default one.",
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
name: "version",
|
|
65
|
+
kind: "string",
|
|
66
|
+
value: "<n|id>",
|
|
67
|
+
summary: "A new link that opens only this version.",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: "scope",
|
|
71
|
+
kind: "list",
|
|
72
|
+
value: "<a,b>",
|
|
73
|
+
summary: "A new link's scopes: view, draft, submit, review, signal, action:*. Default: all.",
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
name: "ttl",
|
|
77
|
+
kind: "duration",
|
|
78
|
+
value: "<d>",
|
|
79
|
+
summary: "A new link that expires after this long, such as 24h.",
|
|
80
|
+
},
|
|
81
|
+
{
|
|
82
|
+
name: "viewer",
|
|
83
|
+
kind: "string",
|
|
84
|
+
value: "<ref>",
|
|
85
|
+
summary: "A new link that acts for this person, by your own stable key for them.",
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
name: "viewer-name",
|
|
89
|
+
kind: "string",
|
|
90
|
+
value: "<name>",
|
|
91
|
+
summary: "The display name of --viewer.",
|
|
92
|
+
},
|
|
93
|
+
{
|
|
94
|
+
name: "rotate",
|
|
95
|
+
kind: "string",
|
|
96
|
+
value: "<tokenId>",
|
|
97
|
+
summary: "Replace this link with a new one; the old one stops working.",
|
|
98
|
+
},
|
|
99
|
+
{ name: "revoke", kind: "string", value: "<tokenId>", summary: "Stop this link from working." },
|
|
100
|
+
{ name: "browser", kind: "boolean", summary: "Open a browser even without a terminal." },
|
|
101
|
+
{ name: "no-browser", kind: "boolean", summary: "Only print the link." },
|
|
102
|
+
{
|
|
103
|
+
name: "request-id",
|
|
104
|
+
kind: "string",
|
|
105
|
+
value: "<id>",
|
|
106
|
+
summary: "Make a retried --new return the same link.",
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
output: { type: "CapabilityLink", topic: "cli" },
|
|
110
|
+
exits: [EXIT.failure],
|
|
111
|
+
example: "open ./sidebar-review",
|
|
112
|
+
async run(context) {
|
|
113
|
+
const artifact = artifactRef(arg(context, "artifact"), context.cwd);
|
|
114
|
+
const revoke = flag(context, "revoke");
|
|
115
|
+
const rotate = flag(context, "rotate");
|
|
116
|
+
if (revoke !== undefined && rotate !== undefined)
|
|
117
|
+
throw usage("Give --rotate or --revoke, not both.");
|
|
118
|
+
if (revoke !== undefined) {
|
|
119
|
+
await context.call(viewRevokeMethod, { tokenId: revoke });
|
|
120
|
+
return { result: { tokenId: revoke, revoked: true }, human: `Revoked ${revoke}\n` };
|
|
121
|
+
}
|
|
122
|
+
if (rotate !== undefined)
|
|
123
|
+
return show(context, await context.call(viewRotateMethod, { tokenId: rotate }));
|
|
124
|
+
const version = flag(context, "version");
|
|
125
|
+
const scopes = context.flags["scope"] as readonly string[] | undefined;
|
|
126
|
+
const ttl = flag(context, "ttl");
|
|
127
|
+
const viewerRef = flag(context, "viewer");
|
|
128
|
+
const viewerName = flag(context, "viewer-name");
|
|
129
|
+
const requestId = flag(context, "request-id");
|
|
130
|
+
if (viewerName !== undefined && viewerRef === undefined)
|
|
131
|
+
throw usage("--viewer-name needs --viewer.");
|
|
132
|
+
const fresh =
|
|
133
|
+
context.flags["new"] === true ||
|
|
134
|
+
version !== undefined ||
|
|
135
|
+
scopes !== undefined ||
|
|
136
|
+
ttl !== undefined ||
|
|
137
|
+
viewerRef !== undefined;
|
|
138
|
+
if (!fresh) return show(context, await context.call(viewDefaultMethod, { artifact }));
|
|
139
|
+
const link = await context.call(viewUrlMethod, {
|
|
140
|
+
artifact,
|
|
141
|
+
...(version !== undefined
|
|
142
|
+
? { version: /^[0-9]+$/.test(version) ? Number(version) : version }
|
|
143
|
+
: {}),
|
|
144
|
+
...(scopes !== undefined ? { scopes: scopes as CapabilityScope[] } : {}),
|
|
145
|
+
...(ttl !== undefined ? { ttl: ttl as `${number}${"s" | "m" | "h" | "d"}` } : {}),
|
|
146
|
+
...(viewerRef !== undefined
|
|
147
|
+
? { viewer: { ref: viewerRef, ...(viewerName !== undefined ? { name: viewerName } : {}) } }
|
|
148
|
+
: {}),
|
|
149
|
+
...(requestId !== undefined ? { requestId } : {}),
|
|
150
|
+
});
|
|
151
|
+
return show(context, link);
|
|
152
|
+
},
|
|
153
|
+
});
|
package/src/docs.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `mostra docs` (D140 item 17): the reference that ships with the CLI, by stable slug. The
|
|
3
|
+
* diagnostic pages are keyed by code (`mostra docs MOSTRA_DEP_MISSING`, as hints say), beside the
|
|
4
|
+
* CLI reference and the telemetry page. This index is the one source WP-15's `docs_search` and
|
|
5
|
+
* WP-26's site read.
|
|
6
|
+
*/
|
|
7
|
+
import { existsSync, readdirSync, readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
import { DEFAULT_DOCS_BASE } from "@mostra/protocol";
|
|
10
|
+
import { fromSource, packageRoot } from "./version.ts";
|
|
11
|
+
|
|
12
|
+
/** One topic of the reference. */
|
|
13
|
+
export interface DocsTopic {
|
|
14
|
+
/** Its stable slug: `cli`, `telemetry`, `errors`, or a diagnostic code. */
|
|
15
|
+
readonly topic: string;
|
|
16
|
+
/** Its title, from the page's first heading. */
|
|
17
|
+
readonly title: string;
|
|
18
|
+
/** Where the docs site serves it. */
|
|
19
|
+
readonly url: string;
|
|
20
|
+
/** Its Markdown file. */
|
|
21
|
+
readonly file: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Where the reference is: bundled in the build (`dist/reference`), or the repository's. */
|
|
25
|
+
export function referenceDir(): string {
|
|
26
|
+
const bundled = join(packageRoot(), "dist", "reference");
|
|
27
|
+
return !fromSource() && existsSync(bundled)
|
|
28
|
+
? bundled
|
|
29
|
+
: join(packageRoot(), "..", "..", "docs", "reference");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function titleOf(file: string): string {
|
|
33
|
+
const first = readFileSync(file, "utf8")
|
|
34
|
+
.split("\n")
|
|
35
|
+
.find((line) => line.startsWith("# "));
|
|
36
|
+
return first ? first.slice(2).trim() : "";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Every topic, the pages first and then the codes, sorted. */
|
|
40
|
+
export function docsIndex(dir: string = referenceDir()): DocsTopic[] {
|
|
41
|
+
const base = DEFAULT_DOCS_BASE.replace(/\/+$/, "");
|
|
42
|
+
const pages: DocsTopic[] = [];
|
|
43
|
+
for (const topic of ["cli", "telemetry"]) {
|
|
44
|
+
const file = join(dir, `${topic}.md`);
|
|
45
|
+
if (existsSync(file))
|
|
46
|
+
pages.push({ topic, title: titleOf(file), url: `${base}/${topic}`, file });
|
|
47
|
+
}
|
|
48
|
+
const errorsDir = join(dir, "errors");
|
|
49
|
+
const codes = existsSync(errorsDir)
|
|
50
|
+
? readdirSync(errorsDir)
|
|
51
|
+
.filter((name) => /^MOSTRA_[A-Z0-9_]+\.md$/.test(name))
|
|
52
|
+
.sort()
|
|
53
|
+
: [];
|
|
54
|
+
const errors: DocsTopic[] = codes.map((name) => {
|
|
55
|
+
const code = name.slice(0, -3);
|
|
56
|
+
const file = join(errorsDir, name);
|
|
57
|
+
return { topic: code, title: titleOf(file), url: `${base}/errors/${code}`, file };
|
|
58
|
+
});
|
|
59
|
+
return [...pages, ...errors];
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** The index as Markdown: what `mostra docs` prints without a topic. */
|
|
63
|
+
export function indexMarkdown(topics: readonly DocsTopic[]): string {
|
|
64
|
+
const pages = topics.filter((topic) => !topic.topic.startsWith("MOSTRA_"));
|
|
65
|
+
const codes = topics.filter((topic) => topic.topic.startsWith("MOSTRA_"));
|
|
66
|
+
return [
|
|
67
|
+
"# Mostra reference",
|
|
68
|
+
"",
|
|
69
|
+
"Run `mostra docs <topic>` to read one. Diagnostic codes are topics too.",
|
|
70
|
+
"",
|
|
71
|
+
...pages.map((topic) => `- \`${topic.topic}\`: ${topic.title}`),
|
|
72
|
+
"",
|
|
73
|
+
"## Diagnostic codes",
|
|
74
|
+
"",
|
|
75
|
+
...codes.map((topic) => `- \`${topic.topic}\`: ${topic.title}`),
|
|
76
|
+
"",
|
|
77
|
+
].join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** The topics nearest an unknown one, for its usage error. */
|
|
81
|
+
export function nearestTopics(wanted: string, topics: readonly DocsTopic[], count = 3): string[] {
|
|
82
|
+
const target = wanted.toUpperCase();
|
|
83
|
+
const scored = topics.map((topic) => {
|
|
84
|
+
const name = topic.topic.toUpperCase();
|
|
85
|
+
const contains = name.includes(target) || target.includes(name) ? 0 : 1;
|
|
86
|
+
return { topic: topic.topic, score: contains * 100 + distance(name, target) };
|
|
87
|
+
});
|
|
88
|
+
return scored
|
|
89
|
+
.sort((a, b) => a.score - b.score || (a.topic < b.topic ? -1 : 1))
|
|
90
|
+
.slice(0, count)
|
|
91
|
+
.map((item) => item.topic);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Levenshtein distance. */
|
|
95
|
+
function distance(a: string, b: string): number {
|
|
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] as number;
|
|
99
|
+
row[0] = i;
|
|
100
|
+
for (let j = 1; j <= b.length; j += 1) {
|
|
101
|
+
const current = row[j] as number;
|
|
102
|
+
row[j] = Math.min(
|
|
103
|
+
(row[j] as number) + 1,
|
|
104
|
+
(row[j - 1] as number) + 1,
|
|
105
|
+
previous + (a[i - 1] === b[j - 1] ? 0 : 1),
|
|
106
|
+
);
|
|
107
|
+
previous = current;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return row[b.length] as number;
|
|
111
|
+
}
|