@mostra/cli 0.3.0 → 0.4.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.
@@ -1,15 +1,14 @@
1
- import { a as catalogCodes, c as detectHarness, d as truthy, f as cliVersion, i as TelemetryEvent, l as buildEndpoint, m as packageRoot, n as startFlusher, o as durationBucket, p as fromSource, r as TelemetryStore, s as minute, u as decide } from "./flush-BCuWs0Zh.js";
1
+ import { a as detachedEnvironment, c as durationBucket, d as buildEndpoint, f as decide, g as packageRoot, h as fromSource, i as detachedDirectory, l as minute, m as cliVersion, n as startFlusher, o as TelemetryEvent, p as truthy, r as TelemetryStore, s as catalogCodes, u as detectHarness } from "./flush-C2lroV03.js";
2
2
  import { n as exitCodeFor, t as EXIT } from "./exit-CBIf8nAU.js";
3
3
  import { spawn } from "node:child_process";
4
- import { existsSync, lstatSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, writeFileSync } from "node:fs";
5
- import { isAbsolute, join, resolve } from "node:path";
4
+ import { existsSync, lstatSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, readlinkSync, rmSync, statSync, writeFileSync } from "node:fs";
5
+ import { isAbsolute, join, resolve, win32 } from "node:path";
6
6
  import { ARTIFACT_PROGRESS, CLI_CLIENT_NAME, DEFAULT_DOCS_BASE, DEV_NOTIFY, FieldOp, HOST_PROTOCOL_REVISIONS, LineSplitter, MutationId, RpcError, RpcPeer, artifactCreateMethod, artifactGetMethod, artifactListMethod, artifactPublishMethod, artifactVerifyMethod, createDiagnostic, depsAddMethod, devStartMethod, devStopMethod, diagnosticCatalog, encodeLine, gcRunMethod, instanceCheckMethod, instanceStatusMethod, livePushMethod, mostraInitializeMethod, mostraShutdownMethod, resultsGetMethod, secretDeleteMethod, secretListMethod, secretSetMethod, stateGetMethod, statePatchMethod, viewDefaultMethod, viewLaunchMethod, viewRevokeMethod, viewRotateMethod, viewUrlMethod, waitMethod } from "@mostra/protocol";
7
- import { checkBrowser, checkFiles, enclosingBundle, readBundle, readSetup, resolveRuntime } from "@mostra/sdk";
7
+ import { checkBrowser, checkFiles, connectControl, enclosingBundle, readBundle, readSetup, resolveRuntime } from "@mostra/sdk";
8
8
  import { homedir, tmpdir } from "node:os";
9
9
  import { parseArgs } from "node:util";
10
10
  import { randomUUID } from "node:crypto";
11
11
  import { createInterface } from "node:readline";
12
- import { createConnection } from "node:net";
13
12
  //#region src/command.ts
14
13
  /** Defines a command; the identity function, for types. */
15
14
  function defineCommand(command) {
@@ -115,24 +114,35 @@ function distance(a, b) {
115
114
  const nonEmpty = (value) => value === void 0 || value === "" ? void 0 : value;
116
115
  /**
117
116
  * The home, in D84's order: `--home`, then `MOSTRA_HOME`, then the platform default (`~/.mostra`
118
- * on macOS, `${XDG_DATA_HOME:-~/.local/share}/mostra` on Linux), the same order the runtime uses.
117
+ * on macOS, `${XDG_DATA_HOME:-~/.local/share}/mostra` on Linux, `%LOCALAPPDATA%\mostra` on
118
+ * Windows), the same order and defaults the runtime uses (its `platform` module).
119
119
  */
120
120
  function resolveHome(options) {
121
121
  const chosen = nonEmpty(options.home) ?? nonEmpty(options.env["MOSTRA_HOME"]);
122
122
  if (chosen !== void 0) return resolve(options.cwd, chosen);
123
+ const platform = options.platform ?? process.platform;
124
+ if (platform === "win32") {
125
+ const local = nonEmpty(options.env["LOCALAPPDATA"]);
126
+ return win32.join(local !== void 0 && win32.isAbsolute(local) ? local : win32.join(homedir(), "AppData", "Local"), "mostra");
127
+ }
123
128
  const user = nonEmpty(options.env["HOME"]) ?? homedir();
124
- if ((options.platform ?? process.platform) === "darwin") return join(user, ".mostra");
129
+ if (platform === "darwin") return join(user, ".mostra");
125
130
  const data = nonEmpty(options.env["XDG_DATA_HOME"]);
126
131
  return join(data !== void 0 && isAbsolute(data) ? data : join(user, ".local", "share"), "mostra");
127
132
  }
128
133
  /**
129
134
  * The CLI's config directory (D140 item 5): `MOSTRA_CONFIG_DIR`, else
130
- * `${XDG_CONFIG_HOME:-~/.config}/mostra`, on both platforms. It holds telemetry's choice and
131
- * queue, per operating-system user, never per home.
135
+ * `${XDG_CONFIG_HOME:-~/.config}/mostra` on macOS and Linux, and `%APPDATA%\mostra` on Windows,
136
+ * where per-user settings live (D151). It holds telemetry's choice and queue, per
137
+ * operating-system user, never per home.
132
138
  */
133
- function configDir(env, cwd) {
139
+ function configDir(env, cwd, platform = process.platform) {
134
140
  const override = nonEmpty(env["MOSTRA_CONFIG_DIR"]);
135
141
  if (override !== void 0) return resolve(cwd, override);
142
+ if (platform === "win32") {
143
+ const roaming = nonEmpty(env["APPDATA"]);
144
+ return win32.join(roaming !== void 0 && win32.isAbsolute(roaming) ? roaming : win32.join(homedir(), "AppData", "Roaming"), "mostra");
145
+ }
136
146
  const xdg = nonEmpty(env["XDG_CONFIG_HOME"]);
137
147
  const base = xdg !== void 0 && isAbsolute(xdg) ? xdg : join(nonEmpty(env["HOME"]) ?? homedir(), ".config");
138
148
  return join(base, "mostra");
@@ -147,7 +157,8 @@ async function locateRuntime(env) {
147
157
  const runtime = await resolveRuntime({ env });
148
158
  return {
149
159
  node: runtime.node,
150
- entry: runtime.runtimeEntry
160
+ entry: runtime.runtimeEntry,
161
+ runtime
151
162
  };
152
163
  }
153
164
  /**
@@ -380,6 +391,23 @@ function parseCommandLine(commands, argv) {
380
391
  };
381
392
  }
382
393
  //#endregion
394
+ //#region src/platform.ts
395
+ /**
396
+ * The CLI's platform gate (D35, D146, RT-07), the runtime's rule (`home/platform.ts` in
397
+ * `@mostra/runtime`) repeated where the CLI must decide before it reaches any runtime: macOS and
398
+ * Linux on x64 and arm64; Windows on x64 and arm64 only behind the development switch
399
+ * `MOSTRA_DEV_WINDOWS=1`, which only Mostra's CI sets until WP-W3; everything else fails closed.
400
+ */
401
+ /** `MOSTRA_PLATFORM_UNSUPPORTED` for a platform Mostra doesn't run on, else `undefined`. */
402
+ function checkPlatform(platform, arch, env) {
403
+ if (arch === "x64" || arch === "arm64") {
404
+ if (platform === "darwin" || platform === "linux") return void 0;
405
+ if (platform === "win32" && env["MOSTRA_DEV_WINDOWS"] === "1") return void 0;
406
+ }
407
+ const name = platform === "win32" ? `Windows (${platform}-${arch})` : `${platform}-${arch}`;
408
+ return createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message: `Mostra doesn't run on ${name}; it supports macOS and Linux on x64 and arm64.` });
409
+ }
410
+ //#endregion
383
411
  //#region src/telemetry/zones.generated.ts
384
412
  /** The time zones where Mostra asks before sending telemetry (D140 item 10), matched as given. */
385
413
  const ASK_FIRST_ZONES = [
@@ -1321,7 +1349,7 @@ function telemetrySession(options) {
1321
1349
  at: minute(new Date(now())),
1322
1350
  installId: id.id,
1323
1351
  version: cliVersion(),
1324
- os: process.platform === "darwin" ? "darwin" : "linux",
1352
+ os: process.platform === "darwin" || process.platform === "win32" ? process.platform : "linux",
1325
1353
  arch: process.arch,
1326
1354
  node: Number(process.versions.node.split(".")[0]),
1327
1355
  harness,
@@ -1635,7 +1663,7 @@ async function service(context, action) {
1635
1663
  const noStart = {
1636
1664
  name: "no-start",
1637
1665
  kind: "boolean",
1638
- summary: "Write or remove the service's file without asking launchd or systemd to load it."
1666
+ summary: "Write or remove the service's definition without asking launchd, systemd or Task Scheduler to load it."
1639
1667
  };
1640
1668
  /** `mostra daemon start`. */
1641
1669
  const daemonStartCommand = defineCommand({
@@ -1747,7 +1775,7 @@ const daemonStatusCommand = defineCommand({
1747
1775
  /** `mostra daemon install`. */
1748
1776
  const daemonInstallCommand = defineCommand({
1749
1777
  path: ["daemon", "install"],
1750
- summary: "Run the home's daemon as a login service (launchd or systemd), so links answer at any hour.",
1778
+ summary: "Run the home's daemon as a login service (launchd, systemd or Task Scheduler), so links answer at any hour.",
1751
1779
  flags: [noStart],
1752
1780
  output: {
1753
1781
  type: "ServiceStatus",
@@ -1898,14 +1926,15 @@ const doctorCommand = defineCommand({
1898
1926
  async run(context) {
1899
1927
  const checks = [];
1900
1928
  const node = Number(process.versions.node.split(".")[0]);
1901
- checks.push(process.platform === "darwin" || process.platform === "linux" ? {
1929
+ const unsupported = checkPlatform(process.platform, process.arch, context.env);
1930
+ checks.push(unsupported === void 0 ? {
1902
1931
  name: "platform",
1903
1932
  status: "ok",
1904
1933
  detail: `${process.platform} ${process.arch}, Node ${process.versions.node}`
1905
1934
  } : {
1906
1935
  name: "platform",
1907
1936
  status: "error",
1908
- diagnostic: createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message: `Mostra runs on macOS and Linux; this is ${process.platform}.` })
1937
+ diagnostic: unsupported
1909
1938
  });
1910
1939
  if (node < 24) checks.push({
1911
1940
  name: "node",
@@ -2523,14 +2552,17 @@ const devCommand = defineCommand({
2523
2552
  }, `Development preview, not published: ${started.url}\nPress Ctrl-C to stop.\n`);
2524
2553
  for (const event of early.splice(0)) if (event.sessionId === sessionId) show(event);
2525
2554
  let stop = () => {};
2555
+ const signals = [
2556
+ "SIGINT",
2557
+ "SIGTERM",
2558
+ "SIGBREAK"
2559
+ ];
2526
2560
  const ended = await new Promise((done) => {
2527
2561
  stop = () => done("signal");
2528
- process.once("SIGINT", stop);
2529
- process.once("SIGTERM", stop);
2562
+ for (const signal of signals) process.once(signal, stop);
2530
2563
  connection?.closed.then(() => done("closed"));
2531
2564
  });
2532
- process.off("SIGINT", stop);
2533
- process.off("SIGTERM", stop);
2565
+ for (const signal of signals) process.off(signal, stop);
2534
2566
  if (ended === "closed") throw new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", {
2535
2567
  message: "The instance ended the development preview (it stopped or restarted).",
2536
2568
  hint: "Run mostra dev again."
@@ -2876,8 +2908,11 @@ const statePatchCommand = defineCommand({
2876
2908
  * review finding 4). A program's arguments are readable by every local user (`ps`,
2877
2909
  * `/proc/<pid>/cmdline`), so the opener gets the path of a page that redirects to the launch URL,
2878
2910
  * written mode 0600 in a fresh directory of mode 0700, as Jupyter's server does for its token.
2879
- * The launch ticket in the page is single-use and lives a minute, so a page left behind is
2880
- * harmless; each launch removes the ones older than that.
2911
+ * Windows has no modes: there the page goes under the user's own profile
2912
+ * (`%LOCALAPPDATA%\mostra-cli\open`), whose access list only the user, `SYSTEM` and
2913
+ * administrators pass, never under `%TEMP%`, which may name a folder other users can read (Opus
2914
+ * review of WP-W2, finding 1). The launch ticket in the page is single-use and lives a minute, so a
2915
+ * page left behind is harmless; each launch removes the ones older than that.
2881
2916
  */
2882
2917
  /** The prefix of the directories launch pages live in. */
2883
2918
  const PREFIX = "mostra-open-";
@@ -2975,17 +3010,49 @@ function splitCommand(command) {
2975
3010
  if (inWord) words.push(word);
2976
3011
  return words;
2977
3012
  }
3013
+ /** The program that opens a file with its default application, by platform. */
3014
+ function platformOpener(platform) {
3015
+ if (platform === "darwin") return ["open"];
3016
+ if (platform === "win32") return ["explorer.exe"];
3017
+ return ["xdg-open"];
3018
+ }
3019
+ /**
3020
+ * Splits a command as Windows programs read their command lines, for `$BROWSER` there: words
3021
+ * separated by spaces, double quotes grouping words with spaces (`"C:\Program Files\Firefox\
3022
+ * firefox.exe" %s`), and backslashes kept as they are.
3023
+ */
3024
+ function splitWindowsCommand(command) {
3025
+ const words = [];
3026
+ let word = "";
3027
+ let inWord = false;
3028
+ let quoted = false;
3029
+ for (const char of command) if (char === "\"") {
3030
+ quoted = !quoted;
3031
+ inWord = true;
3032
+ } else if (!quoted && /\s/.test(char)) {
3033
+ if (inWord) words.push(word);
3034
+ word = "";
3035
+ inWord = false;
3036
+ } else {
3037
+ word += char;
3038
+ inWord = true;
3039
+ }
3040
+ if (inWord) words.push(word);
3041
+ return words;
3042
+ }
2978
3043
  /**
2979
- * The commands that may open `target`, in order: each entry of `$BROWSER` (separated by `:`, its
2980
- * `%s` replaced by the target, else the target appended, `%%` a literal `%`, as `xdg-open` and
2981
- * Python's `webbrowser` read it), else the platform's opener.
3044
+ * The commands that may open `target`, in order: each entry of `$BROWSER` (separated by the
3045
+ * platform's path delimiter, `:` or `;` on Windows, its `%s` replaced by the target, else the
3046
+ * target appended, `%%` a literal `%`, as `xdg-open` and Python's `webbrowser` read it), else the
3047
+ * platform's opener.
2982
3048
  */
2983
3049
  function browserCommands(target, env, platform = process.platform) {
2984
3050
  const chosen = env["BROWSER"];
2985
- const entries = chosen !== void 0 && chosen.trim() !== "" ? chosen.split(":").filter((entry) => entry.trim() !== "") : [platform === "darwin" ? "open" : "xdg-open"];
3051
+ if (chosen === void 0 || chosen.trim() === "") return [[...platformOpener(platform), target]];
3052
+ const entries = chosen.split(platform === "win32" ? ";" : ":").filter((entry) => entry.trim() !== "");
2986
3053
  const commands = [];
2987
3054
  for (const entry of entries) {
2988
- const words = splitCommand(entry);
3055
+ const words = platform === "win32" ? splitWindowsCommand(entry) : splitCommand(entry);
2989
3056
  if (words.length === 0) continue;
2990
3057
  let substituted = false;
2991
3058
  const argv = words.map((word) => word.replaceAll(/%%|%s/g, (match) => {
@@ -3017,11 +3084,26 @@ function start(argv) {
3017
3084
  }
3018
3085
  });
3019
3086
  }
3087
+ /**
3088
+ * Where launch pages go: the system's temporary directory on POSIX (the page's modes protect it),
3089
+ * the user's own profile on Windows, whatever `%TEMP%` names.
3090
+ */
3091
+ function launchBase(env, platform = process.platform) {
3092
+ if (platform !== "win32") return tmpdir();
3093
+ const local = env["LOCALAPPDATA"];
3094
+ const base = local !== void 0 && local !== "" && win32.isAbsolute(local) ? local : win32.join(homedir(), "AppData", "Local");
3095
+ return win32.join(base, "mostra-cli", "open");
3096
+ }
3020
3097
  /** Opens `url` in a browser through a launch page; whether a browser command started. */
3021
3098
  async function openInBrowser(url, env) {
3022
3099
  let page;
3023
3100
  try {
3024
- page = writeLaunchPage(url);
3101
+ const base = launchBase(env);
3102
+ mkdirSync(base, {
3103
+ recursive: true,
3104
+ mode: 448
3105
+ });
3106
+ page = writeLaunchPage(url, base);
3025
3107
  } catch {
3026
3108
  return false;
3027
3109
  }
@@ -3036,10 +3118,13 @@ async function openInBrowser(url, env) {
3036
3118
  * (`browser.ts`). A client of `view/default`, `view/url`,
3037
3119
  * `view/rotate`, `view/revoke` and `view/launch`.
3038
3120
  */
3039
- /** Whether this session can show a browser: not over SSH without a display. */
3121
+ /**
3122
+ * Whether this session can show a browser: not over SSH without a display. On macOS and Windows a
3123
+ * local session always has the user's desktop; an SSH session there has none.
3124
+ */
3040
3125
  function hasDisplay(env, platform = process.platform) {
3041
3126
  const ssh = env["SSH_CONNECTION"] !== void 0 || env["SSH_TTY"] !== void 0;
3042
- if (platform === "darwin") return !ssh;
3127
+ if (platform === "darwin" || platform === "win32") return !ssh;
3043
3128
  return env["DISPLAY"] !== void 0 || env["WAYLAND_DISPLAY"] !== void 0;
3044
3129
  }
3045
3130
  /** Whether `open` should launch a browser: a terminal without `--json`, or `--browser`. */
@@ -3398,6 +3483,19 @@ async function connect(options) {
3398
3483
  }
3399
3484
  return open(started.socket, options, started.status === "started");
3400
3485
  }
3486
+ /**
3487
+ * Opens the endpoint `instance.json` or a starting daemon named: a Unix socket, or on Windows the
3488
+ * owner-only pipe through the runtime's helper, which refuses a pipe another user created under
3489
+ * the name (D150, the SDK's `connectControl`). Only Windows needs the runtime resolved first.
3490
+ */
3491
+ async function openEndpoint(endpoint, options) {
3492
+ const runtime = process.platform === "win32" ? (await locateRuntime(options.env)).runtime : void 0;
3493
+ return connectControl(endpoint, {
3494
+ runtime,
3495
+ env: options.env,
3496
+ timeoutMs: ATTACH_TIMEOUT_MS
3497
+ });
3498
+ }
3401
3499
  /** The socket `instance.json` records (D81), if the file exists and reads. */
3402
3500
  function readSocket(home) {
3403
3501
  try {
@@ -3417,7 +3515,7 @@ async function startDaemon(options) {
3417
3515
  const runtime = await locateRuntime(options.env);
3418
3516
  if (Date.now() >= deadlineAt) throw timedOut();
3419
3517
  const args = [
3420
- runtime.entry,
3518
+ resolve(runtime.entry),
3421
3519
  "daemon",
3422
3520
  "--home",
3423
3521
  options.home
@@ -3425,15 +3523,17 @@ async function startDaemon(options) {
3425
3523
  return new Promise((resolve, reject) => {
3426
3524
  const child = spawn(runtime.node, args, {
3427
3525
  detached: true,
3526
+ cwd: detachedDirectory(options.home),
3527
+ windowsHide: true,
3428
3528
  stdio: [
3429
3529
  "ignore",
3430
3530
  "pipe",
3431
3531
  "ignore"
3432
3532
  ],
3433
- env: {
3533
+ env: detachedEnvironment({
3434
3534
  ...process.env,
3435
3535
  ...options.env
3436
- }
3536
+ })
3437
3537
  });
3438
3538
  const splitter = new LineSplitter();
3439
3539
  let settled = false;
@@ -3468,21 +3568,7 @@ async function startDaemon(options) {
3468
3568
  });
3469
3569
  }
3470
3570
  async function open(socketPath, options, started) {
3471
- const socket = await new Promise((resolve, reject) => {
3472
- const connection = createConnection(socketPath);
3473
- const timer = setTimeout(() => {
3474
- connection.destroy();
3475
- reject(closed(socketPath, "timed out"));
3476
- }, ATTACH_TIMEOUT_MS);
3477
- connection.once("error", (error) => {
3478
- clearTimeout(timer);
3479
- reject(closed(socketPath, error.code ?? "failed"));
3480
- });
3481
- connection.once("connect", () => {
3482
- clearTimeout(timer);
3483
- resolve(connection);
3484
- });
3485
- });
3571
+ const socket = await openEndpoint(socketPath, options);
3486
3572
  const handlers = /* @__PURE__ */ new Map();
3487
3573
  const peer = new RpcPeer({
3488
3574
  send: (message) => {
@@ -3543,9 +3629,6 @@ async function open(socketPath, options, started) {
3543
3629
  }
3544
3630
  };
3545
3631
  }
3546
- function closed(path, why) {
3547
- return new RpcError(createDiagnostic("MOSTRA_HOST_CONNECTION_CLOSED", { message: `Couldn't reach the instance at ${path} (${why}).` }));
3548
- }
3549
3632
  /** A limit as people write it: `250 ms`, `5 s`, `1.5 s`. */
3550
3633
  function formatDuration(ms) {
3551
3634
  if (ms < 1e3) return `${ms} ms`;
@@ -3630,7 +3713,8 @@ async function run(io) {
3630
3713
  return exitCodeFor(diagnostic.code);
3631
3714
  };
3632
3715
  const platform = io.platform ?? process.platform;
3633
- if (platform !== "darwin" && platform !== "linux") return fail(createDiagnostic("MOSTRA_PLATFORM_UNSUPPORTED", { message: `Mostra runs on macOS and Linux; this is ${platform}.` }));
3716
+ const unsupported = checkPlatform(platform, io.arch ?? process.arch, io.env);
3717
+ if (unsupported) return fail(unsupported);
3634
3718
  let parsed;
3635
3719
  try {
3636
3720
  parsed = parseCommandLine(COMMANDS, io.argv);
@@ -3821,4 +3905,4 @@ async function runRuntime(io, args, stdio) {
3821
3905
  //#endregion
3822
3906
  export { COMMANDS as a, referenceMarkdown as i, commandDefinition as n, commandHelp as r, run as t };
3823
3907
 
3824
- //# sourceMappingURL=run-BJaRnz-Q.js.map
3908
+ //# sourceMappingURL=run-DP-RoOv_.js.map