@niroai/niro 0.3.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.
Files changed (43) hide show
  1. package/README.md +291 -0
  2. package/bin/niro.js +2 -0
  3. package/package.json +41 -0
  4. package/src/commands/_resolveProject.js +142 -0
  5. package/src/commands/add-project.js +190 -0
  6. package/src/commands/add-repo.js +270 -0
  7. package/src/commands/build.js +118 -0
  8. package/src/commands/delete.js +66 -0
  9. package/src/commands/edit.js +601 -0
  10. package/src/commands/info.js +172 -0
  11. package/src/commands/init.js +1166 -0
  12. package/src/commands/login.js +214 -0
  13. package/src/commands/logout.js +33 -0
  14. package/src/commands/mcp.js +40 -0
  15. package/src/commands/projects.js +76 -0
  16. package/src/commands/release.js +175 -0
  17. package/src/commands/remove.js +95 -0
  18. package/src/commands/status.js +75 -0
  19. package/src/commands/takeover.js +204 -0
  20. package/src/commands/watch.js +498 -0
  21. package/src/commands/whoami.js +74 -0
  22. package/src/index.js +293 -0
  23. package/src/lib/agentApi.js +276 -0
  24. package/src/lib/api.js +230 -0
  25. package/src/lib/browser.js +30 -0
  26. package/src/lib/color.js +46 -0
  27. package/src/lib/config.js +38 -0
  28. package/src/lib/credentials.js +73 -0
  29. package/src/lib/folderSync.js +503 -0
  30. package/src/lib/git.js +190 -0
  31. package/src/lib/moduleExcludeSuggestions.js +57 -0
  32. package/src/lib/niroProjectFile.js +76 -0
  33. package/src/lib/pendingRequests.js +99 -0
  34. package/src/lib/prompt.js +118 -0
  35. package/src/lib/resolveContext.js +108 -0
  36. package/src/lib/secret.js +114 -0
  37. package/src/lib/service.js +221 -0
  38. package/src/lib/spinner.js +109 -0
  39. package/src/lib/watchDaemon.js +93 -0
  40. package/src/lib/watchState.js +217 -0
  41. package/src/mcp/server.js +764 -0
  42. package/src/mcp/setup.js +1976 -0
  43. package/src/mcp/teardown.js +673 -0
@@ -0,0 +1,75 @@
1
+ const api = require("../lib/api");
2
+ const credentials = require("../lib/credentials");
3
+ const color = require("../lib/color");
4
+ const watchState = require("../lib/watchState");
5
+ const watchDaemon = require("../lib/watchDaemon");
6
+ const resolveProject = require("./_resolveProject");
7
+
8
+ function tintStatus(st) {
9
+ if (st === "COMPLETED") return color.green(st);
10
+ if (st === "FAILED" || st === "CANCELLED") return color.red(st);
11
+ if (st === "PROCESSING" || st === "RUNNING") return color.yellow(st);
12
+ if (st === "PENDING") return color.gray(st);
13
+ return st;
14
+ }
15
+
16
+ /**
17
+ * Daemon state for the status output, or null when this machine has no
18
+ * locally-synced folders on the current backend (then the daemon is
19
+ * irrelevant and a "not installed" line would be a false alarm — e.g. a
20
+ * user who only indexes remote repos).
21
+ *
22
+ * Probing the daemon spawns pm2; only worth it when there's something to sync.
23
+ */
24
+ function autoSyncState() {
25
+ if (watchState.allLocal(api.currentBaseUrl()).length === 0) return null;
26
+ return watchDaemon.watchDaemonState();
27
+ }
28
+
29
+ // Human-readable rendering per watchDaemonState() value. Every non-running
30
+ // state names the exact command that fixes it — this line exists so a broken
31
+ // auto-sync is visible in the command we tell users to run after init.
32
+ const AUTO_SYNC_LINES = {
33
+ running: color.green("running") + color.dim(" — synced folders stay up to date automatically"),
34
+ stopped: color.yellow("stopped — run `niro service install` to restart it"),
35
+ not_installed: color.yellow("not installed — run `niro service install`"),
36
+ pm2_missing: color.yellow("OFF — PM2 is missing. Run `npm install -g pm2`, then `niro service install`"),
37
+ };
38
+
39
+ async function status(projectIdArg, opts = {}) {
40
+ credentials.requireCreds();
41
+ const projectId = await resolveProject.resolveProjectId(projectIdArg, { apiUrl: opts.apiUrl });
42
+ const data = await api.get(`/api/graph/${encodeURIComponent(projectId)}/status`);
43
+ const logMessage = data.log_message || data.logMessage;
44
+ const docGenStatus = data.doc_gen_status || data.docGenStatus;
45
+ const docGenMessage = data.doc_gen_message || data.docGenMessage;
46
+ const updatedAt = data.updated_at || data.updatedAt;
47
+ const autoSync = autoSyncState();
48
+
49
+ if (opts.json) {
50
+ console.log(
51
+ JSON.stringify({
52
+ project_id: projectId,
53
+ status: data.status || null,
54
+ log_message: logMessage || null,
55
+ doc_gen_status: docGenStatus || null,
56
+ doc_gen_message: docGenMessage || null,
57
+ updated_at: updatedAt || null,
58
+ auto_sync: autoSync,
59
+ })
60
+ );
61
+ return;
62
+ }
63
+
64
+ const label = (s) => color.dim(s.padEnd(13));
65
+ console.log(`${label("Project:")} ${color.bold(projectId)}`);
66
+ console.log(`${label("Status:")} ${tintStatus(data.status || "(unknown)")}`);
67
+ if (logMessage) console.log(`${label("Last message:")} ${logMessage}`);
68
+ if (docGenStatus) {
69
+ console.log(`${label("Docs:")} ${tintStatus(docGenStatus)}${docGenMessage ? " — " + docGenMessage : ""}`);
70
+ }
71
+ if (updatedAt) console.log(`${label("Updated:")} ${color.dim(updatedAt)}`);
72
+ if (autoSync) console.log(`${label("Auto-sync:")} ${AUTO_SYNC_LINES[autoSync] || autoSync}`);
73
+ }
74
+
75
+ module.exports = { status };
@@ -0,0 +1,204 @@
1
+ /**
2
+ * `niro new-temp-project [path]` — create a private temporary project for the current folder's project
3
+ * (ADR-013, routing per ADR-018).
4
+ *
5
+ * The folder's git remote is matched to a Niro project P; the server clones P into a new working-copy
6
+ * project P' (graph + analysis state copied, no re-parse/LLM), converting THIS folder's repo to
7
+ * AGENT_UPLOAD. The working tree (including uncommitted edits and any local branch) is then uploaded and
8
+ * the watch daemon streams every subsequent save into P'.
9
+ *
10
+ * ADR-018: the `.niro-project` pin is NEVER touched — it stays on the shared source project. The SERVER
11
+ * routes each MCP call to P' from (git url, current branch, you), so switching branches re-routes by
12
+ * itself and there is no pin to go stale, leak into a commit, or restore later.
13
+ *
14
+ * Undo with `niro discard-temp-project` (deletes P'; routing falls back to P automatically).
15
+ */
16
+ const api = require("../lib/api");
17
+ const color = require("../lib/color");
18
+ const folderSync = require("../lib/folderSync");
19
+ const agentApi = require("../lib/agentApi");
20
+ const git = require("../lib/git");
21
+ const prompt = require("../lib/prompt");
22
+ const watchState = require("../lib/watchState");
23
+ const resolveCtxLib = require("../lib/resolveContext");
24
+
25
+ const ok = (s) => color.green(s);
26
+ const warn = (s) => color.yellow(s);
27
+ const step = (s) => color.cyan(s);
28
+
29
+ async function takeover(pathArg, opts = {}) {
30
+ // --api-url is threaded only into project resolution, not the clone/build POSTs or the agentApi upload
31
+ // chain — so a global `--api-url X` would resolve against X but clone/upload/watch-tag against the DEFAULT
32
+ // backend, mixing two stacks in one flow. Fail fast BEFORE any server mutation rather than corrupt state
33
+ // across backends. (Full per-backend threading is future work; set the default backend via login/config.)
34
+ if (opts.apiUrl) {
35
+ throw new Error("`--api-url` is not supported for `niro new-temp-project` yet (it would mix backends). "
36
+ + "Select the backend with `niro login` / your config first, then run this without --api-url.");
37
+ }
38
+ const targetDir = require("path").resolve(pathArg || process.cwd());
39
+ const { repoRoot, branch } = git.inspect(targetDir);
40
+ if (!repoRoot) {
41
+ throw new Error("Not a git repository — run takeover inside the repo you want to work on.");
42
+ }
43
+
44
+ // 1. Match this folder to a source project (working copies are excluded server-side).
45
+ const ctx = await resolveCtxLib.resolveContext(targetDir, { apiUrl: opts.apiUrl });
46
+ if (ctx.state === "error") throw new Error(`Could not resolve project: ${ctx.error}`);
47
+ if (!ctx.projects.length) {
48
+ throw new Error("This folder's git remote doesn't match any Niro project. Run `niro init` first.");
49
+ }
50
+ let source = ctx.projects[0];
51
+ if (opts.project) {
52
+ source = ctx.projects.find((p) => p.project_id === opts.project || p.alias === opts.project);
53
+ if (!source) {
54
+ throw new Error(`--project "${opts.project}" does not match any project mapped to this repo.`);
55
+ }
56
+ } else if (ctx.projects.length > 1) {
57
+ if (prompt.isNonInteractive()) {
58
+ const list = ctx.projects.map((p) => ` ${p.project_id} (${p.alias})`).join("\n");
59
+ throw new Error(`Multiple projects match this repo — pass --project:\n${list}`);
60
+ }
61
+ console.log("\n Multiple projects match this repo:");
62
+ ctx.projects.forEach((p, i) => console.log(` ${i + 1}. ${p.alias || "(no alias)"} ${color.dim(p.project_id)}`));
63
+ const answer = await prompt.ask(` Take over which one? [1-${ctx.projects.length}]`, "1");
64
+ const idx = parseInt(answer, 10) - 1;
65
+ if (isNaN(idx) || idx < 0 || idx >= ctx.projects.length) {
66
+ throw new Error(`Invalid choice: ${answer}`);
67
+ }
68
+ source = ctx.projects[idx];
69
+ }
70
+ if (!source.git_id) {
71
+ throw new Error("Server did not report which repo of the project matches this folder (git_id missing).");
72
+ }
73
+
74
+ console.log(`\n Creating a temporary private project of ${color.bold(source.alias || source.project_id)}...`);
75
+ console.log(color.dim(" Your teammates' view of the project is not affected."));
76
+
77
+ // 2. Create-or-extend server-side (ADR-016). If you already have a temporary project of this project,
78
+ // the backend EXTENDS it — adds this repo to the same copy — instead of minting a second one, so
79
+ // cross-repo answers stay coherent. Either way this folder's repo becomes AGENT_UPLOAD.
80
+ const outcome = await api.post(
81
+ `/api/projects/${encodeURIComponent(source.project_id)}/clone`,
82
+ { local_git_ids: [source.git_id], branch });
83
+ const newProjectId = outcome.new_project_id || outcome.newProjectId;
84
+ const gitIdMap = outcome.git_id_map || outcome.gitIdMap || {};
85
+ const newGitId = gitIdMap[source.git_id];
86
+ if (!newProjectId || !newGitId) {
87
+ throw new Error("Clone succeeded but the response is missing new_project_id/git_id_map.");
88
+ }
89
+ // Whether this call extended an existing temporary project or created one — drives messaging AND rollback:
90
+ // an extend must NEVER be rolled back by deleting the copy (it holds your other repos' work).
91
+ const wasExtended = outcome.extended === true;
92
+ // ADR-018 version guard (review BLOCKER): this CLI never re-pins — it relies on the SERVER routing
93
+ // calls to the temp project. A pre-ADR-018 backend clones happily but has no routing, so every answer
94
+ // would silently keep coming from the stale source. The backend echoes routing_supported when it can
95
+ // route; its absence means a version mismatch the user must hear about NOW, not discover via wrong answers.
96
+ const serverRoutes = outcome.routing_supported === true || outcome.routingSupported === true;
97
+ if (!serverRoutes) {
98
+ console.log(warn("\n ⚠ Your backend did NOT confirm automatic routing (pre-ADR-018 server?)."));
99
+ console.log(warn(" This CLI no longer re-pins .niro-project, so against this backend Niro's answers"));
100
+ console.log(warn(" will KEEP coming from the original project — NOT this temporary project."));
101
+ console.log(warn(" Fix: upgrade the backend (ai-assistant + ai-orchestrator), or use the previous CLI."));
102
+ }
103
+ if (wasExtended) {
104
+ console.log(` ${step("●")} Added this repo to your temporary project: ${color.bold(outcome.alias || newProjectId)} ` +
105
+ color.dim("(existing copy extended — your other repos are untouched)"));
106
+ } else {
107
+ console.log(` ${step("●")} Temporary project created: ${color.bold(outcome.alias || newProjectId)} ` +
108
+ color.dim(`(${outcome.nodes_copied ?? "?"} nodes copied, no re-index needed)`));
109
+ }
110
+
111
+ // From here on the temporary project EXISTS server-side: any failure below must roll it back, or the
112
+ // user is left with an orphan project they can't `niro discard-temp-project` (the pin was never written).
113
+ try {
114
+ // 3. Upload the working tree (uncommitted edits and local branch included). Operate on the git
115
+ // ROOT, not targetDir: the temp project is the whole repo, so running from a SUBDIR must still upload,
116
+ // pin, and watch the entire repo (targetDir is only used above to locate the repo + resolve the project).
117
+ const manifest = folderSync.buildManifest(repoRoot);
118
+ const filterResult = await agentApi.filterManifest(newGitId, manifest);
119
+ const includedPaths = filterResult.included_paths || filterResult.includedPaths || [];
120
+ const sizes = {};
121
+ for (const m of manifest) sizes[m.path] = m.size_bytes;
122
+ console.log(` ${step("●")} Uploading working tree: ${includedPaths.length} files ` +
123
+ `(branch ${color.cyan(branch || "detached HEAD")})...`);
124
+ // Capture the uploaded hashes: takeover registers its watchState entry AFTER this upload (below), so
125
+ // doInitialSync's own watchState.update no-ops (no entry yet). Seeding these into the final set means
126
+ // the watch daemon delta-syncs from HERE instead of re-uploading the entire tree on its first pass.
127
+ const uploadedHashes = await folderSync.doInitialSync(newGitId, repoRoot, includedPaths, { sizes }) || {};
128
+
129
+ // 3b. Trigger an incremental build so the uploaded tree gets indexed even when the project's
130
+ // auto-sync is disabled (staleness alone does not schedule a build in that case).
131
+ try {
132
+ await api.post(`/api/graph/${encodeURIComponent(newProjectId)}/build`, {}, {
133
+ query: { force_full_rebuild: "false" },
134
+ });
135
+ console.log(` ${step("●")} Incremental index of your working tree triggered.`);
136
+ } catch (buildErr) {
137
+ console.log(warn(` ⚠ Could not trigger the index build automatically (${buildErr.message}). ` +
138
+ "Run `niro build` to index your uploaded tree."));
139
+ }
140
+
141
+ // 4. Register the folder for the watch daemon and record the undo state. ADR-018: the
142
+ // `.niro-project` pin is NEVER touched — it stays on the shared source project, and the
143
+ // SERVER routes each call to your temp project from (git url, branch, you). No pin swap
144
+ // means no stale pin, no accidental commit of a private id, and nothing to restore later.
145
+ // (Requires an ADR-018 backend; against an older server keep the older CLI.)
146
+ const existing = watchState.get(repoRoot, api.currentBaseUrl());
147
+ // Preserve the ORIGINAL pre-takeover backup across re-runs: if this folder was ALREADY taken over,
148
+ // `existing.gitId` is the CLONE's id, not the source's — capturing it would make a later
149
+ // `niro discard-temp-project` restore point at the clone instead of the real source project.
150
+ const priorBackup = existing && existing.takeover ? existing.takeover : null;
151
+ watchState.set(repoRoot, {
152
+ ...(existing || { includePatterns: [], excludePatterns: [], lastSyncedAt: null }),
153
+ // Seed the just-uploaded hashes so the daemon delta-syncs from here (not a full re-upload).
154
+ fileHashes: uploadedHashes,
155
+ lastSyncedAt: new Date().toISOString(),
156
+ gitId: newGitId,
157
+ projectId: newProjectId,
158
+ branch: branch || null,
159
+ takeover: {
160
+ sourceProjectId: source.project_id,
161
+ sourceGitId: source.git_id,
162
+ // The SOURCE branch (what the project indexes) — the discard guard compares each folder's
163
+ // CURRENT branch against this to warn before deleting a temp project that still covers
164
+ // in-flight work on a sibling repo. NOTE: resolveContext's normalizeProjects renames the
165
+ // server's `branch` to `indexed_branch` — reading `.branch` here was ALWAYS undefined and
166
+ // silently killed the guard (review BLOCKER, masked by a mock with an impossible shape).
167
+ sourceBranch: source.indexed_branch || source.branch
168
+ || (priorBackup ? priorBackup.sourceBranch : null) || null,
169
+ // Legacy pin backup: only carried forward from a PRE-ADR-018 takeover of this folder
170
+ // (whose pin was swapped and still needs healing on discard). New takeovers never touch
171
+ // the pin, so there is nothing to back up.
172
+ previousPin: priorBackup ? priorBackup.previousPin : undefined,
173
+ previousGitId: priorBackup ? priorBackup.previousGitId : (existing ? existing.gitId : null),
174
+ },
175
+ }, api.currentBaseUrl());
176
+ } catch (err) {
177
+ if (wasExtended) {
178
+ // We EXTENDED an existing temporary project that already holds your OTHER repos' taken-over work.
179
+ // Deleting it would destroy that — so never roll back by release here. The extend is idempotent
180
+ // (the repo is already AGENT_UPLOAD server-side), so re-running just retries the upload.
181
+ throw new Error(`Adding this repo to your temporary project failed after it was taken over (${err.message}). ` +
182
+ "Your temporary project was NOT deleted (it holds your other repos). Re-run `niro new-temp-project` here to retry.");
183
+ }
184
+ // Created a fresh temporary project — roll it back so no orphan project is left behind.
185
+ try {
186
+ await api.post(`/api/projects/${encodeURIComponent(newProjectId)}/clone/release`, {});
187
+ throw new Error(`Temporary project creation failed after the temporary project was created (${err.message}). ` +
188
+ "The temporary project was rolled back — fix the problem and re-run `niro new-temp-project`.");
189
+ } catch (rollbackErr) {
190
+ if (rollbackErr.message && rollbackErr.message.includes("rolled back")) throw rollbackErr;
191
+ throw new Error(`Temporary project creation failed (${err.message}) AND rollback of the temporary project failed ` +
192
+ `(${rollbackErr.message}). Delete it manually: ` +
193
+ `POST /api/projects/${newProjectId}/clone/release (or from the Niro UI).`);
194
+ }
195
+ }
196
+
197
+ console.log(`\n ${ok("✓")} Niro now answers from your temporary project whenever you are on this branch`);
198
+ console.log(color.dim(" (routing is automatic per branch — your .niro-project file was NOT changed)."));
199
+ console.log(` ${ok("✓")} Local edits stream in live while ${color.bold("niro watch")} is running.`);
200
+ console.log(color.dim("\n When your branch is merged: `niro discard-temp-project` deletes the temporary project;"));
201
+ console.log(color.dim(" routing falls back to the original project by itself."));
202
+ }
203
+
204
+ module.exports = { takeover };