@niroai/niro 0.9.0 → 0.10.1

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/src/lib/prompt.js CHANGED
@@ -18,6 +18,9 @@ function rl() {
18
18
  * Also shared by the MCP setup/teardown wizards' own ask() helpers.
19
19
  */
20
20
  function formatPrompt(question, defaultValue) {
21
+ // Questions asked from inside a nested step sit at that step's depth. readline writes
22
+ // straight to stdout, so withIndent's console wrappers never see them.
23
+ question = " ".repeat(require("./nestedOutput").currentIndent()) + question;
21
24
  return defaultValue != null && defaultValue !== ""
22
25
  ? `${question} ${color.dim(`(default: ${defaultValue})`)}: `
23
26
  : `${question}: `;
@@ -68,26 +68,30 @@ function install() {
68
68
  // Stop any existing instance so we can re-register cleanly
69
69
  spawnSync("pm2", ["delete", PROCESS_NAME], { shell: true, stdio: "pipe" });
70
70
 
71
- // Start niro-watch under PM2
71
+ // PM2's own output is captured, not inherited. Inheriting it put ~55 lines into the middle
72
+ // of setup: a box-drawing process table, the entire generated systemd unit file, and the
73
+ // user's PATH twice. None of it is ours and none of it is actionable while it is working.
74
+ // It IS printed on failure, where it is the only diagnostic there is.
72
75
  const startResult = spawnSync(
73
76
  "pm2",
74
77
  ["start", niroBin, "--name", PROCESS_NAME, "--", "watch"],
75
- { shell: true, stdio: "inherit" }
78
+ { shell: true, encoding: "utf8" }
76
79
  );
77
80
  if (startResult.status !== 0) {
81
+ console.error(color.dim(`${startResult.stdout || ""}${startResult.stderr || ""}`));
78
82
  throw new Error("pm2 start failed");
79
83
  }
80
84
 
81
85
  // Persist the process list so PM2 restores it after a manual `pm2 resurrect`
82
- spawnSync("pm2", ["save"], { shell: true, stdio: "inherit" });
86
+ spawnSync("pm2", ["save"], { shell: true, encoding: "utf8" });
83
87
 
84
88
  // Wire into OS startup — pm2 startup detects systemd / launchd / etc.
85
89
  // It prints the exact privileged command we need to run.
86
90
  console.log(
87
- `\n ${color.bold("Setting up auto-start on login")} — PM2 will ask for your password to register`
91
+ `\n ${color.bold("Setting up auto-start on login")} — PM2 will ask for your password to register`
88
92
  );
89
93
  console.log(
90
- ` with the system startup manager (${getInitSystem()}). This only needs to happen once.\n`
94
+ ` with the system startup manager (${getInitSystem()}). This only needs to happen once.\n`
91
95
  );
92
96
 
93
97
  const startupProbe = spawnSync("pm2", ["startup"], {
@@ -97,21 +101,25 @@ function install() {
97
101
 
98
102
  const setupCmd = extractSetupCommand(startupProbe.stdout + startupProbe.stderr);
99
103
  if (setupCmd) {
100
- console.log(` Running: ${color.dim(setupCmd)}\n`);
104
+ console.log(` Running: ${color.dim(setupCmd)}\n`);
105
+ // stdio is split: stdin stays connected so sudo can still prompt for the password, but
106
+ // PM2's forty lines of generated unit file are captured and only shown if it fails.
101
107
  const r = spawnSync(setupCmd, {
102
108
  shell: true,
103
- stdio: "inherit",
109
+ stdio: ["inherit", "pipe", "pipe"],
110
+ encoding: "utf8",
104
111
  });
105
112
  if (r.status !== 0) {
113
+ console.error(color.dim(`${r.stdout || ""}${r.stderr || ""}`));
106
114
  console.log(
107
115
  color.yellow(
108
- ` ⚠ Startup hook failed (exit ${r.status}). You can run it manually:\n ${setupCmd}`
116
+ ` ⚠ Startup hook failed (exit ${r.status}). You can run it manually:\n ${setupCmd}`
109
117
  )
110
118
  );
111
119
  }
112
120
  } else {
113
121
  // PM2 may print nothing if startup is already configured
114
- console.log(` ${color.green("✓")} Startup already configured.`);
122
+ console.log(` ${color.green("✓")} Startup already configured.`);
115
123
  }
116
124
 
117
125
  return true;
@@ -127,7 +135,7 @@ function uninstall() {
127
135
  spawnSync("pm2", ["save"], { shell: true, stdio: "inherit" });
128
136
 
129
137
  console.log(
130
- `\n ${color.bold("Removing auto-start hook")} — PM2 may ask for your password.\n`
138
+ `\n ${color.bold("Removing auto-start hook")} — PM2 may ask for your password.\n`
131
139
  );
132
140
  const unstartupResult = spawnSync("pm2", ["unstartup"], {
133
141
  shell: true,
@@ -137,7 +145,7 @@ function uninstall() {
137
145
  unstartupResult.stdout + unstartupResult.stderr
138
146
  );
139
147
  if (setupCmd) {
140
- console.log(` Running: ${color.dim(setupCmd)}\n`);
148
+ console.log(` Running: ${color.dim(setupCmd)}\n`);
141
149
  spawnSync(setupCmd, { shell: true, stdio: "inherit" });
142
150
  }
143
151
 
@@ -149,7 +157,7 @@ function uninstall() {
149
157
  */
150
158
  function status() {
151
159
  if (!pm2Available()) {
152
- console.log(color.yellow(" PM2 is not installed. Run: npm install -g pm2"));
160
+ console.log(color.yellow(" PM2 is not installed. Run: npm install -g pm2"));
153
161
  return;
154
162
  }
155
163
  spawnSync("pm2", ["show", PROCESS_NAME], { shell: true, stdio: "inherit" });
@@ -22,6 +22,17 @@ const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "
22
22
  const FRAME_MS = 120;
23
23
  const HEARTBEAT_MS = 30_000;
24
24
  const ERASE_LINE = "\r\x1b[2K";
25
+
26
+ /**
27
+ * Current nesting depth as spaces.
28
+ *
29
+ * The spinner writes straight to the stream, so withIndent's console wrappers never see it.
30
+ * Left alone, the build status and its "Build completed" line print flush left in the middle
31
+ * of an indented step, which is where they were.
32
+ */
33
+ function pad() {
34
+ return " ".repeat(require("./nestedOutput").currentIndent());
35
+ }
25
36
  const ANSI_RE = /\x1b\[[0-9;]*m/g;
26
37
 
27
38
  /**
@@ -40,8 +51,18 @@ function fit(line, width) {
40
51
  * Labels can carry backend text (e.g. build log messages); an embedded
41
52
  * newline would defeat the single-line overwrite, so flatten whitespace.
42
53
  */
54
+ /**
55
+ * Flatten a label to one line, but KEEP its leading indent.
56
+ *
57
+ * The old version trimmed both ends, which silently ate the indent callers use to place the
58
+ * label in the output tree: " Waiting for build..." came out flush left, four columns
59
+ * off from every line around it. Interior whitespace still collapses, because a newline would
60
+ * defeat the single-line overwrite.
61
+ */
43
62
  function sanitize(label) {
44
- return String(label).replace(/\s+/g, " ").trim();
63
+ const text = String(label);
64
+ const indent = /^[ \t]*/.exec(text)[0].replace(/\t/g, " ");
65
+ return indent + text.replace(/\s+/g, " ").trim();
45
66
  }
46
67
 
47
68
  function start(label, opts = {}) {
@@ -58,9 +79,9 @@ function start(label, opts = {}) {
58
79
  let stopped = false;
59
80
 
60
81
  if (!isTty) {
61
- stream.write(`${label}\n`);
82
+ stream.write(`${pad()}${label}\n`);
62
83
  const timer = setInterval(() => {
63
- stream.write(`${label} ${color.dim(`(still waiting — ${elapsed()}s)`)}\n`);
84
+ stream.write(`${pad()}${label} ${color.dim(`(still waiting — ${elapsed()}s)`)}\n`);
64
85
  }, heartbeatMs);
65
86
  if (timer.unref) timer.unref();
66
87
  return {
@@ -68,13 +89,13 @@ function start(label, opts = {}) {
68
89
  newLabel = sanitize(newLabel);
69
90
  if (stopped || newLabel === label) return;
70
91
  label = newLabel;
71
- stream.write(`${label}\n`);
92
+ stream.write(`${pad()}${label}\n`);
72
93
  },
73
94
  stop(finalLine) {
74
95
  clearInterval(timer);
75
96
  if (stopped) return;
76
97
  stopped = true;
77
- if (finalLine) stream.write(finalLine + "\n");
98
+ if (finalLine) stream.write(pad() + finalLine + "\n");
78
99
  },
79
100
  };
80
101
  }
@@ -83,7 +104,7 @@ function start(label, opts = {}) {
83
104
  const render = () => {
84
105
  const frame = FRAMES[i = (i + 1) % FRAMES.length];
85
106
  const line = `${color.cyan(frame)} ${label} ${color.dim("(" + elapsed() + "s)")}`;
86
- stream.write(ERASE_LINE + fit(line, (stream.columns || 80) - 1));
107
+ stream.write(ERASE_LINE + pad() + fit(line, (stream.columns || 80) - 1 - pad().length));
87
108
  };
88
109
 
89
110
  render();
@@ -101,7 +122,7 @@ function start(label, opts = {}) {
101
122
  if (stopped) return;
102
123
  stopped = true;
103
124
  stream.write(ERASE_LINE);
104
- if (finalLine) stream.write(finalLine + "\n");
125
+ if (finalLine) stream.write(pad() + finalLine + "\n");
105
126
  },
106
127
  };
107
128
  }
@@ -33,7 +33,7 @@ const color = require("./color");
33
33
  */
34
34
  async function ensureWatchDaemon({ skipPrompts = false } = {}) {
35
35
  if (!service.pm2Available()) {
36
- console.log(`\n ${color.yellow("⚠")} PM2 is not installed. Niro uses PM2 to keep your synced folders`);
36
+ console.log(`\n ${color.yellow("⚠")} PM2 is not installed. Niro uses PM2 to keep your synced folders`);
37
37
  console.log(` up to date in the background, including after a reboot.`);
38
38
 
39
39
  if (skipPrompts || prompt.isNonInteractive()) {
@@ -49,29 +49,29 @@ async function ensureWatchDaemon({ skipPrompts = false } = {}) {
49
49
 
50
50
  console.log("");
51
51
  if (!service.installPm2()) {
52
- console.log(`\n ${color.yellow("⚠")} PM2 install failed (a permission error is the usual cause).`);
52
+ console.log(`\n ${color.yellow("⚠")} PM2 install failed (a permission error is the usual cause).`);
53
53
  console.log(` Try manually: ${color.cyan("sudo npm install -g pm2")} then ${color.cyan("niro service install")}`);
54
54
  return { ok: false, reason: "pm2_install_failed" };
55
55
  }
56
- console.log(` ${color.green("✓")} PM2 installed.`);
56
+ console.log(` ${color.green("✓")} PM2 installed.`);
57
57
  }
58
58
 
59
- console.log(`\n ${color.cyan("●")} Installing niro-watch daemon (keeps your synced folders up to date, auto-starts on login)...`);
59
+ console.log(`\n ${color.cyan("●")} Installing niro-watch daemon (keeps your synced folders up to date, auto-starts on login)...`);
60
60
  try {
61
61
  // install() returns false when pm2 turns out not to be runnable after
62
62
  // all — e.g. npm installed it into a global bin dir that isn't on PATH
63
63
  // (npm exits 0 for that). Treat it as the failure it is; the old code's
64
64
  // unchecked call here is how "✓ Done." lied to customers.
65
65
  if (!service.install()) {
66
- console.log(` ${color.yellow("⚠")} PM2 was installed but isn't runnable from this shell (its global bin`);
67
- console.log(` directory may not be on your PATH). Open a NEW terminal and run ${color.cyan("niro service install")}.`);
66
+ console.log(` ${color.yellow("⚠")} PM2 was installed but isn't runnable from this shell (its global bin`);
67
+ console.log(` directory may not be on your PATH). Open a NEW terminal and run ${color.cyan("niro service install")}.`);
68
68
  return { ok: false, reason: "daemon_install_failed" };
69
69
  }
70
- console.log(` ${color.green("✓")} niro-watch is running and will start automatically on login.`);
70
+ console.log(` ${color.green("✓")} niro-watch is running and will start automatically on login.`);
71
71
  return { ok: true };
72
72
  } catch (err) {
73
- console.log(` ${color.yellow("⚠")} Could not install the watch daemon: ${err.message}`);
74
- console.log(` Run ${color.cyan("niro service install")} to try again, or ${color.cyan("niro watch")} to sync manually.`);
73
+ console.log(` ${color.yellow("⚠")} Could not install the watch daemon: ${err.message}`);
74
+ console.log(` Run ${color.cyan("niro service install")} to try again, or ${color.cyan("niro watch")} to sync manually.`);
75
75
  return { ok: false, reason: "daemon_install_failed" };
76
76
  }
77
77
  }