@niroai/niro 0.16.0 → 0.18.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 +149 -0
- package/README.md +123 -302
- package/bin/niro.js +12 -1
- package/docs/agent-antigravity.md +37 -0
- package/docs/agent-claude-code.md +48 -0
- package/docs/agent-claude-desktop.md +47 -0
- package/docs/agent-cline.md +42 -0
- package/docs/agent-codex.md +45 -0
- package/docs/agent-copilot-cli.md +42 -0
- package/docs/agent-cursor.md +44 -0
- package/docs/agent-factory.md +42 -0
- package/docs/agent-gemini-cli.md +42 -0
- package/docs/agent-goose.md +42 -0
- package/docs/agent-kiro.md +37 -0
- package/docs/agent-opencode.md +37 -0
- package/docs/agent-vscode.md +45 -0
- package/docs/agent-windsurf.md +38 -0
- package/docs/answer-shapes.md +69 -0
- package/docs/builds-and-updates.md +68 -0
- package/docs/cloud-README.md +372 -0
- package/docs/configuring-a-folder.md +51 -0
- package/docs/connect-ci.md +24 -0
- package/docs/connect-overview.md +20 -0
- package/docs/connect-review-bots.md +17 -0
- package/docs/connect-team-server.md +12 -0
- package/docs/connect-terminal.md +49 -0
- package/docs/connect-web-agents.md +9 -0
- package/docs/connect-your-ai-assistant.md +59 -175
- package/docs/coverage-audit-and-consent.md +51 -0
- package/docs/evaluate-what-niro-did.md +271 -0
- package/docs/faq-assistants-and-mcp.md +122 -0
- package/docs/faq-feedback-and-receipts.md +33 -0
- package/docs/faq-indexing.md +127 -0
- package/docs/faq-install-and-setup.md +103 -0
- package/docs/faq-licence-and-plans.md +86 -0
- package/docs/faq-login-and-account.md +69 -0
- package/docs/faq-offline.md +34 -0
- package/docs/faq-performance.md +33 -0
- package/docs/faq-privacy-and-security.md +113 -0
- package/docs/faq-uninstall.md +47 -0
- package/docs/faq-updates.md +59 -0
- package/docs/faq-worktrees-and-branches.md +59 -0
- package/docs/faq-wrong-or-missing-answers.md +275 -0
- package/docs/hosted-only.md +33 -0
- package/docs/how-the-graph-works.md +47 -0
- package/docs/index.md +157 -0
- package/docs/licence-and-plans.md +72 -0
- package/docs/lifecycle-debug.md +48 -0
- package/docs/lifecycle-onboarding.md +32 -0
- package/docs/lifecycle-overview.md +77 -0
- package/docs/lifecycle-plan-and-design.md +44 -0
- package/docs/lifecycle-review.md +30 -0
- package/docs/lifecycle-security-and-privacy.md +35 -0
- package/docs/lifecycle-test.md +28 -0
- package/docs/lifecycle-write-code.md +35 -0
- package/docs/llms-full.txt +8015 -0
- package/docs/llms.txt +126 -0
- package/docs/playbook-add-a-feature.md +108 -0
- package/docs/playbook-change-safely.md +93 -0
- package/docs/playbook-data-and-events.md +107 -0
- package/docs/playbook-debug-after-a-change.md +96 -0
- package/docs/playbook-debug-config.md +95 -0
- package/docs/playbook-debug-exception.md +124 -0
- package/docs/playbook-debug-failing-service-call.md +119 -0
- package/docs/playbook-debug-slow-path.md +80 -0
- package/docs/playbook-debug-wrong-value.md +137 -0
- package/docs/playbook-onboard.md +95 -0
- package/docs/playbook-review-a-change.md +72 -0
- package/docs/playbook-test-a-change.md +73 -0
- package/docs/reference-cli.md +113 -0
- package/docs/reference-config-files.md +42 -0
- package/docs/reference-engine-commands.md +56 -0
- package/docs/reference-environment-variables.md +89 -0
- package/docs/reference-exit-codes.md +67 -0
- package/docs/reference-limits.md +75 -0
- package/docs/reference-mcp-tools.md +326 -0
- package/docs/reference-messages.md +180 -0
- package/docs/secrets-and-masking.md +48 -0
- package/docs/supported-c.md +130 -0
- package/docs/supported-cpp.md +157 -0
- package/docs/supported-csharp.md +211 -0
- package/docs/supported-go.md +258 -0
- package/docs/supported-java.md +226 -0
- package/docs/supported-javascript.md +223 -0
- package/docs/supported-kotlin.md +124 -0
- package/docs/supported-overview.md +89 -0
- package/docs/supported-php.md +157 -0
- package/docs/supported-python.md +224 -0
- package/docs/supported-ruby.md +162 -0
- package/docs/supported-rust.md +150 -0
- package/docs/supported-swift.md +133 -0
- package/docs/supported-typescript.md +220 -0
- package/docs/tokens-speed-and-accuracy.md +99 -0
- package/docs/troubleshooting.md +42 -0
- package/docs/uninstall.md +74 -0
- package/docs/what-leaves-your-machine.md +71 -0
- package/docs/where-things-are-stored.md +59 -0
- package/docs/worktrees-and-branches.md +55 -0
- package/package.json +5 -3
- package/src/commands/add-repo.js +5 -0
- package/src/commands/auditSend.js +67 -0
- package/src/commands/build.js +6 -0
- package/src/commands/docsSend.js +73 -0
- package/src/commands/engine.js +120 -0
- package/src/commands/engineRoutes.js +105 -0
- package/src/commands/guide.js +254 -0
- package/src/commands/init.js +16 -7
- package/src/commands/initLocal.js +96 -0
- package/src/commands/journalSend.js +47 -0
- package/src/commands/login.js +89 -12
- package/src/commands/mcp.js +22 -6
- package/src/commands/plan.js +99 -0
- package/src/commands/repoPush.js +33 -0
- package/src/commands/requestIntegration.js +38 -0
- package/src/commands/share.js +56 -0
- package/src/commands/start.js +451 -371
- package/src/commands/status.js +57 -0
- package/src/commands/takeover.js +6 -0
- package/src/commands/uninstall.js +180 -0
- package/src/commands/upgrade.js +38 -8
- package/src/commands/watch.js +420 -6
- package/src/index.js +174 -3
- package/src/lib/agentApi.js +32 -1
- package/src/lib/api.js +20 -6
- package/src/lib/auditUpload.js +293 -0
- package/src/lib/branchDrift.js +149 -0
- package/src/lib/choose.js +116 -0
- package/src/lib/cloudGuard.js +74 -0
- package/src/lib/credentials.js +2 -2
- package/src/lib/docsMarked.js +55 -0
- package/src/lib/docsState.js +213 -0
- package/src/lib/docsUpload.js +481 -0
- package/src/lib/engine.js +619 -0
- package/src/lib/folderSync.js +2 -1
- package/src/lib/graphCheck.js +40 -0
- package/src/lib/guide.js +293 -0
- package/src/lib/guideSearch.js +252 -0
- package/src/lib/integrationRequest.js +89 -0
- package/src/lib/journalUpload.js +147 -0
- package/src/lib/languages.js +129 -0
- package/src/lib/lease.js +375 -0
- package/src/lib/localBuild.js +285 -0
- package/src/lib/localEngine.js +210 -0
- package/src/lib/localOnboard.js +124 -0
- package/src/lib/localWorktrees.js +243 -0
- package/src/lib/machineCheck.js +58 -0
- package/src/lib/mcpClients.js +10 -1
- package/src/lib/migrationNotice.js +118 -0
- package/src/lib/modelFetch.js +153 -0
- package/src/lib/nestedOutput.js +23 -9
- package/src/lib/onboardingLog.js +34 -0
- package/src/lib/planUsage.js +165 -0
- package/src/lib/progressBar.js +46 -0
- package/src/lib/prompt.js +1 -1
- package/src/lib/redact.js +155 -0
- package/src/lib/release.js +33 -0
- package/src/lib/repoPush.js +495 -0
- package/src/lib/secret.js +1 -1
- package/src/lib/service.js +372 -63
- package/src/lib/shareCode.js +367 -0
- package/src/lib/shareExclusions.js +114 -0
- package/src/lib/shareFiles.js +129 -0
- package/src/lib/spinner.js +9 -5
- package/src/lib/watchDaemon.js +33 -10
- package/src/lib/watchState.js +12 -1
- package/src/lib/windowsPaths.js +67 -0
- package/src/mcp/lineCheck.js +382 -0
- package/src/mcp/lineName.js +92 -0
- package/src/mcp/localBackend.js +751 -0
- package/src/mcp/localMode.js +383 -0
- package/src/mcp/localSkill.js +189 -0
- package/src/mcp/server.js +610 -79
- package/src/mcp/setup.js +519 -131
- package/src/mcp/shellSearchHook.js +509 -0
- package/src/mcp/teardown.js +173 -91
- package/src/mcp/usingNiro.js +39 -0
- package/docs/working-on-branches.md +0 -162
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `niro guide`: read the documentation that ships in the package, from the terminal, offline.
|
|
3
|
+
*
|
|
4
|
+
* niro guide every page, grouped as docs/index.md groups them, one line each
|
|
5
|
+
* niro guide <topic> one page (by name, title or word); a short list when several match
|
|
6
|
+
* niro guide --search <words> every page holding the words, with the lines that hold them
|
|
7
|
+
* --json one JSON object instead, for an agent; --no-pager prints straight out
|
|
8
|
+
*
|
|
9
|
+
* Not `niro docs`, which is the engine's own route (src/commands/engineRoutes.js). Long output goes
|
|
10
|
+
* through a pager only when it is printed to a terminal and is taller than the window. A topic no page
|
|
11
|
+
* name, title or line matches shows what a search finds instead; only when that finds nothing too, and
|
|
12
|
+
* for a search with no match, does it exit with 1. Every --json answer has the same keys (envelope).
|
|
13
|
+
*/
|
|
14
|
+
"use strict";
|
|
15
|
+
|
|
16
|
+
const { spawnSync } = require("child_process");
|
|
17
|
+
const guideLib = require("../lib/guide");
|
|
18
|
+
const { search } = require("../lib/guideSearch");
|
|
19
|
+
|
|
20
|
+
const MAX_RESULTS = 10;
|
|
21
|
+
/** How many search results a topic with no match shows in their place. */
|
|
22
|
+
const FALLBACK_RESULTS = 3;
|
|
23
|
+
|
|
24
|
+
/** The pager to run: $PAGER, else less (more on Windows). */
|
|
25
|
+
function pagerCommand(env = process.env, platform = process.platform) {
|
|
26
|
+
return env.PAGER || (platform === "win32" ? "more" : "less");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Show `text` through the pager. Returns false when the pager could not be started, so the caller
|
|
31
|
+
* prints the text itself. `less` gets FRX unless LESS is set: quit when it fits, keep colour, leave the
|
|
32
|
+
* text on the screen afterwards. $PAGER runs through a shell, as git and man run it: the value is the
|
|
33
|
+
* person's own setting, so this runs nothing they could not run themselves.
|
|
34
|
+
*
|
|
35
|
+
* Ctrl-C while the pager shows reaches both processes. niro ignores it until the pager ends, as git does,
|
|
36
|
+
* so the pager handles it and the terminal is left as the pager leaves it. The listener is removed
|
|
37
|
+
* straight after: a signal that came in meanwhile is dropped, not delivered later.
|
|
38
|
+
*/
|
|
39
|
+
function runPager(text, { env = process.env, platform = process.platform, spawn = spawnSync } = {}) {
|
|
40
|
+
const command = pagerCommand(env, platform);
|
|
41
|
+
const childEnv = { ...env };
|
|
42
|
+
if (childEnv.LESS === undefined) childEnv.LESS = "FRX";
|
|
43
|
+
const ignore = () => {};
|
|
44
|
+
process.on("SIGINT", ignore);
|
|
45
|
+
let r;
|
|
46
|
+
try {
|
|
47
|
+
r = spawn(command, { shell: true, input: `${text}\n`, stdio: ["pipe", "inherit", "inherit"], env: childEnv });
|
|
48
|
+
} finally {
|
|
49
|
+
process.removeListener("SIGINT", ignore);
|
|
50
|
+
}
|
|
51
|
+
if (r.error) return false;
|
|
52
|
+
// 127 from sh, 9009 from cmd.exe: the pager was not found, and nothing was shown.
|
|
53
|
+
if (r.status === 127 || r.status === 9009) return false;
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Print `text`, through the pager when it is for a terminal and taller than the window. */
|
|
58
|
+
function show(text, io) {
|
|
59
|
+
const tall = text.split("\n").length > Math.max(1, (io.rows || 24) - 1);
|
|
60
|
+
if (io.isTTY && io.pager !== false && tall && io.runPager(text)) return;
|
|
61
|
+
io.write(`${text}\n`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function cut(line, io) {
|
|
65
|
+
if (!io.isTTY || !io.columns || line.length <= io.columns) return line;
|
|
66
|
+
return `${line.slice(0, Math.max(10, io.columns - 3))}...`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function pageLines(pages, io) {
|
|
70
|
+
const width = Math.min(34, Math.max(...pages.map((p) => p.page.length)) + 2);
|
|
71
|
+
return pages.map((p) => cut(` ${p.page.padEnd(width)}${p.summary}`, io));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function listText(guide, io) {
|
|
75
|
+
const count = [...guide.pages.values()].filter((p) => p.listed).length;
|
|
76
|
+
const out = [
|
|
77
|
+
`Niro documentation: ${count} pages, on this computer.`,
|
|
78
|
+
" niro guide <page> read a page",
|
|
79
|
+
" niro guide --search <words> search every page",
|
|
80
|
+
];
|
|
81
|
+
if (guide.start) out.push(`Start with: niro guide ${guide.start}`);
|
|
82
|
+
const all = guide.groups.flatMap((g) => g.pages.map((p) => guide.pages.get(p)));
|
|
83
|
+
const width = Math.min(34, Math.max(0, ...all.map((p) => p.page.length)) + 2);
|
|
84
|
+
for (const g of guide.groups) {
|
|
85
|
+
out.push("", io.bold(g.name));
|
|
86
|
+
for (const name of g.pages) {
|
|
87
|
+
const p = guide.pages.get(name);
|
|
88
|
+
out.push(cut(` ${p.page.padEnd(width)}${p.summary}`, io));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return out.join("\n");
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Every --json answer: the same keys whatever the form, so an agent reads one shape. */
|
|
95
|
+
function envelope(mode, fields = {}) {
|
|
96
|
+
return {
|
|
97
|
+
mode, // "list" | "page" | "choose" | "search" | "none"
|
|
98
|
+
query: null,
|
|
99
|
+
note: null,
|
|
100
|
+
start: null,
|
|
101
|
+
groups: [],
|
|
102
|
+
page: null,
|
|
103
|
+
matches: [],
|
|
104
|
+
match: null, // for "search": "phrase", "words", "pages" or "closest"
|
|
105
|
+
results: [],
|
|
106
|
+
total: 0,
|
|
107
|
+
error: null,
|
|
108
|
+
...fields,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function writeJson(io, value) {
|
|
113
|
+
io.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function brief(p) {
|
|
117
|
+
return { page: p.page, title: p.title, summary: p.summary || p.title };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function listJson(guide) {
|
|
121
|
+
const groups = guide.groups.map((g) => ({ name: g.name, pages: g.pages.map((n) => brief(guide.pages.get(n))) }));
|
|
122
|
+
return envelope("list", { start: guide.start, groups, total: groups.reduce((n, g) => n + g.pages.length, 0) });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/** Nothing to show: one sentence on stderr (or a JSON answer saying it), exit code 1. */
|
|
126
|
+
function nothing(io, json, query, message) {
|
|
127
|
+
if (json) writeJson(io, envelope("none", { query, error: message }));
|
|
128
|
+
else io.writeErr(`${message}\n`);
|
|
129
|
+
io.setExitCode(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function searchHeading(found, typed) {
|
|
133
|
+
const n = found.results.length;
|
|
134
|
+
const query = found.searched ? found.searched : typed;
|
|
135
|
+
const from = found.searched ? ` (from "${typed}")` : "";
|
|
136
|
+
if (found.match === "closest" && found.byPage) return `No page holds every word of "${query}"${from}; closest matches:`;
|
|
137
|
+
if (found.match === "closest") return `No line holds every word of "${query}"${from}. The closest ${n === 1 ? "page" : `${n} pages`}:`;
|
|
138
|
+
if (found.match === "pages") return `No line holds every word of "${query}"${from}; ${n === 1 ? "this page holds" : `${n} pages hold`} them all, best first:`;
|
|
139
|
+
return `${n} page${n === 1 ? " matches" : "s match"} "${query}"${from}.`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function searchLines(results, io, limit) {
|
|
143
|
+
const out = [];
|
|
144
|
+
for (const r of results.slice(0, limit)) {
|
|
145
|
+
out.push("", `${io.bold(`niro guide ${r.page}`)} ${r.title}`);
|
|
146
|
+
for (const l of r.lines) out.push(cut(` ${l.text}`, io));
|
|
147
|
+
if (r.total > r.lines.length) out.push(` (and ${r.total - r.lines.length} more line${r.total - r.lines.length === 1 ? "" : "s"})`);
|
|
148
|
+
}
|
|
149
|
+
return out;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function showPage(p, io, json) {
|
|
153
|
+
if (p.unreadable) return nothing(io, json, p.page, `The page ${p.page} cannot be read (${p.unreadable}). Reinstall niro with npm install -g @niroai/niro.`);
|
|
154
|
+
if (json) {
|
|
155
|
+
writeJson(io, envelope("page", {
|
|
156
|
+
query: p.page,
|
|
157
|
+
page: { page: p.page, title: p.title, text: guideLib.render(p.text), links: guideLib.linkedPages(p.text) },
|
|
158
|
+
total: 1,
|
|
159
|
+
}));
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
show(guideLib.render(p.text, { bold: io.bold }), io);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function showTopic(guide, topic, io, json) {
|
|
166
|
+
const found = guideLib.find(guide, topic);
|
|
167
|
+
if (found.page) return showPage(found.page, io, json);
|
|
168
|
+
if (found.matches.length) {
|
|
169
|
+
if (json) return writeJson(io, envelope("choose", { query: topic, matches: found.matches.map(brief), total: found.matches.length }));
|
|
170
|
+
const lines = [`${found.matches.length} pages match "${topic}". Run one of:`, ...pageLines(found.matches.map(brief), io).map((l) => l.replace(/^ /, " niro guide "))];
|
|
171
|
+
return show(lines.join("\n"), io);
|
|
172
|
+
}
|
|
173
|
+
// No page is called that: show what a search finds, rather than only pointing at --search. A topic of
|
|
174
|
+
// one- and two-letter words ("a", "is") is in nearly every page, so it is not searched for.
|
|
175
|
+
if (guideLib.keyWords(topic).every((w) => w.length <= 2)) {
|
|
176
|
+
return nothing(io, json, topic, `No page matches "${topic}". Give a longer topic, or see every page with: niro guide`);
|
|
177
|
+
}
|
|
178
|
+
const searched = search(guide, topic);
|
|
179
|
+
if (!searched.results.length) return nothing(io, json, topic, `No page matches "${topic}". See every page with: niro guide`);
|
|
180
|
+
// Worded from how the search matched: "come closest" when no line holds every word typed.
|
|
181
|
+
const note = `No page is called "${topic}". ${searched.match === "closest" ? "These pages come closest." : "These pages mention it."}`;
|
|
182
|
+
if (json) return writeJson(io, envelope("search", { query: topic, note, match: searched.match, results: searched.results, total: searched.results.length }));
|
|
183
|
+
const more = searched.results.length > FALLBACK_RESULTS ? ` (${searched.results.length} in all: niro guide --search ${topic})` : "";
|
|
184
|
+
show([`${note}${more}`, ...searchLines(searched.results, io, FALLBACK_RESULTS)].join("\n"), io);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function showSearch(guide, query, io, json) {
|
|
188
|
+
if (!query.trim()) return nothing(io, json, "", "Give the words to search for: niro guide --search <words>");
|
|
189
|
+
const found = search(guide, query);
|
|
190
|
+
if (!found.results.length) return nothing(io, json, query, `No page matches "${query}". Try fewer words, or see every page with: niro guide`);
|
|
191
|
+
if (json) return writeJson(io, envelope("search", { query, match: found.match, results: found.results, total: found.results.length }));
|
|
192
|
+
const out = [searchHeading(found, query), ...searchLines(found.results, io, MAX_RESULTS)];
|
|
193
|
+
if (found.results.length > MAX_RESULTS) out.push("", `and ${found.results.length - MAX_RESULTS} more pages (--json lists them all).`);
|
|
194
|
+
show(out.join("\n"), io);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* A reader that stops early (`niro guide x | head`) closes the pipe: that is not an error, so niro ends
|
|
199
|
+
* quietly with 0 rather than printing Node's crash for the write it could not finish.
|
|
200
|
+
*/
|
|
201
|
+
function quietOnClosedPipe(stream = process.stdout, exit = process.exit) {
|
|
202
|
+
if (stream.listenerCount("error") && stream._niroGuidePipe) return;
|
|
203
|
+
stream._niroGuidePipe = true;
|
|
204
|
+
stream.on("error", (err) => {
|
|
205
|
+
if (err && err.code === "EPIPE") return exit(0);
|
|
206
|
+
throw err;
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* The command. `words` are the words typed after `niro guide`; `opts` carries json, search, pager.
|
|
212
|
+
* `io` replaces the terminal in tests.
|
|
213
|
+
*/
|
|
214
|
+
function guide(words = [], opts = {}, io = {}) {
|
|
215
|
+
const color = require("../lib/color");
|
|
216
|
+
const json = Boolean(opts.json);
|
|
217
|
+
if (!io.write) quietOnClosedPipe();
|
|
218
|
+
const term = {
|
|
219
|
+
write: (s) => process.stdout.write(s),
|
|
220
|
+
writeErr: (s) => process.stderr.write(s),
|
|
221
|
+
setExitCode: (c) => { process.exitCode = c; },
|
|
222
|
+
isTTY: Boolean(process.stdout.isTTY),
|
|
223
|
+
rows: process.stdout.rows,
|
|
224
|
+
columns: process.stdout.columns,
|
|
225
|
+
runPager: (text) => runPager(text),
|
|
226
|
+
bold: color.bold,
|
|
227
|
+
...io,
|
|
228
|
+
};
|
|
229
|
+
term.pager = opts.pager !== false && !json;
|
|
230
|
+
const loaded = guideLib.load(opts.docsDir);
|
|
231
|
+
const query = words.join(" ").trim();
|
|
232
|
+
if (opts.search) return showSearch(loaded, query, term, json);
|
|
233
|
+
if (!query) {
|
|
234
|
+
if (json) return writeJson(term, listJson(loaded));
|
|
235
|
+
return show(listText(loaded, term), term);
|
|
236
|
+
}
|
|
237
|
+
return showTopic(loaded, query, term, json);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Add `niro guide` to the command line. `run` is src/index.js's error wrapper. Registered after every other
|
|
242
|
+
* command, in one line of src/index.js, so the line numbers the docs pages cite in that file stay where they are.
|
|
243
|
+
*/
|
|
244
|
+
function register(program, run) {
|
|
245
|
+
program
|
|
246
|
+
.command("guide [topic...]")
|
|
247
|
+
.description("Read Niro's documentation in the terminal, offline. No topic lists the pages; a page name, title or word shows that page.")
|
|
248
|
+
.option("--search", "search every page for the words given, and show the lines that hold them")
|
|
249
|
+
.option("--json", "emit machine-readable JSON")
|
|
250
|
+
.option("--no-pager", "print straight out, even to a terminal")
|
|
251
|
+
.action((topic, opts) => run(async () => guide(topic, { ...opts })));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
module.exports = { guide, register, runPager, pagerCommand, envelope, quietOnClosedPipe };
|
package/src/commands/init.js
CHANGED
|
@@ -41,16 +41,17 @@ 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 {
|
|
44
|
+
const { currentPad, 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() {
|
|
48
|
-
return
|
|
48
|
+
return currentPad();
|
|
49
49
|
}
|
|
50
50
|
const { fetchPreview, buildCandidates } = require("./edit");
|
|
51
51
|
const { resolveProjectId } = require("./_resolveProject");
|
|
52
52
|
const { projectsToRows } = require("./projects");
|
|
53
53
|
const { isSecretKey, looksSecretValue } = require("../lib/secret");
|
|
54
|
+
const cloudGuard = require("../lib/cloudGuard");
|
|
54
55
|
|
|
55
56
|
const step = (n) => color.bold(color.cyan(n));
|
|
56
57
|
const ok = (s) => color.green(s);
|
|
@@ -66,6 +67,9 @@ async function init(pathArg, opts = {}) {
|
|
|
66
67
|
const nested = !!opts.nested;
|
|
67
68
|
const substep = (n) => (nested ? step("●") : step(n));
|
|
68
69
|
const targetDir = resolveTargetDir(pathArg);
|
|
70
|
+
// Before anything else, including the flag checks: onboarding a folder the engine on this
|
|
71
|
+
// machine already indexes would upload its source and index it twice.
|
|
72
|
+
if (await cloudGuard.refuseIfIndexedLocally(targetDir, { command: "init" })) return;
|
|
69
73
|
// Validate flag values up-front so a bad --auto-build/--confirm-build/--quiet-period
|
|
70
74
|
// fails BEFORE any server-side mutation (otherwise a typo leaves an orphan project).
|
|
71
75
|
parseOnOff(opts.autoBuild);
|
|
@@ -946,10 +950,11 @@ async function scanIndexedChildren(targetDir, opts, resolve = resolveContext) {
|
|
|
946
950
|
* Decide the TARGET of this onboarding up front, before any repo registration:
|
|
947
951
|
* - --add-to <alias|id> -> EXISTING: resolve to a real projectId (throws on
|
|
948
952
|
* ambiguity/not-found via resolveProjectId), attach the new repo to it later.
|
|
949
|
-
* - --project-name /
|
|
950
|
-
*
|
|
953
|
+
* - --project-name / non-interactive -> NEW: derive a free name exactly as
|
|
954
|
+
* before (unchanged behaviour for scripts).
|
|
951
955
|
* - INTERACTIVE with >=1 existing project -> show a numbered menu (new vs each
|
|
952
956
|
* existing project) so the user can attach instead of always creating new.
|
|
957
|
+
* This holds with --defaults too.
|
|
953
958
|
*
|
|
954
959
|
* Returns { projectId, isExisting } or null when the user/flags didn't yield a
|
|
955
960
|
* usable target (caller sets exitCode=1).
|
|
@@ -966,10 +971,14 @@ async function resolveTarget({ defaultName, opts }) {
|
|
|
966
971
|
return { projectId, isExisting: true };
|
|
967
972
|
}
|
|
968
973
|
|
|
969
|
-
// Explicit name
|
|
974
|
+
// Explicit name or a non-interactive shell -> NEW (current path).
|
|
970
975
|
// We only offer the existing-project menu in an interactive shell with at
|
|
971
|
-
// least one project to pick.
|
|
972
|
-
|
|
976
|
+
// least one project to pick. --defaults does NOT skip it: that flag accepts
|
|
977
|
+
// default VALUES (env vars, scope, name), and "which project" has no safe
|
|
978
|
+
// default, so creating a new one silently put repos in the wrong place.
|
|
979
|
+
// Unattended runs have no terminal, so they still go straight to NEW; a
|
|
980
|
+
// script that wants an existing project passes --add-to.
|
|
981
|
+
if (!opts.projectName && !prompt.isNonInteractive()) {
|
|
973
982
|
const existing = await fetchExistingProjects(opts);
|
|
974
983
|
if (existing.length > 0) {
|
|
975
984
|
const chosen = await chooseTarget(existing);
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `niro init [path]` in a release without the cloud: add one folder to the local engine and build
|
|
3
|
+
* it, on a machine `niro start` has already set up.
|
|
4
|
+
*
|
|
5
|
+
* `niro start` is the once-per-machine setup (sign-in and terminal approval, the plan, the engine,
|
|
6
|
+
* the assistant, the watch daemon). Every folder after the first only needs adding and building,
|
|
7
|
+
* so this does just that and stops with a pointer to `niro start` when the machine is not set up.
|
|
8
|
+
* An already indexed folder is built again. The line limit is the engine's rule (`planUsage.refusesBuild`):
|
|
9
|
+
* at or over it, a folder that adds new lines goes to the console's /limit page instead of building,
|
|
10
|
+
* while a folder already counted (a rebuild, a temporary copy, a service inside an indexed root) builds.
|
|
11
|
+
*
|
|
12
|
+
* `niro build` is the same with `rebuildOnly`: it rebuilds a folder already added and refuses one
|
|
13
|
+
* that is not, so each command keeps the engine's meaning (init adds, build rebuilds).
|
|
14
|
+
*
|
|
15
|
+
* Adding and building is one engine command, the engine's own `init`, which builds the folder it adds (Niraj,
|
|
16
|
+
* 2026-11-06: "niro-local init should also build it"), so the folder is built once. `niro init` takes every option
|
|
17
|
+
* of the engine's `init` with the same meaning (`--project`, `--branch`, `--path`, `--base-url`, `--force`): "niro
|
|
18
|
+
* init should also support --project <name> the same way niro-local does. support all option of niro-local init to
|
|
19
|
+
* niro init". So `niro init --project niro` in a second repository joins the project "niro" and builds it.
|
|
20
|
+
*/
|
|
21
|
+
const path = require("path");
|
|
22
|
+
const color = require("../lib/color");
|
|
23
|
+
const engine = require("../lib/engine");
|
|
24
|
+
const lease = require("../lib/lease");
|
|
25
|
+
const localOnboard = require("../lib/localOnboard");
|
|
26
|
+
const planUsage = require("../lib/planUsage");
|
|
27
|
+
const progressBar = require("../lib/progressBar");
|
|
28
|
+
const start = require("./start");
|
|
29
|
+
|
|
30
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
31
|
+
|
|
32
|
+
function stop(message) {
|
|
33
|
+
console.error(message);
|
|
34
|
+
process.exitCode = 1;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function initLocal(pathArg, {
|
|
38
|
+
hasCredentials = start.hasCredentials, resolveEngine = engine.resolve, currentClaims = lease.currentClaims,
|
|
39
|
+
linesUsed = planUsage.linesUsed, alreadyCounted = planUsage.alreadyCounted, onboard = localOnboard,
|
|
40
|
+
openUrl = require("../lib/browser").open, rebuildOnly = false, buildArgs = [], initOptions = {},
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (!hasCredentials()) return stop(`Not signed in. Run ${color.cyan("niro start")} once to sign in and set up this machine.`);
|
|
43
|
+
if (!resolveEngine()) return stop(`The local engine is not installed. Run ${color.cyan("niro start")} once to set up this machine.`);
|
|
44
|
+
const { path: pathOption, ...engineOptions } = initOptions;
|
|
45
|
+
if (pathArg && pathOption && path.resolve(pathArg) !== path.resolve(pathOption)) {
|
|
46
|
+
return stop(`Two folders were given (${pathArg} and --path ${pathOption}). Give one.`);
|
|
47
|
+
}
|
|
48
|
+
const dir = path.resolve(pathOption || pathArg || ".");
|
|
49
|
+
const problem = start.describeProblem(dir);
|
|
50
|
+
if (problem) return stop([problem.headline, ...problem.detail].join("\n"));
|
|
51
|
+
|
|
52
|
+
const known = await onboard.worktreeFor(dir);
|
|
53
|
+
if (!known && rebuildOnly) return stop(`This folder is not added yet. Run ${color.cyan("niro init")} to add it and build it.`);
|
|
54
|
+
|
|
55
|
+
const claims = await currentClaims().catch(() => null);
|
|
56
|
+
const before = await linesUsed();
|
|
57
|
+
if (await planUsage.refusesBuild(claims, before, dir, { counted: alreadyCounted })) {
|
|
58
|
+
const url = planUsage.limitUrl(before, claims.max_lines);
|
|
59
|
+
console.error(`Not built: this machine already holds ${n(before)} lines of code, over the ${n(claims.max_lines)} line limit.`);
|
|
60
|
+
console.error(`Choose how to continue: ${color.cyan(url)}`);
|
|
61
|
+
console.error(`Or see the options with ${color.cyan("niro plan")}, or free lines by stopping a folder: ${color.cyan("niro remove --path <folder>")}`);
|
|
62
|
+
openUrl(url);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
console.log(`${known && !engineOptions.project ? "Rebuilding" : "Building"} the graph for ${dir}`);
|
|
68
|
+
const bar = progressBar.create();
|
|
69
|
+
const onProgress = (d, total) => bar.update(d, total);
|
|
70
|
+
let built;
|
|
71
|
+
try {
|
|
72
|
+
// `niro build` rebuilds what is added; `niro init` asks the engine's `init`, which adds the folder if it is not
|
|
73
|
+
// yet and builds it, and refuses a folder it cannot index before writing anything, in its own words.
|
|
74
|
+
built = rebuildOnly
|
|
75
|
+
? await onboard.build(dir, onProgress, { extraArgs: buildArgs })
|
|
76
|
+
: await onboard.initAndBuild(dir, onProgress, engineOptions);
|
|
77
|
+
} catch (err) {
|
|
78
|
+
bar.stop();
|
|
79
|
+
// A refusal such as "not in project X" is on standard output (`niro build --project X`).
|
|
80
|
+
return stop(((err && (err.refused ? err.message : (err.stderr || err.stdout || err.message))) || String(err)).replace(/^error:\s*/, ""));
|
|
81
|
+
}
|
|
82
|
+
bar.stop();
|
|
83
|
+
// What the engine's init said about the folder (the project it joined, a --branch it ignored), then the count.
|
|
84
|
+
for (const line of built.said || []) console.log(line);
|
|
85
|
+
// No build report: the engine changed nothing and said why (a subfolder of an added folder, named with --project).
|
|
86
|
+
if (built.files == null && built.lines == null && built.said && built.said.length) return;
|
|
87
|
+
const parts = [built.files != null && `${n(built.files)} files`, built.lines != null && `${n(built.lines)} lines`].filter(Boolean);
|
|
88
|
+
console.log(`${color.green("✓")} Graph built: ${parts.join(", ") || "done"} in ${built.seconds} s`);
|
|
89
|
+
const used = await linesUsed();
|
|
90
|
+
const plan = planUsage.describe(claims, used);
|
|
91
|
+
if (plan) console.log(plan);
|
|
92
|
+
const noRoom = planUsage.noRoomNote(claims, used);
|
|
93
|
+
if (noRoom) console.log(color.yellow(`${noRoom} Run niro plan to see your options, or free lines with niro remove --path <folder>.`));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { initLocal };
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `niro journal-send` — hand the local engine's record of what it was asked to Niro.
|
|
3
|
+
*
|
|
4
|
+
* The engine answers an assistant's questions on this machine, so unlike a hosted session
|
|
5
|
+
* none of those calls are visible to us. The engine keeps its own record and this sends it, so
|
|
6
|
+
* the same question ("which tools get used, which fail, where does an assistant fall back to
|
|
7
|
+
* reading files") can be answered for local sessions too.
|
|
8
|
+
*
|
|
9
|
+
* A command of its own so the path can be run and watched by hand; in normal use the watcher
|
|
10
|
+
* calls the same library, next to the audit upload.
|
|
11
|
+
*
|
|
12
|
+
* Prints counts and never a payload.
|
|
13
|
+
*/
|
|
14
|
+
const journalUpload = require("../lib/journalUpload");
|
|
15
|
+
const color = require("../lib/color");
|
|
16
|
+
|
|
17
|
+
async function journalSend(opts = {}) {
|
|
18
|
+
const limit = Number(opts.limit) || 20;
|
|
19
|
+
console.log(`Sending the local engine's MCP journal to ${journalUpload.assistantUrl()}`);
|
|
20
|
+
|
|
21
|
+
const result = await journalUpload.uploadPending({ limit });
|
|
22
|
+
|
|
23
|
+
if (result.sent) {
|
|
24
|
+
console.log(` ${result.sent} session(s) sent, ${result.calls} tool call(s)`);
|
|
25
|
+
} else if (!result.failed && !result.unmasked) {
|
|
26
|
+
// A session still in use is held back by the engine, so "nothing waiting" is the normal
|
|
27
|
+
// answer in the middle of a task rather than a sign that nothing was recorded.
|
|
28
|
+
console.log(" nothing waiting");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (result.unmasked) {
|
|
32
|
+
// Sessions from an engine older than on-machine masking: never sent unmasked, so they wait.
|
|
33
|
+
console.log(color.yellow(
|
|
34
|
+
` ${result.unmasked} session(s) not sent: the local engine is too old to mask secrets. Run \`niro upgrade\`.`
|
|
35
|
+
));
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
if (result.failed) {
|
|
39
|
+
console.log(color.yellow(` ${result.failed} session(s) could not be sent:`));
|
|
40
|
+
for (const failure of result.failures.slice(0, 5)) console.log(` ${failure}`);
|
|
41
|
+
if (result.failures.length > 5) {
|
|
42
|
+
console.log(` ... and ${result.failures.length - 5} more`);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
module.exports = { journalSend };
|
package/src/commands/login.js
CHANGED
|
@@ -150,7 +150,8 @@ async function loginWithDeviceFlow({ apiUrl }) {
|
|
|
150
150
|
const deviceCode = resp.device_code || resp.deviceCode;
|
|
151
151
|
const verificationUri = resp.verification_uri_complete || resp.verificationUriComplete
|
|
152
152
|
|| resp.verification_uri || resp.verificationUri;
|
|
153
|
-
|
|
153
|
+
// `??`, not `||`: a server may legitimately say 0, and a test certainly does.
|
|
154
|
+
const interval = Number(resp.interval ?? DEFAULT_POLL_INTERVAL_SECONDS);
|
|
154
155
|
const expiresIn = Number(resp.expires_in || resp.expiresIn || DEFAULT_EXPIRY_SECONDS);
|
|
155
156
|
|
|
156
157
|
if (!userCode || !deviceCode || !verificationUri) {
|
|
@@ -162,11 +163,17 @@ async function loginWithDeviceFlow({ apiUrl }) {
|
|
|
162
163
|
// without hardcoding a host that is wrong for UAT and wrong for every self-hosted install.
|
|
163
164
|
rememberAppUrl(verificationUri);
|
|
164
165
|
|
|
165
|
-
|
|
166
|
-
|
|
166
|
+
// Written past any rail prefix: the box draws its own left edge, which is the rail.
|
|
167
|
+
const nested = !!require("../lib/nestedOutput").currentPad();
|
|
168
|
+
console.log("");
|
|
169
|
+
for (const line of codeBox(userCode, expiresIn, nested)) process.stdout.write(`${line}\n`);
|
|
170
|
+
// Two columns in, so these line up with the text inside the box above.
|
|
171
|
+
console.log(`\n Opening ${color.cyan(verificationUri)}...`);
|
|
172
|
+
console.log(color.dim(" If the browser didn't open, go to:"));
|
|
173
|
+
console.log(` ${color.cyan(verificationUri)}\n`);
|
|
167
174
|
browser.open(verificationUri);
|
|
168
175
|
|
|
169
|
-
const sp = spinner.start("
|
|
176
|
+
const sp = spinner.start(" Waiting for browser approval...");
|
|
170
177
|
const deadline = Date.now() + expiresIn * 1000;
|
|
171
178
|
try {
|
|
172
179
|
while (Date.now() < deadline) {
|
|
@@ -181,7 +188,8 @@ async function loginWithDeviceFlow({ apiUrl }) {
|
|
|
181
188
|
if (token) {
|
|
182
189
|
const email = (tokenResp.user && tokenResp.user.email) || tokenResp.email || null;
|
|
183
190
|
const cliToken = tokenResp.niro_api_key || tokenResp.niroApiKey || null;
|
|
184
|
-
|
|
191
|
+
// Inside `niro start` the step's own closing line names the account; say it once.
|
|
192
|
+
sp.stop(nested ? undefined : `${color.green("✓")} Logged in${email ? ` as ${color.bold(email)}` : ""}.`);
|
|
185
193
|
credentials.save({
|
|
186
194
|
apiUrl,
|
|
187
195
|
token,
|
|
@@ -190,11 +198,16 @@ async function loginWithDeviceFlow({ apiUrl }) {
|
|
|
190
198
|
email,
|
|
191
199
|
userId: (tokenResp.user && tokenResp.user.id) || tokenResp.user_id || tokenResp.userId || null,
|
|
192
200
|
});
|
|
193
|
-
return;
|
|
201
|
+
return { email };
|
|
194
202
|
}
|
|
195
203
|
} catch (err) {
|
|
196
204
|
if (isPending(err)) continue;
|
|
197
|
-
|
|
205
|
+
const terminal = TERMINAL_ANSWERS[errorCode(err)];
|
|
206
|
+
if (!terminal) throw err;
|
|
207
|
+
sp.stop();
|
|
208
|
+
for (const line of terminal.say) console.log(` ${line}`);
|
|
209
|
+
process.exitCode = terminal.exitCode;
|
|
210
|
+
return terminal.result;
|
|
198
211
|
}
|
|
199
212
|
}
|
|
200
213
|
sp.stop(`${color.red("✗")} Login timed out. Run ${color.cyan("niro login")} to try again.`);
|
|
@@ -204,6 +217,38 @@ async function loginWithDeviceFlow({ apiUrl }) {
|
|
|
204
217
|
}
|
|
205
218
|
}
|
|
206
219
|
|
|
220
|
+
/**
|
|
221
|
+
* The device code in a titled box, so it stands out from the lines around it:
|
|
222
|
+
*
|
|
223
|
+
* Approve in your browser ─────┐
|
|
224
|
+
* │
|
|
225
|
+
* Your code: │
|
|
226
|
+
* ABCD-EFGH │
|
|
227
|
+
* │
|
|
228
|
+
* Code expires in 15 minutes │
|
|
229
|
+
* ───────────────────────────────┘
|
|
230
|
+
*/
|
|
231
|
+
function codeBox(userCode, expiresIn, nested = false) {
|
|
232
|
+
const minutes = Math.max(1, Math.round(expiresIn / 60));
|
|
233
|
+
const expiry = `Code expires in ${minutes} minute${minutes === 1 ? "" : "s"}`;
|
|
234
|
+
const rows = [
|
|
235
|
+
["", ""],
|
|
236
|
+
[color.dim(color.bold("Your authorization code:")), "Your authorization code:"],
|
|
237
|
+
["", ""],
|
|
238
|
+
[` ${color.bold(color.green(userCode))}`, ` ${userCode}`],
|
|
239
|
+
["", ""],
|
|
240
|
+
[color.dim(expiry), expiry],
|
|
241
|
+
["", ""],
|
|
242
|
+
];
|
|
243
|
+
const title = "Browser Authorization";
|
|
244
|
+
const width = Math.max(title.length + 4, ...rows.map(([, plain]) => plain.length + 2));
|
|
245
|
+
// The box's left edge is the setup rail itself: title on the marker row, bottom joins the rail.
|
|
246
|
+
const out = [`${color.green("◇")} ${title} ${color.dim(`${"─".repeat(width - title.length - 1)}┐`)}`];
|
|
247
|
+
for (const [shown, plain] of rows) out.push(`│ ${shown}${" ".repeat(width - plain.length)}${color.dim("│")}`);
|
|
248
|
+
out.push(`${nested ? "├" : "└"}${color.dim(`${"─".repeat(width + 2)}┘`)}`);
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
251
|
+
|
|
207
252
|
/**
|
|
208
253
|
* Store the web app's origin, taken from the device-flow verification URI.
|
|
209
254
|
*
|
|
@@ -219,16 +264,48 @@ function rememberAppUrl(verificationUri) {
|
|
|
219
264
|
}
|
|
220
265
|
}
|
|
221
266
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
267
|
+
/** The error code the device-token endpoint answered with, or null. */
|
|
268
|
+
function errorCode(err) {
|
|
269
|
+
const body = err && err.body;
|
|
270
|
+
if (!body) return null;
|
|
225
271
|
const code = typeof body === "string" ? body : body.error || body.message;
|
|
226
|
-
|
|
272
|
+
return typeof code === "string" ? code.trim() : null;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function isPending(err) {
|
|
276
|
+
const code = errorCode(err);
|
|
277
|
+
if (!code) return false;
|
|
227
278
|
return /authorization[_-]?pending/i.test(code) || /slow[_-]?down/i.test(code);
|
|
228
279
|
}
|
|
229
280
|
|
|
281
|
+
/**
|
|
282
|
+
* Answers that end the wait (contract C1 + addendum). Each one is a sentence for the person
|
|
283
|
+
* at the terminal, not a stack: a waiting account used to sit on the spinner until the code
|
|
284
|
+
* expired, 30 minutes later, because only pending and slow_down were understood.
|
|
285
|
+
*/
|
|
286
|
+
const RESTART = ["The sign-in was not completed. Run niro start again to get a new code."];
|
|
287
|
+
const TERMINAL_ANSWERS = {
|
|
288
|
+
account_waiting: {
|
|
289
|
+
say: ["You are on the list. We will email you when you are in. Then run niro start again."],
|
|
290
|
+
exitCode: 0,
|
|
291
|
+
result: { waiting: true },
|
|
292
|
+
},
|
|
293
|
+
account_blocked: {
|
|
294
|
+
say: ["This account cannot sign in to Niro. If you think that is a mistake, reply to any Niro email."],
|
|
295
|
+
exitCode: 1,
|
|
296
|
+
result: { blocked: true },
|
|
297
|
+
},
|
|
298
|
+
request_submitted: {
|
|
299
|
+
say: ["Request sent. We reply within one business day. Run niro start again once your trial starts."],
|
|
300
|
+
exitCode: 0,
|
|
301
|
+
result: { requestSubmitted: true },
|
|
302
|
+
},
|
|
303
|
+
access_denied: { say: RESTART, exitCode: 1, result: { denied: true } },
|
|
304
|
+
expired_token: { say: RESTART, exitCode: 1, result: { expired: true } },
|
|
305
|
+
};
|
|
306
|
+
|
|
230
307
|
function sleep(ms) {
|
|
231
308
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
232
309
|
}
|
|
233
310
|
|
|
234
|
-
module.exports = { login, rememberAppUrl };
|
|
311
|
+
module.exports = { login, rememberAppUrl, codeBox };
|
package/src/commands/mcp.js
CHANGED
|
@@ -18,18 +18,34 @@ const childProcess = require("child_process");
|
|
|
18
18
|
* `niro upgrade` passes its own `--client <id>`, because the flags on its command line
|
|
19
19
|
* (`--finish`, `--from`) are not the wizard's.
|
|
20
20
|
*/
|
|
21
|
-
function runWizard(scriptName, label, args) {
|
|
21
|
+
function runWizard(scriptName, label, args, { quiet = false } = {}) {
|
|
22
22
|
const wizardArgs = Array.isArray(args) ? args : process.argv.slice(4);
|
|
23
23
|
return new Promise((resolve, reject) => {
|
|
24
24
|
const child = childProcess.spawn(
|
|
25
25
|
process.execPath,
|
|
26
26
|
[path.join(__dirname, "..", "mcp", scriptName), ...wizardArgs],
|
|
27
|
-
|
|
27
|
+
// The child prints for itself, out of reach of withPrefix; it is told the prefix instead,
|
|
28
|
+
// so inside `niro start` its lines stay on the setup rail. `quiet` keeps its lines for a
|
|
29
|
+
// failure instead (`niro uninstall` says one line per step).
|
|
30
|
+
{
|
|
31
|
+
stdio: quiet ? ["inherit", "pipe", "pipe"] : "inherit",
|
|
32
|
+
env: { ...process.env, NIRO_OUTPUT_PREFIX: require("../lib/nestedOutput").currentPad() },
|
|
33
|
+
}
|
|
28
34
|
);
|
|
35
|
+
let said = "";
|
|
36
|
+
if (quiet) {
|
|
37
|
+
child.stdout.on("data", (c) => (said += c));
|
|
38
|
+
child.stderr.on("data", (c) => (said += c));
|
|
39
|
+
}
|
|
29
40
|
child.on("error", reject);
|
|
30
41
|
child.on("exit", (code) => {
|
|
31
|
-
if (code === 0) resolve();
|
|
32
|
-
|
|
42
|
+
if (code === 0) return resolve(said);
|
|
43
|
+
const err = new Error(`MCP ${label} wizard exited with code ${code}${said.trim() ? `\n${said.trim()}` : ""}`);
|
|
44
|
+
// A wizard that printed to the terminal has already said why it stopped; the CLI exits
|
|
45
|
+
// with its code and adds nothing ("Error: MCP setup wizard exited with code 1" read as a crash).
|
|
46
|
+
err.alreadySaid = !quiet;
|
|
47
|
+
err.exitCode = code;
|
|
48
|
+
reject(err);
|
|
33
49
|
});
|
|
34
50
|
});
|
|
35
51
|
}
|
|
@@ -38,8 +54,8 @@ async function install(args) {
|
|
|
38
54
|
return runWizard("setup.js", "setup", args);
|
|
39
55
|
}
|
|
40
56
|
|
|
41
|
-
async function uninstall() {
|
|
42
|
-
return runWizard("teardown.js", "uninstall");
|
|
57
|
+
async function uninstall(args, opts) {
|
|
58
|
+
return runWizard("teardown.js", "uninstall", args, opts);
|
|
43
59
|
}
|
|
44
60
|
|
|
45
61
|
function serve() {
|