@tokenoftrust/cli 1.3.4-rc.1 → 1.3.4-rc.2
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.cjs +60 -0
- package/bin/tot.mjs +1 -0
- package/package.json +3 -3
- package/src/commands/checkout.mjs +2 -0
- package/src/commands/dev.mjs +61 -12
- package/src/commands/doctor.mjs +20 -2
- package/src/commands/start.mjs +4 -4
- package/src/ensure-node.mjs +44 -0
- package/src/sample.mjs +45 -0
package/bin/tot.cjs
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* The `tot` bin entry: an ES5-only CommonJS launcher whose ONLY job is to
|
|
4
|
+
* enforce the Node floor, then hand off to the real ESM CLI (bin/tot.mjs).
|
|
5
|
+
*
|
|
6
|
+
* Why this exists (and why it must stay ES5/CJS): `engines.node` is advisory —
|
|
7
|
+
* npm warns (EBADENGINE) and installs anyway. And the ESM entry can't guard
|
|
8
|
+
* itself: the module loader PARSES the whole static import graph before
|
|
9
|
+
* evaluating a single line, so on an old Node any modern syntax anywhere in
|
|
10
|
+
* src/ becomes a raw SyntaxError before a version check could run. A CJS file
|
|
11
|
+
* written in ES5 parses on every Node ever shipped, so THIS message — not a
|
|
12
|
+
* stack trace — is what a Node 10/12/14/16 user sees.
|
|
13
|
+
*
|
|
14
|
+
* KEEP THIS FILE ES5: var, string concat, no arrow functions, no template
|
|
15
|
+
* literals, no optional chaining, no const/let. The dynamic import() is
|
|
16
|
+
* hidden inside new Function so old parsers never see the syntax.
|
|
17
|
+
*/
|
|
18
|
+
"use strict";
|
|
19
|
+
|
|
20
|
+
// BY-NECESSITY COPY of the floor in src/ensure-node.mjs (this file can't import
|
|
21
|
+
// ESM) — the floor is Astro's engines requirement, the recommendation is the
|
|
22
|
+
// current LTS. A test asserts the two files stay in sync; bump BOTH together.
|
|
23
|
+
var MIN_NODE = "22.12.0";
|
|
24
|
+
var RECOMMENDED_NODE = "24";
|
|
25
|
+
|
|
26
|
+
var nodeVersion = process.versions.node;
|
|
27
|
+
var have = nodeVersion.split(".");
|
|
28
|
+
var floor = MIN_NODE.split(".");
|
|
29
|
+
var meets = false;
|
|
30
|
+
for (var i = 0; i < 3; i++) {
|
|
31
|
+
var h = parseInt(have[i], 10) || 0;
|
|
32
|
+
var f = parseInt(floor[i], 10) || 0;
|
|
33
|
+
if (h !== f) { meets = h > f; break; }
|
|
34
|
+
if (i === 2) meets = true; // equal on all three parts
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!meets) {
|
|
38
|
+
process.stderr.write(
|
|
39
|
+
"✗ tot needs Node 22.12 or newer — you're on Node " + nodeVersion + ".\n" +
|
|
40
|
+
" → next: install Node " + RECOMMENDED_NODE + " (LTS) — nvm: `nvm install " + RECOMMENDED_NODE +
|
|
41
|
+
" && nvm use " + RECOMMENDED_NODE + "`, or https://nodejs.org/ — then re-run the same command.\n"
|
|
42
|
+
);
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Supported Node from here on. Hand off to the ESM CLI; pathToFileURL keeps
|
|
47
|
+
// the import specifier correct on Windows drive-letter paths too.
|
|
48
|
+
var path = require("path");
|
|
49
|
+
var pathToFileURL = require("url").pathToFileURL;
|
|
50
|
+
var entry = pathToFileURL(path.join(__dirname, "tot.mjs")).href;
|
|
51
|
+
|
|
52
|
+
// new Function with a CONSTANT body ("return import(u)") — nothing is ever
|
|
53
|
+
// interpolated into the code string; the entry URL travels as an argument. This
|
|
54
|
+
// indirection exists only so pre-import() parsers never see the import syntax.
|
|
55
|
+
new Function("u", "return import(u)")(entry).catch(function (e) {
|
|
56
|
+
// tot.mjs formats + exits on its own errors; landing here means the CLI
|
|
57
|
+
// itself failed to LOAD on a supported Node — a packaging bug, worth the detail.
|
|
58
|
+
process.stderr.write("✗ tot failed to start: " + ((e && e.message) || e) + "\n");
|
|
59
|
+
process.exit(1);
|
|
60
|
+
});
|
package/bin/tot.mjs
CHANGED
|
@@ -25,6 +25,7 @@
|
|
|
25
25
|
* fetches the published storefront runner; `tot checkout/validate/submit` are
|
|
26
26
|
* pure Node. Dependency-free by design so `npm i -g @tokenoftrust/cli` stays light.
|
|
27
27
|
*/
|
|
28
|
+
import "../src/ensure-node.mjs"; // hard Node-version gate — must stay first (see the module doc)
|
|
28
29
|
import { readFileSync } from "node:fs";
|
|
29
30
|
import { detectContext } from "../src/context.mjs";
|
|
30
31
|
import { printError } from "../src/errors.mjs";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tokenoftrust/cli",
|
|
3
|
-
"version": "1.3.4-rc.
|
|
3
|
+
"version": "1.3.4-rc.2",
|
|
4
4
|
"description": "Token of Trust developer CLI — check out 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",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
],
|
|
21
21
|
"type": "module",
|
|
22
22
|
"bin": {
|
|
23
|
-
"tot": "./bin/tot.
|
|
23
|
+
"tot": "./bin/tot.cjs"
|
|
24
24
|
},
|
|
25
25
|
"files": [
|
|
26
26
|
"bin",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
31
|
"engines": {
|
|
32
|
-
"node": ">=
|
|
32
|
+
"node": ">=22.12.0"
|
|
33
33
|
},
|
|
34
34
|
"publishConfig": {
|
|
35
35
|
"access": "public",
|
|
@@ -23,6 +23,7 @@ import { promisify } from "node:util";
|
|
|
23
23
|
import { createMcpClient } from "../mcp.mjs";
|
|
24
24
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
25
25
|
import { CliError, fail, formatError } from "../errors.mjs";
|
|
26
|
+
import { writeNvmrc } from "../sample.mjs";
|
|
26
27
|
|
|
27
28
|
const execFileP = promisify(execFile);
|
|
28
29
|
|
|
@@ -206,6 +207,7 @@ async function cloneRepo(gitRemote, dir, redact) {
|
|
|
206
207
|
});
|
|
207
208
|
}
|
|
208
209
|
const head = (await git(["-C", dir, "log", "-1", "--oneline"])).trim();
|
|
210
|
+
writeNvmrc(dir); // version-manager hooks land on a supported Node on cd
|
|
209
211
|
return { dir, head };
|
|
210
212
|
}
|
|
211
213
|
|
package/src/commands/dev.mjs
CHANGED
|
@@ -32,7 +32,7 @@
|
|
|
32
32
|
* either way.
|
|
33
33
|
*/
|
|
34
34
|
import { spawn, spawnSync, execFileSync } from "node:child_process";
|
|
35
|
-
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, readdirSync, createWriteStream, openSync, closeSync } from "node:fs";
|
|
35
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync, rmSync, readdirSync, createWriteStream, openSync, closeSync, writeSync } from "node:fs";
|
|
36
36
|
import { homedir, tmpdir } from "node:os";
|
|
37
37
|
import { join, resolve } from "node:path";
|
|
38
38
|
import { createHash } from "node:crypto";
|
|
@@ -852,10 +852,12 @@ export async function installRunnerTarball({ source, version, isUrl = true, stri
|
|
|
852
852
|
// reach) — an invited dev's machine config must never decide where the
|
|
853
853
|
// runner's public deps come from. A project-level .npmrc wins over the user's.
|
|
854
854
|
writeFileSync(join(stagingDir, ".npmrc"), "registry=https://registry.npmjs.org/\n");
|
|
855
|
-
ensureCorepackPnpm(stagingDir);
|
|
856
855
|
// The install is the long, noisy step — tick a spinner while its output goes
|
|
857
856
|
// to a log, so the terminal shows one clean line instead of the pnpm firehose.
|
|
857
|
+
// corepack setup logs to the SAME file so its failures aren't invisible (they
|
|
858
|
+
// were the silent cause of "couldn't set up the store preview engine").
|
|
858
859
|
const installLog = join(RENDERER_CACHE_ROOT, `${version}.install.log`);
|
|
860
|
+
ensureCorepackPnpm(stagingDir, { logPath: installLog });
|
|
859
861
|
const installSpin = startProgress("installing the store preview engine…", {
|
|
860
862
|
stages: [{ afterMs: 20000, text: "still setting up the preview engine (first run only)…" }],
|
|
861
863
|
});
|
|
@@ -919,7 +921,7 @@ function extractTarball(archivePath, destDir, { strip = 0 } = {}) {
|
|
|
919
921
|
* itself is missing (very old Node), pnpm install below will surface that
|
|
920
922
|
* clearly instead.
|
|
921
923
|
*/
|
|
922
|
-
function ensureCorepackPnpm(runnerDir) {
|
|
924
|
+
function ensureCorepackPnpm(runnerDir, { logPath } = {}) {
|
|
923
925
|
const pkgPath = join(runnerDir, "package.json");
|
|
924
926
|
if (!existsSync(pkgPath)) return;
|
|
925
927
|
let pm;
|
|
@@ -929,8 +931,22 @@ function ensureCorepackPnpm(runnerDir) {
|
|
|
929
931
|
return;
|
|
930
932
|
}
|
|
931
933
|
if (!pm) return;
|
|
932
|
-
|
|
933
|
-
|
|
934
|
+
const fd = logPath ? openSync(logPath, "a") : null;
|
|
935
|
+
try {
|
|
936
|
+
// `enable` writes global shims (needs write access to the Node bin dir — an
|
|
937
|
+
// invited dev often lacks it); `prepare --activate` caches+activates the
|
|
938
|
+
// pinned pnpm in corepack's OWN store, which `corepack pnpm …` can then run
|
|
939
|
+
// WITHOUT the global shim (see runPnpmInstall's fallback). Capture both to the
|
|
940
|
+
// log — a silent corepack failure was why the install error carried no cause.
|
|
941
|
+
for (const args of [["enable"], ["prepare", pm, "--activate"]]) {
|
|
942
|
+
const r = spawnSync("corepack", args, { stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"] });
|
|
943
|
+
if (fd !== null && (r.error || r.status !== 0)) {
|
|
944
|
+
writeSync(fd, `[tot] corepack ${args.join(" ")} → ${r.error?.code || r.error?.message || `exit ${r.status}`}\n`);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
} finally {
|
|
948
|
+
if (fd !== null) closeSync(fd);
|
|
949
|
+
}
|
|
934
950
|
}
|
|
935
951
|
|
|
936
952
|
/**
|
|
@@ -942,18 +958,51 @@ function ensureCorepackPnpm(runnerDir) {
|
|
|
942
958
|
*/
|
|
943
959
|
function runPnpmInstall(runnerDir, { logPath } = {}) {
|
|
944
960
|
const fd = logPath ? openSync(logPath, "a") : null;
|
|
961
|
+
const installArgs = ["install", "--config.dangerouslyAllowAllBuilds=true"];
|
|
962
|
+
// npm FIRST: it ships with EVERY Node (including 25+, where corepack is no
|
|
963
|
+
// longer bundled), so it's the launcher with zero machine-specific setup —
|
|
964
|
+
// no global pnpm, no corepack shim dance. The runner tree is built to be
|
|
965
|
+
// npm-installable (build-runner.mjs rewrites `workspace:*` → "*" and emits an
|
|
966
|
+
// npm `workspaces` field; verified end-to-end with both installers). pnpm and
|
|
967
|
+
// the corepack-pinned pnpm remain as fallbacks for hosts with a broken npm.
|
|
968
|
+
// If a launcher isn't installed at all (ENOENT) we move on; a launcher that
|
|
969
|
+
// RAN but whose install failed is the real error and stops the loop.
|
|
970
|
+
// REQUIRES a runner >= 1.3.4-rc.2 — older runner tarballs still carry
|
|
971
|
+
// `workspace:*` deps npm rejects (harmless here: pickRunnerVersion pins the
|
|
972
|
+
// runner to this CLI's version, so this CLI never installs those).
|
|
973
|
+
const attempts = [
|
|
974
|
+
{ cmd: "npm", args: ["install", "--no-audit", "--no-fund"] },
|
|
975
|
+
{ cmd: "pnpm", args: installArgs },
|
|
976
|
+
{ cmd: "corepack", args: ["pnpm", ...installArgs] },
|
|
977
|
+
];
|
|
945
978
|
try {
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
979
|
+
for (const { cmd, args } of attempts) {
|
|
980
|
+
const r = spawnSync(cmd, args, {
|
|
981
|
+
cwd: runnerDir,
|
|
982
|
+
// Send both streams to the log fd (or swallow them) — never inherit.
|
|
983
|
+
stdio: ["ignore", fd ?? "ignore", fd ?? "ignore"],
|
|
984
|
+
});
|
|
985
|
+
if (r.status === 0) return; // installed
|
|
986
|
+
if (r.error?.code === "ENOENT") {
|
|
987
|
+
// This launcher isn't on the machine — record it and try the next one.
|
|
988
|
+
if (fd !== null) writeSync(fd, `[tot] ${cmd} not found (ENOENT) — trying the next launcher\n`);
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
// The launcher ran; the install itself failed. That's the actionable error.
|
|
992
|
+
throw new CliError(
|
|
953
993
|
`couldn't set up the store preview engine${pnpmFailureHint(logPath)}` +
|
|
954
994
|
(logPath ? `\n details: ${logPath}` : ""),
|
|
995
|
+
{ next: "check the details log above, then re-run `tot start` (it resumes from the cache)" },
|
|
955
996
|
);
|
|
956
997
|
}
|
|
998
|
+
// Every launcher ENOENT'd → there's no pnpm on this machine and corepack
|
|
999
|
+
// couldn't provide one (corepack isn't bundled on Node 25+). npm ships with
|
|
1000
|
+
// every Node, so `npm i -g pnpm` is the escape hatch that always exists.
|
|
1001
|
+
throw new CliError(
|
|
1002
|
+
"couldn't set up the store preview engine — pnpm isn't available on this machine" +
|
|
1003
|
+
(logPath ? `\n details: ${logPath}` : ""),
|
|
1004
|
+
{ next: "install pnpm with `npm i -g pnpm` (or `corepack enable`), then re-run `tot start`" },
|
|
1005
|
+
);
|
|
957
1006
|
} finally {
|
|
958
1007
|
if (fd !== null) closeSync(fd);
|
|
959
1008
|
}
|
package/src/commands/doctor.mjs
CHANGED
|
@@ -23,6 +23,7 @@ import { existsSync, mkdirSync } from "node:fs";
|
|
|
23
23
|
import { homedir } from "node:os";
|
|
24
24
|
import { join } from "node:path";
|
|
25
25
|
import { hasOperatorCreds } from "../auth.mjs";
|
|
26
|
+
import { MIN_NODE, nodeMeetsFloor } from "../ensure-node.mjs";
|
|
26
27
|
import { clientPackages, osLabel } from "../mcp.mjs";
|
|
27
28
|
import { defaultCredentialsPath, readCredentials, isExpired } from "../token-store.mjs";
|
|
28
29
|
import { dockerAvailable, tryStartDocker } from "./dev.mjs";
|
|
@@ -58,8 +59,25 @@ const USAGE = `tot doctor — is this machine ready to run the loop?
|
|
|
58
59
|
export function collectChecks(_ctx, env = process.env) {
|
|
59
60
|
const checks = [];
|
|
60
61
|
|
|
61
|
-
|
|
62
|
-
|
|
62
|
+
checks.push({
|
|
63
|
+
name: `node >= ${MIN_NODE}`,
|
|
64
|
+
pass: nodeMeetsFloor(process.versions.node),
|
|
65
|
+
detail: `have ${process.versions.node}`,
|
|
66
|
+
blocking: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
// Native Windows isn't a supported host for the local loop (the runner install
|
|
70
|
+
// + tar extraction assume a POSIX toolchain) — WSL is, and reports as linux
|
|
71
|
+
// here. Say so up front instead of letting `tot dev` fail with an opaque ENOENT.
|
|
72
|
+
const nativeWindows = process.platform === "win32";
|
|
73
|
+
checks.push({
|
|
74
|
+
name: "supported platform",
|
|
75
|
+
pass: !nativeWindows,
|
|
76
|
+
detail: nativeWindows
|
|
77
|
+
? "native Windows isn't supported yet — run tot inside WSL (https://learn.microsoft.com/windows/wsl/install)"
|
|
78
|
+
: "macOS / Linux / WSL",
|
|
79
|
+
blocking: true,
|
|
80
|
+
});
|
|
63
81
|
|
|
64
82
|
// Informational: the versions + OS this invocation is running on — the same
|
|
65
83
|
// context the run banners/error footers stamp, surfaced up front for a bug report.
|
package/src/commands/start.mjs
CHANGED
|
@@ -49,6 +49,7 @@ import { resolve } from "node:path";
|
|
|
49
49
|
import { createInterface } from "node:readline/promises";
|
|
50
50
|
|
|
51
51
|
import { detectContext } from "../context.mjs";
|
|
52
|
+
import { MIN_NODE, RECOMMENDED_NODE, nodeMeetsFloor } from "../ensure-node.mjs";
|
|
52
53
|
import { createMcpClient, versionStamp } from "../mcp.mjs";
|
|
53
54
|
import { establishSession, AuthUnavailableError } from "../auth.mjs";
|
|
54
55
|
import { CliError, fail, formatError, exitCodeFor } from "../errors.mjs";
|
|
@@ -284,10 +285,9 @@ export async function run(argv, ctx) {
|
|
|
284
285
|
async function runSampleStart(args, ctx, env, startedAt) {
|
|
285
286
|
try {
|
|
286
287
|
// Minimal preflight — the free path needs ONLY Node (no git, no Docker, no auth).
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
next: "upgrade Node, then re-run",
|
|
288
|
+
if (!nodeMeetsFloor(process.versions.node)) {
|
|
289
|
+
throw new CliError(`Node ${MIN_NODE}+ is required (have ${process.versions.node})`, {
|
|
290
|
+
next: `install Node ${RECOMMENDED_NODE} (LTS), then re-run`,
|
|
291
291
|
exitCode: 2,
|
|
292
292
|
});
|
|
293
293
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Node floor — single ESM source of truth — plus a runtime gate that is
|
|
3
|
+
* DEFENSE IN DEPTH behind bin/tot.cjs.
|
|
4
|
+
*
|
|
5
|
+
* THE FLOOR IS SET BY WHAT `tot dev` RUNS, not by the CLI's own code: the store
|
|
6
|
+
* preview engine is Astro (engines >=22.12.0) + wrangler (>=22). A CLI that let
|
|
7
|
+
* an older Node through would pass login/checkout and then fail deep inside the
|
|
8
|
+
* runner — the exact opaque dead-end this gate exists to prevent. Keep MIN_NODE
|
|
9
|
+
* aligned with the runner's real dependency floor when upgrading Astro.
|
|
10
|
+
*
|
|
11
|
+
* `engines.node` in package.json is advisory: `npm i -g` prints EBADENGINE and
|
|
12
|
+
* installs anyway (only `engine-strict=true` blocks it), so the floor must be
|
|
13
|
+
* enforced at runtime. The PRIMARY gate is bin/tot.cjs (the ES5 CommonJS
|
|
14
|
+
* launcher): ESM parses the entire static import graph before evaluating
|
|
15
|
+
* anything, so a check inside the .mjs world can be preempted by a SyntaxError
|
|
16
|
+
* on a Node old enough to matter. bin/tot.cjs carries a BY-NECESSITY COPY of
|
|
17
|
+
* this floor (it can't import ESM) — a test keeps the two in sync. This module
|
|
18
|
+
* re-runs the same check on the ESM side for anyone invoking `node bin/tot.mjs`
|
|
19
|
+
* directly (bypassing the bin shim). SIDE-EFFECTING BY DESIGN; bin/tot.mjs
|
|
20
|
+
* imports it first.
|
|
21
|
+
*/
|
|
22
|
+
export const MIN_NODE = "22.12.0"; // Astro's engines floor — see the module doc
|
|
23
|
+
export const RECOMMENDED_NODE = "24"; // current LTS — what the fix-it copy suggests
|
|
24
|
+
|
|
25
|
+
/** Does `version` (e.g. "22.12.0") meet the MIN_NODE floor? */
|
|
26
|
+
export function nodeMeetsFloor(version) {
|
|
27
|
+
const [maj = 0, min = 0, pat = 0] = String(version).split(".").map((n) => parseInt(n, 10) || 0);
|
|
28
|
+
const [fMaj, fMin, fPat] = MIN_NODE.split(".").map((n) => parseInt(n, 10));
|
|
29
|
+
return maj !== fMaj ? maj > fMaj : min !== fMin ? min > fMin : pat >= fPat;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The house-style gate failure, shared by this module and (as copy) bin/tot.cjs. */
|
|
33
|
+
export function floorMessage(haveVersion) {
|
|
34
|
+
return (
|
|
35
|
+
`✗ tot needs Node ${MIN_NODE.replace(/\.0$/, "")} or newer — you're on Node ${haveVersion}.\n` +
|
|
36
|
+
` → next: install Node ${RECOMMENDED_NODE} (LTS) — nvm: \`nvm install ${RECOMMENDED_NODE} && nvm use ${RECOMMENDED_NODE}\`, ` +
|
|
37
|
+
`or https://nodejs.org/ — then re-run the same command.\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (!nodeMeetsFloor(process.versions.node)) {
|
|
42
|
+
process.stderr.write(floorMessage(process.versions.node));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
package/src/sample.mjs
CHANGED
|
@@ -28,11 +28,55 @@
|
|
|
28
28
|
import {
|
|
29
29
|
cpSync, existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync,
|
|
30
30
|
} from "node:fs";
|
|
31
|
+
import { homedir } from "node:os";
|
|
31
32
|
import { fileURLToPath } from "node:url";
|
|
32
33
|
import { dirname, join, resolve } from "node:path";
|
|
34
|
+
import { RECOMMENDED_NODE, nodeMeetsFloor } from "./ensure-node.mjs";
|
|
33
35
|
|
|
34
36
|
const here = dirname(fileURLToPath(import.meta.url)); // packages/cli/src
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Drop an `.nvmrc` into a fresh checkout/scaffold (when the repo doesn't carry
|
|
40
|
+
* one). Inert text — but nvm/fnm/asdf all read it, so developers with
|
|
41
|
+
* version-manager shell hooks land on a supported Node just by cd-ing into
|
|
42
|
+
* their store, and a plain `nvm use` works with no argument. Best-effort:
|
|
43
|
+
* never fails the checkout.
|
|
44
|
+
* @param {string} dir @param {NodeJS.ProcessEnv} [env]
|
|
45
|
+
*/
|
|
46
|
+
export function writeNvmrc(dir, env = process.env) {
|
|
47
|
+
try {
|
|
48
|
+
const p = join(dir, ".nvmrc");
|
|
49
|
+
if (existsSync(p)) return; // the store repo's own pin wins
|
|
50
|
+
writeFileSync(p, pickNvmrcVersion(env) + "\n");
|
|
51
|
+
} catch {
|
|
52
|
+
/* a missing .nvmrc never blocks the loop */
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The version `.nvmrc` should pin: the NEWEST Node the developer ALREADY has
|
|
58
|
+
* installed under nvm that meets the floor — so `nvm use` succeeds with zero
|
|
59
|
+
* new downloads — falling back to the recommended LTS major when nvm is absent
|
|
60
|
+
* or has nothing recent enough (there `nvm use` correctly prompts an install).
|
|
61
|
+
* Pure given env; exported for tests.
|
|
62
|
+
* @param {NodeJS.ProcessEnv} [env]
|
|
63
|
+
* @returns {string}
|
|
64
|
+
*/
|
|
65
|
+
export function pickNvmrcVersion(env = process.env) {
|
|
66
|
+
try {
|
|
67
|
+
const root = join(env.NVM_DIR || join(homedir(), ".nvm"), "versions", "node");
|
|
68
|
+
const best = readdirSync(root)
|
|
69
|
+
.map((name) => /^v(\d+)\.(\d+)\.(\d+)$/.exec(name))
|
|
70
|
+
.filter((m) => m && nodeMeetsFloor(m.slice(1).join(".")))
|
|
71
|
+
.map((m) => m.slice(1).map(Number))
|
|
72
|
+
.sort((a, b) => b[0] - a[0] || b[1] - a[1] || b[2] - a[2])[0];
|
|
73
|
+
if (best) return best.join(".");
|
|
74
|
+
} catch {
|
|
75
|
+
/* no nvm dir — fall through to the LTS recommendation */
|
|
76
|
+
}
|
|
77
|
+
return RECOMMENDED_NODE;
|
|
78
|
+
}
|
|
79
|
+
|
|
36
80
|
/** The generic sample tenant the runner registers with a compliance block (see module doc). */
|
|
37
81
|
export const SAMPLE_TENANT = "sample-store.example";
|
|
38
82
|
/** Dotted scope → the runner's path-prefix route + registry lookup (must contain a dot per context.mjs). */
|
|
@@ -128,6 +172,7 @@ export function scaffoldSample(destDir, { force = false, log = () => {} } = {})
|
|
|
128
172
|
}
|
|
129
173
|
mkdirSync(join(dir, ".tot"), { recursive: true });
|
|
130
174
|
writeFileSync(join(dir, ".tot", "config.json"), JSON.stringify(sampleConfig(), null, 2) + "\n");
|
|
175
|
+
writeNvmrc(dir); // version-manager hooks land on a supported Node on cd
|
|
131
176
|
|
|
132
177
|
log(` ✓ scaffolded a sample store → ${dir}`);
|
|
133
178
|
return { dir, tenant: SAMPLE_TENANT, scope: SAMPLE_SCOPE, created: true };
|