@niroai/niro 0.12.0 → 0.15.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 +190 -0
- package/README.md +62 -0
- package/package.json +2 -2
- package/src/commands/init.js +179 -5
- package/src/commands/mcp.js +16 -7
- package/src/commands/start.js +1 -0
- package/src/commands/upgrade.js +400 -0
- package/src/commands/watch.js +39 -7
- package/src/index.js +11 -0
- package/src/lib/folderSync.js +14 -0
- package/src/lib/mcpClients.js +241 -0
- package/src/lib/onboardTarget.js +13 -4
- package/src/lib/service.js +18 -1
- package/src/mcp/setup.js +3 -0
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,196 @@ Notable changes to `@niroai/niro`. Dates are release dates.
|
|
|
4
4
|
|
|
5
5
|
## Unreleased
|
|
6
6
|
|
|
7
|
+
## 0.15.0 — 2026-09-21
|
|
8
|
+
|
|
9
|
+
### Twenty more file types are offered for upload
|
|
10
|
+
|
|
11
|
+
`niro init` and the folder sync decide which files to offer the server from a fixed list of
|
|
12
|
+
extensions (`src/lib/folderSync.js`). That list was a hand-kept copy of the server's own list and
|
|
13
|
+
had fallen behind it in two places:
|
|
14
|
+
|
|
15
|
+
- the ten C++ extensions the parser routes to its C++ grammar besides `.cpp`, `.h` and `.hpp`:
|
|
16
|
+
`.cc`, `.cxx`, `.hxx`, `.hh`, `.h++`, `.c++`, `.tcc`, `.ipp`, `.inl`, `.tpp`. A C++ project with
|
|
17
|
+
`include/widget.hxx` and `detail/vector.tcc` never had those two files in its graph, because the
|
|
18
|
+
CLI never offered them.
|
|
19
|
+
- ten more the parser has always understood but the CLI never offered: `.kts`, `.csx`, `.rake`,
|
|
20
|
+
`.gemspec`, `.phtml`, `.php3`, `.php4`, `.php5`, `.pyi`, `.pyw`.
|
|
21
|
+
|
|
22
|
+
A test now reads the server's Java list and fails if the two drift apart again
|
|
23
|
+
(`test/sourceExtensionDrift.test.js`); the server carries the mirror-image test.
|
|
24
|
+
|
|
25
|
+
The server's own filter remains the final say, so an older CLI simply offers fewer files and a
|
|
26
|
+
newer CLI against an older server has its extra offers declined. Either order of upgrade is safe;
|
|
27
|
+
the new files reach the graph once both sides are current (ai-orchestrator `cadb5360` or later).
|
|
28
|
+
|
|
29
|
+
Note: 0.14.0 was cut in this repository but never published to npm, so upgrading from 0.13.0 brings
|
|
30
|
+
the 0.14.0 change below as well.
|
|
31
|
+
|
|
32
|
+
### A file whose name contains `+` no longer stops a folder syncing
|
|
33
|
+
|
|
34
|
+
After `niro init`, the server replies with the patterns saying which paths belong to the
|
|
35
|
+
project, and the watch daemon checks every path against them: on each file event, on each
|
|
36
|
+
delta and backstop pass, and once more before it asks the server to delete anything.
|
|
37
|
+
|
|
38
|
+
For a file at the top level of a repository, the pattern the server sends is the filename
|
|
39
|
+
itself. The daemon turned patterns into regular expressions but escaped only the dot, so a
|
|
40
|
+
name carrying a character that means something in a regular expression was read as an
|
|
41
|
+
instruction rather than as a name. With `main.c++` at a repository root, `main.c++` became
|
|
42
|
+
`/^main\.c++$/`, which is not a valid expression. Every sync pass for that folder then failed.
|
|
43
|
+
The daemon stayed up and printed nothing, so the folder simply went quiet.
|
|
44
|
+
|
|
45
|
+
The `.c++` and `.h++` file types added in this same release are what would have exposed it. The
|
|
46
|
+
same flaw had a quieter form that needed no C++ at all: a root-level file called `app (copy).js`
|
|
47
|
+
built a valid expression with a capture group, so the file stopped matching its own pattern and
|
|
48
|
+
was never synced again. A directory named `libstdc++` hit the throwing form, because saying yes
|
|
49
|
+
to "New directory detected. Add to sync?" stores the directory's own name as a pattern.
|
|
50
|
+
|
|
51
|
+
The matcher now escapes every character that has a special meaning, leaving only `*` and `**`
|
|
52
|
+
as wildcards, so a pattern can no longer fail to compile. Behaviour for patterns that already
|
|
53
|
+
worked is unchanged: old and new were compared over 10,701 path-and-pattern pairs taken from
|
|
54
|
+
three real repositories and agreed on every one.
|
|
55
|
+
|
|
56
|
+
## 0.14.0 — 2026-09-19
|
|
57
|
+
|
|
58
|
+
### `niro init` on a folder of repos now indexes each repo, not the folder
|
|
59
|
+
|
|
60
|
+
If you run `niro init` in a folder that is not itself a git repository but contains repositories —
|
|
61
|
+
an `apps/` folder, a `~/projects/github/<org>/` folder, anything that groups checkouts — each
|
|
62
|
+
repository is now onboarded in its own right, with its own git URL and its own branch, into one
|
|
63
|
+
project.
|
|
64
|
+
|
|
65
|
+
Before this, the folder itself was registered as a single repo. A folder has no git remote, so it
|
|
66
|
+
got a synthetic `local://<folder-name>` URL and no branch at all, and the monorepo splitter then
|
|
67
|
+
tried to recover the structure by reading folder layout. On one customer's tree that produced 92
|
|
68
|
+
internal repos from 14 real ones: 92 of 92 carried `local://apps`, none had a branch, 58 of them
|
|
69
|
+
were vendored copies and reference checkouts of other people's projects, and 45% of the indexing
|
|
70
|
+
spend went on code the customer had never written. Because no repo had a URL of its own, MCP could
|
|
71
|
+
not match a repo from inside its own folder, and kept offering to create a temporary project in
|
|
72
|
+
code that was already indexed.
|
|
73
|
+
|
|
74
|
+
The repositories underneath were the boundaries the customer had already declared. Reading them is
|
|
75
|
+
better than re-deriving them.
|
|
76
|
+
|
|
77
|
+
What you will see:
|
|
78
|
+
|
|
79
|
+
```
|
|
80
|
+
apps is not a git repository, but contains 14 that are.
|
|
81
|
+
Niro will index each repo separately, with its own git URL and branch.
|
|
82
|
+
|
|
83
|
+
+ voibe-backend
|
|
84
|
+
+ voibe_app
|
|
85
|
+
- legal-docs — not a git repository, NOT indexed
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
Folders holding code but no repository are reported rather than skipped in silence: from that point
|
|
89
|
+
on, editing a file in one of them does nothing, and you should know which ones they are. Run
|
|
90
|
+
`niro init` inside one to add it.
|
|
91
|
+
|
|
92
|
+
The scan stops at the first repository it finds down any path, so a repository vendored inside
|
|
93
|
+
another — a submodule, a `third_party/` checkout — is indexed as part of its parent rather than as a
|
|
94
|
+
service of its own.
|
|
95
|
+
|
|
96
|
+
Unchanged: a folder that IS a git repository still indexes whole, submodules and all. A folder with
|
|
97
|
+
no git anywhere still uploads as one repo, as before.
|
|
98
|
+
|
|
99
|
+
### A folder whose repositories sit deeper than one level is no longer refused
|
|
100
|
+
|
|
101
|
+
`niro init` requires a build file at the folder or just below it, which is right for a folder of
|
|
102
|
+
documents and wrong for `~/projects/github/<org>/<repo>`, where the repositories are three levels
|
|
103
|
+
down and every folder above them is empty. That was refused with "No project found" and a pointer to
|
|
104
|
+
`--force`, and `--force` would then upload the entire tree as one repo. A folder holding
|
|
105
|
+
repositories is now exempt from that rule, because it has already declared its projects.
|
|
106
|
+
|
|
107
|
+
The refusal message for a folder that genuinely holds nothing indexable now says what Niro looks
|
|
108
|
+
for, and suggests running from the folder where the code actually is.
|
|
109
|
+
|
|
110
|
+
### Fixed
|
|
111
|
+
|
|
112
|
+
- The background watcher was being installed once per repository when onboarding several at once,
|
|
113
|
+
asking for your password each time. It is one process; it is now set up once per run.
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
## 0.13.0 — 2026-09-08
|
|
117
|
+
|
|
118
|
+
### Upgrading
|
|
119
|
+
|
|
120
|
+
You do not have `niro upgrade` yet, so this one takes two commands:
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
npm install -g @niroai/niro@latest
|
|
124
|
+
niro upgrade --finish
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
The first gets the package. The second is the half npm cannot do: it re-runs the installer for
|
|
128
|
+
every assistant you have configured and restarts the background watcher. Then restart your
|
|
129
|
+
assistant. Skipping the second command leaves you on this version with the previous version's
|
|
130
|
+
hooks, skills and instruction files, which is the exact problem this release exists to fix.
|
|
131
|
+
|
|
132
|
+
From here on it is one command, `niro upgrade`, which does all of it.
|
|
133
|
+
|
|
134
|
+
### `niro upgrade` gets the new version AND the things a new version cannot bring with it
|
|
135
|
+
|
|
136
|
+
Upgrading was `npm install -g @niroai/niro@latest`, and every release note in this file said so and
|
|
137
|
+
nothing else. That command is not enough, and has never been enough. The MCP entry is fine: it runs
|
|
138
|
+
plain `niro` off your `PATH` with no version in it, so the new server is picked up for free. But the
|
|
139
|
+
hooks, the skills and the CLAUDE.md/AGENTS.md/rules text are written into your home folder as fixed
|
|
140
|
+
text at install time, not read from the package as it runs. npm cannot touch them. Nothing told you
|
|
141
|
+
that, and nothing re-ran the installer, so a machine that installed once in June was still running
|
|
142
|
+
June's instructions in September while reporting the current version. The background watcher had the
|
|
143
|
+
same problem from the other end: it is a long-running process holding the code it loaded at start,
|
|
144
|
+
so replacing files on disk left it on the old version until something restarted it.
|
|
145
|
+
|
|
146
|
+
`niro upgrade` does all three. It replaces the package, re-runs the installer for every assistant it
|
|
147
|
+
can prove is configured, and restarts the watcher if it is running.
|
|
148
|
+
|
|
149
|
+
It runs as two processes, and the reason is worth writing down. Every command in this CLI is loaded
|
|
150
|
+
lazily, inside its own action handler, and `npm install -g` replaces the folder those files are
|
|
151
|
+
loaded from. One process that ran npm and then carried on would be running the modules it loaded
|
|
152
|
+
before the swap alongside modules loaded after it. So phase one runs npm, executes the NEW binary
|
|
153
|
+
with `--finish`, and stops, requiring nothing in between; phase two is a fresh process reading the
|
|
154
|
+
new tree. `niro start` has always dodged the same trap the same way, by only installing globally
|
|
155
|
+
when it is running from the npx cache and then handing over.
|
|
156
|
+
|
|
157
|
+
Which assistants to refresh is worked out by looking, because nothing on disk records what you
|
|
158
|
+
chose. All 14 clients are covered, because the installer leaves a file this repo owns in every case:
|
|
159
|
+
eight write a JSON config whose path `setup.js` builds, and the other six also get a marker appended
|
|
160
|
+
to a markdown file written here and stripped by `teardown.js` (`~/.claude/CLAUDE.md`,
|
|
161
|
+
`~/.gemini/GEMINI.md`, `~/.copilot/copilot-instructions.md`, `~/.factory/AGENTS.md`), plus codex's
|
|
162
|
+
`[mcp_servers.niro]` table and goose's `extensions:` entry. The marker is appended only after the
|
|
163
|
+
registration command succeeded, so its presence means the client really was configured. Detection is
|
|
164
|
+
read-only, and it is deliberately strict: a client whose entry is `enabled: false` or
|
|
165
|
+
`disabled: true` is one you switched off, so it is left alone rather than switched back on; a
|
|
166
|
+
marker has to be on a line of its own, because a sentence mentioning it is not an install; and every
|
|
167
|
+
path has to be absolute.
|
|
168
|
+
|
|
169
|
+
`niro upgrade --finish` on its own skips npm and only refreshes the assistant files and the watcher,
|
|
170
|
+
which is what you want if you already ran `npm install -g` by hand.
|
|
171
|
+
|
|
172
|
+
Without a usable home folder it checks nothing and says so. Every path would otherwise resolve
|
|
173
|
+
against the current folder, which is how the reverted serve-time self-heal came to write into a
|
|
174
|
+
project's own `.claude/settings.json` (see the 2026-07-30 revert). "Usable" means absolute, and it
|
|
175
|
+
also means rejecting a `HOME` set to the empty string even when `USERPROFILE` would fill in, because
|
|
176
|
+
four of the client path builders read `HOME` directly and would otherwise hand back a relative path.
|
|
177
|
+
This command never writes a settings file itself: refreshing hooks means spawning the existing
|
|
178
|
+
installer, once per client.
|
|
179
|
+
|
|
180
|
+
It refuses to upgrade a copy you are not running. The first real run of this command was from a
|
|
181
|
+
checkout linked with `npm link`, and `npm install -g` deleted that link and put the published
|
|
182
|
+
package in its place. So before npm is allowed to touch anything, `niro upgrade` looks at which
|
|
183
|
+
copy is running: the global package folder being a symlink means `npm link`, and it stops and says
|
|
184
|
+
so; a checkout or an npx copy that is not the global install stops too. `--force` overrides either.
|
|
185
|
+
|
|
186
|
+
It also asks the version it just installed whether it has an `upgrade` command before handing over
|
|
187
|
+
to it, because the newest published version can predate the one you are running. Without that
|
|
188
|
+
check the hand-over fails with a bare `error: unknown command 'upgrade'` and the advice printed is
|
|
189
|
+
to re-run a command that does not exist. Now it prints the two commands that do work.
|
|
190
|
+
|
|
191
|
+
Failures are visible. Each one has its own exit code (npm failed, no binary after npm, hand-over
|
|
192
|
+
failed, refresh incomplete), an interrupted npm is reported as interrupted rather than as "nothing
|
|
193
|
+
changed", a hand-over that cannot start prints why, and the hand-over carries a marker so a version
|
|
194
|
+
that does not understand `--finish` cannot loop back into running npm again.
|
|
195
|
+
|
|
196
|
+
|
|
7
197
|
## 0.12.0 — 2026-09-06
|
|
8
198
|
|
|
9
199
|
### Before an edit to indexed source, Claude is asked to map the method and to verify those flows afterwards
|
package/README.md
CHANGED
|
@@ -71,6 +71,12 @@ Rebuild the index by hand and watch it finish. Like `niro status`, it picks the
|
|
|
71
71
|
branch you are on, so on a branch with a temporary project it rebuilds that one. Name a project to
|
|
72
72
|
rebuild a different one.
|
|
73
73
|
|
|
74
|
+
```bash
|
|
75
|
+
niro upgrade
|
|
76
|
+
```
|
|
77
|
+
Get the latest version, and bring your hooks, skills and watcher with it. See
|
|
78
|
+
[Upgrading](#upgrading).
|
|
79
|
+
|
|
74
80
|
```bash
|
|
75
81
|
niro doctor
|
|
76
82
|
```
|
|
@@ -254,6 +260,58 @@ Watch this folder in the foreground instead, for one session.
|
|
|
254
260
|
|
|
255
261
|
---
|
|
256
262
|
|
|
263
|
+
## Upgrading
|
|
264
|
+
|
|
265
|
+
**Check which you are on first:**
|
|
266
|
+
|
|
267
|
+
```bash
|
|
268
|
+
niro --version
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
**On 0.13.0 or newer**, one command:
|
|
272
|
+
|
|
273
|
+
```bash
|
|
274
|
+
niro upgrade
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
Replaces the package with the latest published version, then puts back the two things replacing the
|
|
278
|
+
package cannot reach: it re-runs the installer for every assistant you have configured, and
|
|
279
|
+
restarts the background watcher. Restart your assistant afterwards.
|
|
280
|
+
|
|
281
|
+
**On anything older than 0.13.0**, you do not have `niro upgrade` yet, so it is two commands, once:
|
|
282
|
+
|
|
283
|
+
```bash
|
|
284
|
+
npm install -g @niroai/niro@latest
|
|
285
|
+
niro upgrade --finish
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
The first gets the package. The second does the half npm cannot: your hooks, skills and the
|
|
289
|
+
watcher. After that, `niro upgrade` on its own is enough. Do not stop after the first command:
|
|
290
|
+
that leaves you on the new version running the old version's hooks, skills and instruction files,
|
|
291
|
+
which is the problem `niro upgrade` exists to solve.
|
|
292
|
+
|
|
293
|
+
**Why it is more than an npm install.** The hooks, skills and instruction files are written into
|
|
294
|
+
your home folder as fixed text when you install, not read out of the package as it runs, so a newer
|
|
295
|
+
package does not change them. The watcher is a long-running process still holding the code it
|
|
296
|
+
started with. The MCP config is the one part that needs nothing done to it: it runs plain `niro`
|
|
297
|
+
from your `PATH` with no version pinned in it, so the new server is picked up on its own.
|
|
298
|
+
|
|
299
|
+
**Restart your assistant last.** Skills and hooks are read when it starts, so nothing above reaches
|
|
300
|
+
a session that is already open.
|
|
301
|
+
|
|
302
|
+
Fully by hand, if you would rather:
|
|
303
|
+
|
|
304
|
+
```bash
|
|
305
|
+
npm install -g @niroai/niro@latest
|
|
306
|
+
niro mcp install # once per assistant you use
|
|
307
|
+
niro service install # only if you run the watcher
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
`niro start` upgrades nothing. On a machine that has connected an assistant once it skips the
|
|
311
|
+
connect step, so the old hooks and skills stay where they are.
|
|
312
|
+
|
|
313
|
+
---
|
|
314
|
+
|
|
257
315
|
## Reference
|
|
258
316
|
|
|
259
317
|
Add `--help` to any command for its flags. Add `--json` to `whoami`, `info`, `status`,
|
|
@@ -302,6 +360,10 @@ does nothing until you do.
|
|
|
302
360
|
|
|
303
361
|
**Auto-sync is not running.** `npm install -g pm2`, then `niro service install`.
|
|
304
362
|
|
|
363
|
+
**A command or feature in the changelog is missing.** You are on an older version. Check with
|
|
364
|
+
`niro --version`, then see [Upgrading](#upgrading): before 0.13.0 it takes two commands, after it
|
|
365
|
+
one. Plain `npm install -g` on its own always leaves your hooks and skills on the old version.
|
|
366
|
+
|
|
305
367
|
**A folder was refused.** Niro will not index your home folder, or a folder with no build file
|
|
306
368
|
in it. Point it at the repo you meant.
|
|
307
369
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@niroai/niro",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Niro
|
|
3
|
+
"version": "0.15.0",
|
|
4
|
+
"description": "Niro \u2014 one package: the niro CLI plus the Niro MCP server (code intelligence for AI coding assistants).",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"niro",
|
|
7
7
|
"mcp",
|
package/src/commands/init.js
CHANGED
|
@@ -41,7 +41,7 @@ const color = require("../lib/color");
|
|
|
41
41
|
const watchDaemon = require("../lib/watchDaemon");
|
|
42
42
|
const { login } = require("./login");
|
|
43
43
|
const { watchStatus } = require("./build");
|
|
44
|
-
const { currentIndent } = require("../lib/nestedOutput");
|
|
44
|
+
const { currentIndent, withIndent } = require("../lib/nestedOutput");
|
|
45
45
|
|
|
46
46
|
/** Depth as spaces, for the \r-written lines that bypass withIndent's console wrappers. */
|
|
47
47
|
function nestPad() {
|
|
@@ -99,9 +99,24 @@ async function init(pathArg, opts = {}) {
|
|
|
99
99
|
// repo for must not start failing its re-run just because it has no build file.
|
|
100
100
|
// See src/lib/onboardTarget.js.
|
|
101
101
|
const knownEntry = watchState.get(targetDir, apiUrlBeforeLogin);
|
|
102
|
+
// A FOLDER THAT HOLDS GIT REPOS ALREADY DECLARES ITS PROJECTS, just not with a build file.
|
|
103
|
+
//
|
|
104
|
+
// The descriptor rule looks for package.json / pom.xml / go.mod in this folder or just below it,
|
|
105
|
+
// and refuses when it finds none. That is right for a folder of holiday photos. It is wrong for
|
|
106
|
+
// ~/projects/github/<org>/<repo>, where the repos are three levels down and the folders above them
|
|
107
|
+
// hold nothing at all: the scan found no descriptor, the user was told "No project found" and
|
|
108
|
+
// pointed at --force, and --force would then upload the whole tree as ONE repo — the exact outcome
|
|
109
|
+
// this release exists to prevent. The comment on CHILD_SCAN_MAX_DEPTH calls that layout the
|
|
110
|
+
// commonest one on disk. Reproduced on a demo tree on 2026-09-19 before this line existed.
|
|
111
|
+
//
|
|
112
|
+
// Cheap because it stops at the first repo it sees, and only runs when the descriptor scan already
|
|
113
|
+
// came back empty. Every other refusal in ensureOnboardable (home directory, filesystem root) still
|
|
114
|
+
// applies: this exempts the descriptor rule alone.
|
|
115
|
+
const holdsGitRepos = () => collectNestedGitRepos(targetDir).repos.length > 0;
|
|
102
116
|
if (!onboardTarget.ensureOnboardable(targetDir, {
|
|
103
117
|
...opts,
|
|
104
118
|
alreadyRegistered: !!(knownEntry && knownEntry.gitId),
|
|
119
|
+
hasGitReposBeneath: holdsGitRepos,
|
|
105
120
|
})) {
|
|
106
121
|
process.exitCode = 1;
|
|
107
122
|
return;
|
|
@@ -244,6 +259,25 @@ async function init(pathArg, opts = {}) {
|
|
|
244
259
|
// onboarded, so resolve each immediate git-repo child against the server too — this
|
|
245
260
|
// catches children indexed via the web UI / as separate repos.
|
|
246
261
|
const childScan = await scanIndexedChildren(targetDir, opts);
|
|
262
|
+
|
|
263
|
+
// THE FOLDER IS NOT A REPO BUT CONTAINS SOME: onboard each of them, not the folder.
|
|
264
|
+
//
|
|
265
|
+
// `git.inspect` walks UPWARDS, so repoRoot can be an ANCESTOR of targetDir. "This folder is a
|
|
266
|
+
// repo" therefore has to compare paths, not just test for a truthy repoRoot.
|
|
267
|
+
//
|
|
268
|
+
// Why this exists. Onboarding the parent registers ONE repo, and a folder with no git remote gets
|
|
269
|
+
// the synthetic `local://<name>` URL from step 4. Measured on a customer's tree on 2026-09-18:
|
|
270
|
+
// `niro init` in an `apps/` folder holding 14 real repos produced 92 internal repos, every one of
|
|
271
|
+
// them carrying `local://apps` and a null branch — so no repo had its own identity, none could be
|
|
272
|
+
// matched by MCP from its own folder (it kept offering to create a temp project inside indexed
|
|
273
|
+
// code), and nothing could track a per-repo branch. The repos beneath were the customer's own
|
|
274
|
+
// declared boundaries; the split was re-deriving them, badly, from folder structure.
|
|
275
|
+
const targetIsRepoRoot = !!gitInfo.repoRoot
|
|
276
|
+
&& path.resolve(gitInfo.repoRoot) === path.resolve(targetDir);
|
|
277
|
+
if (!targetIsRepoRoot && childScan.all.length) {
|
|
278
|
+
return onboardDiscoveredRepos({ targetDir, defaultName, childScan, opts });
|
|
279
|
+
}
|
|
280
|
+
|
|
247
281
|
if (childScan.indexed.length) {
|
|
248
282
|
console.error(fail(`\n This folder contains ${childScan.indexed.length} repo(s) already indexed in Niro:`));
|
|
249
283
|
for (const c of childScan.indexed) {
|
|
@@ -642,7 +676,14 @@ async function init(pathArg, opts = {}) {
|
|
|
642
676
|
}
|
|
643
677
|
|
|
644
678
|
// --defaults promised "no prompts": never surface the PM2 install offer there.
|
|
645
|
-
|
|
679
|
+
// INSTALLED ONCE PER RUN, not once per repo. The daemon is a single global PM2 process that reads
|
|
680
|
+
// ~/.niro/projects.json, so a second install is a no-op that still shells out to `pm2 startup`,
|
|
681
|
+
// which asks for sudo. Onboarding a folder of 6 repos asked six times and printed six identical
|
|
682
|
+
// sudo failures (seen on the demo tree, 2026-09-18); the caller that onboards several repos
|
|
683
|
+
// installs it itself, after the last one.
|
|
684
|
+
const daemon = opts.skipDaemonSetup
|
|
685
|
+
? { ok: true }
|
|
686
|
+
: await watchDaemon.ensureWatchDaemon({ skipPrompts: !!opts.defaults });
|
|
646
687
|
|
|
647
688
|
// The closing banner must not read as an all-clear when auto-sync is dead —
|
|
648
689
|
// customers were only discovering the missing daemon after a reboot.
|
|
@@ -694,6 +735,135 @@ const CHILD_SCAN_MAX = 60; // bound the number of nested repos we resolve
|
|
|
694
735
|
// single repo and 50 for this monorepo, versus 5 and 13 at depth 2.
|
|
695
736
|
const CHILD_SCAN_MAX_DEPTH = 4;
|
|
696
737
|
|
|
738
|
+
/**
|
|
739
|
+
* Onboard every git repo found beneath a folder that is not itself a repo, into ONE project.
|
|
740
|
+
*
|
|
741
|
+
* <b>Why one project and not one per repo.</b> Cross-service answers — who calls this endpoint, what
|
|
742
|
+
* breaks if I change this, trace this request across services — resolve by unioning the repos of a
|
|
743
|
+
* PROJECT. Fourteen projects would give fourteen islands and lose the thing the customer is paying
|
|
744
|
+
* for. So: one project, N repos, each carrying its own origin URL and its own HEAD branch.
|
|
745
|
+
*
|
|
746
|
+
* <b>Why each repo goes through init() again rather than a bespoke path.</b> Registering a repo is
|
|
747
|
+
* not one call: it dedupes against the same repo in this and other projects, reuses a gitId rather
|
|
748
|
+
* than re-uploading, filters the manifest, pushes include/exclude patterns, scans env vars and writes
|
|
749
|
+
* the per-folder watch entry. `start.js` already re-enters init this way with `nested: true`. A second
|
|
750
|
+
* implementation of that would drift from this one within a release.
|
|
751
|
+
*
|
|
752
|
+
* Folders holding code but no repo are REPORTED, never silently skipped: from this point on, editing
|
|
753
|
+
* a file in one of them does nothing at all, and the user has to have been told once. Silently
|
|
754
|
+
* dropping code is the failure ADR-023 was written against.
|
|
755
|
+
*/
|
|
756
|
+
async function onboardDiscoveredRepos({ targetDir, defaultName, childScan, opts },
|
|
757
|
+
runInit = init, readEntry = watchState.peek) {
|
|
758
|
+
const already = childScan.indexed;
|
|
759
|
+
const fresh = childScan.all.filter(
|
|
760
|
+
(r) => !already.some((c) => path.resolve(c.path) === path.resolve(r))
|
|
761
|
+
);
|
|
762
|
+
const codeFoldersWithoutRepo = nonRepoCodeFolders(targetDir, childScan.all);
|
|
763
|
+
|
|
764
|
+
console.log(`\n ${ok(`${path.basename(targetDir)} is not a git repository, but contains ${childScan.all.length} that are.`)}`);
|
|
765
|
+
console.log(` Niro will index each repo separately, with its own git URL and branch.\n`);
|
|
766
|
+
for (const r of fresh) console.log(` ${ok("+")} ${path.relative(targetDir, r) || path.basename(r)}`);
|
|
767
|
+
|
|
768
|
+
// Already indexed elsewhere: SKIPPED, not a reason to refuse the rest.
|
|
769
|
+
//
|
|
770
|
+
// The whole-run refusal further down exists because onboarding the PARENT would upload that child's
|
|
771
|
+
// content a second time. Here the parent is never uploaded, so that reasoning does not apply, and
|
|
772
|
+
// refusing thirteen new repos because the fourteenth is known would be the wrong answer.
|
|
773
|
+
for (const c of already) {
|
|
774
|
+
const where = c.projects.map((x) => `"${x.alias || x.project_id}"`).join(", ");
|
|
775
|
+
console.log(` ${warn("-")} ${path.relative(targetDir, c.path)} — already indexed in ${where}, skipping`);
|
|
776
|
+
}
|
|
777
|
+
for (const f of codeFoldersWithoutRepo) {
|
|
778
|
+
console.log(` ${warn("-")} ${path.relative(targetDir, f)} — not a git repository, NOT indexed`);
|
|
779
|
+
}
|
|
780
|
+
if (childScan.unverified.length) {
|
|
781
|
+
console.log(` ${warn("?")} ${childScan.unverified.length} repo(s) could not be checked against the server; they will be onboarded.`);
|
|
782
|
+
}
|
|
783
|
+
if (childScan.truncated) {
|
|
784
|
+
console.log(color.dim(` (Stopped after the first ${CHILD_SCAN_MAX} repos; deeper ones were not scanned.)`));
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
if (!fresh.length) {
|
|
788
|
+
console.log(`\n ${warn("Nothing new to index.")} Every repo here is already in Niro.`);
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
if (codeFoldersWithoutRepo.length) {
|
|
792
|
+
console.log(color.dim(`\n Folders marked NOT indexed will not sync. Run \`niro init\` inside one to add it.`));
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!opts.defaults && !opts.force && !prompt.isNonInteractive()) {
|
|
796
|
+
if (!(await prompt.confirm(`\n Index these ${fresh.length} repo(s)?`, true))) return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
// The FIRST repo creates the project; the rest attach to whatever it created. Hoisting project
|
|
800
|
+
// creation out of init() instead would mean duplicating the new-vs-existing resolution, the name
|
|
801
|
+
// availability check and the alias-to-project_id reassignment that init already does.
|
|
802
|
+
let projectRef = opts.addTo || null;
|
|
803
|
+
let onboarded = 0;
|
|
804
|
+
for (const repoPath of fresh) {
|
|
805
|
+
const name = path.relative(targetDir, repoPath) || path.basename(repoPath);
|
|
806
|
+
console.log(`\n ${ok("▸")} ${name}`);
|
|
807
|
+
const childOpts = {
|
|
808
|
+
...opts,
|
|
809
|
+
nested: true,
|
|
810
|
+
// One build for the project at the end, not one per repo.
|
|
811
|
+
confirmBuild: "no",
|
|
812
|
+
// ...and one watch-daemon install for the run, done below once every repo is in.
|
|
813
|
+
skipDaemonSetup: true,
|
|
814
|
+
// The project name is the FOLDER's, not the first repo's — `apps` reads better than whichever
|
|
815
|
+
// repo happened to sort first.
|
|
816
|
+
projectName: projectRef ? undefined : (opts.projectName || defaultName),
|
|
817
|
+
addTo: projectRef || undefined,
|
|
818
|
+
};
|
|
819
|
+
try {
|
|
820
|
+
await withIndent(4, () => runInit(repoPath, childOpts));
|
|
821
|
+
} catch (err) {
|
|
822
|
+
console.error(fail(` Could not onboard ${name}: ${err.message}`));
|
|
823
|
+
continue;
|
|
824
|
+
}
|
|
825
|
+
const entry = readEntry(repoPath);
|
|
826
|
+
if (entry && entry.projectId) {
|
|
827
|
+
onboarded += 1;
|
|
828
|
+
if (!projectRef) projectRef = entry.projectId;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
if (!projectRef || !onboarded) {
|
|
833
|
+
console.error(fail(`\n No repo could be onboarded; nothing was created.`));
|
|
834
|
+
process.exitCode = 1;
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
const daemon = await watchDaemon.ensureWatchDaemon({ skipPrompts: !!opts.defaults });
|
|
838
|
+
if (!daemon.ok) {
|
|
839
|
+
console.log(`\n ${warn("auto-sync is OFF")} — these folders will not stay in sync (see above).`);
|
|
840
|
+
}
|
|
841
|
+
console.log(`\n ${ok(`${onboarded} repo(s) indexed into project "${projectRef}".`)}`);
|
|
842
|
+
console.log(` ${color.cyan(`niro build`)} to index them, or ${color.cyan("niro status")} to review first.`);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
/**
|
|
846
|
+
* Immediate subfolders that hold files but are neither a git repo nor on the way to one.
|
|
847
|
+
*
|
|
848
|
+
* A grouping folder like `services/` that CONTAINS repos is not "skipped code" — its repos are being
|
|
849
|
+
* onboarded. Only a folder with no repo anywhere beneath it is genuinely being left out.
|
|
850
|
+
*/
|
|
851
|
+
function nonRepoCodeFolders(targetDir, discoveredRepos) {
|
|
852
|
+
const out = [];
|
|
853
|
+
let entries;
|
|
854
|
+
try { entries = fs.readdirSync(targetDir, { withFileTypes: true }); } catch { return out; }
|
|
855
|
+
for (const e of entries) {
|
|
856
|
+
if (!e.isDirectory() || e.name.startsWith(".") || CHILD_SCAN_SKIP.has(e.name)) continue;
|
|
857
|
+
const dir = path.join(targetDir, e.name);
|
|
858
|
+
const leadsToARepo = discoveredRepos.some((r) => {
|
|
859
|
+
const rel = path.relative(dir, r);
|
|
860
|
+
return rel === "" || (!rel.startsWith("..") && !path.isAbsolute(rel));
|
|
861
|
+
});
|
|
862
|
+
if (!leadsToARepo) out.push(dir);
|
|
863
|
+
}
|
|
864
|
+
return out;
|
|
865
|
+
}
|
|
866
|
+
|
|
697
867
|
/**
|
|
698
868
|
* Collect directories under `root` that are their OWN git repo (have a `.git` entry),
|
|
699
869
|
* scanning immediate children AND one level of NON-git grouping dirs (so `services/foo`,
|
|
@@ -741,7 +911,9 @@ function collectNestedGitRepos(root) {
|
|
|
741
911
|
*/
|
|
742
912
|
async function scanIndexedChildren(targetDir, opts, resolve = resolveContext) {
|
|
743
913
|
const { repos: gitChildren, truncated } = collectNestedGitRepos(targetDir);
|
|
744
|
-
|
|
914
|
+
// `all` is every git repo found beneath targetDir, indexed or not. The refusal below only ever
|
|
915
|
+
// needed the indexed ones; onboarding them (onboardDiscoveredRepos) needs the whole list.
|
|
916
|
+
if (!gitChildren.length) return { indexed: [], unverified: [], truncated, all: [] };
|
|
745
917
|
|
|
746
918
|
const indexed = [];
|
|
747
919
|
const unverified = [];
|
|
@@ -767,7 +939,7 @@ async function scanIndexedChildren(targetDir, opts, resolve = resolveContext) {
|
|
|
767
939
|
else unverified.push(r);
|
|
768
940
|
}
|
|
769
941
|
}
|
|
770
|
-
return { indexed, unverified, truncated };
|
|
942
|
+
return { indexed, unverified, truncated, all: gitChildren };
|
|
771
943
|
}
|
|
772
944
|
|
|
773
945
|
/**
|
|
@@ -1226,4 +1398,6 @@ function logicalCwd() {
|
|
|
1226
1398
|
return process.cwd();
|
|
1227
1399
|
}
|
|
1228
1400
|
|
|
1229
|
-
module.exports = {
|
|
1401
|
+
module.exports = {
|
|
1402
|
+
onboardDiscoveredRepos,
|
|
1403
|
+
nonRepoCodeFolders, init, parseOnOff, parseYesNo, parseQuietPeriod, promptEnvVars, promptActivePropertyFiles, scanIndexedChildren };
|
package/src/commands/mcp.js
CHANGED
|
@@ -2,19 +2,28 @@
|
|
|
2
2
|
* `niro mcp install` / `niro mcp uninstall` / `niro mcp serve` — drive the in-package MCP server.
|
|
3
3
|
*
|
|
4
4
|
* install(): spawn the bundled editor-config wizard as a child so it can prompt.
|
|
5
|
+
* Takes optional wizard arguments; see runWizard.
|
|
5
6
|
* uninstall(): spawn the teardown wizard — the exact inverse of install.
|
|
6
7
|
* serve(): run the in-package stdio bridge directly in this process so the
|
|
7
8
|
* editor's MCP stdio talks straight to it.
|
|
8
9
|
*/
|
|
9
10
|
const path = require("path");
|
|
10
|
-
const
|
|
11
|
+
const childProcess = require("child_process");
|
|
11
12
|
|
|
12
|
-
/**
|
|
13
|
-
|
|
13
|
+
/**
|
|
14
|
+
* Spawn a bundled wizard (setup.js / teardown.js) as a child so it can prompt on the TTY.
|
|
15
|
+
*
|
|
16
|
+
* `args` overrides what is forwarded to the wizard. Callers that are themselves a `niro mcp ...`
|
|
17
|
+
* command leave it out and the user's own flags after `niro mcp install` are passed through.
|
|
18
|
+
* `niro upgrade` passes its own `--client <id>`, because the flags on its command line
|
|
19
|
+
* (`--finish`, `--from`) are not the wizard's.
|
|
20
|
+
*/
|
|
21
|
+
function runWizard(scriptName, label, args) {
|
|
22
|
+
const wizardArgs = Array.isArray(args) ? args : process.argv.slice(4);
|
|
14
23
|
return new Promise((resolve, reject) => {
|
|
15
|
-
const child = spawn(
|
|
24
|
+
const child = childProcess.spawn(
|
|
16
25
|
process.execPath,
|
|
17
|
-
[path.join(__dirname, "..", "mcp", scriptName), ...
|
|
26
|
+
[path.join(__dirname, "..", "mcp", scriptName), ...wizardArgs],
|
|
18
27
|
{ stdio: "inherit" }
|
|
19
28
|
);
|
|
20
29
|
child.on("error", reject);
|
|
@@ -25,8 +34,8 @@ function runWizard(scriptName, label) {
|
|
|
25
34
|
});
|
|
26
35
|
}
|
|
27
36
|
|
|
28
|
-
async function install() {
|
|
29
|
-
return runWizard("setup.js", "setup");
|
|
37
|
+
async function install(args) {
|
|
38
|
+
return runWizard("setup.js", "setup", args);
|
|
30
39
|
}
|
|
31
40
|
|
|
32
41
|
async function uninstall() {
|