@tokenoftrust/cli 3.10.2 → 3.10.4
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 +0 -12
- package/package.json +1 -1
- package/src/commands/dev.mjs +103 -9
- package/src/commands/doctor.mjs +14 -22
- package/src/git-credential.mjs +8 -1
- package/src/open.mjs +13 -14
- package/src/platform.mjs +8 -68
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.
|
|
3
|
+
"version": "3.10.4",
|
|
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",
|
package/src/commands/dev.mjs
CHANGED
|
@@ -913,7 +913,20 @@ export function rendererNativeBindingGaps(runnerDir, { platform = process.platfo
|
|
|
913
913
|
* `platform`/`arch` are injectable so this is testable off the host's real arch.
|
|
914
914
|
*/
|
|
915
915
|
export function rendererCacheHealthy(runnerDir, opts = {}) {
|
|
916
|
-
return rendererNativeBindingGaps(runnerDir, opts).length === 0;
|
|
916
|
+
return rendererNativeBindingGaps(runnerDir, opts).length === 0 && rendererDepsResolve(runnerDir);
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Does the renderer's own dependency tree resolve from where it sits? `existsSync`
|
|
921
|
+
* follows links, so a `node_modules` entry whose target is gone (a junction left
|
|
922
|
+
* pointing into a renamed staging directory) reads as absent. A tree with no
|
|
923
|
+
* `apps/storefront/package.json` has nothing to check and passes.
|
|
924
|
+
* @param {string} runnerDir
|
|
925
|
+
*/
|
|
926
|
+
export function rendererDepsResolve(runnerDir) {
|
|
927
|
+
const app = join(runnerDir, "apps", "storefront");
|
|
928
|
+
if (!existsSync(join(app, "package.json"))) return true;
|
|
929
|
+
return existsSync(join(app, "node_modules", "astro", "package.json"));
|
|
917
930
|
}
|
|
918
931
|
|
|
919
932
|
/**
|
|
@@ -1266,7 +1279,7 @@ export async function installRunnerTarball(
|
|
|
1266
1279
|
// Bounded to one retry so a genuinely-broken source still fails loudly.
|
|
1267
1280
|
for (let attempt = 1; ; attempt++) {
|
|
1268
1281
|
const archivePath = /** @type {string} */ (isUrl ? join(tmpdir(), `tot-renderer-${process.pid}-${attempt}.tar.gz`) : localSource);
|
|
1269
|
-
const stagingDir = `${runnerDir}.staging-${process.pid}`;
|
|
1282
|
+
const stagingDir = installsInPlace() ? runnerDir : `${runnerDir}.staging-${process.pid}`;
|
|
1270
1283
|
try {
|
|
1271
1284
|
if (isUrl) {
|
|
1272
1285
|
// The fetch itself is otherwise silent (no per-byte output) and can run
|
|
@@ -1339,8 +1352,10 @@ export async function installRunnerTarball(
|
|
|
1339
1352
|
// Atomic-ish: only rename into the final, discoverable path once install
|
|
1340
1353
|
// succeeded, so a crashed/interrupted run never leaves a half-built cache
|
|
1341
1354
|
// entry that a later `tot dev` would treat as ready.
|
|
1342
|
-
|
|
1343
|
-
|
|
1355
|
+
if (stagingDir !== runnerDir) {
|
|
1356
|
+
rmSync(runnerDir, { recursive: true, force: true });
|
|
1357
|
+
renameSync(stagingDir, runnerDir);
|
|
1358
|
+
}
|
|
1344
1359
|
// Fence a fresh install against npm/cli#4828: if the installer left the wrong
|
|
1345
1360
|
// arch's native bindings (or none) for this host, DON'T stamp the completion
|
|
1346
1361
|
// marker — an unmarked tree is never reused, so the next run reinstalls cleanly
|
|
@@ -1368,6 +1383,19 @@ export async function installRunnerTarball(
|
|
|
1368
1383
|
/** @type {any} */ (err).permanent = true;
|
|
1369
1384
|
throw err;
|
|
1370
1385
|
}
|
|
1386
|
+
if (!rendererDepsResolve(runnerDir)) {
|
|
1387
|
+
await emitObstacle("install-failed");
|
|
1388
|
+
const err = new CliError(
|
|
1389
|
+
`the store preview engine installed but its dependencies don't resolve from where it was installed` +
|
|
1390
|
+
`\n details: ${installLog}`,
|
|
1391
|
+
{
|
|
1392
|
+
next: `re-run \`tot start --reset-cache\`; if it repeats, file feedback naming preview engine ${version} and your OS`,
|
|
1393
|
+
exitCode: 2,
|
|
1394
|
+
},
|
|
1395
|
+
);
|
|
1396
|
+
/** @type {any} */ (err).permanent = true;
|
|
1397
|
+
throw err;
|
|
1398
|
+
}
|
|
1371
1399
|
writeCacheMarker(runnerDir, { version, integrity: contentId });
|
|
1372
1400
|
return runnerDir;
|
|
1373
1401
|
} catch (e) {
|
|
@@ -1547,9 +1575,9 @@ function extractTarball(archivePath, destDir, { strip = 0 } = {}) {
|
|
|
1547
1575
|
* missing or disabled (a locked-down host), pnpm install below falls through to
|
|
1548
1576
|
* the pnpm-missing obstacle instead of failing here.
|
|
1549
1577
|
* @param {string} runnerDir
|
|
1550
|
-
* @param {{ logPath?: string, spawnFn?:
|
|
1578
|
+
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1551
1579
|
*/
|
|
1552
|
-
export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn =
|
|
1580
|
+
export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnLauncherSync } = {}) {
|
|
1553
1581
|
const pkgPath = join(runnerDir, "package.json");
|
|
1554
1582
|
if (!existsSync(pkgPath)) return;
|
|
1555
1583
|
let pm;
|
|
@@ -1598,13 +1626,68 @@ export function ensureCorepackPnpm(runnerDir, { logPath, spawnFn = spawnSync } =
|
|
|
1598
1626
|
* @param {string} cmd @param {string[]} args @param {object} opts
|
|
1599
1627
|
* @returns {Promise<{ status: number|null, error: Error|null, signal?: string|null }>}
|
|
1600
1628
|
*/
|
|
1629
|
+
/**
|
|
1630
|
+
* Windows installs the renderer where it will live. pnpm there writes `node_modules`
|
|
1631
|
+
* junctions with ABSOLUTE targets into the directory it installs in, so renaming a
|
|
1632
|
+
* staging directory into place leaves every one dangling. The completion marker is still
|
|
1633
|
+
* written last and an unmarked tree is never reused, so a half install cannot be served.
|
|
1634
|
+
* @param {string} [platform]
|
|
1635
|
+
*/
|
|
1636
|
+
export function installsInPlace(platform = process.platform) {
|
|
1637
|
+
return platform === "win32";
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
/**
|
|
1641
|
+
* On Windows `pnpm`, `corepack` and `npm` are `.cmd` shims, which Node refuses to
|
|
1642
|
+
* spawn without a shell.
|
|
1643
|
+
* @param {string} [platform]
|
|
1644
|
+
*/
|
|
1645
|
+
export function launcherNeedsShell(platform = process.platform) {
|
|
1646
|
+
return platform === "win32";
|
|
1647
|
+
}
|
|
1648
|
+
|
|
1649
|
+
const SAFE_LAUNCHER_ARG = /^[\w@.:+=/-]+$/;
|
|
1650
|
+
|
|
1651
|
+
/**
|
|
1652
|
+
* `[file, args, opts]` to spawn a package-manager launcher. On Windows that is one
|
|
1653
|
+
* command string run through a shell (an args array alongside `shell: true` trips
|
|
1654
|
+
* Node's DEP0190 warning). The shell sees every argument, so each must match a safe
|
|
1655
|
+
* alphabet; anything else throws instead of reaching it.
|
|
1656
|
+
* @param {string} cmd @param {string[]} args @param {object} [opts] @param {string} [platform]
|
|
1657
|
+
* @returns {[string, string[], object]}
|
|
1658
|
+
*/
|
|
1659
|
+
export function launcherSpawn(cmd, args, opts = {}, platform = process.platform) {
|
|
1660
|
+
if (!launcherNeedsShell(platform)) return [cmd, args, opts];
|
|
1661
|
+
for (const a of args) {
|
|
1662
|
+
if (!SAFE_LAUNCHER_ARG.test(a)) throw new Error(`refusing to pass an unsafe argument to ${cmd} through a shell: ${a}`);
|
|
1663
|
+
}
|
|
1664
|
+
return [[cmd, ...args].join(" "), [], { ...opts, shell: true }];
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1667
|
+
/**
|
|
1668
|
+
* Is the launcher on PATH? POSIX answers with ENOENT at spawn, so this is always true
|
|
1669
|
+
* there. Through cmd.exe a missing launcher is an ordinary non-zero exit that cannot be
|
|
1670
|
+
* told from a failed install, so Windows asks `where` (a real executable, no shell).
|
|
1671
|
+
* @param {string} cmd @param {string} [platform]
|
|
1672
|
+
*/
|
|
1673
|
+
export function launcherAvailable(cmd, platform = process.platform) {
|
|
1674
|
+
if (!launcherNeedsShell(platform)) return true;
|
|
1675
|
+
return spawnSync("where", [cmd], { stdio: "ignore" }).status === 0;
|
|
1676
|
+
}
|
|
1677
|
+
|
|
1678
|
+
/** @param {string} cmd @param {string[]} args @param {object} [opts] */
|
|
1679
|
+
function spawnLauncherSync(cmd, args, opts = {}) {
|
|
1680
|
+
const [file, fileArgs, fileOpts] = launcherSpawn(cmd, args, opts);
|
|
1681
|
+
return spawnSync(file, fileArgs, fileOpts);
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1601
1684
|
function spawnAsyncResult(cmd, args, opts = {}) {
|
|
1602
1685
|
return new Promise((resolvePromise) => {
|
|
1603
1686
|
let settled = false;
|
|
1604
1687
|
const settle = (v) => { if (!settled) { settled = true; resolvePromise(v); } };
|
|
1605
1688
|
let child;
|
|
1606
1689
|
try {
|
|
1607
|
-
child = spawn(cmd, args, opts);
|
|
1690
|
+
child = spawn(...launcherSpawn(cmd, args, opts));
|
|
1608
1691
|
} catch (error) {
|
|
1609
1692
|
settle({ status: null, error });
|
|
1610
1693
|
return;
|
|
@@ -1616,10 +1699,10 @@ function spawnAsyncResult(cmd, args, opts = {}) {
|
|
|
1616
1699
|
|
|
1617
1700
|
/**
|
|
1618
1701
|
* @param {string} runnerDir
|
|
1619
|
-
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any }} [opts]
|
|
1702
|
+
* @param {{ logPath?: string, spawnFn?: (cmd: string, args: string[], opts?: any) => any, available?: (cmd: string) => boolean }} [opts]
|
|
1620
1703
|
* @returns {Promise<string>} the launcher that completed the install ("pnpm" | "corepack" | "npm").
|
|
1621
1704
|
*/
|
|
1622
|
-
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult } = {}) {
|
|
1705
|
+
export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncResult, available = launcherAvailable } = {}) {
|
|
1623
1706
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
1624
1707
|
const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
|
|
1625
1708
|
// pnpm FIRST: the renderer tree is a bundle of native optional deps (rolldown,
|
|
@@ -1644,6 +1727,10 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1644
1727
|
];
|
|
1645
1728
|
try {
|
|
1646
1729
|
for (const { cmd, args } of attempts) {
|
|
1730
|
+
if (!available(cmd)) {
|
|
1731
|
+
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found on PATH — trying the next launcher\n`);
|
|
1732
|
+
continue;
|
|
1733
|
+
}
|
|
1647
1734
|
// Awaited: the default spawnFn (spawnAsyncResult) runs the launcher
|
|
1648
1735
|
// NON-BLOCKING so the caller's "installing…" spinner keeps ticking during
|
|
1649
1736
|
// the (long, one-time) npm install instead of freezing at "(0s)" — a
|
|
@@ -1661,6 +1748,13 @@ export async function runPnpmInstall(runnerDir, { logPath, spawnFn = spawnAsyncR
|
|
|
1661
1748
|
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
|
|
1662
1749
|
continue;
|
|
1663
1750
|
}
|
|
1751
|
+
// corepack is the one launcher that can run and still fail for a reason that has
|
|
1752
|
+
// nothing to do with the install (no write access to its shim directory, no
|
|
1753
|
+
// network to fetch pnpm); npm below is the escape hatch that always exists.
|
|
1754
|
+
if (cmd === "corepack") {
|
|
1755
|
+
if (fd !== null) writeSync(fd, `[tot] corepack exited ${r.status} — trying npm\n`);
|
|
1756
|
+
continue;
|
|
1757
|
+
}
|
|
1664
1758
|
// The launcher ran; the install itself failed. That's the actionable error.
|
|
1665
1759
|
// installRunnerTarball retries transient failures once from a clean slate;
|
|
1666
1760
|
// deterministic broken-release 404s are marked permanent below and skip it.
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -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
|
|
75
|
-
// + tar extraction assume a POSIX toolchain) — WSL is
|
|
76
|
-
//
|
|
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: "
|
|
80
|
-
pass:
|
|
75
|
+
name: "platform",
|
|
76
|
+
pass: true,
|
|
81
77
|
detail: nativeWindows ? nativeWindowsDetail() : "macOS / Linux / WSL",
|
|
82
|
-
blocking:
|
|
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
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
console.log("
|
|
189
|
-
|
|
190
|
-
|
|
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
|
}
|
package/src/git-credential.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
|
2
|
+
* Host-platform copy shared by `tot doctor` and login recovery hints.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
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
|
-
|
|
22
|
-
|
|
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
|
-
*
|
|
45
|
-
*
|
|
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
|
-
}
|