@niroai/niro 0.8.0 → 0.10.0
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/CHANGELOG.md +110 -0
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/commands/ab.js +198 -0
- package/src/commands/add-project.js +11 -11
- package/src/commands/add-repo.js +23 -23
- package/src/commands/build.js +5 -5
- package/src/commands/delete.js +3 -3
- package/src/commands/edit.js +11 -11
- package/src/commands/info.js +2 -2
- package/src/commands/init.js +108 -87
- package/src/commands/login.js +25 -5
- package/src/commands/projects.js +1 -1
- package/src/commands/release.js +81 -15
- package/src/commands/remove.js +5 -5
- package/src/commands/start.js +567 -0
- package/src/commands/takeover.js +105 -23
- package/src/commands/watch.js +35 -35
- package/src/index.js +57 -3
- package/src/lib/folderSync.js +13 -8
- package/src/lib/git.js +21 -3
- package/src/lib/moduleExcludeSuggestions.js +5 -5
- package/src/lib/nestedOutput.js +49 -0
- package/src/lib/onboardTarget.js +5 -5
- package/src/lib/prompt.js +3 -0
- package/src/lib/service.js +20 -12
- package/src/lib/spinner.js +28 -7
- package/src/lib/tempProject.js +25 -1
- package/src/lib/watchDaemon.js +9 -9
- package/src/mcp/server.js +7 -2
- package/src/mcp/setup.js +198 -160
- package/src/mcp/teardown.js +53 -53
package/src/lib/git.js
CHANGED
|
@@ -7,6 +7,25 @@ const fs = require("fs");
|
|
|
7
7
|
const path = require("path");
|
|
8
8
|
const { execSync } = require("child_process");
|
|
9
9
|
|
|
10
|
+
/**
|
|
11
|
+
* The branch name from `git rev-parse --abbrev-ref HEAD`, or null when there is no branch.
|
|
12
|
+
*
|
|
13
|
+
* On a detached HEAD (a bisect, a tag checkout, a plain commit checkout) that command does not
|
|
14
|
+
* fail — it prints the literal string "HEAD". A caller that trusts it verbatim therefore reports a
|
|
15
|
+
* branch named "HEAD", which no repository has, and everything downstream reads the checkout as
|
|
16
|
+
* being on an unknown branch rather than on no branch at all.
|
|
17
|
+
*
|
|
18
|
+
* `inspect` below has always collapsed that to null, matching what the `.git/HEAD` file read
|
|
19
|
+
* returns. This is that one rule, shared, so a second caller cannot drift from it. One already had:
|
|
20
|
+
* the MCP bridge sent "HEAD" straight through, so the server's routing ladder took every detached
|
|
21
|
+
* checkout for a diverged branch and refused to answer it, even though the ladder has an explicit
|
|
22
|
+
* guard meant to route exactly that case to the indexed project.
|
|
23
|
+
*/
|
|
24
|
+
function normalizeBranch(raw) {
|
|
25
|
+
const b = typeof raw === "string" ? raw.trim() : "";
|
|
26
|
+
return b && b !== "HEAD" ? b : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
10
29
|
function findGitDir(startDir) {
|
|
11
30
|
let dir = path.resolve(startDir);
|
|
12
31
|
const root = path.parse(dir).root;
|
|
@@ -93,8 +112,7 @@ function inspect(startDir) {
|
|
|
93
112
|
// unavailable the calls return null and behaviour is identical to before.
|
|
94
113
|
if (originUrl === null) originUrl = redactRemoteCreds(gitOut("git remote get-url origin", found.repoRoot));
|
|
95
114
|
if (branch === null) {
|
|
96
|
-
|
|
97
|
-
branch = b && b !== "HEAD" ? b : null; // "HEAD" = detached; keep null like the file read
|
|
115
|
+
branch = normalizeBranch(gitOut("git rev-parse --abbrev-ref HEAD", found.repoRoot));
|
|
98
116
|
}
|
|
99
117
|
return { repoRoot: found.repoRoot, originUrl, branch };
|
|
100
118
|
}
|
|
@@ -210,4 +228,4 @@ function isTracked(repoRoot, relativePath) {
|
|
|
210
228
|
}
|
|
211
229
|
|
|
212
230
|
module.exports = { inspect, mainWorktreeRoot, deriveRepoName, checkIgnored, checkIgnoredPaths,
|
|
213
|
-
redactRemoteCreds, isTracked };
|
|
231
|
+
redactRemoteCreds, isTracked, normalizeBranch };
|
|
@@ -23,7 +23,7 @@ async function offerModuleExcludeSuggestions(gitId, opts = {}, alreadySupplied =
|
|
|
23
23
|
}
|
|
24
24
|
if (!Array.isArray(suggestions) || suggestions.length === 0) return;
|
|
25
25
|
|
|
26
|
-
console.log(`\n
|
|
26
|
+
console.log(`\n ${color.cyan("Suggested module exclusions")} (modules that are usually noise):`);
|
|
27
27
|
const accepted = [];
|
|
28
28
|
for (const s of suggestions) {
|
|
29
29
|
const count = s.module_count || s.moduleCount || 0;
|
|
@@ -33,24 +33,24 @@ async function offerModuleExcludeSuggestions(gitId, opts = {}, alreadySupplied =
|
|
|
33
33
|
` ${color.dim(s.pattern.padEnd(28))} ${String(count).padStart(3)} module${count === 1 ? "" : "s"} ${color.dim(label)}`
|
|
34
34
|
);
|
|
35
35
|
if (samples.length > 0) {
|
|
36
|
-
console.log(`
|
|
36
|
+
console.log(` ${color.dim("e.g. " + samples.join(", "))}`);
|
|
37
37
|
}
|
|
38
38
|
const yes = await prompt.confirm(` Apply ${color.cyan(s.pattern)}?`, true);
|
|
39
39
|
if (yes) accepted.push(s.pattern);
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
if (accepted.length === 0) {
|
|
43
|
-
console.log(`
|
|
43
|
+
console.log(` ${color.dim("No exclusions applied.")}`);
|
|
44
44
|
return;
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
try {
|
|
48
48
|
await agentApi.updateModuleExcludePatterns(gitId, accepted);
|
|
49
49
|
console.log(
|
|
50
|
-
`
|
|
50
|
+
` ${color.green("✓")} Applied ${accepted.length} exclusion${accepted.length === 1 ? "" : "s"}.`
|
|
51
51
|
);
|
|
52
52
|
} catch (err) {
|
|
53
|
-
console.log(`
|
|
53
|
+
console.log(` ${color.yellow("Warning:")} failed to save exclusions: ${err.message || err}`);
|
|
54
54
|
}
|
|
55
55
|
}
|
|
56
56
|
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Run something and shift everything it prints one rung deeper.
|
|
3
|
+
*
|
|
4
|
+
* `niro start` is a shell around commands that also run on their own. `niro init` printing at
|
|
5
|
+
* the top level is right when you typed `niro init`; inside the wizard the same lines belong
|
|
6
|
+
* UNDER "4/5 Onboarding and building the index", and printing them flush with the step markers
|
|
7
|
+
* is what made step 4 look like a different program had taken over.
|
|
8
|
+
*
|
|
9
|
+
* Wrapping the call is the only approach that does not require every helper it reaches
|
|
10
|
+
* (folderSync, onboardTarget, moduleExcludeSuggestions, watchDaemon, service) to know how deep
|
|
11
|
+
* it happens to be running. They keep printing at their own level; this adds the offset.
|
|
12
|
+
*
|
|
13
|
+
* Blank lines are left blank rather than padded with spaces, so nothing trails whitespace.
|
|
14
|
+
* process.stdout.write is untouched: the upload progress bar repaints one line with \r, and
|
|
15
|
+
* prefixing every repaint would smear it across the terminal.
|
|
16
|
+
*/
|
|
17
|
+
let current = 0;
|
|
18
|
+
|
|
19
|
+
/** How deep the output is nested right now. prompt.js prefixes its questions with this. */
|
|
20
|
+
function currentIndent() {
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function withIndent(spaces, fn) {
|
|
25
|
+
const pad = " ".repeat(spaces);
|
|
26
|
+
const shift = (orig) => (...args) => {
|
|
27
|
+
const text = args.map((a) => (typeof a === "string" ? a : String(a))).join(" ");
|
|
28
|
+
// A line with nothing but spaces is emitted empty. Some callers print a whitespace-only
|
|
29
|
+
// string as a spacer, and padding it left a visible run of trailing whitespace.
|
|
30
|
+
orig(text.split("\n").map((l) => (l.trim() ? pad + l : "")).join("\n"));
|
|
31
|
+
};
|
|
32
|
+
const log = console.log;
|
|
33
|
+
const error = console.error;
|
|
34
|
+
const warn = console.warn;
|
|
35
|
+
current += spaces;
|
|
36
|
+
console.log = shift(log);
|
|
37
|
+
console.error = shift(error);
|
|
38
|
+
console.warn = shift(warn);
|
|
39
|
+
return Promise.resolve()
|
|
40
|
+
.then(fn)
|
|
41
|
+
.finally(() => {
|
|
42
|
+
current -= spaces;
|
|
43
|
+
console.log = log;
|
|
44
|
+
console.error = error;
|
|
45
|
+
console.warn = warn;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
module.exports = { withIndent, currentIndent };
|
package/src/lib/onboardTarget.js
CHANGED
|
@@ -208,7 +208,7 @@ function ensureOnboardable(targetDir, opts = {}) {
|
|
|
208
208
|
|
|
209
209
|
const reason = protectedRootReason(resolved);
|
|
210
210
|
if (reason) {
|
|
211
|
-
console.error(color.red(`\n
|
|
211
|
+
console.error(color.red(`\n ✗ Refusing to onboard ${reason} (${resolved}).`));
|
|
212
212
|
console.error(" That would upload source from every project underneath it, pin this folder");
|
|
213
213
|
console.error(" with a .niro-project file, and watch the whole tree for changes.");
|
|
214
214
|
console.error(` Change into the project folder you meant, then run the command there.`);
|
|
@@ -223,17 +223,17 @@ function ensureOnboardable(targetDir, opts = {}) {
|
|
|
223
223
|
// The protected-root rule above still applied, and still refuses.
|
|
224
224
|
if (opts.alreadyRegistered) {
|
|
225
225
|
console.log(color.dim(
|
|
226
|
-
`
|
|
226
|
+
` No build file in ${resolved}, but it is already registered here — continuing.`
|
|
227
227
|
));
|
|
228
228
|
return true;
|
|
229
229
|
}
|
|
230
230
|
if (opts.force) {
|
|
231
231
|
console.log(color.yellow(
|
|
232
|
-
`\n
|
|
232
|
+
`\n ⚠ No project found in ${resolved}, but --force was passed. Onboarding it anyway.`
|
|
233
233
|
));
|
|
234
234
|
return true;
|
|
235
235
|
}
|
|
236
|
-
console.error(color.red(`\n
|
|
236
|
+
console.error(color.red(`\n ✗ No project found in ${resolved}.`));
|
|
237
237
|
console.error(" Niro looks for a build file (package.json, pom.xml, go.mod, ...) in this");
|
|
238
238
|
console.error(` folder or just below it${truncated ? " (the scan stopped early — the tree is very large)" : ""}. None found.`);
|
|
239
239
|
console.error(" Pass --force if you really mean to upload this folder.");
|
|
@@ -241,7 +241,7 @@ function ensureOnboardable(targetDir, opts = {}) {
|
|
|
241
241
|
}
|
|
242
242
|
|
|
243
243
|
console.log(
|
|
244
|
-
`
|
|
244
|
+
` ${color.green("✓")} Found ${roots} project${roots === 1 ? "" : "s"} ` +
|
|
245
245
|
`(${describeIndicators(indicators)}).`
|
|
246
246
|
);
|
|
247
247
|
return true;
|
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}: `;
|
package/src/lib/service.js
CHANGED
|
@@ -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
|
-
//
|
|
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,
|
|
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,
|
|
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
|
|
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
|
-
`
|
|
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(`
|
|
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
|
-
`
|
|
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(`
|
|
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
|
|
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(`
|
|
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("
|
|
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" });
|
package/src/lib/spinner.js
CHANGED
|
@@ -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
|
-
|
|
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
|
}
|
package/src/lib/tempProject.js
CHANGED
|
@@ -34,4 +34,28 @@ async function currentBranch(tempProjectId, gitId, opts = {}) {
|
|
|
34
34
|
return match && match.branch ? match.branch : null;
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
/**
|
|
38
|
+
* True when an error from a project read means "this project does not exist on this backend".
|
|
39
|
+
*
|
|
40
|
+
* Status alone cannot answer it. A deleted project fails its access check before anything looks for
|
|
41
|
+
* the row, and that check raises AccessDeniedException, which the backend maps to 403 — the SAME
|
|
42
|
+
* status it returns when the project is alive but belongs to someone else
|
|
43
|
+
* (AccessControlService.validateProjectAccess, GlobalExceptionHandler). Treating every 403 as "gone"
|
|
44
|
+
* would let a permissions problem wipe a healthy local registration, and treating none of them as
|
|
45
|
+
* gone is what left dead registrations behind in the first place. So the message decides: only a
|
|
46
|
+
* missing row produces "Project not found", while the other two say "User does not have access to
|
|
47
|
+
* project" and "User not found".
|
|
48
|
+
*
|
|
49
|
+
* A bare status is deliberately NOT enough, not even a 404. This route does not produce one today,
|
|
50
|
+
* so a 404 here means something other than the API answered: a proxy, or an HTML error page. Its
|
|
51
|
+
* sibling agentApi.isRepoNotFound does accept a bare 404, and can afford to — being wrong there
|
|
52
|
+
* costs a re-registration. Being wrong here destroys the record of how to undo a takeover, so this
|
|
53
|
+
* one fails toward keeping it: an unrecognised failure throws and changes nothing.
|
|
54
|
+
*/
|
|
55
|
+
function isProjectNotFound(err) {
|
|
56
|
+
if (!err) return false;
|
|
57
|
+
const text = `${err.message || ""} ${typeof err.body === "string" ? err.body : ""}`;
|
|
58
|
+
return /project not found/i.test(text);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
module.exports = { currentBranch, isProjectNotFound };
|
package/src/lib/watchDaemon.js
CHANGED
|
@@ -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
|
|
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
|
|
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(`
|
|
56
|
+
console.log(` ${color.green("✓")} PM2 installed.`);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
|
-
console.log(`\n
|
|
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(`
|
|
67
|
-
console.log(`
|
|
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(`
|
|
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(`
|
|
74
|
-
console.log(`
|
|
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
|
}
|
package/src/mcp/server.js
CHANGED
|
@@ -41,7 +41,7 @@ const path = require("path");
|
|
|
41
41
|
const readline = require("readline");
|
|
42
42
|
const { execSync } = require("child_process");
|
|
43
43
|
const niroProjectFile = require("../lib/niroProjectFile");
|
|
44
|
-
const { redactRemoteCreds } = require("../lib/git");
|
|
44
|
+
const { redactRemoteCreds, normalizeBranch } = require("../lib/git");
|
|
45
45
|
const receiptStore = require("../lib/receiptStore");
|
|
46
46
|
|
|
47
47
|
/** Absolute path to ~/.niro/projects.json. Uses os.homedir() so it also resolves on Windows, where HOME is
|
|
@@ -161,8 +161,13 @@ function detectGitContext(dir) {
|
|
|
161
161
|
if (dir) opts.cwd = dir;
|
|
162
162
|
const remote = execSync("git remote get-url origin 2>/dev/null", opts).trim();
|
|
163
163
|
if (!remote) return null;
|
|
164
|
+
// normalizeBranch, not a bare trim: on a detached HEAD this command PRINTS "HEAD" instead of
|
|
165
|
+
// failing, and sending that verbatim told the server the checkout was on a branch called "HEAD".
|
|
166
|
+
// Nothing indexes such a branch, so the routing ladder read every bisect / tag checkout as
|
|
167
|
+
// diverged and refused to answer it — past its own guard for detached HEAD, which only fires on
|
|
168
|
+
// a null branch. `git.inspect` has always collapsed "HEAD" to null; this shares that one rule.
|
|
164
169
|
let branch = null;
|
|
165
|
-
try { branch = execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", opts)
|
|
170
|
+
try { branch = normalizeBranch(execSync("git rev-parse --abbrev-ref HEAD 2>/dev/null", opts)); } catch (_) {}
|
|
166
171
|
// The freshness/takeover notices compare the local HEAD + dirty state against what Niro indexed, so
|
|
167
172
|
// the bridge must report them — without git_commit the server-side check no-ops entirely.
|
|
168
173
|
let commit = null;
|