nunmai 0.1.2 → 0.1.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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/bin/nunmai.js +102 -3
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -15,10 +15,13 @@ engine; the first run opens the AI-account wizard (Claude, ChatGPT, Kimi,
15
15
  Gemini, OpenRouter).
16
16
 
17
17
  - macOS, Linux, Windows 10/11 (PowerShell), Android (Termux).
18
- - **Local install** (`npm install nunmai`, no `-g`): run it with `npx nunmai`.
18
+ - **Local install** (`npm install nunmai`, no `-g`): run it once with `npx nunmai`.
19
19
  npm ≥ 11.19 blocks dependency install scripts by default, so the engine is
20
- installed on the first `npx nunmai` run. To get a plain `nunmai` command from
21
- a local install, allow the script: `npm install nunmai --allow-scripts=nunmai`.
20
+ installed on that first run. The launcher then puts a plain `nunmai` command
21
+ on your PATH itself `~/.local/bin` on macOS/Linux, `%LOCALAPPDATA%\nunmai\bin`
22
+ (registered on the user PATH) on Windows — so after opening a new terminal
23
+ `nunmai` just works. To skip even that first `npx`, allow the script:
24
+ `npm install nunmai --allow-scripts=nunmai`.
22
25
  - If npm skipped the install script (`--ignore-scripts`, `CI` set, or the
23
26
  allow-scripts policy above), the install simply happens on the first run.
24
27
  - After `nunmai uninstall`, the `nunmai` command falls back to this launcher —
package/bin/nunmai.js CHANGED
@@ -33,11 +33,27 @@ const IS_WIN = process.platform === "win32";
33
33
  const SELF = fs.realpathSync(__filename);
34
34
  const DRY = process.env.NUNMAI_BOOTSTRAP_DRY_RUN === "1";
35
35
 
36
+ // Marker stamped into the Windows fallback shim (a .cmd that re-enters this
37
+ // file). findLauncher() must never treat that shim as the engine launcher or
38
+ // `nunmai` would recurse into itself forever.
39
+ const WIN_SHIM_MARKER = "rem nunmai-npm-shim";
40
+
41
+ function winBinDir() {
42
+ // Same managed dir install.ps1 uses for the real launchers (Set-PathVariable):
43
+ // %NUNMAI_HOME%\bin, defaulting to %LOCALAPPDATA%\nunmai\bin.
44
+ const home = os.homedir();
45
+ const nunmaiHome = process.env.NUNMAI_HOME || path.join(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "nunmai");
46
+ return path.join(nunmaiHome, "bin");
47
+ }
48
+
49
+ function isWinShim(p) {
50
+ try { return p.toLowerCase().endsWith(".cmd") && fs.readFileSync(p, "utf8").includes(WIN_SHIM_MARKER); } catch (_) { return false; }
51
+ }
52
+
36
53
  function candidates() {
37
54
  const home = os.homedir();
38
55
  if (IS_WIN) {
39
- const nunmaiHome = process.env.NUNMAI_HOME || path.join(process.env.LOCALAPPDATA || path.join(home, "AppData", "Local"), "nunmai");
40
- const bin = path.join(nunmaiHome, "bin");
56
+ const bin = winBinDir();
41
57
  return [path.join(bin, "nunmai.exe"), path.join(bin, "nunmai.cmd")];
42
58
  }
43
59
  const list = [];
@@ -52,6 +68,7 @@ function findLauncher() {
52
68
  for (const p of candidates()) {
53
69
  try {
54
70
  if (fs.realpathSync(p) === SELF) continue; // never recurse into this shim
71
+ if (IS_WIN && isWinShim(p)) continue; // ...nor into the Windows .cmd shim
55
72
  fs.accessSync(p, fs.constants.X_OK);
56
73
  return p;
57
74
  } catch (_) { /* not there */ }
@@ -64,7 +81,7 @@ function ensureFallbackLauncher() {
64
81
  // ./node_modules/.bin, which is not on PATH. Make sure `nunmai` resolves
65
82
  // anyway by linking this shim into ~/.local/bin (or $PREFIX/bin on Termux).
66
83
  // The real installer replaces the link with the engine launcher (ln -sf).
67
- if (IS_WIN) return null;
84
+ if (IS_WIN) return ensureWinFallbackLauncher();
68
85
  const binDir = process.env.PREFIX && fs.existsSync(path.join(process.env.PREFIX, "bin"))
69
86
  ? path.join(process.env.PREFIX, "bin")
70
87
  : path.join(os.homedir(), ".local", "bin");
@@ -83,6 +100,51 @@ function ensureFallbackLauncher() {
83
100
  }
84
101
  }
85
102
 
103
+ function ensureWinFallbackLauncher() {
104
+ // Windows twin of the symlink above: drop a nunmai.cmd shim into the managed
105
+ // bin dir and register that dir on the User PATH, so a plain `nunmai` works
106
+ // after a local `npm install nunmai` (where npm >= 11.19 skips postinstall
107
+ // and node_modules\.bin is not on PATH). install.ps1 later overwrites the
108
+ // shim with the real launcher (Install-NunmaiCommandLaunchers) and keeps the
109
+ // same PATH entry, so nothing here has to be undone.
110
+ const binDir = winBinDir();
111
+ const target = path.join(binDir, "nunmai.cmd");
112
+ try {
113
+ fs.mkdirSync(binDir, { recursive: true });
114
+ // Never clobber a real engine launcher.
115
+ if (fs.existsSync(path.join(binDir, "nunmai.exe"))) return null;
116
+ if (fs.existsSync(target) && !isWinShim(target)) return null;
117
+ const body = `@echo off\r\n${WIN_SHIM_MARKER}: ${SELF}\r\n"${process.execPath}" "${SELF}" %*\r\n`;
118
+ fs.writeFileSync(target, body);
119
+ ensureWinUserPath(binDir);
120
+ return target;
121
+ } catch (e) {
122
+ console.error(`nunmai: could not stage launcher into ${binDir}: ${e.message}`);
123
+ return null;
124
+ }
125
+ }
126
+
127
+ function ensureWinUserPath(binDir) {
128
+ // Persist binDir at the front of the User PATH (registry), the same way
129
+ // install.ps1's Set-PathVariable does. Idempotent; best effort.
130
+ const norm = (p) => p.trim().replace(/[\\/]+$/, "").toLowerCase();
131
+ const onPath = (process.env.PATH || "").split(";").some((p) => norm(p) === norm(binDir));
132
+ if (onPath || DRY) return;
133
+ const ps = [
134
+ `$d='${binDir.replace(/'/g, "''")}'`,
135
+ "$cur=[Environment]::GetEnvironmentVariable('Path','User')",
136
+ "$items=@(); if ($cur) { $items=@($cur -split ';') }",
137
+ "if (-not ($items | Where-Object { $_.TrimEnd('\\') -ieq $d })) { [Environment]::SetEnvironmentVariable('Path', ((@($d) + $items) -join ';'), 'User'); exit 0 }",
138
+ "exit 3",
139
+ ].join("; ");
140
+ const r = spawnSync("powershell", ["-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-Command", ps], { stdio: "ignore" });
141
+ if (r.status === 0) {
142
+ console.log(`nunmai: added ${binDir} to your user PATH (open a new terminal, then run \`nunmai\`).`);
143
+ } else if (r.status !== 3) {
144
+ console.error(`nunmai: could not update the user PATH automatically. Add this folder to PATH to use \`nunmai\` directly: ${binDir}`);
145
+ }
146
+ }
147
+
86
148
  function ensurePathLine(binDir) {
87
149
  // Add ~/.local/bin to PATH in the user's shell rc if it is not there already.
88
150
  if (binDir !== path.join(os.homedir(), ".local", "bin")) return;
@@ -168,12 +230,49 @@ function postinstall() {
168
230
  return 0;
169
231
  }
170
232
 
233
+ // Commands that must never trigger an engine install when the engine is
234
+ // absent: a user asking to remove, or merely inspect, Nunmai should not be
235
+ // handed a multi-minute full install first.
236
+ function handleWithoutEngine(argv) {
237
+ const cmd = (argv[0] || "").toLowerCase();
238
+ if (cmd === "uninstall") {
239
+ let removed = false;
240
+ if (IS_WIN) {
241
+ // Drop our own .cmd shim (never a real launcher — those carry no marker).
242
+ const shim = path.join(winBinDir(), "nunmai.cmd");
243
+ if (isWinShim(shim)) { try { fs.unlinkSync(shim); removed = true; } catch (_) { /* best effort */ } }
244
+ }
245
+ console.log("Nunmai Engine is not installed on this machine — nothing to remove." + (removed ? " (Removed the launcher shim.)" : ""));
246
+ console.log("To remove this npm launcher too: npm uninstall -g nunmai (or `npm uninstall nunmai` for a local install)");
247
+ return 0;
248
+ }
249
+ if (cmd === "--version" || cmd === "-v" || cmd === "version") {
250
+ console.log(`nunmai launcher ${require("../package.json").version} (engine not installed — run \`nunmai\` to install)`);
251
+ return 0;
252
+ }
253
+ if (cmd === "--help" || cmd === "-h" || cmd === "help") {
254
+ console.log([
255
+ "Nunmai Engine is not installed on this machine yet.",
256
+ "",
257
+ " nunmai install the engine, then start it",
258
+ " nunmai --version show the launcher version",
259
+ " nunmai uninstall remove the engine (nothing to remove right now)",
260
+ "",
261
+ `Manual install: ${manualHint().trim()}`,
262
+ ].join("\n"));
263
+ return 0;
264
+ }
265
+ return null;
266
+ }
267
+
171
268
  function main() {
172
269
  const argv = process.argv.slice(2);
173
270
  if (argv[0] === "--bootstrap-postinstall") process.exit(postinstall());
174
271
 
175
272
  let launcher = findLauncher();
176
273
  if (!launcher) {
274
+ const handled = handleWithoutEngine(argv);
275
+ if (handled !== null) process.exit(handled);
177
276
  const code = install(false);
178
277
  if (code !== 0) {
179
278
  console.error(`\nnunmai: installer exited with code ${code}. You can retry it manually:\n${manualHint()}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "nunmai",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "description": "Nunmai Engine — `npm i nunmai` (global or local) installs the engine with all features and dependencies; run `nunmai` to start.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://nunmai.in",