@niroai/niro 0.9.0 → 0.10.1
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 +69 -0
- package/README.md +131 -211
- package/package.json +1 -1
- package/src/commands/ab.js +198 -0
- package/src/commands/add-project.js +11 -11
- package/src/commands/add-repo.js +23 -23
- package/src/commands/build.js +5 -5
- package/src/commands/delete.js +3 -3
- package/src/commands/edit.js +11 -11
- package/src/commands/info.js +2 -2
- package/src/commands/init.js +108 -87
- package/src/commands/login.js +25 -5
- package/src/commands/projects.js +1 -1
- package/src/commands/release.js +7 -7
- package/src/commands/remove.js +5 -5
- package/src/commands/start.js +567 -0
- package/src/commands/takeover.js +19 -19
- package/src/commands/watch.js +35 -35
- package/src/index.js +57 -3
- package/src/lib/folderSync.js +13 -8
- package/src/lib/moduleExcludeSuggestions.js +5 -5
- package/src/lib/nestedOutput.js +49 -0
- package/src/lib/onboardTarget.js +5 -5
- package/src/lib/prompt.js +3 -0
- package/src/lib/service.js +20 -12
- package/src/lib/spinner.js +28 -7
- package/src/lib/watchDaemon.js +9 -9
- package/src/mcp/setup.js +198 -160
- package/src/mcp/teardown.js +53 -53
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `niro ab` — turn the A/B measurement window on or off, and read the result.
|
|
3
|
+
*
|
|
4
|
+
* What it measures: while a window is open, ai-assistant puts each MCP session in an "on" or
|
|
5
|
+
* "off" bucket by a deterministic 50/50 split of the session id. In the OFF bucket your
|
|
6
|
+
* assistant works WITHOUT Niro. Comparing the two buckets is the only way to see what Niro
|
|
7
|
+
* changes for you rather than take our word for it.
|
|
8
|
+
*
|
|
9
|
+
* Two things about the scope, both said out loud before anything is switched on:
|
|
10
|
+
* - It is per ACCOUNT, not per repo or per machine, so it reaches sessions beyond the
|
|
11
|
+
* folder you are standing in.
|
|
12
|
+
* - It runs for a fixed window (14 days by default) and can be stopped at any time.
|
|
13
|
+
*
|
|
14
|
+
* These endpoints live on ai-assistant, NOT the orchestrator every other command talks to,
|
|
15
|
+
* so the base URL is resolved through the MCP installer's own resolver rather than a second
|
|
16
|
+
* copy of that precedence. Deliberately no --url flag here, for the reason index.js refuses
|
|
17
|
+
* --api-url outside login: a per-command backend override reads one server and writes
|
|
18
|
+
* another. A mismatched pair fails as a 401 from the assistant rather than doing something
|
|
19
|
+
* quietly wrong.
|
|
20
|
+
*/
|
|
21
|
+
const api = require("../lib/api");
|
|
22
|
+
const credentials = require("../lib/credentials");
|
|
23
|
+
const color = require("../lib/color");
|
|
24
|
+
const prompt = require("../lib/prompt");
|
|
25
|
+
const { resolveMcpServerUrl } = require("../mcp/setup");
|
|
26
|
+
|
|
27
|
+
const ok = (s) => color.green(s);
|
|
28
|
+
const warn = (s) => color.yellow(s);
|
|
29
|
+
const fail = (s) => color.red(s);
|
|
30
|
+
|
|
31
|
+
const DEFAULT_WINDOW_DAYS = 14;
|
|
32
|
+
|
|
33
|
+
/** The ai-assistant base for this session, by the same precedence `niro mcp install` uses. */
|
|
34
|
+
function assistantUrl() {
|
|
35
|
+
return resolveMcpServerUrl(null).url;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function call(method, path, opts = {}) {
|
|
39
|
+
// Checked here rather than left to the server: without it an unauthenticated user gets a
|
|
40
|
+
// raw "GET /api/abtest/mode failed: Unauthorized (HTTP 401)", while every other command in
|
|
41
|
+
// the CLI says "Not logged in. Run `niro login` first." requireCreds throws the ENOAUTH
|
|
42
|
+
// that index.js already knows how to print.
|
|
43
|
+
credentials.requireCreds();
|
|
44
|
+
return api.request(method, path, { ...opts, apiUrl: assistantUrl() });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** A run's window as a plain sentence, or null when there is no window. */
|
|
48
|
+
function describeWindow(run) {
|
|
49
|
+
if (!run || !run.window_start) return null;
|
|
50
|
+
const start = String(run.window_start).slice(0, 10);
|
|
51
|
+
const end = run.window_end ? String(run.window_end).slice(0, 10) : "no end date";
|
|
52
|
+
return `${start} to ${end}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isActive(run) {
|
|
56
|
+
return !!(run && run.status === "active");
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function status() {
|
|
60
|
+
const run = await call("GET", "/api/abtest/mode");
|
|
61
|
+
if (!isActive(run)) {
|
|
62
|
+
console.log(`\n A/B measurement is ${color.bold("off")}.`);
|
|
63
|
+
console.log(` Start a ${DEFAULT_WINDOW_DAYS}-day window with ${color.cyan("niro ab enable")}.\n`);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
console.log(`\n A/B measurement is ${ok("on")}${run.name ? ` (${run.name})` : ""}.`);
|
|
67
|
+
const window = describeWindow(run);
|
|
68
|
+
if (window) console.log(` Window: ${window}`);
|
|
69
|
+
console.log(` Half your sessions are running without Niro so the two can be compared.`);
|
|
70
|
+
console.log(`\n ${color.cyan("niro ab report")} see the comparison`);
|
|
71
|
+
console.log(` ${color.cyan("niro ab disable")} stop early\n`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Starting a window changes how HALF of every future session behaves, account-wide, so the
|
|
76
|
+
* consequence is stated and confirmed rather than assumed from the fact that the command was
|
|
77
|
+
* typed. --yes is the scripted escape.
|
|
78
|
+
*/
|
|
79
|
+
async function enable(opts = {}) {
|
|
80
|
+
const days = parseDays(opts.days);
|
|
81
|
+
if (days === null) {
|
|
82
|
+
console.error(fail(` --days must be a whole number of days, got "${opts.days}".`));
|
|
83
|
+
process.exitCode = 1;
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const existing = await call("GET", "/api/abtest/mode").catch(() => null);
|
|
88
|
+
if (isActive(existing)) {
|
|
89
|
+
console.log(`\n ${warn("A/B measurement is already on.")}`);
|
|
90
|
+
const window = describeWindow(existing);
|
|
91
|
+
if (window) console.log(` Window: ${window}`);
|
|
92
|
+
console.log(` ${color.cyan("niro ab report")} for the comparison so far.\n`);
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (!opts.yes) {
|
|
97
|
+
console.log(`\n This runs Niro ${color.bold("off")} for half your sessions for ${days} days,`);
|
|
98
|
+
console.log(" so the two halves can be compared. It applies to your whole account,");
|
|
99
|
+
console.log(" not just this repo, and you can stop it at any time.\n");
|
|
100
|
+
if (prompt.isNonInteractive()) {
|
|
101
|
+
console.error(fail(" No terminal to confirm on. Pass --yes if you mean it."));
|
|
102
|
+
process.exitCode = 1;
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (!(await prompt.confirm(" Start the measurement window?", false))) {
|
|
106
|
+
console.log("\n Left off. Nothing changed.\n");
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const run = await call("POST", "/api/abtest/mode/enable", {
|
|
112
|
+
query: { windowDays: days, name: opts.name || undefined },
|
|
113
|
+
});
|
|
114
|
+
console.log(`\n ${ok("✓")} A/B measurement is on for ${days} days.`);
|
|
115
|
+
const window = describeWindow(run);
|
|
116
|
+
if (window) console.log(` Window: ${window}`);
|
|
117
|
+
console.log(` Come back with ${color.cyan("niro ab report")}, or stop early with ${color.cyan("niro ab disable")}.\n`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function disable() {
|
|
121
|
+
const existing = await call("GET", "/api/abtest/mode").catch(() => null);
|
|
122
|
+
if (!isActive(existing)) {
|
|
123
|
+
console.log(`\n A/B measurement was already off. Nothing changed.\n`);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
await call("POST", "/api/abtest/mode/disable");
|
|
127
|
+
console.log(`\n ${ok("✓")} A/B measurement stopped. Every session uses Niro again.`);
|
|
128
|
+
console.log(` The result so far is still readable with ${color.cyan("niro ab report")}.\n`);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* The comparison, as two columns.
|
|
133
|
+
*
|
|
134
|
+
* Rows are only printed when the server actually returned a number for them: a median that
|
|
135
|
+
* never arrived must read as absent, not as zero, because zero is a real and very different
|
|
136
|
+
* measurement here (an off-bucket session genuinely makes 0 Niro tool calls).
|
|
137
|
+
*/
|
|
138
|
+
async function report(opts = {}) {
|
|
139
|
+
const r = await call("GET", "/api/abtest/report", {
|
|
140
|
+
query: opts.runId ? { runId: opts.runId } : undefined,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
if (opts.json) {
|
|
144
|
+
console.log(JSON.stringify(r, null, 2));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const on = r.niro_on || {};
|
|
149
|
+
const off = r.niro_off || {};
|
|
150
|
+
|
|
151
|
+
console.log(`\n A/B comparison${r.run_name ? `: ${r.run_name}` : ""}`);
|
|
152
|
+
const window = describeWindow(r);
|
|
153
|
+
if (window) console.log(color.dim(` ${window}${r.active ? ", still running" : ", ended"}`));
|
|
154
|
+
|
|
155
|
+
if (!r.total_sessions) {
|
|
156
|
+
console.log(`\n No sessions recorded yet${r.message ? `: ${r.message}` : "."}`);
|
|
157
|
+
console.log(" Use your assistant for a while, then run this again.\n");
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
console.log(`\n ${pad("", 34)}${pad("with Niro", 14)}without Niro`);
|
|
162
|
+
console.log(` ${color.dim("-".repeat(60))}`);
|
|
163
|
+
row("Sessions", on.session_count, off.session_count);
|
|
164
|
+
row("Median session time (ms)", on.median_session_ms, off.median_session_ms);
|
|
165
|
+
row("Median tool calls per session", on.median_tool_calls, off.median_tool_calls);
|
|
166
|
+
row("Median Niro tool calls", on.median_niro_tool_calls, off.median_niro_tool_calls);
|
|
167
|
+
row("Median context (bytes)", on.median_context_bytes, off.median_context_bytes);
|
|
168
|
+
console.log(`\n ${r.total_sessions} sessions in total.`);
|
|
169
|
+
if (!r.active) console.log(color.dim(" This window has ended."));
|
|
170
|
+
console.log("");
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function row(label, a, b) {
|
|
174
|
+
console.log(` ${pad(label, 34)}${pad(fmt(a), 14)}${fmt(b)}`);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function fmt(v) {
|
|
178
|
+
// Spelled out rather than a dash: this cell has to be distinguishable from 0 at a
|
|
179
|
+
// glance, and 0 is a real measurement here (an off-bucket session makes 0 Niro calls).
|
|
180
|
+
if (v === null || v === undefined) return color.dim("not measured");
|
|
181
|
+
return typeof v === "number" ? v.toLocaleString("en-GB") : String(v);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** Pads on the VISIBLE length, so a dimmed cell still lines up with a plain one. */
|
|
185
|
+
function pad(s, width) {
|
|
186
|
+
const visible = String(s).replace(/\x1b\[[0-9;]*m/g, "").length;
|
|
187
|
+
return String(s) + " ".repeat(Math.max(0, width - visible));
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** null for anything that is not a positive whole number of days. */
|
|
191
|
+
function parseDays(raw) {
|
|
192
|
+
if (raw === undefined || raw === null || raw === "") return DEFAULT_WINDOW_DAYS;
|
|
193
|
+
const n = Number(raw);
|
|
194
|
+
if (!Number.isInteger(n) || n <= 0) return null;
|
|
195
|
+
return n;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = { status, enable, disable, report, DEFAULT_WINDOW_DAYS, parseDays, pad, fmt };
|
|
@@ -47,7 +47,7 @@ async function addProject(opts = {}) {
|
|
|
47
47
|
localByGitId[entry.gitId] = entry;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
console.log(`\n
|
|
50
|
+
console.log(`\n ${color.bold("Available repos:")}`);
|
|
51
51
|
allRepos.forEach((repo, i) => {
|
|
52
52
|
const gitId = repo.id || repo.gitId;
|
|
53
53
|
const url = repo.repository_url || repo.repositoryUrl || "";
|
|
@@ -79,12 +79,12 @@ async function addProject(opts = {}) {
|
|
|
79
79
|
? color.dim(" last synced " + timeSince(local.lastSyncedAt))
|
|
80
80
|
: "";
|
|
81
81
|
|
|
82
|
-
console.log(`
|
|
82
|
+
console.log(` [${i + 1}] ${color.bold(displayName)} ${label}${syncInfo}`);
|
|
83
83
|
});
|
|
84
|
-
console.log(`
|
|
84
|
+
console.log(` [${allRepos.length + 1}] Register a new repo first (niro add repo)`);
|
|
85
85
|
|
|
86
86
|
const selectionRaw = await prompt.ask(
|
|
87
|
-
`\n
|
|
87
|
+
`\n Select repos to include (e.g. 1,3)`,
|
|
88
88
|
""
|
|
89
89
|
);
|
|
90
90
|
if (!selectionRaw.trim()) {
|
|
@@ -114,10 +114,10 @@ async function addProject(opts = {}) {
|
|
|
114
114
|
// on create below.
|
|
115
115
|
let alias;
|
|
116
116
|
while (true) {
|
|
117
|
-
const candidate = await prompt.ask("
|
|
117
|
+
const candidate = await prompt.ask(" Project name", defaultName);
|
|
118
118
|
if (!candidate) { console.error(color.red("Project name is required.")); return; }
|
|
119
119
|
if (await aliasTaken(candidate, opts)) {
|
|
120
|
-
console.log(color.yellow(`
|
|
120
|
+
console.log(color.yellow(` "${candidate}" already exists — pick another.`));
|
|
121
121
|
} else {
|
|
122
122
|
alias = candidate;
|
|
123
123
|
break;
|
|
@@ -130,7 +130,7 @@ async function addProject(opts = {}) {
|
|
|
130
130
|
gitIdToMapping[repo.id || repo.gitId] = {};
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
-
console.log(`\n
|
|
133
|
+
console.log(`\n Creating project "${color.bold(alias)}"...`);
|
|
134
134
|
const project = await api.post("/api/projects", {
|
|
135
135
|
alias,
|
|
136
136
|
git_id_to_mapping: gitIdToMapping,
|
|
@@ -148,11 +148,11 @@ async function addProject(opts = {}) {
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
console.log(`
|
|
151
|
+
console.log(` ${color.green("✓")} Project created: ${color.bold(alias)}`);
|
|
152
152
|
|
|
153
|
-
const shouldBuild = await prompt.confirm("
|
|
153
|
+
const shouldBuild = await prompt.confirm(" Trigger a full index build now?", true);
|
|
154
154
|
if (shouldBuild) {
|
|
155
|
-
console.log(`
|
|
155
|
+
console.log(` Triggering build...`);
|
|
156
156
|
await api.post(
|
|
157
157
|
`/api/graph/${encodeURIComponent(projectId)}/build`,
|
|
158
158
|
{},
|
|
@@ -161,7 +161,7 @@ async function addProject(opts = {}) {
|
|
|
161
161
|
if (opts.watch !== false) {
|
|
162
162
|
await watchStatus(projectId);
|
|
163
163
|
} else {
|
|
164
|
-
console.log(`
|
|
164
|
+
console.log(` Build triggered. Run ${color.cyan("niro status")} to track progress.`);
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
|
package/src/commands/add-repo.js
CHANGED
|
@@ -87,7 +87,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
87
87
|
const reuse = noConfirm
|
|
88
88
|
? true
|
|
89
89
|
: await prompt.confirm(
|
|
90
|
-
`
|
|
90
|
+
` This folder is already registered (gitId: ${color.dim(existing.gitId)}). Re-sync without re-registering?`,
|
|
91
91
|
true
|
|
92
92
|
);
|
|
93
93
|
if (reuse) {
|
|
@@ -101,7 +101,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
101
101
|
let branch = opts.branch || null;
|
|
102
102
|
|
|
103
103
|
if (gitInfo.repoRoot) {
|
|
104
|
-
console.log(`\n
|
|
104
|
+
console.log(`\n Detected a git repository.`);
|
|
105
105
|
let choice;
|
|
106
106
|
if (syncMode) {
|
|
107
107
|
choice = syncMode === "remote" ? "2" : "1";
|
|
@@ -109,14 +109,14 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
109
109
|
choice = "1"; // default to local sync without prompting
|
|
110
110
|
} else {
|
|
111
111
|
choice = await prompt.ask(
|
|
112
|
-
`
|
|
112
|
+
` Sync on: [1] local file changes [2] commits to remote`,
|
|
113
113
|
"1"
|
|
114
114
|
);
|
|
115
115
|
}
|
|
116
116
|
if (choice.trim() === "2") {
|
|
117
117
|
// Register as remote repo using the origin URL
|
|
118
118
|
if (!gitInfo.originUrl) {
|
|
119
|
-
console.error(color.red("
|
|
119
|
+
console.error(color.red(" No remote origin found. Cannot register as remote repo."));
|
|
120
120
|
process.exitCode = 1;
|
|
121
121
|
return;
|
|
122
122
|
}
|
|
@@ -128,10 +128,10 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
128
128
|
const confirmed = (noConfirm || prompt.isNonInteractive())
|
|
129
129
|
? true
|
|
130
130
|
: await prompt.confirm(
|
|
131
|
-
`
|
|
131
|
+
` Detected branch: ${color.cyan(gitInfo.branch)}. Use this?`,
|
|
132
132
|
true
|
|
133
133
|
);
|
|
134
|
-
branch = confirmed ? gitInfo.branch : await prompt.ask("
|
|
134
|
+
branch = confirmed ? gitInfo.branch : await prompt.ask(" Branch name", "main");
|
|
135
135
|
}
|
|
136
136
|
}
|
|
137
137
|
|
|
@@ -141,10 +141,10 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
141
141
|
const displayName = opts.name
|
|
142
142
|
|| ((noConfirm || prompt.isNonInteractive())
|
|
143
143
|
? folderName
|
|
144
|
-
: await prompt.ask("
|
|
144
|
+
: await prompt.ask(" Repo display name", folderName));
|
|
145
145
|
|
|
146
146
|
// Build manifest
|
|
147
|
-
process.stdout.write(`\n
|
|
147
|
+
process.stdout.write(`\n Scanning ${color.cyan(targetDir)}...`);
|
|
148
148
|
const manifest = folderSync.buildManifest(targetDir);
|
|
149
149
|
process.stdout.write(` ${manifest.length} files found.\n`);
|
|
150
150
|
|
|
@@ -154,7 +154,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
154
154
|
let excludePatterns;
|
|
155
155
|
|
|
156
156
|
// Register first (server allocates workspace dir) so we can call /filter
|
|
157
|
-
console.log(`
|
|
157
|
+
console.log(` Registering repo with server...`);
|
|
158
158
|
const cliModuleExcludes = Array.isArray(opts.moduleExclude) ? opts.moduleExclude : [];
|
|
159
159
|
const cliFileExcludes = Array.isArray(opts.fileExclude) ? opts.fileExclude : [];
|
|
160
160
|
gitId = await agentApi.registerRepo({
|
|
@@ -164,7 +164,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
164
164
|
moduleExcludePatterns: cliModuleExcludes,
|
|
165
165
|
fileExcludePatterns: cliFileExcludes,
|
|
166
166
|
});
|
|
167
|
-
console.log(`
|
|
167
|
+
console.log(` gitId: ${color.dim(gitId)}`);
|
|
168
168
|
|
|
169
169
|
// Offer backend-suggested module exclusions (skipped if --defaults or
|
|
170
170
|
// --module-exclude already supplied or stdin is non-interactive).
|
|
@@ -172,31 +172,31 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
172
172
|
await offerModuleExcludeSuggestions(gitId, opts, cliModuleExcludes);
|
|
173
173
|
|
|
174
174
|
// Get filter recommendation
|
|
175
|
-
console.log(`
|
|
175
|
+
console.log(` Analyzing project structure...`);
|
|
176
176
|
const filterResult = await agentApi.filterManifest(gitId, manifest);
|
|
177
177
|
folderSync.warnOversizeFiles(manifest, filterResult, (m) => console.log(color.yellow(m)));
|
|
178
178
|
const includedCount = filterResult.included_count || filterResult.includedCount || 0;
|
|
179
179
|
const excludedCount = filterResult.excluded_count || filterResult.excludedCount || 0;
|
|
180
180
|
|
|
181
|
-
console.log(`\n
|
|
182
|
-
console.log(`
|
|
181
|
+
console.log(`\n ${color.green("Will sync:")} ${includedCount} files`);
|
|
182
|
+
console.log(` ${color.yellow("Excluded:")} ${excludedCount} files`);
|
|
183
183
|
|
|
184
184
|
const suggestedIncludes = filterResult.include_patterns || filterResult.includePatterns || [];
|
|
185
185
|
const suggestedExcludes = filterResult.exclude_patterns || filterResult.excludePatterns || [];
|
|
186
186
|
|
|
187
187
|
if (suggestedIncludes.length > 0) {
|
|
188
|
-
console.log(`
|
|
188
|
+
console.log(` Include patterns: ${color.dim(suggestedIncludes.join(", "))}`);
|
|
189
189
|
}
|
|
190
190
|
if (suggestedExcludes.length > 0) {
|
|
191
|
-
console.log(`
|
|
191
|
+
console.log(` Exclude patterns: ${color.dim(suggestedExcludes.join(", "))}`);
|
|
192
192
|
}
|
|
193
193
|
|
|
194
194
|
const adjust = (noConfirm || prompt.isNonInteractive())
|
|
195
195
|
? false
|
|
196
|
-
: await prompt.confirm("
|
|
196
|
+
: await prompt.confirm(" Adjust patterns?", false);
|
|
197
197
|
if (adjust) {
|
|
198
|
-
const addIncludes = await prompt.ask("
|
|
199
|
-
const addExcludes = await prompt.ask("
|
|
198
|
+
const addIncludes = await prompt.ask(" Add include patterns (comma-separated, e.g. src/**)", "");
|
|
199
|
+
const addExcludes = await prompt.ask(" Add exclude patterns (comma-separated, e.g. docs/**)", "");
|
|
200
200
|
if (addIncludes) suggestedIncludes.push(...addIncludes.split(",").map(s => s.trim()).filter(Boolean));
|
|
201
201
|
if (addExcludes) suggestedExcludes.push(...addExcludes.split(",").map(s => s.trim()).filter(Boolean));
|
|
202
202
|
}
|
|
@@ -228,7 +228,7 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
228
228
|
excluded: excludePatterns,
|
|
229
229
|
});
|
|
230
230
|
} catch (err) {
|
|
231
|
-
console.log(color.yellow(`
|
|
231
|
+
console.log(color.yellow(` ⚠ Could not save patterns to server: ${err.message}`));
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
|
|
@@ -244,12 +244,12 @@ async function addRepo(pathArg, opts = {}) {
|
|
|
244
244
|
async function addRemoteRepo(url, opts) {
|
|
245
245
|
const noConfirm = opts.confirm === false;
|
|
246
246
|
const branch = opts.branch
|
|
247
|
-
|| ((noConfirm || prompt.isNonInteractive()) ? "main" : await prompt.ask("
|
|
247
|
+
|| ((noConfirm || prompt.isNonInteractive()) ? "main" : await prompt.ask(" Branch", "main"));
|
|
248
248
|
const tokenId = opts.tokenId || null;
|
|
249
249
|
const cliModuleExcludes = Array.isArray(opts.moduleExclude) ? opts.moduleExclude : [];
|
|
250
250
|
const cliFileExcludes = Array.isArray(opts.fileExclude) ? opts.fileExclude : [];
|
|
251
251
|
|
|
252
|
-
console.log(`
|
|
252
|
+
console.log(` Registering remote repo: ${color.cyan(url)} (branch: ${branch})`);
|
|
253
253
|
const reqBody = { repository_url: url, branch, token_id: tokenId };
|
|
254
254
|
if (cliModuleExcludes.length > 0) reqBody.module_exclude_patterns = cliModuleExcludes;
|
|
255
255
|
if (cliFileExcludes.length > 0) reqBody.file_exclude_patterns = cliFileExcludes;
|
|
@@ -258,12 +258,12 @@ async function addRemoteRepo(url, opts) {
|
|
|
258
258
|
if (!rec || !rec.gitId) {
|
|
259
259
|
throw new Error(`Failed to register remote repo: ${(rec && rec.status) || "unknown error"}`);
|
|
260
260
|
}
|
|
261
|
-
console.log(`
|
|
261
|
+
console.log(` ${color.green("✓")} Remote repo registered. gitId: ${color.dim(rec.gitId)}`);
|
|
262
262
|
|
|
263
263
|
const { offerModuleExcludeSuggestions } = require("../lib/moduleExcludeSuggestions");
|
|
264
264
|
await offerModuleExcludeSuggestions(rec.gitId, opts, cliModuleExcludes);
|
|
265
265
|
|
|
266
|
-
console.log(`
|
|
266
|
+
console.log(` Run ${color.cyan("niro add project")} to attach it to a project.`);
|
|
267
267
|
}
|
|
268
268
|
|
|
269
269
|
/** Parse --sync-mode local|remote. Returns the mode, or null if unset. Throws on garbage. */
|
package/src/commands/build.js
CHANGED
|
@@ -25,7 +25,7 @@ async function build(projectIdArg, opts = {}) {
|
|
|
25
25
|
if (asJson) {
|
|
26
26
|
emitJson({ event: "triggered", project_id: projectId, force_full_rebuild: force });
|
|
27
27
|
} else {
|
|
28
|
-
console.log(
|
|
28
|
+
console.log(` ${color.green("✓")} Build triggered for ${color.bold(projectId)}.`);
|
|
29
29
|
}
|
|
30
30
|
if (opts.watch === false) return;
|
|
31
31
|
await watchStatus(projectId, { json: asJson });
|
|
@@ -75,7 +75,7 @@ async function watchStatusJson(projectId) {
|
|
|
75
75
|
}
|
|
76
76
|
|
|
77
77
|
async function watchStatusHuman(projectId) {
|
|
78
|
-
const sp = spinner.start(`Waiting for build to start... [${projectId}]`);
|
|
78
|
+
const sp = spinner.start(` Waiting for build to start... [${projectId}]`);
|
|
79
79
|
let lastStatus = null;
|
|
80
80
|
let lastLog = null;
|
|
81
81
|
try {
|
|
@@ -84,7 +84,7 @@ async function watchStatusHuman(projectId) {
|
|
|
84
84
|
try {
|
|
85
85
|
data = await api.get(`/api/graph/${encodeURIComponent(projectId)}/status`);
|
|
86
86
|
} catch (err) {
|
|
87
|
-
sp.stop(`Failed to fetch status: ${err.message}`);
|
|
87
|
+
sp.stop(` Failed to fetch status: ${err.message}`);
|
|
88
88
|
process.exitCode = 1;
|
|
89
89
|
return;
|
|
90
90
|
}
|
|
@@ -92,7 +92,7 @@ async function watchStatusHuman(projectId) {
|
|
|
92
92
|
const logMessage = data.log_message || data.logMessage;
|
|
93
93
|
if (st !== lastStatus || logMessage !== lastLog) {
|
|
94
94
|
const coloredSt = colorizeStatus(st);
|
|
95
|
-
const label = logMessage ? `${coloredSt}
|
|
95
|
+
const label = ` ${logMessage ? `${coloredSt}: ${logMessage}` : coloredSt}`;
|
|
96
96
|
sp.update(label);
|
|
97
97
|
lastStatus = st;
|
|
98
98
|
lastLog = logMessage;
|
|
@@ -100,7 +100,7 @@ async function watchStatusHuman(projectId) {
|
|
|
100
100
|
if (TERMINAL.has(st)) {
|
|
101
101
|
const mark = st === "COMPLETED" ? color.green("✓") : color.red("✗");
|
|
102
102
|
const tint = st === "COMPLETED" ? color.green : st === "FAILED" || st === "CANCELLED" ? color.red : (s) => s;
|
|
103
|
-
sp.stop(
|
|
103
|
+
sp.stop(` ${mark} Build ${tint(st.toLowerCase())}.`);
|
|
104
104
|
if (st !== "COMPLETED") process.exitCode = 1;
|
|
105
105
|
return;
|
|
106
106
|
}
|
package/src/commands/delete.js
CHANGED
|
@@ -22,7 +22,7 @@ async function deleteProject(projectIdArg, opts = {}) {
|
|
|
22
22
|
|
|
23
23
|
if (!opts.yes) {
|
|
24
24
|
console.log(`${color.yellow("⚠")} About to delete project ${color.bold(projectId)}.`);
|
|
25
|
-
console.log(color.dim("
|
|
25
|
+
console.log(color.dim(" This removes the project and its graph from the server. Registered repos stay."));
|
|
26
26
|
const typed = await prompt.ask(`Type the project name to confirm`, "");
|
|
27
27
|
if (typed !== projectId) {
|
|
28
28
|
console.log(`${color.red("✗")} Project name did not match — aborted.`);
|
|
@@ -53,9 +53,9 @@ function unpinIfMatches(projectId) {
|
|
|
53
53
|
if (fs.existsSync(p)) {
|
|
54
54
|
try {
|
|
55
55
|
fs.unlinkSync(p);
|
|
56
|
-
console.log(`
|
|
56
|
+
console.log(` Removed local pin at ${color.dim(p)}`);
|
|
57
57
|
} catch (err) {
|
|
58
|
-
console.log(color.yellow(`
|
|
58
|
+
console.log(color.yellow(` Could not remove ${p}: ${err.message}`));
|
|
59
59
|
}
|
|
60
60
|
return;
|
|
61
61
|
}
|
package/src/commands/edit.js
CHANGED
|
@@ -218,7 +218,7 @@ async function setEnv(projectId, pairs, opts) {
|
|
|
218
218
|
console.log(JSON.stringify({ project_id: projectId, git_id: gitId, changed_keys: changedKeys }));
|
|
219
219
|
} else {
|
|
220
220
|
console.log(`${color.green("✓")} Updated env var(s) on repo ${color.dim(gitId)}: ${changedKeys.map((k) => color.bold(k)).join(", ")}`);
|
|
221
|
-
console.log(color.dim("
|
|
221
|
+
console.log(color.dim(" (values are not shown)"));
|
|
222
222
|
}
|
|
223
223
|
}
|
|
224
224
|
|
|
@@ -291,12 +291,12 @@ async function listEnv(projectId, opts) {
|
|
|
291
291
|
const appears = s.appears_in > 1
|
|
292
292
|
? color.yellow(` (appears in ${s.appears_in} files)`)
|
|
293
293
|
: (s.appears_in === 1 ? color.dim(" (1 file)") : "");
|
|
294
|
-
console.log(`
|
|
294
|
+
console.log(` - ${color.bold(s.key)}${set}${src}${appears}`);
|
|
295
295
|
if (s.appears_in > 1) {
|
|
296
296
|
for (const c of s.candidates) {
|
|
297
297
|
const shown = c.value == null ? color.dim("(empty)") : c.value;
|
|
298
298
|
const marker = c.is_current ? color.green(" [current]") : "";
|
|
299
|
-
console.log(`
|
|
299
|
+
console.log(` ${color.dim("•")} ${c.source_file}: ${shown}${marker}`);
|
|
300
300
|
}
|
|
301
301
|
}
|
|
302
302
|
}
|
|
@@ -583,14 +583,14 @@ function maskPair(pair) {
|
|
|
583
583
|
|
|
584
584
|
function printUsage() {
|
|
585
585
|
console.error("niro edit — change an existing project. At least one action is required:");
|
|
586
|
-
console.error("
|
|
587
|
-
console.error("
|
|
588
|
-
console.error("
|
|
589
|
-
console.error("
|
|
590
|
-
console.error("
|
|
591
|
-
console.error("
|
|
592
|
-
console.error("
|
|
593
|
-
console.error("
|
|
586
|
+
console.error(" --add-repo <gitId> map a registered repo to the project");
|
|
587
|
+
console.error(" --remove-repo <gitId> unmap a repo (non-destructive)");
|
|
588
|
+
console.error(" --set-env KEY=VALUE set an env var (repeatable; value never printed)");
|
|
589
|
+
console.error(" --set-env-from KEY=FILE adopt the value FILE carries for KEY (repeatable; value never printed)");
|
|
590
|
+
console.error(" --active-files <files> property files to keep active for env-var defaults; the rest are ignored (default: all)");
|
|
591
|
+
console.error(" --list-env list env var keys + per-file candidates (secret values masked)");
|
|
592
|
+
console.error(" --rescan-env rescan the repo for env vars");
|
|
593
|
+
console.error(" --alias <newAlias> rename the project");
|
|
594
594
|
console.error("Target project: --project <alias|id>, or a .niro-project pin in this folder.");
|
|
595
595
|
console.error("Env actions target --repo <gitId> (auto-selected when the project has one repo).");
|
|
596
596
|
}
|
package/src/commands/info.js
CHANGED
|
@@ -218,7 +218,7 @@ function printHuman(p) {
|
|
|
218
218
|
if (proj.status) bits.push(`status: ${proj.status}`);
|
|
219
219
|
if (proj.doc_gen_status) bits.push(`docs: ${proj.doc_gen_status}`);
|
|
220
220
|
if (proj.updated_at) bits.push(`updated: ${proj.updated_at}`);
|
|
221
|
-
console.log(`
|
|
221
|
+
console.log(` - ${name}${bits.length ? " — " + color.dim(bits.join(", ")) : ""}`);
|
|
222
222
|
for (const f of proj.folders || []) {
|
|
223
223
|
// Marked so the folder you are STANDING IN is obvious at a glance; the whole point is telling
|
|
224
224
|
// "this worktree is the one Niro reflects" from "this one is not".
|
|
@@ -233,7 +233,7 @@ function printHuman(p) {
|
|
|
233
233
|
temp = color.dim(` via temporary project ${f.temp_project_id}`)
|
|
234
234
|
+ (f.reflected === null ? color.dim(" (could not confirm)") : "");
|
|
235
235
|
}
|
|
236
|
-
console.log(color.dim(`
|
|
236
|
+
console.log(color.dim(` syncing from ${f.path}`) +
|
|
237
237
|
(f.branch ? color.dim(` on branch ${f.branch}`) : "") + temp + here);
|
|
238
238
|
}
|
|
239
239
|
}
|