@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 @@
|
|
|
1
|
+
export {}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { r as TelemetryStore, t as flush } from "../flush-BCuWs0Zh.js";
|
|
2
|
+
//#region src/telemetry/flush-main.ts
|
|
3
|
+
/**
|
|
4
|
+
* The flusher process (D140 item 6): `node flush-main.js <config dir>`, started detached by the
|
|
5
|
+
* CLI. It sends what the queue holds and exits; nothing it does reaches the command that started
|
|
6
|
+
* it.
|
|
7
|
+
*/
|
|
8
|
+
const dir = process.argv[2];
|
|
9
|
+
if (dir !== void 0) await flush(new TelemetryStore(dir), process.env, Date.now).catch(() => {});
|
|
10
|
+
process.exit(0);
|
|
11
|
+
//#endregion
|
|
12
|
+
export {};
|
|
13
|
+
|
|
14
|
+
//# sourceMappingURL=flush-main.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"flush-main.js","names":[],"sources":["../../src/telemetry/flush-main.ts"],"sourcesContent":["/**\n * The flusher process (D140 item 6): `node flush-main.js <config dir>`, started detached by the\n * CLI. It sends what the queue holds and exits; nothing it does reaches the command that started\n * it.\n */\nimport { flush } from \"./flush.ts\";\nimport { TelemetryStore } from \"./store.ts\";\n\nconst dir = process.argv[2];\nif (dir !== undefined) {\n await flush(new TelemetryStore(dir), process.env, Date.now).catch(() => {});\n}\nprocess.exit(0);\n"],"mappings":";;;;;;;AAQA,MAAM,MAAM,QAAQ,KAAK;AACzB,IAAI,QAAQ,KAAA,GACV,MAAM,MAAM,IAAI,eAAe,GAAG,GAAG,QAAQ,KAAK,KAAK,GAAG,CAAC,CAAC,YAAY,CAAC,CAAC;AAE5E,QAAQ,KAAK,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,65 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mostra/cli",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "The mostra command: author, verify, publish, share and wait on agent artifacts, as a thin client of the Mostra host protocol.",
|
|
5
|
+
"license": "Apache-2.0",
|
|
6
|
+
"author": "The Mostra Authors",
|
|
7
|
+
"homepage": "https://github.com/mostra-dev/mostra/tree/main/packages/cli#readme",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/mostra-dev/mostra.git",
|
|
11
|
+
"directory": "packages/cli"
|
|
12
|
+
},
|
|
13
|
+
"bugs": "https://github.com/mostra-dev/mostra/issues",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"sideEffects": false,
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"@mostra/source": "./src/index.ts",
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"default": "./dist/index.js"
|
|
21
|
+
},
|
|
22
|
+
"./package.json": "./package.json"
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"src",
|
|
27
|
+
"!src/**/*.test.ts"
|
|
28
|
+
],
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=22.19"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public",
|
|
34
|
+
"provenance": true
|
|
35
|
+
},
|
|
36
|
+
"dependencies": {
|
|
37
|
+
"@mostra/protocol": "0.2.0",
|
|
38
|
+
"@mostra/sdk": "0.2.0",
|
|
39
|
+
"zod": "4.6.5"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@mostra/runtime": "0.2.0",
|
|
43
|
+
"@mostra/tools": "0.0.0",
|
|
44
|
+
"@types/node": "24.19.1",
|
|
45
|
+
"tsdown": "0.23.0",
|
|
46
|
+
"typescript": "7.0.2",
|
|
47
|
+
"vitest": "5.0.3"
|
|
48
|
+
},
|
|
49
|
+
"bin": {
|
|
50
|
+
"mostra": "./dist/bin.js"
|
|
51
|
+
},
|
|
52
|
+
"keywords": [
|
|
53
|
+
"mostra",
|
|
54
|
+
"agent",
|
|
55
|
+
"artifact",
|
|
56
|
+
"cli"
|
|
57
|
+
],
|
|
58
|
+
"scripts": {
|
|
59
|
+
"build": "tsdown",
|
|
60
|
+
"typecheck": "tsc -b",
|
|
61
|
+
"test": "vitest run --project unit",
|
|
62
|
+
"api": "mostra-tools api",
|
|
63
|
+
"test:integration": "vitest run --project integration"
|
|
64
|
+
}
|
|
6
65
|
}
|
package/src/bin.ts
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* `mostra`, the CLI's binary (D60, INTERFACES §9). A copy installed from npm first moves into the
|
|
4
|
+
* runtime bundle its release pins (`bootstrap.ts`, RT-01); otherwise it runs one command and exits
|
|
5
|
+
* with its code, after stdout has drained, so a reader never loses the end of the envelope.
|
|
6
|
+
*/
|
|
7
|
+
import { MINIMUM_NODE, nodeTooOld } from "./node-gate.ts";
|
|
8
|
+
|
|
9
|
+
process.stdout.on("error", () => {});
|
|
10
|
+
process.stderr.on("error", () => {});
|
|
11
|
+
|
|
12
|
+
/** Refuses a Node.js too old to start Mostra, before anything that needs a newer one loads. */
|
|
13
|
+
async function refuseOldNode(): Promise<number> {
|
|
14
|
+
const message = `Node.js ${MINIMUM_NODE.join(".")} or later is needed to start Mostra; this is ${process.versions.node}.`;
|
|
15
|
+
const json = process.argv.includes("--json");
|
|
16
|
+
try {
|
|
17
|
+
const { createDiagnostic } = await import("@mostra/protocol");
|
|
18
|
+
const diagnostic = createDiagnostic("MOSTRA_ENV_NODE_UNSUPPORTED", { message });
|
|
19
|
+
process.stderr.write(`mostra: ${diagnostic.message}\nmostra: ${diagnostic.hint ?? ""}\n`);
|
|
20
|
+
if (json)
|
|
21
|
+
process.stdout.write(`${JSON.stringify({ ok: false, error: diagnostic, warnings: [] })}\n`);
|
|
22
|
+
} catch {
|
|
23
|
+
process.stderr.write(`mostra: ${message} (MOSTRA_ENV_NODE_UNSUPPORTED)\n`);
|
|
24
|
+
}
|
|
25
|
+
return 3;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const code = nodeTooOld(process.versions.node) ? await refuseOldNode() : await start();
|
|
29
|
+
|
|
30
|
+
async function start(): Promise<number> {
|
|
31
|
+
const { bootstrap } = await import("./bootstrap.ts");
|
|
32
|
+
const booted = await bootstrap({
|
|
33
|
+
argv: process.argv.slice(2),
|
|
34
|
+
env: process.env,
|
|
35
|
+
stdout: (text) => {
|
|
36
|
+
process.stdout.write(text);
|
|
37
|
+
},
|
|
38
|
+
stderr: (text) => {
|
|
39
|
+
process.stderr.write(text);
|
|
40
|
+
},
|
|
41
|
+
stderrTty: process.stderr.isTTY === true,
|
|
42
|
+
terminal:
|
|
43
|
+
process.stdin.isTTY === true ||
|
|
44
|
+
process.stdout.isTTY === true ||
|
|
45
|
+
process.stderr.isTTY === true,
|
|
46
|
+
});
|
|
47
|
+
return (
|
|
48
|
+
booted ??
|
|
49
|
+
(await (
|
|
50
|
+
await import("./run.ts")
|
|
51
|
+
).run({
|
|
52
|
+
argv: process.argv.slice(2),
|
|
53
|
+
env: process.env,
|
|
54
|
+
cwd: process.cwd(),
|
|
55
|
+
stdout: (text) => {
|
|
56
|
+
process.stdout.write(text);
|
|
57
|
+
},
|
|
58
|
+
stderr: (text) => {
|
|
59
|
+
process.stderr.write(text);
|
|
60
|
+
},
|
|
61
|
+
stdin: process.stdin,
|
|
62
|
+
tty: {
|
|
63
|
+
stdin: process.stdin.isTTY === true,
|
|
64
|
+
stdout: process.stdout.isTTY === true,
|
|
65
|
+
stderr: process.stderr.isTTY === true,
|
|
66
|
+
},
|
|
67
|
+
}))
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
process.stdout.write("", () => process.exit(code));
|
package/src/bootstrap.ts
ADDED
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The npm bootstrap (RT-01, D142): a `mostra` installed from npm runs on whatever Node.js the
|
|
3
|
+
* system has, so before anything else it finds the runtime bundle its release pins (from the
|
|
4
|
+
* cache, or downloaded once, with progress on stderr) and runs the bundle's own `mostra` with the
|
|
5
|
+
* same arguments on the bundle's private Node.js. Every command, telemetry included, then runs in
|
|
6
|
+
* the bundle, and this process only waits and passes the exit code on.
|
|
7
|
+
*
|
|
8
|
+
* Inside a bundle, in a development checkout, or with `MOSTRA_BUNDLE`/`MOSTRA_RUNTIME_BIN` set,
|
|
9
|
+
* there is nothing to bootstrap and the CLI runs in this process.
|
|
10
|
+
*
|
|
11
|
+
* This module runs on the system's Node.js, so it uses nothing newer than Node.js 22.19 and loads
|
|
12
|
+
* none of the CLI's commands.
|
|
13
|
+
*/
|
|
14
|
+
import { spawn } from "node:child_process";
|
|
15
|
+
import { constants } from "node:os";
|
|
16
|
+
import { createDiagnostic, type Diagnostic, RpcError } from "@mostra/protocol";
|
|
17
|
+
import { enclosingBundle, type InstallProgress, readPins, resolveRuntime } from "@mostra/sdk";
|
|
18
|
+
import { exitCodeFor } from "./exit.ts";
|
|
19
|
+
import type { Environment } from "./home.ts";
|
|
20
|
+
|
|
21
|
+
/** How the bootstrap talks to the terminal. */
|
|
22
|
+
export interface BootstrapIo {
|
|
23
|
+
readonly argv: readonly string[];
|
|
24
|
+
readonly env: Environment;
|
|
25
|
+
readonly stdout: (text: string) => void;
|
|
26
|
+
readonly stderr: (text: string) => void;
|
|
27
|
+
/** Whether stderr is a terminal: progress then rewrites one line. */
|
|
28
|
+
readonly stderrTty: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* Whether a terminal is attached (stdin, stdout or stderr is one). Ctrl-C there signals the
|
|
31
|
+
* whole foreground process group, the bundle's `mostra` included, so SIGINT isn't forwarded:
|
|
32
|
+
* a second one would cut short a command that stops gracefully on the first (`mostra dev`).
|
|
33
|
+
*/
|
|
34
|
+
readonly terminal: boolean;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Whether this CLI should move into its pinned bundle before running a command. */
|
|
38
|
+
export function needsBootstrap(env: Environment): boolean {
|
|
39
|
+
if (env["MOSTRA_BUNDLE"] || env["MOSTRA_RUNTIME_BIN"]) return false;
|
|
40
|
+
if (enclosingBundle() !== undefined) return false;
|
|
41
|
+
return readPins(env) !== undefined;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Progress lines for a person waiting on the first download. */
|
|
45
|
+
function progressReporter(io: BootstrapIo): (progress: InstallProgress) => void {
|
|
46
|
+
let lastPercent = -1;
|
|
47
|
+
let lastStep = "";
|
|
48
|
+
return (progress) => {
|
|
49
|
+
if (progress.step === "download") {
|
|
50
|
+
const percent = Math.floor((progress.received / progress.total) * 100);
|
|
51
|
+
const mb = (bytes: number): string => `${Math.round(bytes / 1e6)} MB`;
|
|
52
|
+
if (io.stderrTty) {
|
|
53
|
+
if (percent !== lastPercent)
|
|
54
|
+
io.stderr(
|
|
55
|
+
`\rmostra: downloading the Mostra runtime, ${mb(progress.received)} of ${mb(progress.total)} (${percent}%)`,
|
|
56
|
+
);
|
|
57
|
+
} else if (
|
|
58
|
+
lastStep !== "download" ||
|
|
59
|
+
Math.floor(percent / 25) > Math.floor(lastPercent / 25)
|
|
60
|
+
) {
|
|
61
|
+
io.stderr(`mostra: downloading the Mostra runtime (${mb(progress.total)}): ${percent}%\n`);
|
|
62
|
+
}
|
|
63
|
+
lastPercent = percent;
|
|
64
|
+
lastStep = "download";
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (progress.step === lastStep) return;
|
|
68
|
+
if (lastStep === "download" && io.stderrTty) io.stderr("\n");
|
|
69
|
+
lastStep = progress.step;
|
|
70
|
+
const words: Partial<Record<InstallProgress["step"], string>> = {
|
|
71
|
+
files: "checking the runtime's files",
|
|
72
|
+
browser: "fetching the browser verify uses",
|
|
73
|
+
};
|
|
74
|
+
const said = words[progress.step];
|
|
75
|
+
if (said !== undefined) io.stderr(`mostra: ${said}…\n`);
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** The envelope `--json` promises, for a bootstrap that fails before any command runs. */
|
|
80
|
+
function failed(io: BootstrapIo, diagnostic: Diagnostic): number {
|
|
81
|
+
io.stderr(`mostra: ${diagnostic.message}\n`);
|
|
82
|
+
if (diagnostic.hint) io.stderr(`mostra: ${diagnostic.hint}\n`);
|
|
83
|
+
if (io.argv.includes("--json")) {
|
|
84
|
+
io.stdout(`${JSON.stringify({ ok: false, error: diagnostic, warnings: [] })}\n`);
|
|
85
|
+
}
|
|
86
|
+
return exitCodeFor(diagnostic.code);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* When this copy needs the bundle its release pins (`needsBootstrap`), runs the bundle's
|
|
91
|
+
* `mostra` with `io.argv` and resolves with its exit code; otherwise resolves `undefined`, and the
|
|
92
|
+
* caller runs the command itself. Any failure, deciding included, is a diagnostic (one envelope
|
|
93
|
+
* with `--json`) and its exit code. Signals this process gets are passed on to the child.
|
|
94
|
+
*/
|
|
95
|
+
export async function bootstrap(io: BootstrapIo): Promise<number | undefined> {
|
|
96
|
+
try {
|
|
97
|
+
// Deciding is inside the boundary too: pins that can't be read are a diagnostic, never a
|
|
98
|
+
// stack trace (GPT review finding 3).
|
|
99
|
+
if (!needsBootstrap(io.env)) return undefined;
|
|
100
|
+
return await runInBundle(io);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
// Whatever goes wrong, `--json` still gets exactly one envelope (D140 item 16).
|
|
103
|
+
if (error instanceof RpcError) return failed(io, error.diagnostic);
|
|
104
|
+
return failed(
|
|
105
|
+
io,
|
|
106
|
+
createDiagnostic("MOSTRA_HOST_INTERNAL_ERROR", {
|
|
107
|
+
message: `The npm bootstrap failed: ${error instanceof Error ? error.message : String(error)}`,
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function runInBundle(io: BootstrapIo): Promise<number> {
|
|
114
|
+
const runtime = await resolveRuntime({ env: io.env, onProgress: progressReporter(io) });
|
|
115
|
+
if (runtime.cliEntry === undefined) {
|
|
116
|
+
throw new RpcError(
|
|
117
|
+
createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", {
|
|
118
|
+
message: "The runtime the bootstrap found has no CLI entry.",
|
|
119
|
+
}),
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
const cli = runtime.cliEntry;
|
|
123
|
+
return new Promise((resolve) => {
|
|
124
|
+
const child = spawn(runtime.node, [cli, ...io.argv], {
|
|
125
|
+
stdio: "inherit",
|
|
126
|
+
env: io.env as NodeJS.ProcessEnv,
|
|
127
|
+
});
|
|
128
|
+
const forward = (signal: NodeJS.Signals): void => {
|
|
129
|
+
if (signal === "SIGINT" && io.terminal) return; // the child got the terminal's own
|
|
130
|
+
child.kill(signal);
|
|
131
|
+
};
|
|
132
|
+
const signals: NodeJS.Signals[] = ["SIGINT", "SIGTERM", "SIGHUP"];
|
|
133
|
+
for (const signal of signals) process.on(signal, forward);
|
|
134
|
+
let settled = false;
|
|
135
|
+
const settle = (code: number): void => {
|
|
136
|
+
if (settled) return;
|
|
137
|
+
settled = true;
|
|
138
|
+
for (const signal of signals) process.off(signal, forward);
|
|
139
|
+
resolve(code);
|
|
140
|
+
};
|
|
141
|
+
child.on("error", (error) =>
|
|
142
|
+
settle(
|
|
143
|
+
failed(
|
|
144
|
+
io,
|
|
145
|
+
createDiagnostic("MOSTRA_ENV_RUNTIME_MISSING", {
|
|
146
|
+
message: `The runtime bundle's CLI couldn't be started (${runtime.node}): ${error.message}`,
|
|
147
|
+
}),
|
|
148
|
+
),
|
|
149
|
+
),
|
|
150
|
+
);
|
|
151
|
+
// A child ended by a signal exits as a shell reports it: 128 plus the signal's number.
|
|
152
|
+
child.on("exit", (code, signal) =>
|
|
153
|
+
settle(code ?? 128 + (signal ? (constants.signals[signal] ?? 0) : 0)),
|
|
154
|
+
);
|
|
155
|
+
});
|
|
156
|
+
}
|
package/src/browser.ts
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Opening a browser on a launch URL without putting it on a command line (D140 item 15, Opus
|
|
3
|
+
* review finding 4). A program's arguments are readable by every local user (`ps`,
|
|
4
|
+
* `/proc/<pid>/cmdline`), so the opener gets the path of a page that redirects to the launch URL,
|
|
5
|
+
* written mode 0600 in a fresh directory of mode 0700, as Jupyter's server does for its token.
|
|
6
|
+
* The launch ticket in the page is single-use and lives a minute, so a page left behind is
|
|
7
|
+
* harmless; each launch removes the ones older than that.
|
|
8
|
+
*/
|
|
9
|
+
import { spawn } from "node:child_process";
|
|
10
|
+
import { lstatSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
11
|
+
import { tmpdir } from "node:os";
|
|
12
|
+
import { join } from "node:path";
|
|
13
|
+
import type { Environment } from "./home.ts";
|
|
14
|
+
|
|
15
|
+
/** The prefix of the directories launch pages live in. */
|
|
16
|
+
const PREFIX = "mostra-open-";
|
|
17
|
+
/** A launch page older than this holds a ticket that expired: it goes on the next launch. */
|
|
18
|
+
const STALE_MS = 2 * 60_000;
|
|
19
|
+
|
|
20
|
+
/** Escapes text for an HTML attribute or element. */
|
|
21
|
+
const html = (text: string): string =>
|
|
22
|
+
text
|
|
23
|
+
.replaceAll("&", "&")
|
|
24
|
+
.replaceAll('"', """)
|
|
25
|
+
.replaceAll("<", "<")
|
|
26
|
+
.replaceAll(">", ">");
|
|
27
|
+
|
|
28
|
+
/** The page that sends the browser on to `url`. */
|
|
29
|
+
export function launchPage(url: string): string {
|
|
30
|
+
// JSON in a script, with `<` escaped so nothing in it ends the script element.
|
|
31
|
+
const script = JSON.stringify(url).replaceAll("<", "\\u003c");
|
|
32
|
+
return `<!DOCTYPE html>
|
|
33
|
+
<html lang="en">
|
|
34
|
+
<head>
|
|
35
|
+
<meta charset="utf-8">
|
|
36
|
+
<meta name="referrer" content="no-referrer">
|
|
37
|
+
<meta http-equiv="refresh" content="0;url=${html(url)}">
|
|
38
|
+
<title>Opening Mostra</title>
|
|
39
|
+
<script>location.replace(${script});</script>
|
|
40
|
+
</head>
|
|
41
|
+
<body>
|
|
42
|
+
<p>Opening the artifact. If nothing happens, <a href="${html(url)}">open it here</a>.</p>
|
|
43
|
+
</body>
|
|
44
|
+
</html>
|
|
45
|
+
`;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Removes launch pages of earlier launches that are this user's and older than a minute or two. */
|
|
49
|
+
function sweep(base: string, now: number): void {
|
|
50
|
+
const uid = process.getuid?.();
|
|
51
|
+
let names: string[];
|
|
52
|
+
try {
|
|
53
|
+
names = readdirSync(base);
|
|
54
|
+
} catch {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
for (const name of names) {
|
|
58
|
+
if (!name.startsWith(PREFIX)) continue;
|
|
59
|
+
const dir = join(base, name);
|
|
60
|
+
try {
|
|
61
|
+
const stats = lstatSync(dir);
|
|
62
|
+
if (!stats.isDirectory() || (uid !== undefined && stats.uid !== uid)) continue;
|
|
63
|
+
if (now - stats.mtimeMs < STALE_MS) continue;
|
|
64
|
+
rmSync(dir, { recursive: true, force: true });
|
|
65
|
+
} catch {
|
|
66
|
+
// Gone already, or not ours to remove.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Writes the launch page for `url`; its path. */
|
|
72
|
+
export function writeLaunchPage(
|
|
73
|
+
url: string,
|
|
74
|
+
base: string = tmpdir(),
|
|
75
|
+
now: number = Date.now(),
|
|
76
|
+
): string {
|
|
77
|
+
sweep(base, now);
|
|
78
|
+
// mkdtemp makes the directory with a random name and mode 0700.
|
|
79
|
+
const dir = mkdtempSync(join(base, PREFIX));
|
|
80
|
+
const file = join(dir, "open.html");
|
|
81
|
+
writeFileSync(file, launchPage(url), { mode: 0o600, flag: "wx" });
|
|
82
|
+
return file;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Splits a command the way a shell would split plain words and quotes, without running one:
|
|
87
|
+
* `firefox --new-window %s` or `"/Applications/My Browser" %s`.
|
|
88
|
+
*/
|
|
89
|
+
export function splitCommand(command: string): string[] {
|
|
90
|
+
const words: string[] = [];
|
|
91
|
+
let word = "";
|
|
92
|
+
let inWord = false;
|
|
93
|
+
let quote: '"' | "'" | undefined;
|
|
94
|
+
for (let index = 0; index < command.length; index += 1) {
|
|
95
|
+
const char = command[index] as string;
|
|
96
|
+
if (quote) {
|
|
97
|
+
if (char === quote) quote = undefined;
|
|
98
|
+
else if (char === "\\" && quote === '"' && index + 1 < command.length) {
|
|
99
|
+
index += 1;
|
|
100
|
+
word += command[index];
|
|
101
|
+
} else word += char;
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
if (char === '"' || char === "'") {
|
|
105
|
+
quote = char;
|
|
106
|
+
inWord = true;
|
|
107
|
+
} else if (char === "\\" && index + 1 < command.length) {
|
|
108
|
+
index += 1;
|
|
109
|
+
word += command[index];
|
|
110
|
+
inWord = true;
|
|
111
|
+
} else if (/\s/.test(char)) {
|
|
112
|
+
if (inWord) words.push(word);
|
|
113
|
+
word = "";
|
|
114
|
+
inWord = false;
|
|
115
|
+
} else {
|
|
116
|
+
word += char;
|
|
117
|
+
inWord = true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (inWord) words.push(word);
|
|
121
|
+
return words;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* The commands that may open `target`, in order: each entry of `$BROWSER` (separated by `:`, its
|
|
126
|
+
* `%s` replaced by the target, else the target appended, `%%` a literal `%`, as `xdg-open` and
|
|
127
|
+
* Python's `webbrowser` read it), else the platform's opener.
|
|
128
|
+
*/
|
|
129
|
+
export function browserCommands(
|
|
130
|
+
target: string,
|
|
131
|
+
env: Environment,
|
|
132
|
+
platform: string = process.platform,
|
|
133
|
+
): string[][] {
|
|
134
|
+
const chosen = env["BROWSER"];
|
|
135
|
+
const entries =
|
|
136
|
+
chosen !== undefined && chosen.trim() !== ""
|
|
137
|
+
? chosen.split(":").filter((entry) => entry.trim() !== "")
|
|
138
|
+
: [platform === "darwin" ? "open" : "xdg-open"];
|
|
139
|
+
const commands: string[][] = [];
|
|
140
|
+
for (const entry of entries) {
|
|
141
|
+
const words = splitCommand(entry);
|
|
142
|
+
if (words.length === 0) continue;
|
|
143
|
+
let substituted = false;
|
|
144
|
+
const argv = words.map((word) =>
|
|
145
|
+
word.replaceAll(/%%|%s/g, (match) => {
|
|
146
|
+
if (match === "%%") return "%";
|
|
147
|
+
substituted = true;
|
|
148
|
+
return target;
|
|
149
|
+
}),
|
|
150
|
+
);
|
|
151
|
+
commands.push(substituted ? argv : [...argv, target]);
|
|
152
|
+
}
|
|
153
|
+
return commands;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/** Starts one command, detached; whether it started (its program was found and could run). */
|
|
157
|
+
function start(argv: readonly string[]): Promise<boolean> {
|
|
158
|
+
const [program, ...args] = argv;
|
|
159
|
+
if (program === undefined) return Promise.resolve(false);
|
|
160
|
+
return new Promise((resolve) => {
|
|
161
|
+
try {
|
|
162
|
+
const child = spawn(program, args, { detached: true, stdio: "ignore" });
|
|
163
|
+
child.once("spawn", () => {
|
|
164
|
+
child.unref();
|
|
165
|
+
resolve(true);
|
|
166
|
+
});
|
|
167
|
+
child.once("error", () => resolve(false));
|
|
168
|
+
} catch {
|
|
169
|
+
resolve(false);
|
|
170
|
+
}
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/** Opens `url` in a browser through a launch page; whether a browser command started. */
|
|
175
|
+
export async function openInBrowser(url: string, env: Environment): Promise<boolean> {
|
|
176
|
+
let page: string;
|
|
177
|
+
try {
|
|
178
|
+
page = writeLaunchPage(url);
|
|
179
|
+
} catch {
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
for (const command of browserCommands(page, env)) {
|
|
183
|
+
if (await start(command)) return true;
|
|
184
|
+
}
|
|
185
|
+
return false;
|
|
186
|
+
}
|