@niroai/niro 0.11.0 → 0.13.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 +128 -0
- package/README.md +72 -3
- package/docs/working-on-branches.md +8 -0
- package/package.json +1 -1
- package/src/commands/_resolveProject.js +59 -3
- package/src/commands/delete.js +16 -1
- package/src/commands/mcp.js +16 -7
- package/src/commands/remove.js +13 -0
- package/src/commands/start.js +1 -0
- package/src/commands/upgrade.js +400 -0
- package/src/index.js +11 -0
- package/src/lib/folderSync.js +1 -0
- package/src/lib/impactLog.js +251 -0
- package/src/lib/mcpClients.js +241 -0
- package/src/lib/receiptStore.js +1 -1
- package/src/lib/service.js +18 -1
- package/src/mcp/server.js +30 -10
- package/src/mcp/setup.js +273 -17
- package/src/mcp/teardown.js +7 -2
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,134 @@
|
|
|
2
2
|
|
|
3
3
|
Notable changes to `@niroai/niro`. Dates are release dates.
|
|
4
4
|
|
|
5
|
+
## Unreleased
|
|
6
|
+
|
|
7
|
+
## 0.13.0 — 2026-09-08
|
|
8
|
+
|
|
9
|
+
### Upgrading
|
|
10
|
+
|
|
11
|
+
You do not have `niro upgrade` yet, so this one takes two commands:
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm install -g @niroai/niro@latest
|
|
15
|
+
niro upgrade --finish
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
The first gets the package. The second is the half npm cannot do: it re-runs the installer for
|
|
19
|
+
every assistant you have configured and restarts the background watcher. Then restart your
|
|
20
|
+
assistant. Skipping the second command leaves you on this version with the previous version's
|
|
21
|
+
hooks, skills and instruction files, which is the exact problem this release exists to fix.
|
|
22
|
+
|
|
23
|
+
From here on it is one command, `niro upgrade`, which does all of it.
|
|
24
|
+
|
|
25
|
+
### `niro upgrade` gets the new version AND the things a new version cannot bring with it
|
|
26
|
+
|
|
27
|
+
Upgrading was `npm install -g @niroai/niro@latest`, and every release note in this file said so and
|
|
28
|
+
nothing else. That command is not enough, and has never been enough. The MCP entry is fine: it runs
|
|
29
|
+
plain `niro` off your `PATH` with no version in it, so the new server is picked up for free. But the
|
|
30
|
+
hooks, the skills and the CLAUDE.md/AGENTS.md/rules text are written into your home folder as fixed
|
|
31
|
+
text at install time, not read from the package as it runs. npm cannot touch them. Nothing told you
|
|
32
|
+
that, and nothing re-ran the installer, so a machine that installed once in June was still running
|
|
33
|
+
June's instructions in September while reporting the current version. The background watcher had the
|
|
34
|
+
same problem from the other end: it is a long-running process holding the code it loaded at start,
|
|
35
|
+
so replacing files on disk left it on the old version until something restarted it.
|
|
36
|
+
|
|
37
|
+
`niro upgrade` does all three. It replaces the package, re-runs the installer for every assistant it
|
|
38
|
+
can prove is configured, and restarts the watcher if it is running.
|
|
39
|
+
|
|
40
|
+
It runs as two processes, and the reason is worth writing down. Every command in this CLI is loaded
|
|
41
|
+
lazily, inside its own action handler, and `npm install -g` replaces the folder those files are
|
|
42
|
+
loaded from. One process that ran npm and then carried on would be running the modules it loaded
|
|
43
|
+
before the swap alongside modules loaded after it. So phase one runs npm, executes the NEW binary
|
|
44
|
+
with `--finish`, and stops, requiring nothing in between; phase two is a fresh process reading the
|
|
45
|
+
new tree. `niro start` has always dodged the same trap the same way, by only installing globally
|
|
46
|
+
when it is running from the npx cache and then handing over.
|
|
47
|
+
|
|
48
|
+
Which assistants to refresh is worked out by looking, because nothing on disk records what you
|
|
49
|
+
chose. All 14 clients are covered, because the installer leaves a file this repo owns in every case:
|
|
50
|
+
eight write a JSON config whose path `setup.js` builds, and the other six also get a marker appended
|
|
51
|
+
to a markdown file written here and stripped by `teardown.js` (`~/.claude/CLAUDE.md`,
|
|
52
|
+
`~/.gemini/GEMINI.md`, `~/.copilot/copilot-instructions.md`, `~/.factory/AGENTS.md`), plus codex's
|
|
53
|
+
`[mcp_servers.niro]` table and goose's `extensions:` entry. The marker is appended only after the
|
|
54
|
+
registration command succeeded, so its presence means the client really was configured. Detection is
|
|
55
|
+
read-only, and it is deliberately strict: a client whose entry is `enabled: false` or
|
|
56
|
+
`disabled: true` is one you switched off, so it is left alone rather than switched back on; a
|
|
57
|
+
marker has to be on a line of its own, because a sentence mentioning it is not an install; and every
|
|
58
|
+
path has to be absolute.
|
|
59
|
+
|
|
60
|
+
`niro upgrade --finish` on its own skips npm and only refreshes the assistant files and the watcher,
|
|
61
|
+
which is what you want if you already ran `npm install -g` by hand.
|
|
62
|
+
|
|
63
|
+
Without a usable home folder it checks nothing and says so. Every path would otherwise resolve
|
|
64
|
+
against the current folder, which is how the reverted serve-time self-heal came to write into a
|
|
65
|
+
project's own `.claude/settings.json` (see the 2026-07-30 revert). "Usable" means absolute, and it
|
|
66
|
+
also means rejecting a `HOME` set to the empty string even when `USERPROFILE` would fill in, because
|
|
67
|
+
four of the client path builders read `HOME` directly and would otherwise hand back a relative path.
|
|
68
|
+
This command never writes a settings file itself: refreshing hooks means spawning the existing
|
|
69
|
+
installer, once per client.
|
|
70
|
+
|
|
71
|
+
It refuses to upgrade a copy you are not running. The first real run of this command was from a
|
|
72
|
+
checkout linked with `npm link`, and `npm install -g` deleted that link and put the published
|
|
73
|
+
package in its place. So before npm is allowed to touch anything, `niro upgrade` looks at which
|
|
74
|
+
copy is running: the global package folder being a symlink means `npm link`, and it stops and says
|
|
75
|
+
so; a checkout or an npx copy that is not the global install stops too. `--force` overrides either.
|
|
76
|
+
|
|
77
|
+
It also asks the version it just installed whether it has an `upgrade` command before handing over
|
|
78
|
+
to it, because the newest published version can predate the one you are running. Without that
|
|
79
|
+
check the hand-over fails with a bare `error: unknown command 'upgrade'` and the advice printed is
|
|
80
|
+
to re-run a command that does not exist. Now it prints the two commands that do work.
|
|
81
|
+
|
|
82
|
+
Failures are visible. Each one has its own exit code (npm failed, no binary after npm, hand-over
|
|
83
|
+
failed, refresh incomplete), an interrupted npm is reported as interrupted rather than as "nothing
|
|
84
|
+
changed", a hand-over that cannot start prints why, and the hand-over carries a marker so a version
|
|
85
|
+
that does not understand `--finish` cannot loop back into running npm again.
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
## 0.12.0 — 2026-09-06
|
|
89
|
+
|
|
90
|
+
### Before an edit to indexed source, Claude is asked to map the method and to verify those flows afterwards
|
|
91
|
+
|
|
92
|
+
Every instruction file niro writes tells the assistant to map who depends on a method before changing
|
|
93
|
+
it. In practice the assistant finds the file, reads it, and edits; the rule is read early in a session
|
|
94
|
+
and forgotten by the middle of it. And the map was always meant for afterwards: the point is to go back
|
|
95
|
+
to the callers and flows once the edit is made and check they still hold.
|
|
96
|
+
|
|
97
|
+
Claude Code now gets a third niro hook, on Edit and Write. Before an edit to source in an indexed repo
|
|
98
|
+
it puts a short reminder in front of the assistant: decide whether the change can alter behaviour (a log
|
|
99
|
+
line or a comment cannot, say so and go ahead); if it can, map who depends on the method now with
|
|
100
|
+
get_blast_radius or get_impact_analysis and keep that list as the checklist for after the edit; and do
|
|
101
|
+
not re-query the graph to verify, because it updates only after the edit is synced and rebuilt. If an
|
|
102
|
+
impact call already named the file this session, the reminder instead says to go back to that list
|
|
103
|
+
after the edit. Each reminder appears once per file per session. It is advisory: it never approves or
|
|
104
|
+
refuses an edit. The evidence comes from the bridge, which writes one line per answered impact call
|
|
105
|
+
under `~/.niro/impact/`. New files, tests, config and docs are never mentioned. Off switch:
|
|
106
|
+
`touch ~/.niro/impact-gate-off`. `niro mcp uninstall` removes the hook and the evidence files.
|
|
107
|
+
|
|
108
|
+
The same three points were added to every instruction surface niro writes: the rule sentence in each
|
|
109
|
+
client's always-on file, the niro skill, the Cursor, Cline and Kiro rules, and the PostCompact and
|
|
110
|
+
prompt hooks. The clients without hooks read them there. Details: `docs/usecases/impact-gate.md`.
|
|
111
|
+
|
|
112
|
+
### `niro mcp install` now refreshes Claude Code hooks on a machine where niro was already set up
|
|
113
|
+
|
|
114
|
+
Re-running the install on a machine with an existing registration failed at the first step, because
|
|
115
|
+
`claude mcp add` refuses a name that already exists, and the installer read that refusal as "Claude
|
|
116
|
+
Code is not installed" and stopped before writing any hook or the CLAUDE.md section. So the step the
|
|
117
|
+
out-of-date notice asks for did nothing for anyone who had installed once. The installer now reads the
|
|
118
|
+
existing registration first: same URL, it keeps it; a different `--url`, it replaces it; none, it adds
|
|
119
|
+
it. Hooks and sections are written in every case.
|
|
120
|
+
|
|
121
|
+
### CommonJS and ES-module files (.cjs, .mjs, .cts, .mts) are now uploaded for indexing
|
|
122
|
+
|
|
123
|
+
When niro decided which files to upload for indexing, it skipped `.cjs`, `.mjs`, `.cts` and `.mts`,
|
|
124
|
+
so a `build.cjs` or an `index.mjs` was never indexed and never appeared in the graph. Those four
|
|
125
|
+
extensions are now included, matching the server's own file filter.
|
|
126
|
+
|
|
127
|
+
### The niro rules now name get_exception_flow
|
|
128
|
+
|
|
129
|
+
The explore-first rule niro writes into each client now mentions `get_exception_flow`, the tool that
|
|
130
|
+
reports which exceptions can escape a method (and who must handle one). It is served by the niro
|
|
131
|
+
backend; this only makes the assistants aware it exists.
|
|
132
|
+
|
|
5
133
|
## 0.11.0 — 2026-09-03
|
|
6
134
|
|
|
7
135
|
### The watcher stops pausing and resuming your uploads over and over
|
package/README.md
CHANGED
|
@@ -45,7 +45,9 @@ Then restart your assistant and ask it something:
|
|
|
45
45
|
```bash
|
|
46
46
|
niro status
|
|
47
47
|
```
|
|
48
|
-
Is my index up to date?
|
|
48
|
+
Is my index up to date? It answers about the project this folder is synced to **on the branch you
|
|
49
|
+
are on**, so once you have run `niro new-temp-project` it reports that copy rather than the shared
|
|
50
|
+
project. Name one explicitly to ask about a different project: `niro status <project-id>`.
|
|
49
51
|
|
|
50
52
|
```bash
|
|
51
53
|
niro start
|
|
@@ -65,7 +67,15 @@ Every project in your account.
|
|
|
65
67
|
```bash
|
|
66
68
|
niro build
|
|
67
69
|
```
|
|
68
|
-
Rebuild the index by hand and watch it finish.
|
|
70
|
+
Rebuild the index by hand and watch it finish. Like `niro status`, it picks the project for the
|
|
71
|
+
branch you are on, so on a branch with a temporary project it rebuilds that one. Name a project to
|
|
72
|
+
rebuild a different one.
|
|
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).
|
|
69
79
|
|
|
70
80
|
```bash
|
|
71
81
|
niro doctor
|
|
@@ -224,7 +234,10 @@ Detach a repo from a project. Nothing is deleted.
|
|
|
224
234
|
```bash
|
|
225
235
|
niro remove --project --confirm-name <name>
|
|
226
236
|
```
|
|
227
|
-
Delete a whole project. Destructive, hence the confirmation.
|
|
237
|
+
Delete a whole project. Destructive, hence the confirmation. It refuses on a temporary project and
|
|
238
|
+
sends you to `niro discard-temp-project` instead, because that removes the copy **and** points this
|
|
239
|
+
folder back at the shared project; deleting it here would leave the folder aimed at a project that
|
|
240
|
+
no longer exists. `niro delete` refuses for the same reason.
|
|
228
241
|
|
|
229
242
|
---
|
|
230
243
|
|
|
@@ -247,6 +260,58 @@ Watch this folder in the foreground instead, for one session.
|
|
|
247
260
|
|
|
248
261
|
---
|
|
249
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
|
+
|
|
250
315
|
## Reference
|
|
251
316
|
|
|
252
317
|
Add `--help` to any command for its flags. Add `--json` to `whoami`, `info`, `status`,
|
|
@@ -295,6 +360,10 @@ does nothing until you do.
|
|
|
295
360
|
|
|
296
361
|
**Auto-sync is not running.** `npm install -g pm2`, then `niro service install`.
|
|
297
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
|
+
|
|
298
367
|
**A folder was refused.** Niro will not index your home folder, or a folder with no build file
|
|
299
368
|
in it. Point it at the repo you meant.
|
|
300
369
|
|
|
@@ -46,6 +46,14 @@ It creates your own private copy of the project inside Niro.
|
|
|
46
46
|
|
|
47
47
|
From then on, answers come from YOUR code.
|
|
48
48
|
|
|
49
|
+
The commands follow too. `niro status` and `niro build` act on the copy for the branch you are on,
|
|
50
|
+
not on the shared project, so "is my index up to date" and "rebuild it" mean what you expect while
|
|
51
|
+
you are working on a branch. Name a project explicitly if you want a different one.
|
|
52
|
+
|
|
53
|
+
`niro delete` and `niro remove --project` will not delete a private copy. They send you to
|
|
54
|
+
`niro discard-temp-project`, which removes the copy AND points your folder back at the shared
|
|
55
|
+
project. Deleting it any other way leaves your folder aimed at something that is gone.
|
|
56
|
+
|
|
49
57
|
## Switching branches
|
|
50
58
|
|
|
51
59
|
Nothing to do. Niro checks which branch you are on for every question.
|
package/package.json
CHANGED
|
@@ -7,11 +7,16 @@
|
|
|
7
7
|
* - an alias or git URL -> resolved via GET /api/projects/resolve.
|
|
8
8
|
* If an alias/URL maps to MULTIPLE projects, a clear ambiguity error is
|
|
9
9
|
* thrown listing the candidates (we never silently pick the first).
|
|
10
|
-
* 2.
|
|
10
|
+
* 2. The project THIS FOLDER IS CURRENTLY SYNCED TO, when it was recorded for the
|
|
11
|
+
* branch that is checked out. See resolveFromSyncedFolder for why this comes
|
|
12
|
+
* before the pin.
|
|
13
|
+
* 3. Walk up from cwd looking for .niro-project (same logic the MCP bridge uses).
|
|
11
14
|
*/
|
|
12
15
|
const api = require("../lib/api");
|
|
13
16
|
const prompt = require("../lib/prompt");
|
|
14
17
|
const niroProjectFile = require("../lib/niroProjectFile");
|
|
18
|
+
const git = require("../lib/git");
|
|
19
|
+
const watchState = require("../lib/watchState");
|
|
15
20
|
const { normalizeProjects, resolveContext } = require("../lib/resolveContext");
|
|
16
21
|
|
|
17
22
|
// A canonical project_id is a UUID. Anything that doesn't look like one and
|
|
@@ -119,6 +124,36 @@ async function resolveProjectFromGit(opts = {}) {
|
|
|
119
124
|
throw e;
|
|
120
125
|
}
|
|
121
126
|
|
|
127
|
+
/**
|
|
128
|
+
* The project this folder is actually synced to right now, when the recorded branch is the
|
|
129
|
+
* one checked out. Null when nothing is recorded, when the branch has moved on, or on any error.
|
|
130
|
+
*
|
|
131
|
+
* <p>Why this has to come BEFORE the .niro-project pin. The pin holds the SHARED project's id and
|
|
132
|
+
* never changes. When a developer runs `niro new-temp-project`, the folder starts syncing to a
|
|
133
|
+
* private temporary project that indexes their branch, and only ~/.niro/projects.json records it.
|
|
134
|
+
* Reading the pin therefore answers about a project the developer is not working in. Measured on
|
|
135
|
+
* 2026-09-08: in a repo on branch feat/shipping-wrap-rethrow, `niro status` reported the shared
|
|
136
|
+
* project with "Updated 2026-09-06T09:28", two days stale, while the branch's own project had
|
|
137
|
+
* finished a build four minutes earlier. `niro build` had the same fault and would have rebuilt the
|
|
138
|
+
* shared project.
|
|
139
|
+
*
|
|
140
|
+
* <p>The branch comparison is the safety catch. An entry recorded on another branch must not
|
|
141
|
+
* redirect the command, so a mismatch falls through to the pin, which is the old behaviour.
|
|
142
|
+
*/
|
|
143
|
+
function resolveFromSyncedFolder(opts = {}) {
|
|
144
|
+
try {
|
|
145
|
+
const ctx = git.inspect(process.cwd());
|
|
146
|
+
if (!ctx || !ctx.repoRoot) return null;
|
|
147
|
+
const entry = watchState.get(ctx.repoRoot, opts.apiUrl || api.currentBaseUrl());
|
|
148
|
+
if (!entry || !entry.projectId) return null;
|
|
149
|
+
if (entry.branch && ctx.branch && entry.branch !== ctx.branch) return null;
|
|
150
|
+
return entry.projectId;
|
|
151
|
+
} catch (_) {
|
|
152
|
+
// Never block a command on this: falling through to the pin is the previous behaviour.
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
122
157
|
async function resolveProjectId(explicit, opts = {}) {
|
|
123
158
|
if (explicit) {
|
|
124
159
|
// A UUID is used directly — unchanged behavior. Anything else is an
|
|
@@ -126,7 +161,10 @@ async function resolveProjectId(explicit, opts = {}) {
|
|
|
126
161
|
if (looksLikeProjectId(explicit)) return explicit;
|
|
127
162
|
return resolveAliasOrUrl(explicit, opts);
|
|
128
163
|
}
|
|
129
|
-
// No explicit arg:
|
|
164
|
+
// No explicit arg: the project this folder is synced to on THIS branch wins over the pin.
|
|
165
|
+
const fromFolder = resolveFromSyncedFolder(opts);
|
|
166
|
+
if (fromFolder) return fromFolder;
|
|
167
|
+
// Then a .niro-project pin (walk up from cwd).
|
|
130
168
|
const fromFile = readNiroProjectFile(process.cwd());
|
|
131
169
|
if (fromFile) {
|
|
132
170
|
// A pin that is already a project_id is used directly. A pin holding an
|
|
@@ -139,4 +177,22 @@ async function resolveProjectId(explicit, opts = {}) {
|
|
|
139
177
|
return resolveProjectFromGit(opts);
|
|
140
178
|
}
|
|
141
179
|
|
|
142
|
-
|
|
180
|
+
/**
|
|
181
|
+
* The temporary project this id names, or null when it is an ordinary shared project.
|
|
182
|
+
*
|
|
183
|
+
* <p>Used by the destructive commands. Deleting a temporary project through `niro delete` leaves
|
|
184
|
+
* the folder pinned to a project that is gone and skips the local cleanup that
|
|
185
|
+
* `niro discard-temp-project` does, so those commands send the developer there instead.
|
|
186
|
+
* Never throws: if the project cannot be fetched, the caller carries on as before.
|
|
187
|
+
*/
|
|
188
|
+
async function tempProjectSourceId(projectId, opts = {}) {
|
|
189
|
+
try {
|
|
190
|
+
const project = await api.get(`/api/projects/${encodeURIComponent(projectId)}`, { apiUrl: opts.apiUrl });
|
|
191
|
+
return (project && (project.cloneOfProjectId || project.clone_of_project_id)) || null;
|
|
192
|
+
} catch (_) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
module.exports = { resolveProjectId, readNiroProjectFile, resolveAliasOrUrl, looksLikeProjectId,
|
|
198
|
+
resolveFromSyncedFolder, tempProjectSourceId };
|
package/src/commands/delete.js
CHANGED
|
@@ -14,12 +14,27 @@ const api = require("../lib/api");
|
|
|
14
14
|
const credentials = require("../lib/credentials");
|
|
15
15
|
const prompt = require("../lib/prompt");
|
|
16
16
|
const color = require("../lib/color");
|
|
17
|
-
const { resolveProjectId, readNiroProjectFile } = require("./_resolveProject");
|
|
17
|
+
const { resolveProjectId, readNiroProjectFile, tempProjectSourceId } = require("./_resolveProject");
|
|
18
18
|
|
|
19
19
|
async function deleteProject(projectIdArg, opts = {}) {
|
|
20
20
|
credentials.requireCreds();
|
|
21
21
|
const projectId = await resolveProjectId(projectIdArg, { apiUrl: opts.apiUrl });
|
|
22
22
|
|
|
23
|
+
// A temporary project is not deleted through here. It belongs to a folder and a branch, and
|
|
24
|
+
// `discard-temp-project` is what unwinds both: it removes the project AND points the folder back
|
|
25
|
+
// at the shared one, repairing the local pin. Deleting it server-side does neither, so the folder
|
|
26
|
+
// is left aimed at a project that no longer exists. Refusing costs one command; the alternative is
|
|
27
|
+
// a broken checkout the developer then has to work out how to repair.
|
|
28
|
+
const cloneOf = await tempProjectSourceId(projectId, opts);
|
|
29
|
+
if (cloneOf) {
|
|
30
|
+
console.log(`${color.yellow("⚠")} ${color.bold(projectId)} is a temporary project for your branch, not a shared one.`);
|
|
31
|
+
console.log(color.dim(` It is a private copy of ${cloneOf}.`));
|
|
32
|
+
console.log(` Use ${color.bold("niro discard-temp-project")} in this folder instead — that removes it AND`);
|
|
33
|
+
console.log(` points this folder back at the shared project. ${color.dim("Nothing has been deleted.")}`);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
23
38
|
if (!opts.yes) {
|
|
24
39
|
console.log(`${color.yellow("⚠")} About to delete project ${color.bold(projectId)}.`);
|
|
25
40
|
console.log(color.dim(" This removes the project and its graph from the server. Registered repos stay."));
|
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() {
|
package/src/commands/remove.js
CHANGED
|
@@ -57,14 +57,27 @@ async function remove(opts = {}) {
|
|
|
57
57
|
// --project: destructive. Fetch the project so we can match --confirm-name
|
|
58
58
|
// against either the alias or the project_id.
|
|
59
59
|
let alias = null;
|
|
60
|
+
let cloneOf = null;
|
|
60
61
|
try {
|
|
61
62
|
const project = await api.get(`/api/projects/${encodeURIComponent(projectId)}`, { apiUrl: opts.apiUrl });
|
|
62
63
|
alias = (project && (project.alias)) || null;
|
|
64
|
+
cloneOf = (project && (project.cloneOfProjectId || project.clone_of_project_id)) || null;
|
|
63
65
|
} catch (err) {
|
|
64
66
|
// If we can't read the project, fall back to matching the id only.
|
|
65
67
|
alias = null;
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Same rule as `niro delete`: a temporary project is unwound by discard-temp-project, which also
|
|
71
|
+
// points this folder back at the shared project. Removing it here would leave the folder aimed at
|
|
72
|
+
// a project that is gone. Read from the fetch above rather than a second request.
|
|
73
|
+
if (cloneOf) {
|
|
74
|
+
console.log(`${color.yellow("⚠")} ${color.bold(projectId)} is a temporary project for your branch, not a shared one.`);
|
|
75
|
+
console.log(color.dim(` It is a private copy of ${cloneOf}.`));
|
|
76
|
+
console.log(` Use ${color.bold("niro discard-temp-project")} in this folder instead. ${color.dim("Nothing has been removed.")}`);
|
|
77
|
+
process.exitCode = 1;
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
68
81
|
const confirm = opts.confirmName == null ? null : String(opts.confirmName).trim();
|
|
69
82
|
const matches = confirm != null && (confirm === projectId || (alias != null && confirm === alias));
|
|
70
83
|
if (!matches) {
|