@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
package/src/client.ts
ADDED
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The CLI's connection to its home's instance (D45, D81, INTERFACES §9): it attaches to the
|
|
3
|
+
* daemon recorded in `instance.json`, or starts one (`mostra-runtime daemon`, which itself
|
|
4
|
+
* attaches when another starter won), and opens a session as `mostra-cli`. The SDK (WP-11) does
|
|
5
|
+
* the same few steps over the same protocol.
|
|
6
|
+
*/
|
|
7
|
+
import { spawn } from "node:child_process";
|
|
8
|
+
import { readFileSync } from "node:fs";
|
|
9
|
+
import { createConnection, type Socket } from "node:net";
|
|
10
|
+
import { join } from "node:path";
|
|
11
|
+
import {
|
|
12
|
+
ARTIFACT_PROGRESS,
|
|
13
|
+
CLI_CLIENT_NAME,
|
|
14
|
+
createDiagnostic,
|
|
15
|
+
DEV_NOTIFY,
|
|
16
|
+
type Diagnostic,
|
|
17
|
+
encodeLine,
|
|
18
|
+
HOST_PROTOCOL_REVISIONS,
|
|
19
|
+
LineSplitter,
|
|
20
|
+
type MethodDefinition,
|
|
21
|
+
type MostraInitializeResult,
|
|
22
|
+
mostraInitializeMethod,
|
|
23
|
+
RpcError,
|
|
24
|
+
RpcPeer,
|
|
25
|
+
} from "@mostra/protocol";
|
|
26
|
+
import { type Environment, locateRuntime } from "./home.ts";
|
|
27
|
+
import { cliVersion } from "./version.ts";
|
|
28
|
+
|
|
29
|
+
/** The instance a command reached: the `instance` of the `--json` envelope (the attach target). */
|
|
30
|
+
export interface InstanceSummary {
|
|
31
|
+
/** The home it owns. */
|
|
32
|
+
readonly home: string;
|
|
33
|
+
/** The control socket the CLI reached. */
|
|
34
|
+
readonly socket: string;
|
|
35
|
+
/** Its process ID. */
|
|
36
|
+
readonly pid: number;
|
|
37
|
+
/** `daemon`, or `host` for an instance a host spawned (telemetry sends nothing then, D140). */
|
|
38
|
+
readonly mode: "host" | "daemon";
|
|
39
|
+
/** The runtime's release. */
|
|
40
|
+
readonly version: string;
|
|
41
|
+
/** Whether this command started the daemon. */
|
|
42
|
+
readonly started: boolean;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** An open session with the home's instance. */
|
|
46
|
+
export interface InstanceConnection {
|
|
47
|
+
/** What the instance is. */
|
|
48
|
+
readonly summary: InstanceSummary;
|
|
49
|
+
/** `mostra/initialize`'s warnings. */
|
|
50
|
+
readonly warnings: readonly Diagnostic[];
|
|
51
|
+
/** Calls a host protocol method. */
|
|
52
|
+
call<P, R>(method: MethodDefinition<P, R>, params: P, timeoutMs?: number): Promise<R>;
|
|
53
|
+
/** Registers a handler for a notification (`artifact/progress`, `dev/notify`). */
|
|
54
|
+
onNotification(method: string, handler: (params: unknown) => void): void;
|
|
55
|
+
/** Resolves when the connection closes. */
|
|
56
|
+
readonly closed: Promise<void>;
|
|
57
|
+
/** Ends the session. */
|
|
58
|
+
close(): void;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** How to reach an instance. */
|
|
62
|
+
export interface ConnectOptions {
|
|
63
|
+
readonly home: string;
|
|
64
|
+
readonly env: Environment;
|
|
65
|
+
/** Start the daemon when none runs. Default true. */
|
|
66
|
+
readonly autostart?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* Told when this connection starts the home's daemon (`true`) and when the start ends
|
|
69
|
+
* (`false`), so the command's own `--timeout` names the start if it runs out during it.
|
|
70
|
+
*/
|
|
71
|
+
readonly onStarting?: (starting: boolean) => void;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** How long one attempt to reach a recorded socket waits. */
|
|
75
|
+
const ATTACH_TIMEOUT_MS = 5000;
|
|
76
|
+
/** How long a daemon this command starts may take to say where it listens. */
|
|
77
|
+
const START_TIMEOUT_MS = 30_000;
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Opens a session with the home's instance, starting its daemon when none runs. Returns
|
|
81
|
+
* `undefined` when `autostart` is false and no instance runs. Throws the start's diagnostic when
|
|
82
|
+
* the daemon can't start (`MOSTRA_HOME_LOCKED`, `MOSTRA_PLATFORM_UNSUPPORTED` …).
|
|
83
|
+
*/
|
|
84
|
+
export async function connect(options: ConnectOptions): Promise<InstanceConnection | undefined> {
|
|
85
|
+
const recorded = readSocket(options.home);
|
|
86
|
+
if (recorded !== undefined) {
|
|
87
|
+
const session = await open(recorded, options, false).catch(() => undefined);
|
|
88
|
+
if (session) return session;
|
|
89
|
+
}
|
|
90
|
+
if (options.autostart === false) return undefined;
|
|
91
|
+
options.onStarting?.(true);
|
|
92
|
+
let started: DaemonLine;
|
|
93
|
+
try {
|
|
94
|
+
started = await startDaemon(options);
|
|
95
|
+
} finally {
|
|
96
|
+
options.onStarting?.(false);
|
|
97
|
+
}
|
|
98
|
+
return open(started.socket, options, started.status === "started");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** The socket `instance.json` records (D81), if the file exists and reads. */
|
|
102
|
+
function readSocket(home: string): string | undefined {
|
|
103
|
+
try {
|
|
104
|
+
const record = JSON.parse(readFileSync(join(home, "instance.json"), "utf8")) as {
|
|
105
|
+
socket?: unknown;
|
|
106
|
+
};
|
|
107
|
+
return typeof record.socket === "string" ? record.socket : undefined;
|
|
108
|
+
} catch {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** What `mostra-runtime daemon` prints on its one line. */
|
|
114
|
+
interface DaemonLine {
|
|
115
|
+
readonly status: "started" | "attached" | "failed";
|
|
116
|
+
readonly socket: string;
|
|
117
|
+
readonly pid?: number;
|
|
118
|
+
readonly diagnostic?: Diagnostic;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Starts the home's daemon, detached from this process, and reads the one line it prints: where
|
|
123
|
+
* it listens, or why it couldn't start. A daemon another starter won prints `attached` and exits.
|
|
124
|
+
*/
|
|
125
|
+
export async function startDaemon(options: ConnectOptions): Promise<DaemonLine> {
|
|
126
|
+
// The start's own limit. A shorter `--timeout` is the command's: its one timer names the
|
|
127
|
+
// start when it runs out during it (run.ts), so two timers never race for one deadline.
|
|
128
|
+
const deadlineAt = Date.now() + START_TIMEOUT_MS;
|
|
129
|
+
const timedOut = (): RpcError =>
|
|
130
|
+
new RpcError(
|
|
131
|
+
createDiagnostic("MOSTRA_HOST_TIMEOUT", {
|
|
132
|
+
message: `The home's daemon didn't start within ${formatDuration(START_TIMEOUT_MS)}.`,
|
|
133
|
+
}),
|
|
134
|
+
);
|
|
135
|
+
const runtime = await locateRuntime(options.env);
|
|
136
|
+
if (Date.now() >= deadlineAt) throw timedOut();
|
|
137
|
+
const args = [
|
|
138
|
+
runtime.entry,
|
|
139
|
+
"daemon",
|
|
140
|
+
// The home this CLI resolved, so the daemon owns exactly it whatever its own environment.
|
|
141
|
+
"--home",
|
|
142
|
+
options.home,
|
|
143
|
+
];
|
|
144
|
+
return new Promise((resolve, reject) => {
|
|
145
|
+
const child = spawn(runtime.node, args, {
|
|
146
|
+
detached: true,
|
|
147
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
148
|
+
env: { ...process.env, ...options.env } as NodeJS.ProcessEnv,
|
|
149
|
+
});
|
|
150
|
+
const splitter = new LineSplitter();
|
|
151
|
+
let settled = false;
|
|
152
|
+
const finish = (outcome: () => void): void => {
|
|
153
|
+
if (settled) return;
|
|
154
|
+
settled = true;
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
child.stdout?.removeAllListeners("data");
|
|
157
|
+
child.stdout?.destroy();
|
|
158
|
+
child.unref();
|
|
159
|
+
outcome();
|
|
160
|
+
};
|
|
161
|
+
const timer = setTimeout(
|
|
162
|
+
() => finish(() => reject(timedOut())),
|
|
163
|
+
Math.max(0, deadlineAt - Date.now()),
|
|
164
|
+
);
|
|
165
|
+
child.on("error", (error) =>
|
|
166
|
+
finish(() =>
|
|
167
|
+
reject(
|
|
168
|
+
new RpcError(
|
|
169
|
+
createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", {
|
|
170
|
+
message: `The runtime couldn't be started: ${error.message}`,
|
|
171
|
+
}),
|
|
172
|
+
),
|
|
173
|
+
),
|
|
174
|
+
),
|
|
175
|
+
);
|
|
176
|
+
// `close`, not `exit`: a daemon that found another one prints `attached` and exits at once,
|
|
177
|
+
// and its line may still be in the pipe when `exit` fires.
|
|
178
|
+
child.on("close", (code) =>
|
|
179
|
+
finish(() =>
|
|
180
|
+
reject(
|
|
181
|
+
new RpcError(
|
|
182
|
+
createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", {
|
|
183
|
+
message: `The home's daemon exited (${code ?? "signal"}) before it said where it listens.`,
|
|
184
|
+
}),
|
|
185
|
+
),
|
|
186
|
+
),
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
child.stdout?.setEncoding("utf8");
|
|
190
|
+
child.stdout?.on("data", (chunk: string) => {
|
|
191
|
+
for (const line of splitter.push(chunk)) {
|
|
192
|
+
let parsed: DaemonLine;
|
|
193
|
+
try {
|
|
194
|
+
parsed = JSON.parse(line) as DaemonLine;
|
|
195
|
+
} catch {
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
finish(() => {
|
|
199
|
+
if (parsed.status === "failed") {
|
|
200
|
+
reject(
|
|
201
|
+
new RpcError(
|
|
202
|
+
parsed.diagnostic ??
|
|
203
|
+
createDiagnostic("MOSTRA_HOST_INTERNAL_ERROR", {
|
|
204
|
+
message: "The home's daemon failed to start.",
|
|
205
|
+
}),
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
} else resolve(parsed);
|
|
209
|
+
});
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
async function open(
|
|
217
|
+
socketPath: string,
|
|
218
|
+
options: ConnectOptions,
|
|
219
|
+
started: boolean,
|
|
220
|
+
): Promise<InstanceConnection> {
|
|
221
|
+
const socket = await new Promise<Socket>((resolve, reject) => {
|
|
222
|
+
const connection = createConnection(socketPath);
|
|
223
|
+
const timer = setTimeout(() => {
|
|
224
|
+
connection.destroy();
|
|
225
|
+
reject(closed(socketPath, "timed out"));
|
|
226
|
+
}, ATTACH_TIMEOUT_MS);
|
|
227
|
+
connection.once("error", (error: NodeJS.ErrnoException) => {
|
|
228
|
+
clearTimeout(timer);
|
|
229
|
+
reject(closed(socketPath, error.code ?? "failed"));
|
|
230
|
+
});
|
|
231
|
+
connection.once("connect", () => {
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
resolve(connection);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
const handlers = new Map<string, (params: unknown) => void>();
|
|
237
|
+
const peer = new RpcPeer({
|
|
238
|
+
send: (message) => {
|
|
239
|
+
if (!socket.destroyed) socket.write(encodeLine(message));
|
|
240
|
+
},
|
|
241
|
+
onNotification: (method, params) => handlers.get(method)?.(params),
|
|
242
|
+
});
|
|
243
|
+
let markClosed: () => void = () => {};
|
|
244
|
+
const closedPromise = new Promise<void>((done) => {
|
|
245
|
+
markClosed = done;
|
|
246
|
+
});
|
|
247
|
+
const splitter = new LineSplitter();
|
|
248
|
+
socket.setEncoding("utf8");
|
|
249
|
+
socket.on("data", (chunk: string) => {
|
|
250
|
+
for (const line of splitter.push(chunk)) peer.receiveLine(line);
|
|
251
|
+
});
|
|
252
|
+
const end = (): void => {
|
|
253
|
+
peer.close();
|
|
254
|
+
markClosed();
|
|
255
|
+
};
|
|
256
|
+
socket.on("close", end);
|
|
257
|
+
socket.on("error", () => socket.destroy());
|
|
258
|
+
let result: MostraInitializeResult;
|
|
259
|
+
try {
|
|
260
|
+
result = await peer.call(
|
|
261
|
+
mostraInitializeMethod,
|
|
262
|
+
{
|
|
263
|
+
client: { name: CLI_CLIENT_NAME, version: cliVersion() },
|
|
264
|
+
revisions: [...HOST_PROTOCOL_REVISIONS],
|
|
265
|
+
capabilities: { notifications: [ARTIFACT_PROGRESS, DEV_NOTIFY] },
|
|
266
|
+
ports: [],
|
|
267
|
+
// A check, never a setting (D84): the instance refuses a session for another home.
|
|
268
|
+
home: { path: options.home },
|
|
269
|
+
},
|
|
270
|
+
{ timeoutMs: ATTACH_TIMEOUT_MS },
|
|
271
|
+
);
|
|
272
|
+
} catch (error) {
|
|
273
|
+
socket.destroy();
|
|
274
|
+
throw error;
|
|
275
|
+
}
|
|
276
|
+
return {
|
|
277
|
+
summary: {
|
|
278
|
+
home: result.instance.home,
|
|
279
|
+
socket: socketPath,
|
|
280
|
+
pid: result.instance.pid,
|
|
281
|
+
mode: result.instance.mode,
|
|
282
|
+
version: result.instance.version,
|
|
283
|
+
started,
|
|
284
|
+
},
|
|
285
|
+
warnings: result.warnings,
|
|
286
|
+
call: (method, params, timeoutMs) =>
|
|
287
|
+
peer.call(method, params, timeoutMs !== undefined ? { timeoutMs } : {}),
|
|
288
|
+
onNotification: (method, handler) => {
|
|
289
|
+
handlers.set(method, handler);
|
|
290
|
+
},
|
|
291
|
+
closed: closedPromise,
|
|
292
|
+
close: () => {
|
|
293
|
+
peer.close();
|
|
294
|
+
socket.end();
|
|
295
|
+
socket.destroy();
|
|
296
|
+
},
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function closed(path: string, why: string): RpcError {
|
|
301
|
+
return new RpcError(
|
|
302
|
+
createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", {
|
|
303
|
+
message: `Couldn't reach the instance at ${path} (${why}).`,
|
|
304
|
+
}),
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** A limit as people write it: `250 ms`, `5 s`, `1.5 s`. */
|
|
309
|
+
function formatDuration(ms: number): string {
|
|
310
|
+
if (ms < 1000) return `${ms} ms`;
|
|
311
|
+
const seconds = ms / 1000;
|
|
312
|
+
return `${Number.isInteger(seconds) ? seconds : seconds.toFixed(1)} s`;
|
|
313
|
+
}
|
package/src/command.ts
ADDED
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a command is (D140 items 13, 16): a definition the parser, the help, the reference page,
|
|
3
|
+
* `--help --json`, telemetry's command names and the contract tests all read. Commands never
|
|
4
|
+
* print or exit on their own; they return a result (or throw a diagnostic) and the runner decides
|
|
5
|
+
* the envelope, the human text and the exit code.
|
|
6
|
+
*/
|
|
7
|
+
import type { Diagnostic, MethodDefinition } from "@mostra/protocol";
|
|
8
|
+
import type { InstanceConnection } from "./client.ts";
|
|
9
|
+
|
|
10
|
+
/** A flag's value kind. Every flag is `--long`; booleans take no value. */
|
|
11
|
+
export type FlagKind = "boolean" | "string" | "number" | "duration" | "list";
|
|
12
|
+
|
|
13
|
+
/** One flag of a command. */
|
|
14
|
+
export interface FlagDef {
|
|
15
|
+
/** The flag's name, without the dashes. */
|
|
16
|
+
readonly name: string;
|
|
17
|
+
/** What it takes. */
|
|
18
|
+
readonly kind: FlagKind;
|
|
19
|
+
/** One line for help: what it does. */
|
|
20
|
+
readonly summary: string;
|
|
21
|
+
/** The placeholder in usage and help: `<path>`, `<d>` … */
|
|
22
|
+
readonly value?: string;
|
|
23
|
+
/** The allowed values, for a closed set. */
|
|
24
|
+
readonly choices?: readonly string[];
|
|
25
|
+
/** The default, as help shows it. */
|
|
26
|
+
readonly default?: string;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** One positional argument. */
|
|
30
|
+
export interface ArgDef {
|
|
31
|
+
/** The argument's name, as help shows it. */
|
|
32
|
+
readonly name: string;
|
|
33
|
+
/** One line for help: what it is. */
|
|
34
|
+
readonly summary: string;
|
|
35
|
+
/** Whether it may be left out. */
|
|
36
|
+
readonly optional?: boolean;
|
|
37
|
+
/** Whether it takes every remaining word (`<package…>`). */
|
|
38
|
+
readonly variadic?: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** What the runner gives a command. */
|
|
42
|
+
export interface CommandContext {
|
|
43
|
+
/** The positional arguments, after the command path, by name. */
|
|
44
|
+
readonly args: Readonly<Record<string, string | readonly string[] | undefined>>;
|
|
45
|
+
/** The command's flags, parsed by kind (durations in milliseconds, lists as arrays). */
|
|
46
|
+
readonly flags: Readonly<
|
|
47
|
+
Record<string, string | number | boolean | readonly string[] | undefined>
|
|
48
|
+
>;
|
|
49
|
+
/** The working directory relative paths resolve from (`--cwd`, else the process's). */
|
|
50
|
+
readonly cwd: string;
|
|
51
|
+
/** Whether `--json` was given. */
|
|
52
|
+
readonly json: boolean;
|
|
53
|
+
/** The request timeout from `--timeout`, in milliseconds. */
|
|
54
|
+
readonly timeoutMs: number | undefined;
|
|
55
|
+
/** Calls a host protocol method on the home's instance, starting its daemon when needed. */
|
|
56
|
+
call<P, R>(method: MethodDefinition<P, R>, params: P): Promise<R>;
|
|
57
|
+
/** Reads the whole of stdin, for commands that take a value there. */
|
|
58
|
+
readStdin(): Promise<string>;
|
|
59
|
+
/** Writes one line of progress or a log line to stderr (never stdout). */
|
|
60
|
+
log(line: string): void;
|
|
61
|
+
/** Emits one event of a streaming command (an NDJSON line in `--json` mode). */
|
|
62
|
+
emit(event: unknown, human?: string): void;
|
|
63
|
+
/** Adds a warning to the envelope (and stderr). */
|
|
64
|
+
warn(diagnostic: Diagnostic): void;
|
|
65
|
+
/** The environment the CLI runs in. */
|
|
66
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
67
|
+
/** Whether stdout and stdin are terminals. */
|
|
68
|
+
readonly tty: { readonly stdin: boolean; readonly stdout: boolean; readonly stderr: boolean };
|
|
69
|
+
/** The resolved home and the CLI's own facts, for commands that report them. */
|
|
70
|
+
readonly home: string;
|
|
71
|
+
/** Records a telemetry event of this command (D140 item 8), when telemetry allows it. */
|
|
72
|
+
record(event: TelemetryEventInput): void;
|
|
73
|
+
/**
|
|
74
|
+
* The session with the home's instance, starting its daemon unless `autostart` is false; then
|
|
75
|
+
* `undefined` when none runs. `call` uses the same session.
|
|
76
|
+
*/
|
|
77
|
+
connection(autostart?: boolean): Promise<InstanceConnection | undefined>;
|
|
78
|
+
/** Runs the runtime binary (`mostra-runtime …`) and returns its exit code and stdout. */
|
|
79
|
+
runtime(
|
|
80
|
+
args: readonly string[],
|
|
81
|
+
stdio?: "pipe" | "inherit",
|
|
82
|
+
): Promise<{ code: number; stdout: string }>;
|
|
83
|
+
/** The CLI's config directory (D140 item 5). */
|
|
84
|
+
readonly configDir: string;
|
|
85
|
+
/** Writes to stderr. */
|
|
86
|
+
readonly stderr: (text: string) => void;
|
|
87
|
+
/** Writes to stdout as the command runs (`host`, `dev`'s stream without `--json`). */
|
|
88
|
+
readonly stdout: (text: string) => void;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** A command-specific telemetry event: its name and its own fields. */
|
|
92
|
+
export interface TelemetryEventInput {
|
|
93
|
+
/** The event's name. */
|
|
94
|
+
readonly event: "verify.result" | "publish.done";
|
|
95
|
+
/** Its own fields; the schema keeps only the published ones. */
|
|
96
|
+
readonly fields: Readonly<Record<string, unknown>>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** What a command's run returns. */
|
|
100
|
+
export interface CommandOutcome {
|
|
101
|
+
/** The `result` of the envelope. */
|
|
102
|
+
readonly result: unknown;
|
|
103
|
+
/** The text for people (stdout); default: the result as indented JSON. */
|
|
104
|
+
readonly human?: string;
|
|
105
|
+
/** The exit code when it isn't 0: a failing verdict (1), `still_waiting` (5), conflicts (6). */
|
|
106
|
+
readonly exit?: number;
|
|
107
|
+
/** The command wrote its own stdout (`host`): the runner prints no envelope and no text. */
|
|
108
|
+
readonly raw?: boolean;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** One command. */
|
|
112
|
+
export interface CommandDef {
|
|
113
|
+
/** Its words after `mostra`: `["daemon", "install"]`. */
|
|
114
|
+
readonly path: readonly string[];
|
|
115
|
+
/** One line: what it does. */
|
|
116
|
+
readonly summary: string;
|
|
117
|
+
/** Its positional arguments, in order. */
|
|
118
|
+
readonly args?: readonly ArgDef[];
|
|
119
|
+
/** Its own flags, besides the global ones. */
|
|
120
|
+
readonly flags?: readonly FlagDef[];
|
|
121
|
+
/** The result type `--json` puts in `result`, and its `mostra docs` topic. */
|
|
122
|
+
readonly output: { readonly type: string; readonly topic: string };
|
|
123
|
+
/** The exit codes it can end with, besides 2 and 3, which every command can. */
|
|
124
|
+
readonly exits: readonly number[];
|
|
125
|
+
/** One example invocation, without `mostra`. */
|
|
126
|
+
readonly example: string;
|
|
127
|
+
/** It reads stdin, so telemetry's question is never asked before it (D140 item 11). */
|
|
128
|
+
readonly readsStdin?: boolean | ((flags: CommandContext["flags"]) => boolean);
|
|
129
|
+
/** It never reaches an instance (`docs`, `telemetry`): no `instance` in its envelope. */
|
|
130
|
+
readonly local?: boolean;
|
|
131
|
+
/** It writes a stream of events before its result (NDJSON in `--json` mode). */
|
|
132
|
+
readonly streams?: boolean;
|
|
133
|
+
/** Telemetry and its question leave it alone (`telemetry`, `docs`, D140 item 11). */
|
|
134
|
+
readonly quiet?: boolean;
|
|
135
|
+
/** Does the command's work. */
|
|
136
|
+
run(context: CommandContext): Promise<CommandOutcome>;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Defines a command; the identity function, for types. */
|
|
140
|
+
export function defineCommand(command: CommandDef): CommandDef {
|
|
141
|
+
return command;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** The command path as one string: `daemon install`. */
|
|
145
|
+
export function commandName(command: Pick<CommandDef, "path">): string {
|
|
146
|
+
return command.path.join(" ");
|
|
147
|
+
}
|