@tokenoftrust/cli 3.10.2 → 3.10.3

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/bin/tot.mjs CHANGED
@@ -29,10 +29,6 @@ import {
29
29
  recordDiagnostic,
30
30
  resetInvocationDiagnostics,
31
31
  } from "../src/diagnostics.mjs";
32
- import {
33
- nativeWindowsRefuseMessage,
34
- shouldRefuseNativeWindows,
35
- } from "../src/platform.mjs";
36
32
 
37
33
  const BUILD_ORDER = ["clone", "validate", "dev", "preview"];
38
34
 
@@ -120,14 +116,6 @@ async function dispatch(cmd, rest, ctx) {
120
116
  return cmd ? 0 : 2;
121
117
  }
122
118
 
123
- // Native Windows: refuse before login/--device/start burn cycles on a host the
124
- // local loop cannot run on. doctor/feedback/ideas stay available so the gate
125
- // itself is discoverable. WSL reports as linux and passes through.
126
- if (shouldRefuseNativeWindows(cmd)) {
127
- process.stderr.write(nativeWindowsRefuseMessage(cmd));
128
- return 2;
129
- }
130
-
131
119
  // A mounted publication space's checkout (spaces/<id>/, recorded by `tot clone`) is its own
132
120
  // repository: the store-root verbs below would push it with the ROOT's credential or merge the
133
121
  // root's preview branch into it. Refuse them there, naming the space's own path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tokenoftrust/cli",
3
- "version": "3.10.2",
3
+ "version": "3.10.3",
4
4
  "description": "Token of Trust developer CLI — clone a tenant store, run it locally with save→reload, and submit it for preview. Installs the `tot` command.",
5
5
  "license": "Apache-2.0",
6
6
  "author": "Token of Trust",
@@ -1567,7 +1567,7 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
1567
1567
  // WITHOUT the global shim (see runPnpmInstall's fallback). Capture both to the
1568
1568
  // log — a silent corepack failure was why the install error carried no cause.
1569
1569
  for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
1570
- const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
1570
+ const r = spawnFn("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"], shell: launcherNeedsShell() });
1571
1571
  if (fd !== null && (r.error || r.status !== 0)) {
1572
1572
  const rerr = /** @type {any} */ (r.error);
1573
1573
  writeSync(fd, `[tot] corepack ${args.join(" ")} → ${rerr?.code || rerr?.message || `exit ${r.status}`}\n`);
@@ -1598,6 +1598,15 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
1598
1598
  * @param {string} cmd @param {string[]} args @param {object} opts
1599
1599
  * @returns {Promise<{ status: number|null, error: Error|null, signal?: string|null }>}
1600
1600
  */
1601
+ /**
1602
+ * On Windows `pnpm`, `corepack` and `npm` are `.cmd` shims, which Node refuses to
1603
+ * spawn without a shell (and a bare name ENOENTs). Their args here are fixed
1604
+ * literals, so the shell sees no user input.
1605
+ */
1606
+ export function launcherNeedsShell(platform = process.platform) {
1607
+ return platform === "win32";
1608
+ }
1609
+
1601
1610
  function spawnAsyncResult(cmd, args, opts = {}) {
1602
1611
  return new Promise((resolvePromise) => {
1603
1612
  let settled = false;
@@ -1654,9 +1663,11 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
1654
1663
  cwd: runnerDir,
1655
1664
  // Send both streams to the log fd (or swallow them) — never inherit.
1656
1665
  stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
1666
+ shell: launcherNeedsShell(),
1657
1667
  });
1658
1668
  if (r.status === 0) return cmd; // installed — the launcher that did it
1659
- if (/** @type {any} */ (r.error)?.code === "ENOENT") {
1669
+ // Through cmd.exe a missing launcher is exit 9009 ("not recognized"), not ENOENT.
1670
+ if (/** @type {any} */ (r.error)?.code === "ENOENT" || (launcherNeedsShell() && r.status === 9009)) {
1660
1671
  // This launcher isn't on the machine — record it and try the next one.
1661
1672
  if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
1662
1673
  continue;
@@ -25,11 +25,7 @@ import { join } from "node:path";
25
25
  import { hasLegacyOperatorEnv, legacyOperatorEnvAdvisory } from "../auth.mjs";
26
26
  import { MIN_NODE, nodeMeetsFloor } from "../ensure-node.mjs";
27
27
  import { clientPackages, osLabel } from "../mcp.mjs";
28
- import {
29
- isNativeWindows,
30
- nativeWindowsDetail,
31
- shouldAttemptDoctorAutoLogin,
32
- } from "../platform.mjs";
28
+ import { isNativeWindows, nativeWindowsDetail } from "../platform.mjs";
33
29
  import { readSessionCredentials, isExpired } from "../token-store.mjs";
34
30
  import { dockerAvailable, tryStartDocker, RENDERER_CACHE_ROOT, rendererArtifactComplete } from "./dev.mjs";
35
31
  import { loginAndCache } from "./login.mjs";
@@ -71,15 +67,15 @@ export function collectChecks(_ctx, env = process.env) {
71
67
  blocking: true,
72
68
  });
73
69
 
74
- // Native Windows isn't a supported host for the local loop (the runner install
75
- // + tar extraction assume a POSIX toolchain) — WSL is, and reports as linux
76
- // here. Say so up front instead of letting `tot login` / `tot dev` fail opaquely.
70
+ // Native Windows runs everything except the local renderer (`dev` / `start`: the
71
+ // runner install + tar extraction assume a POSIX toolchain) — WSL is the host for
72
+ // those, and reports as linux here. Informational, so the gap is visible up front.
77
73
  const nativeWindows = isNativeWindows();
78
74
  checks.push({
79
- name: "supported platform",
80
- pass: !nativeWindows,
75
+ name: "platform",
76
+ pass: true,
81
77
  detail: nativeWindows ? nativeWindowsDetail() : "macOS / Linux / WSL",
82
- blocking: true,
78
+ blocking: false,
83
79
  });
84
80
 
85
81
  // Informational: the versions + OS this invocation is running on — the same
@@ -181,17 +177,13 @@ async function applyFixes(checks, env) {
181
177
 
182
178
  const auth = checks.find((c) => c.name === "auth identity");
183
179
  if (auth && !auth.pass) {
184
- if (!shouldAttemptDoctorAutoLogin()) {
185
- console.log(` ~ not signed in — skipping auto-login (${nativeWindowsDetail()})`);
186
- } else {
187
- const mcpUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
188
- console.log(" ~ not signed in — opening the browser to sign in…");
189
- try {
190
- await loginAndCache(mcpUrl, env, { log: (m) => console.log(` ${m}`) });
191
- console.log(" ✓ signed in");
192
- } catch (e) {
193
- console.log(` ~ couldn't sign in automatically (${e?.message || e}) — run \`tot login\``);
194
- }
180
+ const mcpUrl = env.MCP_BASE_URL || env.TOT_MCP_URL || DEFAULT_MCP_URL;
181
+ console.log(" ~ not signed in — opening the browser to sign in…");
182
+ try {
183
+ await loginAndCache(mcpUrl, env, { log: (m) => console.log(` ${m}`) });
184
+ console.log(" ✓ signed in");
185
+ } catch (e) {
186
+ console.log(` ~ couldn't sign in automatically (${e?.message || e}) — run \`tot login\``);
195
187
  }
196
188
  }
197
189
  }
@@ -252,6 +252,13 @@ export function forgeShimPath(env = process.env) {
252
252
  return join(home, ".tot", "bin", "tot-git-credential");
253
253
  }
254
254
 
255
+ /** The shim path as git's `!` helper runs it (through sh). Git for Windows' sh reads
256
+ * forward slashes, not backslashes, and a home directory may contain spaces, so a
257
+ * Windows path is slash-normalized and single-quoted. POSIX paths pass through. Pure. */
258
+ export function helperCommandPath(shim, platform = process.platform) {
259
+ return platform === "win32" ? shq(shim.replace(/\\/g, "/")) : shim;
260
+ }
261
+
255
262
  /** The shim's contents: pin the ABSOLUTE node + CLI entry active at write time, with a
256
263
  * PATH-search fallback for node, and FAIL LOUD to stderr (never silently) when no node
257
264
  * is found — so a broken helper says how to fix itself instead of yielding an opaque
@@ -324,7 +331,7 @@ export function installForgeCredentialHelper(git, {
324
331
  } = {}) {
325
332
  if (!host) return false;
326
333
  const shim = writeShim(env, nodePath, entryPath);
327
- const helperValue = shim ? `!${shim}` : CREDENTIAL_HELPER;
334
+ const helperValue = shim ? `!${helperCommandPath(shim)}` : CREDENTIAL_HELPER;
328
335
  const key = `credential.https://${host}.helper`;
329
336
  let raw = null;
330
337
  try { raw = git(["config", "--local", "--get-all", key]); } catch { raw = null; }
package/src/open.mjs CHANGED
@@ -58,6 +58,17 @@ export async function firstFreePort(preferred, maxTries = 64) {
58
58
  return preferred;
59
59
  }
60
60
 
61
+ /**
62
+ * The OS opener as [command, args]. Windows goes through `rundll32` with no shell:
63
+ * `cmd /c start` would treat the `&` in an OAuth URL as a command separator.
64
+ * @param {string} url @param {string} platform @returns {[string, string[]]}
65
+ */
66
+ export function openerCommand(url, platform) {
67
+ if (platform === "darwin") return ["open", [url]];
68
+ if (platform === "win32") return ["rundll32", ["url.dll,FileProtocolHandler", url]];
69
+ return ["xdg-open", [url]];
70
+ }
71
+
61
72
  /**
62
73
  * Open `url` in the user's default browser. Best-effort and non-blocking: the
63
74
  * child is detached + unref'd so it never holds `tot` open, and any failure
@@ -65,21 +76,9 @@ export async function firstFreePort(preferred, maxTries = 64) {
65
76
  * @returns {boolean} whether we managed to launch an opener.
66
77
  */
67
78
  export function openBrowser(url, platform = process.platform) {
68
- let cmd, args, shell = false;
69
- if (platform === "darwin") {
70
- cmd = "open";
71
- args = [url];
72
- } else if (platform === "win32") {
73
- // `start` is a shell builtin; the empty first arg is the window title.
74
- cmd = "start";
75
- args = ["", url];
76
- shell = true;
77
- } else {
78
- cmd = "xdg-open";
79
- args = [url];
80
- }
79
+ const [cmd, args] = openerCommand(url, platform);
81
80
  try {
82
- const child = spawn(cmd, args, { stdio: "ignore", detached: true, shell });
81
+ const child = spawn(cmd, args, { stdio: "ignore", detached: true });
83
82
  child.on("error", () => {}); // swallow "opener not found" async errors
84
83
  child.unref();
85
84
  return true;
package/src/platform.mjs CHANGED
@@ -1,10 +1,8 @@
1
1
  /**
2
- * Host-platform readiness for the local loop.
2
+ * Host-platform copy shared by `tot doctor` and login recovery hints.
3
3
  *
4
- * Native Windows is unsupported (runner install + tar extraction assume a POSIX
5
- * toolchain). WSL reports as `linux` and is the supported path on a Windows box.
6
- * Keep the check + copy in ONE place so doctor, the CLI gate, and login recovery
7
- * hints cannot drift.
4
+ * Every command runs on native Windows. WSL reports as `linux` and remains the
5
+ * better-trodden path for the local renderer (`tot dev` / `tot start`).
8
6
  */
9
7
 
10
8
  export const WSL_INSTALL_URL = "https://learn.microsoft.com/windows/wsl/install";
@@ -17,75 +15,17 @@ export function isNativeWindows(platform = process.platform) {
17
15
  return platform === "win32";
18
16
  }
19
17
 
20
- /**
21
- * One-line detail used by `tot doctor` and CliError footers.
22
- * @param {{ url?: string }} [opts]
23
- */
24
- export function nativeWindowsDetail(opts = {}) {
25
- const url = opts.url ?? WSL_INSTALL_URL;
26
- return `native Windows isn't supported yet — run tot inside WSL (${url})`;
27
- }
28
-
29
- /**
30
- * Multi-line refuse message printed before exit. Names the attempted command so
31
- * a pasted invite (`tot login --code … && tot start`) still points at WSL.
32
- * @param {string} [command] e.g. "login", "start"
33
- */
34
- export function nativeWindowsRefuseMessage(command) {
35
- const verb = command ? `tot ${command}` : "tot";
36
- return (
37
- `✗ ${nativeWindowsDetail()}\n` +
38
- ` → next: install WSL, open an Ubuntu (or other Linux) terminal, install Node 24 LTS + git there,\n` +
39
- ` then re-run \`${verb}\` inside WSL — don't keep retrying login/--device on native Windows.\n`
40
- );
18
+ /** One-line detail used by `tot doctor`. */
19
+ export function nativeWindowsDetail() {
20
+ return `native Windows — if \`tot dev\` or \`tot start\` misbehaves, WSL is the better-trodden path (${WSL_INSTALL_URL})`;
41
21
  }
42
22
 
43
23
  /**
44
- * Commands that are safe / useful to run on native Windows so the developer can
45
- * learn the gate (`doctor`) or report friction (`feedback`) without a false start.
46
- */
47
- export const NATIVE_WINDOWS_ALLOWED_COMMANDS = new Set([
48
- "help",
49
- "--help",
50
- "-h",
51
- "--version",
52
- "-v",
53
- "version",
54
- "doctor",
55
- "feedback",
56
- "ideas",
57
- ]);
58
-
59
- /**
60
- * @param {string | null | undefined} command
61
- * @param {string} [platform] Node `process.platform` value (defaults to current).
62
- * @returns {boolean} true when this command must refuse on native Windows
63
- */
64
- export function shouldRefuseNativeWindows(command, platform = process.platform) {
65
- if (!isNativeWindows(/** @type {string} */ (platform))) return false;
66
- const cmd = String(command ?? "").trim();
67
- if (!cmd) return false; // bare `tot` → help
68
- return !NATIVE_WINDOWS_ALLOWED_COMMANDS.has(cmd);
69
- }
70
-
71
- /**
72
- * Recovery hint after a failed `tot login`. Never steers native Windows at
73
- * `--device` — that path still can't run the local loop on win32.
74
- * @param {{ device?: boolean, platform?: string }} [opts]
24
+ * Recovery hint after a failed `tot login`.
25
+ * @param {{ device?: boolean }} [opts]
75
26
  */
76
27
  export function loginFailureNext(opts = {}) {
77
- const platform = /** @type {string} */ (opts.platform ?? process.platform);
78
- if (isNativeWindows(platform)) return nativeWindowsDetail();
79
28
  return opts.device
80
29
  ? "re-run `tot login --device` and approve the code."
81
30
  : "re-run `tot login` and finish the browser sign-in (approve the request) — or try `tot login --device`.";
82
31
  }
83
-
84
- /**
85
- * `tot doctor --fix` must not open OAuth on native Windows — the session
86
- * would be unusable for the local loop, and the browser hop burns a cycle.
87
- * @param {string} [platform] Node `process.platform` value (defaults to current).
88
- */
89
- export function shouldAttemptDoctorAutoLogin(platform = process.platform) {
90
- return !isNativeWindows(/** @type {string} */ (platform));
91
- }