@niroai/niro 0.13.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 +109 -0
- package/package.json +2 -2
- package/src/commands/init.js +179 -5
- package/src/commands/watch.js +39 -7
- package/src/lib/folderSync.js +14 -0
- package/src/lib/onboardTarget.js +13 -4
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,115 @@ 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
|
+
|
|
7
116
|
## 0.13.0 — 2026-09-08
|
|
8
117
|
|
|
9
118
|
### Upgrading
|
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/watch.js
CHANGED
|
@@ -878,13 +878,42 @@ function matchesPatterns(relativePath, includePatterns, excludePatterns) {
|
|
|
878
878
|
return !excludePatterns.some(p => minimatch(relativePath, p));
|
|
879
879
|
}
|
|
880
880
|
|
|
881
|
-
|
|
881
|
+
// Every character that is special inside a regular expression EXCEPT `*`, which is the
|
|
882
|
+
// only wildcard this glob syntax has. Escaping `.` alone was the defect, because a pattern
|
|
883
|
+
// is not ours to choose: the server names a ROOT-LEVEL file by its own filename
|
|
884
|
+
// (ai-orchestrator SourceFileFilterService.derivePatterns, the `else` branch) and the new
|
|
885
|
+
// directory prompt above names a directory by its own path, so a real name reaches this
|
|
886
|
+
// function verbatim. `main.c++` at a repo root became /^main\.c++$/, "Nothing to repeat",
|
|
887
|
+
// and that throw killed EVERY sync pass for the folder: the file event gate, the delta and
|
|
888
|
+
// backstop walk, and the rescue check that runs before a delete. The quieter half of the
|
|
889
|
+
// same defect never threw at all: `app (copy).js` built a valid regex with a capture group,
|
|
890
|
+
// so the file silently stopped matching its own pattern and was never synced again.
|
|
891
|
+
// No /g flag: `test` on a global regex advances lastIndex between calls.
|
|
892
|
+
const REGEX_SPECIAL_CHARS = /[.+?^${}()|[\]\\]/;
|
|
893
|
+
|
|
894
|
+
/**
|
|
895
|
+
* Simple glob matching without a dependency: handles ** and * patterns.
|
|
896
|
+
*
|
|
897
|
+
* One pass over the pattern, so every character that is not a wildcard is escaped as a
|
|
898
|
+
* literal and the result is always a valid regular expression. The previous version
|
|
899
|
+
* escaped `.` and then routed `**` through a `§§` placeholder, which also corrupted any
|
|
900
|
+
* pattern that genuinely contained `§§`.
|
|
901
|
+
*/
|
|
882
902
|
function minimatch(filePath, pattern) {
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
903
|
+
let regexStr = "";
|
|
904
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
905
|
+
const ch = pattern[i];
|
|
906
|
+
if (ch === "*") {
|
|
907
|
+
if (pattern[i + 1] === "*") {
|
|
908
|
+
regexStr += ".*"; // ** crosses directory separators
|
|
909
|
+
i++;
|
|
910
|
+
} else {
|
|
911
|
+
regexStr += "[^/]*"; // * stays inside one path segment
|
|
912
|
+
}
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
regexStr += REGEX_SPECIAL_CHARS.test(ch) ? "\\" + ch : ch;
|
|
916
|
+
}
|
|
888
917
|
return new RegExp("^" + regexStr + "$").test(filePath);
|
|
889
918
|
}
|
|
890
919
|
|
|
@@ -944,8 +973,11 @@ function buildMinimalManifest(rootDir, includePatterns, excludePatterns) {
|
|
|
944
973
|
}
|
|
945
974
|
|
|
946
975
|
// handleFileEvent + deltaSyncOnResume are exported for unit testing the
|
|
947
|
-
// gitignore/fail-closed guards without a live server or fs.watch loop.
|
|
976
|
+
// gitignore/fail-closed guards without a live server or fs.watch loop. matchesPatterns is
|
|
977
|
+
// exported for the same reason: it is the gate all three sync paths share, and a pattern that
|
|
978
|
+
// cannot be compiled stops every one of them, so it is worth testing on its own.
|
|
948
979
|
module.exports = { watch, startWatch, startPeriodicSweep, handleFileEvent, deltaSyncOnResume, handleEntryFailure,
|
|
980
|
+
matchesPatterns,
|
|
949
981
|
queueDelete, flushPendingDeletes, pendingDeletes, DELETE_SETTLE_MS, backstopTick, notHolding,
|
|
950
982
|
unreadableRoots,
|
|
951
983
|
nextBackoffMs, shouldAttemptBackstop, recordBackstopFailure, recordBackstopSuccess };
|
package/src/lib/folderSync.js
CHANGED
|
@@ -34,11 +34,25 @@ const prompt = require("./prompt");
|
|
|
34
34
|
const color = require("./color");
|
|
35
35
|
|
|
36
36
|
const BATCH_SIZE = 50;
|
|
37
|
+
// MUST stay identical to ai-orchestrator SourceFileFilterService.SOURCE_EXTENSIONS.
|
|
38
|
+
// Both are hand-maintained and nothing enforced that until test/sourceExtensionDrift.test.js
|
|
39
|
+
// here, which reads the Java file, and ai-orchestrator SourceExtensionDriftTest, which reads
|
|
40
|
+
// THIS file. The two repositories build separately, so the guard is on both sides. The
|
|
41
|
+
// server's /filter is the authoritative gate, so an extension missing here is never
|
|
42
|
+
// offered and never uploaded, whatever the server would have accepted.
|
|
37
43
|
const SOURCE_EXTENSIONS = new Set([
|
|
38
44
|
".java", ".py", ".go", ".js", ".ts", ".tsx", ".jsx",
|
|
39
45
|
".cjs", ".mjs", ".cts", ".mts",
|
|
40
46
|
".kt", ".scala", ".rs", ".rb", ".php", ".cs", ".swift",
|
|
41
47
|
".c", ".cpp", ".h", ".hpp", ".dart",
|
|
48
|
+
// The remaining C++ extensions ast-parser routes to its C++ grammar and CppMapper.
|
|
49
|
+
".cc", ".cxx", ".hxx", ".hh", ".h++", ".c++", ".tcc", ".ipp", ".inl", ".tpp",
|
|
50
|
+
// Ten more the parser routes to a real mapper and that this list did not offer, so the
|
|
51
|
+
// user's Kotlin build scripts, C# scripts, Rakefiles, gemspecs, legacy PHP files and
|
|
52
|
+
// Python stubs were never uploaded. Added 2026-09-14 alongside .jsx.
|
|
53
|
+
".kts", ".csx", ".rake", ".gemspec",
|
|
54
|
+
".phtml", ".php3", ".php4", ".php5",
|
|
55
|
+
".pyi", ".pyw",
|
|
42
56
|
]);
|
|
43
57
|
|
|
44
58
|
// Build/config files the server's project-structure resolver needs to detect
|
package/src/lib/onboardTarget.js
CHANGED
|
@@ -227,16 +227,25 @@ function ensureOnboardable(targetDir, opts = {}) {
|
|
|
227
227
|
));
|
|
228
228
|
return true;
|
|
229
229
|
}
|
|
230
|
+
// A folder holding git repos declares its projects with .git rather than with a build file.
|
|
231
|
+
// Onboarding handles that case by indexing each repo separately, so refusing here would send the
|
|
232
|
+
// user to --force, which uploads the whole tree as one repo instead.
|
|
233
|
+
if (typeof opts.hasGitReposBeneath === "function" && opts.hasGitReposBeneath()) {
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
230
236
|
if (opts.force) {
|
|
231
237
|
console.log(color.yellow(
|
|
232
238
|
`\n ⚠ No project found in ${resolved}, but --force was passed. Onboarding it anyway.`
|
|
233
239
|
));
|
|
234
240
|
return true;
|
|
235
241
|
}
|
|
236
|
-
console.error(color.red(`\n ✗
|
|
237
|
-
console.error(" Niro
|
|
238
|
-
console.error(`
|
|
239
|
-
console.error("
|
|
242
|
+
console.error(color.red(`\n ✗ Nothing to index in ${resolved}.`));
|
|
243
|
+
console.error(" Niro indexes either a git repository, or a folder holding a build file");
|
|
244
|
+
console.error(` (package.json, pom.xml, go.mod, ...) at it or just below it${truncated ? ", though the scan stopped early because the tree is very large" : ""}.`);
|
|
245
|
+
console.error(" This folder has neither, so there is nothing to build a graph from.");
|
|
246
|
+
console.error("");
|
|
247
|
+
console.error(" If the code you meant is deeper down, run niro init from that folder.");
|
|
248
|
+
console.error(" Pass --force to upload this folder anyway.");
|
|
240
249
|
return false;
|
|
241
250
|
}
|
|
242
251
|
|