@niroai/niro 0.8.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/commands/ab.js +198 -0
- package/src/commands/add-project.js +11 -11
- package/src/commands/add-repo.js +23 -23
- package/src/commands/build.js +5 -5
- package/src/commands/delete.js +3 -3
- package/src/commands/edit.js +11 -11
- package/src/commands/info.js +2 -2
- package/src/commands/init.js +108 -87
- package/src/commands/login.js +25 -5
- package/src/commands/projects.js +1 -1
- package/src/commands/release.js +81 -15
- package/src/commands/remove.js +5 -5
- package/src/commands/start.js +567 -0
- package/src/commands/takeover.js +105 -23
- package/src/commands/watch.js +35 -35
- package/src/index.js +57 -3
- package/src/lib/folderSync.js +13 -8
- package/src/lib/git.js +21 -3
- package/src/lib/moduleExcludeSuggestions.js +5 -5
- package/src/lib/nestedOutput.js +49 -0
- package/src/lib/onboardTarget.js +5 -5
- package/src/lib/prompt.js +3 -0
- package/src/lib/service.js +20 -12
- package/src/lib/spinner.js +28 -7
- package/src/lib/tempProject.js +25 -1
- package/src/lib/watchDaemon.js +9 -9
- package/src/mcp/server.js +7 -2
- package/src/mcp/setup.js +198 -160
- package/src/mcp/teardown.js +53 -53
package/src/commands/release.js
CHANGED
|
@@ -22,6 +22,7 @@ const git = require("../lib/git");
|
|
|
22
22
|
const niroProjectFile = require("../lib/niroProjectFile");
|
|
23
23
|
const prompt = require("../lib/prompt");
|
|
24
24
|
const watchState = require("../lib/watchState");
|
|
25
|
+
const tempProject = require("../lib/tempProject");
|
|
25
26
|
|
|
26
27
|
const ok = (s) => color.green(s);
|
|
27
28
|
const warn = (s) => color.yellow(s);
|
|
@@ -56,26 +57,80 @@ async function release(pathArg, opts = {}) {
|
|
|
56
57
|
"`niro discard-temp-project --project <temp-project-id>`. The server refuses to delete anything " +
|
|
57
58
|
"that is not a temporary project, so this is always safe.");
|
|
58
59
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
60
|
+
// The temporary project may ALREADY be gone from the server, and nothing tells this machine when
|
|
61
|
+
// that happens: the idle sweeper reaps it after N days, and deleting it in the UI goes down the
|
|
62
|
+
// ordinary project-delete path. This fetch used to throw on that, BEFORE any local cleanup ran, so
|
|
63
|
+
// the folder kept a registration pointing at a project that no longer exists — and the only advice
|
|
64
|
+
// anything offered ("run `niro discard-temp-project` in that folder first") named this very command,
|
|
65
|
+
// which then failed the same way. A definitive not-found means there is nothing left to delete, so
|
|
66
|
+
// skip the server half and do the local half, which is the half that was still needed.
|
|
67
|
+
let project = null;
|
|
68
|
+
let cloneOf = null;
|
|
69
|
+
let alreadyGone = false;
|
|
70
|
+
try {
|
|
71
|
+
project = await api.get(`/api/projects/${encodeURIComponent(tempProjectId)}`);
|
|
72
|
+
} catch (err) {
|
|
73
|
+
// Only a definitive "no such project" counts. A network blip, a 401, a 500 or a genuine
|
|
74
|
+
// permissions refusal must still fail loudly — rewriting local registrations because the server
|
|
75
|
+
// could not be reached is exactly the mistake this command exists to undo.
|
|
76
|
+
if (!tempProject.isProjectNotFound(err)) throw err;
|
|
77
|
+
alreadyGone = true;
|
|
78
|
+
}
|
|
79
|
+
if (alreadyGone && !opts.project && !(entry && entry.takeover)) {
|
|
80
|
+
// The id did not come from a takeover backup or from the user — it came from a `.niro-project`
|
|
81
|
+
// pin that happens to name a missing project. Nothing here says this folder was ever taken over,
|
|
82
|
+
// so there is no warrant to touch its registration.
|
|
83
|
+
throw new Error(`Project ${tempProjectId} does not exist on this backend, and nothing on this ` +
|
|
84
|
+
"machine records a temporary project for this folder — refusing to change anything. " +
|
|
85
|
+
"If you know the temporary project's id, pass it explicitly: " +
|
|
86
|
+
"`niro discard-temp-project --project <temp-project-id>`.");
|
|
87
|
+
}
|
|
88
|
+
if (!alreadyGone) {
|
|
89
|
+
cloneOf = project.clone_of_project_id || project.cloneOfProjectId;
|
|
90
|
+
if (!cloneOf) {
|
|
91
|
+
throw new Error(`Project ${tempProjectId} is not a temporary project — refusing. ` +
|
|
92
|
+
"(discard only deletes projects created by `niro new-temp-project`)");
|
|
93
|
+
}
|
|
64
94
|
}
|
|
65
95
|
|
|
66
96
|
// Every folder registered against this temp project (multi-repo takeover, ADR-016).
|
|
67
97
|
const currentDir = path.resolve(targetDir);
|
|
68
|
-
|
|
98
|
+
let folders = watchState.allLocal(baseUrl).filter((e) => e.projectId === tempProjectId);
|
|
69
99
|
if (!folders.some((e) => path.resolve(e.folderPath) === currentDir)) {
|
|
70
100
|
folders.push({ folderPath: targetDir, ...(entry || {}) });
|
|
71
101
|
}
|
|
102
|
+
if (alreadyGone) {
|
|
103
|
+
// Two corrections that only matter once the server is no longer there to check the id against.
|
|
104
|
+
//
|
|
105
|
+
// Drop any folder not actually registered against THIS project. The line above adds the current
|
|
106
|
+
// folder unconditionally, which is right when the id came from that folder, and wrong when it came
|
|
107
|
+
// from `--project`: a mistyped or already-discarded id, run from a folder holding a DIFFERENT live
|
|
108
|
+
// working copy, would rewrite that copy's registration and report it as cleaned up. On the alive
|
|
109
|
+
// path the server's own answer bounds the damage; here nothing does.
|
|
110
|
+
folders = folders.filter((e) => e.projectId === tempProjectId);
|
|
111
|
+
// Then recover the source project from whichever folder recorded it. Every folder in one working
|
|
112
|
+
// copy is a clone of the SAME source project (ADR-016 extends a copy with more repos, it never
|
|
113
|
+
// spans two sources), so any one backup answers for all of them — including a sibling that has no
|
|
114
|
+
// backup of its own, which would otherwise be left pointing at the deleted project while the alive
|
|
115
|
+
// path restores it from `clone_of_project_id`.
|
|
116
|
+
const witness = folders.find((e) => e.takeover && e.takeover.sourceProjectId);
|
|
117
|
+
if (!witness) {
|
|
118
|
+
throw new Error(`Project ${tempProjectId} does not exist on this backend, and no folder on this ` +
|
|
119
|
+
"machine records which project it was a working copy of — refusing to guess. " +
|
|
120
|
+
"Nothing has been changed.");
|
|
121
|
+
}
|
|
122
|
+
cloneOf = witness.takeover.sourceProjectId;
|
|
123
|
+
}
|
|
72
124
|
|
|
73
125
|
// ADR-018 discard guard: deleting the temp project kills coverage for EVERY repo in it. Check each
|
|
74
126
|
// folder's CURRENT branch against its recorded source branch; a folder still on a feature branch is
|
|
75
127
|
// in-flight work whose coverage this discard would destroy. Old entries without sourceBranch can't be
|
|
76
128
|
// verified client-side (the server-side nudge guard still covers the agent-driven path).
|
|
129
|
+
// Skipped when the copy is already gone: this guard exists to warn BEFORE destroying coverage, and
|
|
130
|
+
// there is no coverage left to destroy. Leaving it on would ask the developer to confirm losing
|
|
131
|
+
// something they have already lost, and then refuse non-interactively over it.
|
|
77
132
|
const stillDiverged = [];
|
|
78
|
-
for (const e of folders) {
|
|
133
|
+
for (const e of alreadyGone ? [] : folders) {
|
|
79
134
|
const sourceBranch = e.takeover && e.takeover.sourceBranch;
|
|
80
135
|
if (!sourceBranch || !fs.existsSync(e.folderPath)) continue;
|
|
81
136
|
try {
|
|
@@ -86,11 +141,11 @@ async function release(pathArg, opts = {}) {
|
|
|
86
141
|
} catch { /* can't inspect — don't block on it */ }
|
|
87
142
|
}
|
|
88
143
|
if (stillDiverged.length) {
|
|
89
|
-
console.log(warn(`\n
|
|
144
|
+
console.log(warn(`\n ⚠ This temporary project still covers in-flight work:`));
|
|
90
145
|
for (const d of stillDiverged) {
|
|
91
146
|
console.log(warn(` ${d.folder} is on \`${d.branch}\` (source: \`${d.sourceBranch}\`)`));
|
|
92
147
|
}
|
|
93
|
-
console.log(warn("
|
|
148
|
+
console.log(warn(" Discarding deletes the WHOLE temporary project — those repos lose their coverage" +
|
|
94
149
|
" until you run `niro new-temp-project` there again (local code is untouched)."));
|
|
95
150
|
if (!opts.yes) {
|
|
96
151
|
if (!process.stdout.isTTY || prompt.isNonInteractive()) {
|
|
@@ -103,7 +158,7 @@ async function release(pathArg, opts = {}) {
|
|
|
103
158
|
return;
|
|
104
159
|
}
|
|
105
160
|
}
|
|
106
|
-
} else if (process.stdout.isTTY && !opts.yes) {
|
|
161
|
+
} else if (!alreadyGone && process.stdout.isTTY && !opts.yes) {
|
|
107
162
|
const sure = await prompt.confirm(
|
|
108
163
|
`Discard temporary project ${color.bold(project.alias || tempProjectId)}?`);
|
|
109
164
|
if (!sure) {
|
|
@@ -112,7 +167,9 @@ async function release(pathArg, opts = {}) {
|
|
|
112
167
|
}
|
|
113
168
|
}
|
|
114
169
|
|
|
115
|
-
|
|
170
|
+
if (!alreadyGone) {
|
|
171
|
+
await api.post(`/api/projects/${encodeURIComponent(tempProjectId)}/clone/release`, {});
|
|
172
|
+
}
|
|
116
173
|
|
|
117
174
|
// Restore each folder's local registration. ADR-018: the .niro-project file is left ALONE unless it
|
|
118
175
|
// still carries the OLD-STYLE swapped pin (pre-ADR-018 takeover) — then it points at a now-deleted
|
|
@@ -175,17 +232,26 @@ async function release(pathArg, opts = {}) {
|
|
|
175
232
|
}
|
|
176
233
|
}
|
|
177
234
|
|
|
178
|
-
|
|
235
|
+
if (alreadyGone) {
|
|
236
|
+
console.log(`\n ${ok("✓")} Temporary project ${color.bold(tempProjectId)} was already gone from the server` +
|
|
237
|
+
color.dim(" (deleted from the UI, or reaped after sitting idle)."));
|
|
238
|
+
console.log(repinned.length
|
|
239
|
+
? ` ${ok("✓")} Cleaned up ${repinned.length} local registration(s) that still pointed at it. ` +
|
|
240
|
+
"Niro routes back to the original project automatically."
|
|
241
|
+
: ` ${ok("✓")} Nothing left to clean up locally.`);
|
|
242
|
+
} else {
|
|
243
|
+
console.log(`\n ${ok("✓")} Temporary project discarded. Niro routes back to the original project automatically.`);
|
|
244
|
+
}
|
|
179
245
|
if (healed.length) {
|
|
180
|
-
console.log(color.dim("
|
|
246
|
+
console.log(color.dim(" Healed old-style pins:"));
|
|
181
247
|
for (const h of healed) console.log(color.dim(` ${h.folder} → ${h.to}`));
|
|
182
248
|
}
|
|
183
249
|
if (failed.length) {
|
|
184
|
-
console.log(warn(`\n
|
|
250
|
+
console.log(warn(`\n ⚠ ${failed.length} folder(s) could not be restored locally (the temporary project` +
|
|
185
251
|
" is already gone server-side):"));
|
|
186
252
|
for (const f of failed) console.log(color.dim(` ${f.folder} — ${f.error}`));
|
|
187
253
|
}
|
|
188
|
-
console.log(color.dim("
|
|
254
|
+
console.log(color.dim(" The original project picks up your merged commits via its normal remote polling."));
|
|
189
255
|
}
|
|
190
256
|
|
|
191
257
|
module.exports = { release };
|
package/src/commands/remove.js
CHANGED
|
@@ -71,9 +71,9 @@ async function remove(opts = {}) {
|
|
|
71
71
|
const expected = alias ? `"${alias}" or ${projectId}` : projectId;
|
|
72
72
|
console.error(`${color.red("✗")} Refusing to delete: --confirm-name must equal the project's name (${expected}).`);
|
|
73
73
|
if (confirm == null) {
|
|
74
|
-
console.error(color.dim("
|
|
74
|
+
console.error(color.dim(" Pass --confirm-name <name> to confirm this destructive delete."));
|
|
75
75
|
} else {
|
|
76
|
-
console.error(color.dim(`
|
|
76
|
+
console.error(color.dim(` You passed --confirm-name "${confirm}", which does not match.`));
|
|
77
77
|
}
|
|
78
78
|
process.exitCode = 1;
|
|
79
79
|
return;
|
|
@@ -86,9 +86,9 @@ async function remove(opts = {}) {
|
|
|
86
86
|
|
|
87
87
|
function printUsage() {
|
|
88
88
|
console.error("niro remove — remove a project or a repo mapping. Exactly one mode required:");
|
|
89
|
-
console.error("
|
|
90
|
-
console.error("
|
|
91
|
-
console.error("
|
|
89
|
+
console.error(" --project delete the whole project (DESTRUCTIVE)");
|
|
90
|
+
console.error(" requires --confirm-name <project alias or id>");
|
|
91
|
+
console.error(" --unmap-repo <gitId> remove one repo from the project (non-destructive)");
|
|
92
92
|
console.error("Target project: --target <alias|id>, or a .niro-project pin in this folder.");
|
|
93
93
|
}
|
|
94
94
|
|