@niroai/niro 0.17.0 → 1.0.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 +136 -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 +6 -2
- 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
package/src/commands/start.js
CHANGED
|
@@ -1,270 +1,503 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `niro start` — the one
|
|
2
|
+
* `niro start` — the first run: one wizard, then a ready screen. The graph is built and kept
|
|
3
|
+
* on this machine (Niro Local). A cloud ("web") mode is planned for a later version.
|
|
3
4
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* 1/5 Checking this machine OS and arch, free disk, Node, the folder. Windows stops
|
|
6
|
+
* here, before any account exists.
|
|
7
|
+
* The npx-to-global install happens here too, quietly.
|
|
8
|
+
* 2/5 Sign in login.js device flow. Waiting, blocked and a Teams request
|
|
9
|
+
* end the wizard with login.js's own words and exit code.
|
|
10
|
+
* 3/5 Local engine engine.ensure(); checksum only, never "signature". Then
|
|
11
|
+
* the engine is asked whether its graph file opens.
|
|
12
|
+
* 4/5 Building your graph niro-local init --progress (adds and builds), streamed into a bar.
|
|
13
|
+
* 5/5 Connect your AI assistant the existing `niro mcp install` wizard, full client list.
|
|
14
|
+
* Niro is ready graph, assistant, plan; the watch daemon keeps it current.
|
|
8
15
|
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* 4. Onboard — init.js, unchanged.
|
|
14
|
-
* 5. Connect — mcp.js install(), skipped when this machine already did it.
|
|
16
|
+
* Each step checks its own completion and prints ✓ instead of redoing it, so a second run
|
|
17
|
+
* resumes at the first unfinished step and a run after full success changes nothing:
|
|
18
|
+
* credentials, the engine resolved, the folder indexed per `niro-local projects --json`, and
|
|
19
|
+
* `mcpConnectedAt`.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* before they answer.
|
|
21
|
+
* No cloud fallback (design §9.3, and Niraj's D2 of 2026-10-31): an engine that cannot open its
|
|
22
|
+
* graph says why in its own words and the wizard stops. Each step's timing and outcome go to
|
|
23
|
+
* ~/.niro/onboarding.json.
|
|
20
24
|
*
|
|
21
|
-
* Why
|
|
25
|
+
* Why the npx hand-over exists at all: service.js registers the watch daemon with PM2 using
|
|
22
26
|
* `process.argv[1]`. Under npx that path lives in ~/.npm/_npx/<hash>/, which npm
|
|
23
|
-
* garbage-collects on its own schedule, so a watcher registered from an npx run would
|
|
24
|
-
*
|
|
25
|
-
* bootstrapper, not the way to run Niro.
|
|
27
|
+
* garbage-collects on its own schedule, so a watcher registered from an npx run would look
|
|
28
|
+
* healthy and then stop days later with no error anywhere.
|
|
26
29
|
*/
|
|
27
30
|
const path = require("path");
|
|
28
31
|
const fs = require("fs");
|
|
29
|
-
|
|
30
|
-
//
|
|
32
|
+
const os = require("os");
|
|
33
|
+
// Required as namespaces, not destructured, so tests can substitute each collaborator.
|
|
31
34
|
const childProcess = require("child_process");
|
|
32
35
|
|
|
33
36
|
const color = require("../lib/color");
|
|
34
37
|
const prompt = require("../lib/prompt");
|
|
35
38
|
const credentials = require("../lib/credentials");
|
|
36
39
|
const config = require("../lib/config");
|
|
37
|
-
const git = require("../lib/git");
|
|
38
40
|
const onboardTarget = require("../lib/onboardTarget");
|
|
41
|
+
const machineCheck = require("../lib/machineCheck");
|
|
42
|
+
const engine = require("../lib/engine");
|
|
43
|
+
const graphCheck = require("../lib/graphCheck");
|
|
44
|
+
const localOnboard = require("../lib/localOnboard");
|
|
45
|
+
const languages = require("../lib/languages");
|
|
46
|
+
const lease = require("../lib/lease");
|
|
47
|
+
const progressBar = require("../lib/progressBar");
|
|
48
|
+
const onboardingLog = require("../lib/onboardingLog");
|
|
49
|
+
const watchDaemon = require("../lib/watchDaemon");
|
|
50
|
+
const mcpClients = require("../lib/mcpClients");
|
|
51
|
+
const planUsage = require("../lib/planUsage");
|
|
52
|
+
const service = require("../lib/service");
|
|
53
|
+
const watchState = require("../lib/watchState");
|
|
54
|
+
const migrationNotice = require("../lib/migrationNotice");
|
|
55
|
+
const release = require("../lib/release");
|
|
39
56
|
|
|
40
|
-
const step = (n) => color.bold(color.cyan(n));
|
|
41
57
|
const ok = (s) => color.green(s);
|
|
42
58
|
const warn = (s) => color.yellow(s);
|
|
43
59
|
const fail = (s) => color.red(s);
|
|
44
60
|
|
|
45
|
-
|
|
61
|
+
/** The steps, in order; a step's number is its place here. */
|
|
62
|
+
const order = ["machine", "sign-in", "engine", "build", "assistant"];
|
|
46
63
|
const PACKAGE_NAME = "@niroai/niro";
|
|
47
|
-
|
|
48
|
-
/** Offered in the welcome screen and again at the end. Not shown to returning users. */
|
|
49
|
-
const BOOK_A_CALL = "https://tidycal.com/nirajaswani/45-minutes-meeting-mpy606v";
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Private-beta copy. There is no endpoint that reports whether the gate is on, and
|
|
53
|
-
* adding one to serve a first prompt is not worth the round-trip, so this is a local
|
|
54
|
-
* constant. DELETE THIS BLOCK (and betaGate below) when the beta ends — see
|
|
55
|
-
* docs/usecases/first-run-wizard.md.
|
|
56
|
-
*/
|
|
57
|
-
const BETA = {
|
|
58
|
-
on: true,
|
|
59
|
-
// The marketing site, NOT the app: niro.ai, not the niroai.dev hosts everything else in
|
|
60
|
-
// this CLI talks to. Confirmed by the user 2026-08-27 — it cannot be derived from anything
|
|
61
|
-
// in this repo, so it must not be guessed at from the backend hostnames.
|
|
62
|
-
requestUrl: "https://www.niro.ai",
|
|
63
|
-
requestAction: '"Request Access"', // the button on that home page
|
|
64
|
-
};
|
|
64
|
+
const GB = 1024 ** 3;
|
|
65
65
|
|
|
66
66
|
/** Set on the re-exec so the installed copy can never bootstrap a second time. */
|
|
67
67
|
const BOOTSTRAP_ENV = "NIRO_START_BOOTSTRAPPED";
|
|
68
68
|
|
|
69
|
+
/** Where the graph lives, said plainly on the ready screen: niro-local's one SQLite file. */
|
|
70
|
+
|
|
71
|
+
// The setup rail: ┌ opens, │ carries, ◆ is the step under way, ◇ a finished one, └ closes.
|
|
72
|
+
const rail = (s = "") => console.log(s ? `│ ${s}` : "│");
|
|
73
|
+
const num = (name) => `${order.indexOf(name) + 1}/${order.length}`;
|
|
74
|
+
// A step that already printed its ◆ heading closes without repeating the number.
|
|
75
|
+
const opened = new Set();
|
|
76
|
+
const active = (name, title) => { opened.add(name); console.log(`◆ ${num(name)} ${title}`); };
|
|
77
|
+
const done = (name, text) => console.log(opened.has(name) ? `◇ ${text}` : `◇ ${num(name)} ${text}`);
|
|
78
|
+
|
|
79
|
+
/** Time one step and leave a note of how it ended. `fn` returns its outcome, or throws. */
|
|
80
|
+
async function timed(step, fn) {
|
|
81
|
+
const startedAt = new Date().toISOString();
|
|
82
|
+
let outcome = "error";
|
|
83
|
+
try {
|
|
84
|
+
outcome = await fn();
|
|
85
|
+
return outcome;
|
|
86
|
+
} finally {
|
|
87
|
+
onboardingLog.record({ step, startedAt, endedAt: new Date().toISOString(), outcome: String(outcome) });
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** Stop the wizard: every step returns a string outcome, and these are the ones that end it. */
|
|
92
|
+
const GO = "ok";
|
|
93
|
+
const STOPPED = new Set(["refused", "waiting", "blocked", "request_submitted", "failed", "handed_over"]);
|
|
94
|
+
|
|
69
95
|
async function start(pathArg, opts = {}) {
|
|
96
|
+
opened.clear();
|
|
70
97
|
if (prompt.isNonInteractive()) {
|
|
71
98
|
console.error(fail("`niro start` is interactive and there is no terminal to prompt on."));
|
|
72
|
-
console.error("
|
|
73
|
-
console.error(` ${color.cyan("niro
|
|
74
|
-
console.error(` ${color.cyan("niro init
|
|
99
|
+
console.error(" Run it once in a terminal: signing in opens your browser. After that, these take flags:");
|
|
100
|
+
console.error(` ${color.cyan("niro engine install")}`);
|
|
101
|
+
console.error(` ${color.cyan("niro init <folder>")}`);
|
|
75
102
|
console.error(` ${color.cyan("niro mcp install --client <id>")}`);
|
|
76
103
|
process.exitCode = 1;
|
|
77
104
|
return;
|
|
78
105
|
}
|
|
79
106
|
|
|
80
|
-
|
|
81
|
-
// the banner, the welcome screen, the what-is-installed note, the beta gate and the
|
|
82
|
-
// booking offer. Separate checks would eventually disagree with each other.
|
|
83
|
-
const firstRun = !hasCredentials();
|
|
107
|
+
const dir = path.resolve(pathArg || process.cwd());
|
|
84
108
|
|
|
85
|
-
//
|
|
86
|
-
|
|
87
|
-
// SECOND time. The parent already showed both and already got an answer; repeating them
|
|
88
|
-
// makes the install look like a restart. Observed live: the whole opening printed twice.
|
|
89
|
-
const handedOver = !!process.env[BOOTSTRAP_ENV];
|
|
109
|
+
// The handed-over child repeats nothing the parent already printed.
|
|
110
|
+
if (!process.env[BOOTSTRAP_ENV]) printWelcome();
|
|
90
111
|
|
|
91
|
-
|
|
112
|
+
const ctx = { dir, claims: null, built: null, worktree: null };
|
|
113
|
+
const run = {
|
|
114
|
+
machine: () => checkMachine(ctx, pathArg),
|
|
115
|
+
"sign-in": () => signIn(ctx),
|
|
116
|
+
engine: () => ensureEngine(),
|
|
117
|
+
build: () => buildGraph(ctx),
|
|
118
|
+
assistant: () => connectAssistant(ctx),
|
|
119
|
+
};
|
|
120
|
+
for (const step of order) {
|
|
121
|
+
const outcome = await timed(step, run[step]);
|
|
122
|
+
if (STOPPED.has(outcome)) return;
|
|
123
|
+
}
|
|
124
|
+
await moveWebFolders(ctx);
|
|
125
|
+
await keepItCurrent(ctx);
|
|
126
|
+
await printReady(ctx);
|
|
127
|
+
}
|
|
92
128
|
|
|
93
|
-
|
|
129
|
+
/** Close the rail the way it opened: the outcome on └, then what Niro does, off the rail. */
|
|
130
|
+
const LINKS = {
|
|
131
|
+
app: "https://console.niroai.dev/",
|
|
132
|
+
docs: "https://help.niroai.dev/cli/",
|
|
133
|
+
discord: "https://discord.gg/gcbzePZHn",
|
|
134
|
+
};
|
|
94
135
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
136
|
+
function closeReady(who = "your assistant", restart = false) {
|
|
137
|
+
rail();
|
|
138
|
+
console.log(`└ ${color.bold(ok("Niro is ready"))}`);
|
|
139
|
+
// Right under "Niro is ready": what to do next, when an assistant was just connected.
|
|
140
|
+
if (restart) console.log(color.bold(`Restart ${who} to start using Niro.`));
|
|
141
|
+
console.log("");
|
|
142
|
+
console.log(`Carry on using ${who} as usual, and ask it about your code. When it needs`);
|
|
143
|
+
console.log(`facts, it calls Niro, and you will see lines such as ${color.cyan("niro · get_reverse_call_chain")}.`);
|
|
144
|
+
console.log(`\n${color.bold(color.cyan("Useful commands"))}`);
|
|
145
|
+
for (const [cmd, what] of USEFUL) console.log(` ${color.cyan(cmd.padEnd(18))}${what}`);
|
|
146
|
+
const link = (label, url) => `${color.dim(label)} ${color.cyan(url)}`;
|
|
147
|
+
console.log(`\n${[link("App", LINKS.app), link("Docs", LINKS.docs), link("Discord", LINKS.discord)].join(" ")}`);
|
|
148
|
+
console.log("");
|
|
149
|
+
}
|
|
99
150
|
|
|
100
|
-
|
|
101
|
-
|
|
151
|
+
function printWelcome() {
|
|
152
|
+
console.log("");
|
|
153
|
+
console.log(`┌ Niro setup ${color.dim(`v${require("../../package.json").version}`)}`);
|
|
154
|
+
rail();
|
|
155
|
+
rail(color.bold("Give your AI assistant verified facts about your code: call chains, impact, cross-repo links."));
|
|
156
|
+
rail(color.dim("Built on this machine · 5 steps · about 5 minutes"));
|
|
157
|
+
rail();
|
|
158
|
+
}
|
|
102
159
|
|
|
103
|
-
|
|
104
|
-
|
|
160
|
+
/** Step 1. Nothing here contacts Niro or changes the machine, bar the npx hand-over. */
|
|
161
|
+
async function checkMachine(ctx, pathArg) {
|
|
162
|
+
active("machine", "Checking this machine");
|
|
163
|
+
const m = machineCheck.check();
|
|
164
|
+
if (m.platformProblem) {
|
|
165
|
+
rail(`${fail("✗")} ${m.os}, ${m.arch}`);
|
|
166
|
+
rail(m.platformProblem);
|
|
167
|
+
rail("Niro Local runs on macOS and Linux. On Windows, use WSL 2.");
|
|
168
|
+
console.log("└");
|
|
169
|
+
process.exitCode = 1;
|
|
170
|
+
return "refused";
|
|
171
|
+
}
|
|
172
|
+
rail(`${ok("✓")} ${m.os}, ${m.arch}`);
|
|
173
|
+
if (m.freeBytes !== null) {
|
|
174
|
+
const free = `${(m.freeBytes / GB).toFixed(1)} GB free`;
|
|
175
|
+
rail(m.lowDisk ? `${warn("⚠")} ${free}. A large repository's graph may need more.` : `${ok("✓")} ${free}`);
|
|
176
|
+
}
|
|
177
|
+
if (!m.nodeOk) {
|
|
178
|
+
rail(`${fail("✗")} Node ${m.node}. Niro needs Node 18 or newer.`);
|
|
179
|
+
console.log("└");
|
|
180
|
+
process.exitCode = 1;
|
|
181
|
+
return "refused";
|
|
182
|
+
}
|
|
105
183
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
184
|
+
const problem = describeProblem(ctx.dir);
|
|
185
|
+
if (problem) {
|
|
186
|
+
rail(`${fail("✗")} ${problem.headline}`);
|
|
187
|
+
for (const line of problem.detail) rail(line);
|
|
188
|
+
console.log("└");
|
|
189
|
+
process.exitCode = 1;
|
|
190
|
+
return "refused";
|
|
191
|
+
}
|
|
192
|
+
rail();
|
|
193
|
+
return (await bootstrapIfNpx(pathArg)) ? "handed_over" : GO;
|
|
194
|
+
}
|
|
109
195
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
196
|
+
/** Step 2. login.js prints its own outcome; this only decides whether to go on. */
|
|
197
|
+
async function signIn(ctx) {
|
|
198
|
+
if (!hasCredentials()) {
|
|
199
|
+
active("sign-in", "Sign in");
|
|
200
|
+
rail("Sign in, choose a plan and approve this terminal in one screen.");
|
|
201
|
+
const { withPrefix } = require("../lib/nestedOutput");
|
|
202
|
+
const result = await withPrefix("│", () => require("./login").login({}));
|
|
203
|
+
if (result && result.waiting) return "waiting";
|
|
204
|
+
if (result && result.blocked) return "blocked";
|
|
205
|
+
if (result && result.requestSubmitted) return "request_submitted";
|
|
206
|
+
if (!hasCredentials()) {
|
|
207
|
+
console.error(fail("\n✗ Not signed in, so there is nothing to set up yet."));
|
|
208
|
+
console.error(` Run ${color.cyan("niro start")} again when you are ready.`);
|
|
209
|
+
process.exitCode = 1;
|
|
210
|
+
return "failed";
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
const email = credentials.load().email;
|
|
214
|
+
ctx.claims = await lease.currentClaims().catch(() => null);
|
|
215
|
+
const plan = ctx.claims && ctx.claims.plan_name ? ` · ${ctx.claims.plan_name}` : "";
|
|
216
|
+
done("sign-in", `${ok("✓")} Signed in${email ? ` as ${email}` : ""}${plan}`);
|
|
217
|
+
return GO;
|
|
218
|
+
}
|
|
117
219
|
|
|
118
|
-
|
|
119
|
-
|
|
220
|
+
/** Step 3. */
|
|
221
|
+
async function ensureEngine() {
|
|
222
|
+
const found = engine.resolve();
|
|
223
|
+
if (found) {
|
|
224
|
+
if (!(await graphOpens())) return "failed";
|
|
225
|
+
done("engine", ok(`✓ Engine ready${engineVersion(found.path)}`));
|
|
226
|
+
return GO;
|
|
227
|
+
}
|
|
228
|
+
active("engine", "Local engine");
|
|
229
|
+
let where;
|
|
230
|
+
try {
|
|
231
|
+
// engine.ensure says "Downloading the local engine <version> for <label>..." itself.
|
|
232
|
+
where = await engine.ensure({ log: (line) => rail(line) });
|
|
233
|
+
} catch (err) {
|
|
234
|
+
rail(`${fail("✗")} ${err && err.message ? err.message : err}`);
|
|
235
|
+
if (!err || err.code !== "EENGINEREFUSED") rail(`Try again with ${color.cyan("niro start")}.`);
|
|
236
|
+
console.log("└");
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
return "failed";
|
|
239
|
+
}
|
|
240
|
+
if (!(await graphOpens())) return "failed";
|
|
241
|
+
rail(ok(`✓ Engine ready${engineVersion(where)}`));
|
|
242
|
+
rail();
|
|
243
|
+
return GO;
|
|
244
|
+
}
|
|
120
245
|
|
|
121
|
-
|
|
246
|
+
/** Can the engine open its graph file? If not, its own words, and the wizard stops. */
|
|
247
|
+
async function graphOpens() {
|
|
248
|
+
const usable = await graphCheck.graphDatabaseUsable();
|
|
249
|
+
if (usable.ok) return true;
|
|
250
|
+
rail(`${fail("✗")} ${usable.problem}`);
|
|
251
|
+
console.log("└");
|
|
252
|
+
process.exitCode = 1;
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
122
255
|
|
|
123
|
-
|
|
256
|
+
/** Step 4. */
|
|
257
|
+
async function buildGraph(ctx) {
|
|
258
|
+
const known = await localOnboard.worktreeFor(ctx.dir);
|
|
259
|
+
ctx.worktree = known;
|
|
260
|
+
if (known && known.worktree.indexed) {
|
|
261
|
+
const when = known.worktree.last_build_at ? `, ${ago(known.worktree.last_build_at)}` : "";
|
|
262
|
+
done("build", ok(`✓ Graph built for ${known.project.name}${when}`));
|
|
263
|
+
return GO;
|
|
264
|
+
}
|
|
265
|
+
active("build", "Building your graph");
|
|
266
|
+
rail(`Project ${known ? known.project.name : path.basename(ctx.dir)}`);
|
|
267
|
+
// At or over the limit the engine refuses a folder that adds new lines, so the same question is
|
|
268
|
+
// asked of the engine here first (`refusesBuild`) and such a folder goes to the upgrade page, not to
|
|
269
|
+
// an error. A folder the engine already counts (a temporary copy, a service inside an indexed root)
|
|
270
|
+
// builds.
|
|
271
|
+
const before = await planUsage.linesUsed();
|
|
272
|
+
if (await planUsage.refusesBuild(ctx.claims, before, ctx.dir)) return overLimit(ctx, before);
|
|
273
|
+
const bar = progressBar.create({ prefix: "│ " });
|
|
274
|
+
const onProgress = (d, n) => bar.update(d, n);
|
|
275
|
+
try {
|
|
276
|
+
// A folder not added yet: the engine's `init` adds it and builds it, in one command, so it is built once. A
|
|
277
|
+
// folder added but not built yet (a step 4 that stopped part way): its build.
|
|
278
|
+
ctx.built = known ? await localOnboard.build(ctx.dir, onProgress) : await localOnboard.initAndBuild(ctx.dir, onProgress);
|
|
279
|
+
} catch (err) {
|
|
280
|
+
bar.stop();
|
|
281
|
+
const words = (err && (err.stderr || err.message)) || String(err);
|
|
282
|
+
if (/^Nothing that looks like a project in /m.test(words)) {
|
|
283
|
+
// The engine's refusal names niro-local and its own command; say it in niro start's words.
|
|
284
|
+
rail(`${fail("✗")} No project found here`);
|
|
285
|
+
rail(` Niro looks for a build file (package.json, pom.xml, go.mod, …) in this folder or up to 3 folders down.`);
|
|
286
|
+
rail(` Run ${color.cyan("niro start")} in your project's folder, or index this one anyway: ${color.cyan("niro init --force")}`);
|
|
287
|
+
rail(` ${color.dim(`Languages supported: ${languages.supportedList()}.`)}`);
|
|
288
|
+
} else {
|
|
289
|
+
for (const line of words.split("\n")) rail(`${line.replace(/^error:\s*/, "")}`);
|
|
290
|
+
rail(`${fail("✗")} The graph was not built. Fix the above and run ${color.cyan("niro start")} again.`);
|
|
291
|
+
}
|
|
292
|
+
console.log("└");
|
|
293
|
+
process.exitCode = 1;
|
|
294
|
+
return "failed";
|
|
295
|
+
}
|
|
296
|
+
bar.stop();
|
|
297
|
+
done("build", ok(`✓ Graph built: ${counts(ctx.built)} in ${ctx.built.seconds} s`));
|
|
298
|
+
if (!foundLanguages(ctx).length) {
|
|
299
|
+
rail(`${warn("⚠")} No code in a supported language here. Niro reads ${languages.supportedList()}.`);
|
|
300
|
+
}
|
|
301
|
+
const used = await planUsage.linesUsed();
|
|
302
|
+
ctx.plan = planUsage.describe(ctx.claims, used);
|
|
303
|
+
ctx.noRoom = planUsage.noRoomNote(ctx.claims, used);
|
|
304
|
+
// The plan is shown once, in the Setup complete summary (2026-11-11), not under the build line.
|
|
305
|
+
if (ctx.noRoom) {
|
|
306
|
+
rail(color.yellow(ctx.noRoom));
|
|
307
|
+
rail(`Run ${color.cyan("niro plan")} to unlock unlimited lines or start a Pro free trial, or free lines with ${color.cyan("niro remove --path <folder>")}.`);
|
|
308
|
+
}
|
|
309
|
+
rail();
|
|
310
|
+
return GO;
|
|
124
311
|
}
|
|
125
312
|
|
|
126
|
-
/**
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
console.log(` ${color.cyan("·")} Ask anything about your code, in the assistant you already use`);
|
|
141
|
-
console.log(` ${color.cyan("·")} See what breaks before you change it, across services`);
|
|
142
|
-
console.log(` ${color.cyan("·")} Get a standing review of the issues already in your code`);
|
|
143
|
-
console.log(` ${color.cyan("·")} Find bugs' root causes from your alerts`);
|
|
144
|
-
console.log(` ${color.cyan("·")} Measure it: run half your sessions without Niro and compare\n`);
|
|
145
|
-
console.log(" Setting up takes about five minutes.");
|
|
146
|
-
console.log(" Prefer a walkthrough first? 45 minutes with us:");
|
|
147
|
-
console.log(` ${color.cyan(BOOK_A_CALL)}\n`);
|
|
313
|
+
/** No room under the line limit: say so in two lines and open the console's choice page. */
|
|
314
|
+
function overLimit(ctx, used) {
|
|
315
|
+
const max = ctx.claims.max_lines;
|
|
316
|
+
const n = (x) => x.toLocaleString("en-US");
|
|
317
|
+
done("build", `${fail("✗")} Not built: this machine already holds ${n(used)} lines of code, over the ${n(max)} line limit`);
|
|
318
|
+
const url = planUsage.limitUrl(used, max);
|
|
319
|
+
rail(`Choose how to continue: unlock unlimited lines, a Pro free trial, or stay under the limit.`);
|
|
320
|
+
rail(`Opening ${color.cyan(url)}`);
|
|
321
|
+
require("../lib/browser").open(url);
|
|
322
|
+
rail(`Then run ${color.cyan("niro start")} again. ${color.dim(`Or see the options here: niro plan`)}`);
|
|
323
|
+
rail(`To free lines instead, stop indexing a folder: ${color.cyan("niro remove --path <folder>")}`);
|
|
324
|
+
console.log("└");
|
|
325
|
+
process.exitCode = 1;
|
|
326
|
+
return "failed";
|
|
148
327
|
}
|
|
149
328
|
|
|
150
329
|
/**
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
* Not in the opening, even though the capability is claimed there: enabling needs a signed-in
|
|
154
|
-
* account, and asking someone to measure a thing they have not yet seen work is the wrong
|
|
155
|
-
* order. First run only, and it defaults to NO — this is a measurement option, not something
|
|
156
|
-
* to be nudged into. A returning user finds it in the next-steps list instead.
|
|
157
|
-
*
|
|
158
|
-
* Never fails the wizard. The repo IS indexed by this point; a measurement window is a nicety
|
|
159
|
-
* and a network hiccup here must not read as a broken setup.
|
|
330
|
+
* Step 5. The `niro mcp install` wizard, which keeps the full client list. `[]` so the
|
|
331
|
+
* wizard's own argv (`start <path>`) is never forwarded to it as client flags.
|
|
160
332
|
*/
|
|
161
|
-
async function
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
console.log(" our word for it. It applies to your whole account, and");
|
|
167
|
-
console.log(` ${color.cyan("niro ab disable")} stops it at any time.\n`);
|
|
168
|
-
|
|
169
|
-
if (!(await prompt.confirm(" Turn on A/B measurement?", false))) {
|
|
170
|
-
console.log(`\n Left off. Turn it on later with ${color.cyan("niro ab enable")}.`);
|
|
171
|
-
return;
|
|
333
|
+
async function connectAssistant(ctx) {
|
|
334
|
+
if (config.load().mcpConnectedAt) {
|
|
335
|
+
const names = assistantNames();
|
|
336
|
+
done("assistant", ok(`✓ ${names.length ? names.join(", ") : "Assistant"} connected`));
|
|
337
|
+
return GO;
|
|
172
338
|
}
|
|
339
|
+
active("assistant", "Connect your AI assistant");
|
|
173
340
|
try {
|
|
174
|
-
|
|
341
|
+
const { withPrefix } = require("../lib/nestedOutput");
|
|
342
|
+
await withPrefix("│", () => require("./mcp").install([]));
|
|
343
|
+
config.save({ mcpConnectedAt: new Date().toISOString() });
|
|
344
|
+
done("assistant", ok("✓ Assistant connected"));
|
|
345
|
+
ctx.restartAssistant = true; // said after "Niro is ready", where it is the next thing to do
|
|
175
346
|
} catch (err) {
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
347
|
+
// The graph IS built by now; one command retries this. A wizard that printed to the terminal has
|
|
348
|
+
// already said why ("✗ Claude Code could not be set up"), so its exit code is not repeated.
|
|
349
|
+
done("assistant", warn("⚠ No assistant connected"));
|
|
350
|
+
if (!(err && err.alreadySaid)) rail(`Could not configure an assistant: ${err && err.message ? err.message : err}`);
|
|
351
|
+
rail(`Run ${color.cyan("niro mcp install")} when you are ready.`);
|
|
352
|
+
rail();
|
|
353
|
+
return "skipped";
|
|
179
354
|
}
|
|
355
|
+
rail();
|
|
356
|
+
return GO;
|
|
180
357
|
}
|
|
181
358
|
|
|
182
359
|
/**
|
|
183
|
-
*
|
|
184
|
-
*
|
|
185
|
-
*
|
|
186
|
-
* straight away, then the three things they will reach for over the next few days. The web app
|
|
187
|
-
* line is where code review and the graph views get named, because those are things you look
|
|
188
|
-
* at rather than ask for.
|
|
189
|
-
*
|
|
190
|
-
* The web address is whatever the server told us at login, never a constant: a self-hosted
|
|
191
|
-
* install has its own, and a wrong link here is worse than none. Omitted when unknown, which
|
|
192
|
-
* is the case for anyone who logged in before the CLI started recording it.
|
|
360
|
+
* Folders set up with Niro web, in a release without the cloud: say they are no longer uploaded,
|
|
361
|
+
* offer to index them here, then retire the list so it is said once. The folder just built is
|
|
362
|
+
* skipped, and indexing stops at the line limit rather than sending anyone to the upgrade page.
|
|
193
363
|
*/
|
|
194
|
-
function
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
console.log(" check the index is current");
|
|
212
|
-
console.log(` ${color.cyan("niro ab")}`);
|
|
213
|
-
console.log(" measure what Niro changes, by running it off half the time");
|
|
214
|
-
|
|
215
|
-
if (firstRun) {
|
|
216
|
-
console.log(`\n Questions, or want us to walk through your codebase with you?`);
|
|
217
|
-
console.log(` 45 minutes with us: ${color.cyan(BOOK_A_CALL)}`);
|
|
364
|
+
async function moveWebFolders(ctx, { folders = watchState.allLocal, ask = prompt.confirm,
|
|
365
|
+
init = (dir) => require("./initLocal").initLocal(dir, { openUrl: () => {} }) } = {}) {
|
|
366
|
+
if (release.cloud) return;
|
|
367
|
+
const web = (() => { try { return folders() || []; } catch { return []; } })();
|
|
368
|
+
if (web.length === 0) return;
|
|
369
|
+
for (const line of migrationNotice.webLines(web)) console.log(line);
|
|
370
|
+
const here = web.filter((f) => path.resolve(f.folderPath) !== ctx.dir && fs.existsSync(f.folderPath));
|
|
371
|
+
if (here.length > 0 && await ask(` Index ${here.length === 1 ? "it" : `these ${here.length} folders`} on this machine now?`, false)) {
|
|
372
|
+
for (const f of here) {
|
|
373
|
+
const used = await planUsage.linesUsed().catch(() => 0);
|
|
374
|
+
if (await planUsage.refusesBuild(ctx.claims, used, f.folderPath)) {
|
|
375
|
+
console.log(` Stopped: the line limit is reached. See ${color.cyan("niro plan")}.`);
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
await init(f.folderPath);
|
|
379
|
+
process.exitCode = 0;
|
|
380
|
+
}
|
|
218
381
|
}
|
|
382
|
+
try { watchState.retireToWeb(); } catch { /* kept as is; the note shows again next time */ }
|
|
219
383
|
console.log("");
|
|
220
384
|
}
|
|
221
385
|
|
|
222
386
|
/**
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
* Printed at the install step, because that is the moment software lands and the honest time
|
|
226
|
-
* to say what it is. Three things arrive from one package and nothing else in the flow says
|
|
227
|
-
* so: the CLI is visible (they just typed it), but the MCP server and the skills are not, and
|
|
228
|
-
* a developer who does not know the skills exist will never think to ask for a rebuild in
|
|
229
|
-
* plain English.
|
|
230
|
-
*
|
|
231
|
-
* First run only, like the welcome screen. Someone setting up their fifth repo knows.
|
|
387
|
+
* True when niro-watch started before this copy was installed, so it still runs the code of the
|
|
388
|
+
* version before: `npm install -g` swaps the files but not a running process.
|
|
232
389
|
*/
|
|
233
|
-
function
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
console.log("");
|
|
390
|
+
function watcherIsOld() {
|
|
391
|
+
const at = service.startedAt();
|
|
392
|
+
if (!at) return false;
|
|
393
|
+
try {
|
|
394
|
+
return at < fs.statSync(path.join(__dirname, "..", "..", "package.json")).ctimeMs; // ctime: npm may stamp a fixed mtime
|
|
395
|
+
} catch {
|
|
396
|
+
return false;
|
|
397
|
+
}
|
|
242
398
|
}
|
|
243
399
|
|
|
244
|
-
/**
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
console.log(
|
|
264
|
-
console.log("
|
|
265
|
-
|
|
400
|
+
/** The watch daemon, so "updates automatically" on the ready screen is true. */
|
|
401
|
+
async function keepItCurrent(ctx) {
|
|
402
|
+
if (watchDaemon.watchDaemonState() === "running") {
|
|
403
|
+
// A watcher saved by an older niro-cli holds the whole shell environment in ~/.pm2/dump.pm2:
|
|
404
|
+
// this starts it again with only what it needs (no password, the login hook is left alone).
|
|
405
|
+
let refreshed;
|
|
406
|
+
try { refreshed = service.refreshSavedEnv(); } catch { refreshed = null; }
|
|
407
|
+
if (refreshed !== null) {
|
|
408
|
+
if (!refreshed && watcherIsOld()) service.restart();
|
|
409
|
+
ctx.watching = true;
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
// Starting it again failed, so it is not running now: the steps below install it as usual.
|
|
413
|
+
}
|
|
414
|
+
const heading = "◆ Keeping your graph current";
|
|
415
|
+
console.log(heading);
|
|
416
|
+
const { withPrefix } = require("../lib/nestedOutput");
|
|
417
|
+
const r = await withPrefix("│", () => watchDaemon.ensureWatchDaemon({ quiet: true, heading }));
|
|
418
|
+
ctx.watching = !!(r && r.ok);
|
|
419
|
+
if (!ctx.watching) console.log(`◇ ${warn("⚠")} The graph will not update on its own. Run ${color.cyan("niro service install")} to retry`);
|
|
420
|
+
else if (r.autostart === false) console.log(`◇ ${ok("✓")} Updating as files change, until you log out`);
|
|
421
|
+
else console.log(`◇ ${ok("✓")} Updating as files change, and after every login`);
|
|
422
|
+
rail();
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/** The commands worth knowing after setup, on the ready screen. */
|
|
426
|
+
const USEFUL = [
|
|
427
|
+
["niro init", "Add another repo: run it inside that folder"],
|
|
428
|
+
["niro status", "Check this folder's graph"],
|
|
429
|
+
["niro plan", "See your line usage, folders and options"],
|
|
430
|
+
["niro mcp install", "Connect another AI assistant"],
|
|
431
|
+
["niro uninstall", "Remove Niro from this machine"],
|
|
432
|
+
];
|
|
433
|
+
|
|
434
|
+
/** The folder's languages, read and not, looked at once per run: listing a large repository takes a while. */
|
|
435
|
+
function scanLanguages(ctx) {
|
|
436
|
+
if (!ctx.languages) ctx.languages = languages.scan(ctx.dir);
|
|
437
|
+
return ctx.languages;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
const foundLanguages = (ctx) => scanLanguages(ctx).found;
|
|
441
|
+
|
|
442
|
+
/** The request command for a language name, quoted when the shell would read it otherwise ("visual basic", "f#"). */
|
|
443
|
+
function requestCommand(name) {
|
|
444
|
+
const word = name.toLowerCase();
|
|
445
|
+
return `niro request-integration ${/^[a-z0-9.+-]+$/.test(word) ? word : `"${word}"`}`;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
async function printReady(ctx) {
|
|
449
|
+
const names = assistantNames();
|
|
450
|
+
console.log(`${ok("◆")} Setup complete`);
|
|
451
|
+
const last = ctx.worktree && ctx.worktree.worktree && ctx.worktree.worktree.last_build_at;
|
|
452
|
+
rail(`Graph ${ctx.built ? `${counts(ctx.built)} · built just now` : last ? `built ${ago(last)}` : "built"}`);
|
|
453
|
+
const { found, others } = scanLanguages(ctx);
|
|
454
|
+
rail(`Languages ${found.length ? languages.short(found, 4) : `none found. Niro reads ${languages.supportedList()}`}`);
|
|
455
|
+
if (others.length) {
|
|
456
|
+
rail(`Not read ${languages.short(others, 3)}. Ask for ${others.length === 1 ? "it" : "one"}: ${color.cyan(requestCommand(others[0]))}`);
|
|
457
|
+
}
|
|
458
|
+
rail(`Assistant ${names.length ? names.join(", ") : `not connected. Run ${color.cyan("niro mcp install")}`}`);
|
|
459
|
+
rail(`Sync ${ctx.watching ? "on, updates as files change" : `off. Run ${color.cyan("niro service install")}`}`);
|
|
460
|
+
const plan = ctx.plan || planUsage.describe(ctx.claims, await planUsage.linesUsed());
|
|
461
|
+
if (plan) rail(`Plan ${plan}`);
|
|
462
|
+
if (ctx.noRoom) {
|
|
463
|
+
rail(` ${color.yellow(ctx.noRoom)}`);
|
|
464
|
+
rail(` Run ${color.cyan("niro plan")} to unlock unlimited lines or start a Pro free trial.`);
|
|
465
|
+
}
|
|
466
|
+
closeReady(names.length === 1 ? names[0] : "your assistant", !!ctx.restartAssistant);
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
function assistantNames() {
|
|
470
|
+
try {
|
|
471
|
+
return mcpClients.clientNames(mcpClients.configuredClients());
|
|
472
|
+
} catch {
|
|
473
|
+
return [];
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** " (0.1.1)": the version folder of an engine from the download cache, else nothing. */
|
|
478
|
+
function engineVersion(executable) {
|
|
479
|
+
const m = /[\\/]bin[\\/]([^\\/]+)[\\/]niro-local-[^\\/]+[\\/]/.exec(String(executable || ""));
|
|
480
|
+
return m ? ` (${m[1]})` : "";
|
|
266
481
|
}
|
|
267
482
|
|
|
483
|
+
/** "just now", "5 min ago", "3 h ago", then the local date: when the graph was last built. */
|
|
484
|
+
function ago(iso, now = Date.now()) {
|
|
485
|
+
const minutes = Math.floor((now - Date.parse(iso)) / 60000);
|
|
486
|
+
if (!Number.isFinite(minutes)) return `last built ${iso}`;
|
|
487
|
+
if (minutes < 1) return "just now";
|
|
488
|
+
if (minutes < 60) return `${minutes} min ago`;
|
|
489
|
+
if (minutes < 24 * 60) return `${Math.floor(minutes / 60)} h ago`;
|
|
490
|
+
return `on ${new Date(iso).toLocaleDateString("en-CA")}`;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
function counts(b) {
|
|
494
|
+
const parts = [];
|
|
495
|
+
if (b.files !== null && b.files !== undefined) parts.push(`${b.files.toLocaleString("en-US")} files`);
|
|
496
|
+
if (b.lines !== null && b.lines !== undefined) parts.push(`${b.lines.toLocaleString("en-US")} lines`);
|
|
497
|
+
return parts.join(", ") || "done";
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
|
|
268
501
|
/**
|
|
269
502
|
* The same condition credentials.requireCreds() enforces, and deliberately not a second
|
|
270
503
|
* definition of it: any usable credential counts, because the request layer refreshes an
|
|
@@ -293,7 +526,7 @@ function runningFromNpxCache(argv1 = process.argv[1]) {
|
|
|
293
526
|
* npm's own global prefix is the only answer that cannot be the cache.
|
|
294
527
|
*/
|
|
295
528
|
function globalNiroPath() {
|
|
296
|
-
const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell:
|
|
529
|
+
const r = childProcess.spawnSync("npm", ["prefix", "-g"], { shell: process.platform === "win32", encoding: "utf8" });
|
|
297
530
|
if (r.status !== 0) return null;
|
|
298
531
|
const prefix = String(r.stdout || "").trim();
|
|
299
532
|
if (!prefix) return null;
|
|
@@ -332,27 +565,18 @@ function isOlder(a, b) {
|
|
|
332
565
|
* prompted for: the alternative is a question whose wrong answer produces a silently
|
|
333
566
|
* dead watch daemon days later.
|
|
334
567
|
*/
|
|
335
|
-
async function bootstrapIfNpx(pathArg
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
// properly in the first place. Still announced, so the wizard shows all five steps
|
|
340
|
-
// and reads as a finite process rather than an open-ended one.
|
|
341
|
-
console.log(` ${step(`1/${TOTAL_STEPS}`)} ${ok("✓")} niro is installed.\n`);
|
|
342
|
-
if (firstRun && !handedOver) printWhatIsInstalled();
|
|
343
|
-
return false;
|
|
344
|
-
}
|
|
568
|
+
async function bootstrapIfNpx(pathArg) {
|
|
569
|
+
// Nothing to do, and nothing said: either this IS the handed-over child, or niro was
|
|
570
|
+
// installed properly in the first place. Step 1's machine lines already cover it.
|
|
571
|
+
if (process.env[BOOTSTRAP_ENV] || !runningFromNpxCache()) return false;
|
|
345
572
|
|
|
346
|
-
|
|
347
|
-
if (firstRun) printWhatIsInstalled();
|
|
348
|
-
console.log(" Background sync has to outlive npx's temporary cache, so Niro");
|
|
349
|
-
console.log(" installs itself properly first.\n");
|
|
573
|
+
rail("Installing niro so background updates outlive npx's temporary cache…");
|
|
350
574
|
|
|
351
575
|
let bin = globalNiroPath();
|
|
352
576
|
if (bin && isCurrent(bin)) {
|
|
353
|
-
|
|
577
|
+
rail(`${ok("✓")} Already installed at ${bin}`);
|
|
354
578
|
} else {
|
|
355
|
-
const r = childProcess.spawnSync("npm", ["install", "-g", PACKAGE_NAME], { shell:
|
|
579
|
+
const r = childProcess.spawnSync("npm", ["install", "-g", PACKAGE_NAME], { shell: process.platform === "win32", stdio: "inherit" });
|
|
356
580
|
bin = r.status === 0 ? globalNiroPath() : null;
|
|
357
581
|
// Only claim success once the binary is actually THERE. The old wording announced an
|
|
358
582
|
// install that had been skipped, which is how an empty global folder came to look fine.
|
|
@@ -363,7 +587,7 @@ async function bootstrapIfNpx(pathArg, firstRun = false) {
|
|
|
363
587
|
process.exitCode = 1;
|
|
364
588
|
return true;
|
|
365
589
|
}
|
|
366
|
-
|
|
590
|
+
rail(`${ok("✓")} ${PACKAGE_NAME} installed at ${bin}`);
|
|
367
591
|
}
|
|
368
592
|
|
|
369
593
|
const args = ["start"];
|
|
@@ -378,84 +602,15 @@ async function bootstrapIfNpx(pathArg, firstRun = false) {
|
|
|
378
602
|
return true;
|
|
379
603
|
}
|
|
380
604
|
|
|
381
|
-
/** Step 2. Returns false only when login failed. */
|
|
382
|
-
async function ensureSignedIn() {
|
|
383
|
-
if (hasCredentials()) {
|
|
384
|
-
const creds = credentials.load();
|
|
385
|
-
console.log(` ${step(`2/${TOTAL_STEPS}`)} ${ok("✓")} Signed in${creds.email ? ` as ${color.bold(creds.email)}` : ""}.\n`);
|
|
386
|
-
return true;
|
|
387
|
-
}
|
|
388
|
-
|
|
389
|
-
console.log(` ${step(`2/${TOTAL_STEPS}`)} Sign in\n`);
|
|
390
|
-
console.log(" Opening your browser. Sign in there, or create your account");
|
|
391
|
-
console.log(" with your access code.\n");
|
|
392
|
-
console.log(" Creating an account? You will get a verification email. Take");
|
|
393
|
-
console.log(" your time. This keeps waiting.\n");
|
|
394
|
-
|
|
395
|
-
const loginCmd = require("./login");
|
|
396
|
-
await loginCmd.login({});
|
|
397
|
-
if (!hasCredentials()) {
|
|
398
|
-
console.error(fail("\n ✗ Not signed in, so there is nothing to onboard to."));
|
|
399
|
-
console.error(` Run ${color.cyan("niro start")} again when you are ready.`);
|
|
400
|
-
process.exitCode = 1;
|
|
401
|
-
return false;
|
|
402
|
-
}
|
|
403
|
-
return true;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
605
|
/**
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
* The refusal RULES are not re-implemented here: protectedRootReason and
|
|
410
|
-
* findProjectIndicators are the same functions init.js validates with, so the preview
|
|
411
|
-
* can never disagree with the gate. init.js re-runs the authoritative check.
|
|
606
|
+
* Why this folder can never be indexed, in the wizard's voice, or null when that is for the engine to say.
|
|
412
607
|
*
|
|
413
|
-
*
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
console.log(` ${step(`3/${TOTAL_STEPS}`)} Choose what to index\n`);
|
|
420
|
-
|
|
421
|
-
const problem = describeProblem(candidate);
|
|
422
|
-
if (problem) {
|
|
423
|
-
console.log(` ${fail("✗")} ${problem.headline}\n`);
|
|
424
|
-
for (const line of problem.detail) console.log(` ${line}`);
|
|
425
|
-
console.log("");
|
|
426
|
-
const answer = await prompt.ask(" Which folder do you want to index? (blank to give up)");
|
|
427
|
-
if (!answer || !answer.trim()) {
|
|
428
|
-
console.log(`\n Stopped. Nothing was changed. Run ${color.cyan("niro start")} from a project folder.\n`);
|
|
429
|
-
return null;
|
|
430
|
-
}
|
|
431
|
-
candidate = path.resolve(expandHome(answer.trim()));
|
|
432
|
-
console.log("");
|
|
433
|
-
continue;
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
printPreview(candidate);
|
|
437
|
-
if (await prompt.confirm(" Index this folder?", true)) return candidate;
|
|
438
|
-
|
|
439
|
-
const answer = await prompt.ask(" Which folder instead? (blank to give up)");
|
|
440
|
-
if (!answer || !answer.trim()) {
|
|
441
|
-
console.log(`\n Stopped. Nothing was changed.\n`);
|
|
442
|
-
return null;
|
|
443
|
-
}
|
|
444
|
-
candidate = path.resolve(expandHome(answer.trim()));
|
|
445
|
-
console.log("");
|
|
446
|
-
}
|
|
447
|
-
}
|
|
448
|
-
|
|
449
|
-
/** A leading ~ is expanded here because a prompt answer never goes through a shell. */
|
|
450
|
-
function expandHome(p) {
|
|
451
|
-
if (p === "~") return require("os").homedir();
|
|
452
|
-
if (p.startsWith("~/") || p.startsWith("~\\")) return path.join(require("os").homedir(), p.slice(2));
|
|
453
|
-
return p;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Why this folder cannot be onboarded, in the wizard's voice, or null if it can.
|
|
458
|
-
* Mirrors ensureOnboardable's two rules by calling the same functions it calls.
|
|
608
|
+
* Only what needs no engine: a path that is not a folder, and a protected folder (home, the filesystem root, a system
|
|
609
|
+
* folder). Whether the folder holds a project is the engine's question, asked by the engine's own `init`
|
|
610
|
+
* (localOnboard.initAndBuild), because niro-cli's own answer disagreed with it: niro-cli looked for a build file two levels
|
|
611
|
+
* down and had no rule for a folder of repositories, the engine looks three levels down and accepts one, so the
|
|
612
|
+
* engine offered `niro init` for folders `niro init` then refused (VP-release-texts-2 note A; Niraj, 2026-11-06:
|
|
613
|
+
* "niro should ask niro-engine"). `niro start` runs this before the engine is downloaded, so no engine can be asked.
|
|
459
614
|
*/
|
|
460
615
|
function describeProblem(dir) {
|
|
461
616
|
if (!fs.existsSync(dir)) {
|
|
@@ -469,100 +624,25 @@ function describeProblem(dir) {
|
|
|
469
624
|
if (reason) {
|
|
470
625
|
return {
|
|
471
626
|
headline: `${dir} is ${reason}. Niro will not index it.`,
|
|
472
|
-
// Says only what is true. folderSync never descends into a dot directory
|
|
473
|
-
// (folderSync.js walkDir) and only uploads source, build and committed config files
|
|
474
|
-
// (isUploadableName), so keys, documents and photos are never eligible and claiming
|
|
475
|
-
// otherwise is scaremongering. What IS lost is .gitignore: it is only honoured when
|
|
476
|
-
// the onboarded folder is itself a git work tree, and a home folder is not one, so
|
|
477
|
-
// each nested repo's ignored files stop being protected.
|
|
478
627
|
detail: [
|
|
479
|
-
"It would
|
|
480
|
-
"
|
|
481
|
-
"has to be the git repository for that to apply.",
|
|
482
|
-
],
|
|
483
|
-
};
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
const { indicators } = onboardTarget.findProjectIndicators(dir);
|
|
487
|
-
if (indicators.size === 0) {
|
|
488
|
-
return {
|
|
489
|
-
headline: `No project found in ${dir}.`,
|
|
490
|
-
detail: [
|
|
491
|
-
"Niro looks for a build file (package.json, pom.xml, go.mod, ...) in",
|
|
492
|
-
"this folder or just below it. None found.",
|
|
493
|
-
`If you really mean this folder, use ${color.cyan("niro init --force")} instead.`,
|
|
628
|
+
"It would index every repository under it as one project.",
|
|
629
|
+
"Run niro start from your project's folder instead.",
|
|
494
630
|
],
|
|
495
631
|
};
|
|
496
632
|
}
|
|
497
633
|
return null;
|
|
498
634
|
}
|
|
499
635
|
|
|
500
|
-
/**
|
|
501
|
-
* The screen that prevents the accident: what is about to be uploaded, before it is.
|
|
502
|
-
*
|
|
503
|
-
* Deliberately cheap — no full tree walk. The project scan has already run for the
|
|
504
|
-
* refusal check and git.inspect reads two small files, so this adds nothing the wizard
|
|
505
|
-
* was not doing anyway. init.js does the real manifest walk a moment later.
|
|
506
|
-
*/
|
|
507
|
-
function printPreview(dir) {
|
|
508
|
-
const { roots, indicators } = onboardTarget.findProjectIndicators(dir);
|
|
509
|
-
const info = git.inspect(dir);
|
|
510
|
-
|
|
511
|
-
console.log(` Folder: ${color.bold(dir)}`);
|
|
512
|
-
console.log(` Detected: ${onboardTarget.describeIndicators(indicators)}`);
|
|
513
|
-
console.log(` Projects: ${roots} module${roots === 1 ? "" : "s"}`);
|
|
514
|
-
if (info.repoRoot) {
|
|
515
|
-
console.log(` Git: branch ${info.branch || "(detached)"}${info.originUrl ? `, ${info.originUrl}` : ""}`);
|
|
516
|
-
} else {
|
|
517
|
-
console.log(` Git: ${color.dim("not a git repo, which is fine; it uploads as-is")}`);
|
|
518
|
-
}
|
|
519
|
-
console.log("");
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
/**
|
|
523
|
-
* Step 5. `mcp install` prompts for the client itself and resolves the key from the
|
|
524
|
-
* login session, so there is nothing to pass it.
|
|
525
|
-
*
|
|
526
|
-
* "Already connected" is a wizard-local flag rather than a probe of 14 clients' config
|
|
527
|
-
* files: those paths live inside src/mcp/setup.js and are not exported, and a wrong
|
|
528
|
-
* guess here would silently skip the step that makes Niro usable. The flag only ever
|
|
529
|
-
* records what THIS wizard did, and re-running mcp install is harmless anyway.
|
|
530
|
-
*/
|
|
531
|
-
async function connectAssistant() {
|
|
532
|
-
const saved = config.load();
|
|
533
|
-
if (saved.mcpConnectedAt) {
|
|
534
|
-
console.log(`\n ${step(`5/${TOTAL_STEPS}`)} ${ok("✓")} Assistant already connected on this machine.`);
|
|
535
|
-
console.log(` Connect another with ${color.cyan("niro mcp install")}.`);
|
|
536
|
-
return;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
console.log(`\n ${step(`5/${TOTAL_STEPS}`)} Connect your AI assistant\n`);
|
|
540
|
-
const mcpCmd = require("./mcp");
|
|
541
|
-
try {
|
|
542
|
-
await mcpCmd.install();
|
|
543
|
-
config.save({ mcpConnectedAt: new Date().toISOString() });
|
|
544
|
-
} catch (err) {
|
|
545
|
-
// Never fail the whole wizard here: the repo IS indexed by this point, and the
|
|
546
|
-
// user can retry a single command.
|
|
547
|
-
console.log(`\n ${warn("⚠")} Could not configure an assistant automatically.`);
|
|
548
|
-
console.log(` Run ${color.cyan("niro mcp install")} when you are ready.`);
|
|
549
|
-
console.log(` (${err && err.message ? err.message : err})`);
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
636
|
|
|
553
637
|
module.exports = {
|
|
554
638
|
start,
|
|
555
639
|
// exported for tests
|
|
556
640
|
hasCredentials,
|
|
557
|
-
printWelcome,
|
|
558
|
-
printWhatIsInstalled,
|
|
559
|
-
offerMeasurement,
|
|
560
|
-
printNextSteps,
|
|
561
|
-
BOOK_A_CALL,
|
|
562
641
|
runningFromNpxCache,
|
|
563
642
|
globalNiroPath,
|
|
564
643
|
isOlder,
|
|
565
644
|
describeProblem,
|
|
566
|
-
|
|
645
|
+
moveWebFolders,
|
|
646
|
+
watcherIsOld,
|
|
567
647
|
BOOTSTRAP_ENV,
|
|
568
648
|
};
|