@niroai/niro 0.9.0 → 0.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +69 -0
- package/README.md +55 -1
- package/package.json +1 -1
- package/src/commands/ab.js +198 -0
- package/src/commands/add-project.js +11 -11
- package/src/commands/add-repo.js +23 -23
- package/src/commands/build.js +5 -5
- package/src/commands/delete.js +3 -3
- package/src/commands/edit.js +11 -11
- package/src/commands/info.js +2 -2
- package/src/commands/init.js +108 -87
- package/src/commands/login.js +25 -5
- package/src/commands/projects.js +1 -1
- package/src/commands/release.js +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
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
|
}
|
package/src/commands/init.js
CHANGED
|
@@ -41,6 +41,12 @@ const color = require("../lib/color");
|
|
|
41
41
|
const watchDaemon = require("../lib/watchDaemon");
|
|
42
42
|
const { login } = require("./login");
|
|
43
43
|
const { watchStatus } = require("./build");
|
|
44
|
+
const { currentIndent } = require("../lib/nestedOutput");
|
|
45
|
+
|
|
46
|
+
/** Depth as spaces, for the \r-written lines that bypass withIndent's console wrappers. */
|
|
47
|
+
function nestPad() {
|
|
48
|
+
return " ".repeat(currentIndent());
|
|
49
|
+
}
|
|
44
50
|
const { fetchPreview, buildCandidates } = require("./edit");
|
|
45
51
|
const { resolveProjectId } = require("./_resolveProject");
|
|
46
52
|
const { projectsToRows } = require("./projects");
|
|
@@ -52,6 +58,13 @@ const warn = (s) => color.yellow(s);
|
|
|
52
58
|
const fail = (s) => color.red(s);
|
|
53
59
|
|
|
54
60
|
async function init(pathArg, opts = {}) {
|
|
61
|
+
// Set by `niro start`, which frames its own steps around this one. Two things then change,
|
|
62
|
+
// both presentation only: the internal 1/3..3/3 markers are dropped (they collided with the
|
|
63
|
+
// wizard's own 4/5 and read as a second, conflicting count), and the closing banner is left
|
|
64
|
+
// to the wizard, which was otherwise telling the user to run `niro mcp install` immediately
|
|
65
|
+
// before doing it for them.
|
|
66
|
+
const nested = !!opts.nested;
|
|
67
|
+
const substep = (n) => (nested ? step("●") : step(n));
|
|
55
68
|
const targetDir = resolveTargetDir(pathArg);
|
|
56
69
|
// Validate flag values up-front so a bad --auto-build/--confirm-build/--quiet-period
|
|
57
70
|
// fails BEFORE any server-side mutation (otherwise a typo leaves an orphan project).
|
|
@@ -143,10 +156,10 @@ async function init(pathArg, opts = {}) {
|
|
|
143
156
|
// inconclusive — make the user (or an agent, via --force) confirm rather than proceed.
|
|
144
157
|
if (gitInfo.originUrl && (resolveFailed || ctx.state === "error")) {
|
|
145
158
|
console.log(warn(
|
|
146
|
-
`\n
|
|
159
|
+
`\n ⚠ Couldn't verify whether this repo is already registered` +
|
|
147
160
|
`${ctx.error ? ` (${ctx.error})` : ""}.`
|
|
148
161
|
));
|
|
149
|
-
if (!(await requireProceed("
|
|
162
|
+
if (!(await requireProceed(" Proceed without the duplicate check?", opts))) {
|
|
150
163
|
process.exitCode = 1;
|
|
151
164
|
return;
|
|
152
165
|
}
|
|
@@ -181,7 +194,7 @@ async function init(pathArg, opts = {}) {
|
|
|
181
194
|
branch: defaultBranch || null,
|
|
182
195
|
tracked: true, // locally-tracked for detection only; no gitId => daemon never syncs it
|
|
183
196
|
}, currentApiUrl);
|
|
184
|
-
console.log(color.dim("
|
|
197
|
+
console.log(color.dim(" Niro is now tracking this folder locally (no sandbox) so cross-repo answers can include it."));
|
|
185
198
|
}
|
|
186
199
|
|
|
187
200
|
if (onIndexedBranch.length) {
|
|
@@ -192,12 +205,12 @@ async function init(pathArg, opts = {}) {
|
|
|
192
205
|
if (!opts.addTo) {
|
|
193
206
|
if (prompt.isNonInteractive()) {
|
|
194
207
|
console.log(color.dim(
|
|
195
|
-
"
|
|
208
|
+
" Already indexed — nothing to do. Pass --add-to <project> to also map it elsewhere, or run 'niro build' to re-index."
|
|
196
209
|
));
|
|
197
210
|
return;
|
|
198
211
|
}
|
|
199
|
-
if (!(await prompt.confirm("
|
|
200
|
-
console.log(color.dim("
|
|
212
|
+
if (!(await prompt.confirm(" Add it to a DIFFERENT project too?", false))) {
|
|
213
|
+
console.log(color.dim(" Nothing to do — use 'niro build' to re-index or 'niro edit' to manage it."));
|
|
201
214
|
return;
|
|
202
215
|
}
|
|
203
216
|
}
|
|
@@ -209,17 +222,17 @@ async function init(pathArg, opts = {}) {
|
|
|
209
222
|
.join(", ");
|
|
210
223
|
const yourBranch = defaultBranch ? `\`${defaultBranch}\`` : "your current branch";
|
|
211
224
|
console.log(`\n${color.green("✓")} This repo is part of: ${where}.`);
|
|
212
|
-
console.log(`
|
|
213
|
-
console.log(`
|
|
225
|
+
console.log(` But ${yourBranch} isn't indexed by Niro — answers would reflect the indexed branch, not your local work.`);
|
|
226
|
+
console.log(` ${color.cyan("Run `niro sandbox` here")} to index your branch privately ` +
|
|
214
227
|
color.dim("(original project unchanged; `niro discard` to undo)."));
|
|
215
228
|
if (!opts.addTo) {
|
|
216
229
|
if (prompt.isNonInteractive()) {
|
|
217
230
|
console.log(color.dim(
|
|
218
|
-
"
|
|
231
|
+
" Pass --add-to <project> to map this repo into a DIFFERENT project, or run 'niro sandbox' for your branch."
|
|
219
232
|
));
|
|
220
233
|
return;
|
|
221
234
|
}
|
|
222
|
-
if (!(await prompt.confirm("
|
|
235
|
+
if (!(await prompt.confirm(" Add this repo to a DIFFERENT project instead?", false))) {
|
|
223
236
|
return;
|
|
224
237
|
}
|
|
225
238
|
}
|
|
@@ -232,29 +245,29 @@ async function init(pathArg, opts = {}) {
|
|
|
232
245
|
// catches children indexed via the web UI / as separate repos.
|
|
233
246
|
const childScan = await scanIndexedChildren(targetDir, opts);
|
|
234
247
|
if (childScan.indexed.length) {
|
|
235
|
-
console.error(fail(`\n
|
|
248
|
+
console.error(fail(`\n This folder contains ${childScan.indexed.length} repo(s) already indexed in Niro:`));
|
|
236
249
|
for (const c of childScan.indexed) {
|
|
237
250
|
const where = c.projects.map((p) => `"${p.alias || p.project_id}"`).join(", ");
|
|
238
|
-
console.error(`
|
|
251
|
+
console.error(` ${c.name} -> ${where}`);
|
|
239
252
|
}
|
|
240
|
-
console.error("
|
|
241
|
-
console.error("
|
|
253
|
+
console.error(" A parent of already-indexed repos can't be onboarded — it would upload that content again.");
|
|
254
|
+
console.error(" Onboard each repo from its own folder instead.");
|
|
242
255
|
// Hard refuse: there's no legitimate reason to upload a parent over indexed repos.
|
|
243
256
|
// --force stays as a deliberate escape hatch for scripts/agents that really mean it.
|
|
244
257
|
if (!opts.force) {
|
|
245
258
|
process.exitCode = 1;
|
|
246
259
|
return;
|
|
247
260
|
}
|
|
248
|
-
console.log(warn("
|
|
261
|
+
console.log(warn(" --force set — onboarding the parent anyway."));
|
|
249
262
|
}
|
|
250
263
|
if (childScan.unverified.length) {
|
|
251
264
|
console.log(warn(
|
|
252
|
-
`
|
|
265
|
+
` ⚠ Couldn't check ${childScan.unverified.length} nested repo(s) ` +
|
|
253
266
|
`(${childScan.unverified.map((c) => c.name).join(", ")}) — they may also be indexed.`
|
|
254
267
|
));
|
|
255
268
|
}
|
|
256
269
|
if (childScan.truncated) {
|
|
257
|
-
console.log(color.dim(`
|
|
270
|
+
console.log(color.dim(` (Stopped after the first ${CHILD_SCAN_MAX} nested repos; deeper ones weren't checked.)`));
|
|
258
271
|
}
|
|
259
272
|
const indexedChildPaths = new Set(childScan.indexed.map((c) => path.resolve(c.path)));
|
|
260
273
|
|
|
@@ -307,17 +320,17 @@ async function init(pathArg, opts = {}) {
|
|
|
307
320
|
const crossRemote = ctxProjects.find((p) => p.source_type === "REMOTE_GIT");
|
|
308
321
|
if (crossUpload) {
|
|
309
322
|
console.log(warn(
|
|
310
|
-
`\n
|
|
323
|
+
`\n ⚠ This repo is already registered in project "${crossUpload.project_id}"; ` +
|
|
311
324
|
`reusing that repo, attaching to "${projectId}" without re-uploading.`
|
|
312
325
|
));
|
|
313
326
|
reuseGitId = crossUpload.git_id;
|
|
314
327
|
skipUpload = true;
|
|
315
328
|
} else if (crossRemote) {
|
|
316
329
|
console.log(warn(
|
|
317
|
-
`\n
|
|
330
|
+
`\n ⚠ This repo is already a REMOTE repo in project "${crossRemote.project_id}"; ` +
|
|
318
331
|
`proceeding uploads a SEPARATE local copy.`
|
|
319
332
|
));
|
|
320
|
-
if (!(await requireProceed("
|
|
333
|
+
if (!(await requireProceed(" Proceed and upload a separate local copy?", opts))) {
|
|
321
334
|
process.exitCode = 1;
|
|
322
335
|
return;
|
|
323
336
|
}
|
|
@@ -334,10 +347,10 @@ async function init(pathArg, opts = {}) {
|
|
|
334
347
|
return;
|
|
335
348
|
}
|
|
336
349
|
console.log(warn(
|
|
337
|
-
`\n
|
|
350
|
+
`\n ⚠ This folder overlaps "${o.path}"` +
|
|
338
351
|
`${o.projectId ? ` (in project "${o.projectId}")` : ""}.`
|
|
339
352
|
));
|
|
340
|
-
if (!(await requireProceed("
|
|
353
|
+
if (!(await requireProceed(" Proceed despite the folder overlap?", opts))) {
|
|
341
354
|
process.exitCode = 1;
|
|
342
355
|
return;
|
|
343
356
|
}
|
|
@@ -357,27 +370,27 @@ async function init(pathArg, opts = {}) {
|
|
|
357
370
|
// reuseGitId came straight from resolveContext (the current backend), so it
|
|
358
371
|
// is known-good — no existence check needed.
|
|
359
372
|
gitId = reuseGitId;
|
|
360
|
-
console.log(`\n
|
|
373
|
+
console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
|
|
361
374
|
} else if (existing && existing.gitId) {
|
|
362
375
|
if (ctxProjects.some((p) => p.git_id === existing.gitId)) {
|
|
363
376
|
// Already mapped to >=1 project on THIS backend -> fully onboarded, and the
|
|
364
377
|
// server just vouched for it via resolveContext. Reuse, skip the upload.
|
|
365
378
|
gitId = existing.gitId;
|
|
366
379
|
skipUpload = true;
|
|
367
|
-
console.log(`\n
|
|
380
|
+
console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}); skipping re-upload.`);
|
|
368
381
|
} else if (await agentApi.repoExists(existing.gitId)) {
|
|
369
382
|
// Registered but not yet in any project (partial onboarding interrupted
|
|
370
383
|
// before the project POST). The repo still exists on this backend -> finish
|
|
371
384
|
// it by uploading.
|
|
372
385
|
gitId = existing.gitId;
|
|
373
|
-
console.log(`\n
|
|
386
|
+
console.log(`\n ${step("●")} Reusing already-registered repo (gitId ${color.dim(gitId)}).`);
|
|
374
387
|
} else {
|
|
375
388
|
// Self-heal: the cached gitId does NOT exist on the current backend — the
|
|
376
389
|
// backend was switched (cached gitId belongs to another stack) or the repo
|
|
377
390
|
// was deleted. Drop the stale entry and register fresh instead of crashing
|
|
378
391
|
// on the first call with a dead gitId.
|
|
379
392
|
console.log(warn(
|
|
380
|
-
`\n
|
|
393
|
+
`\n ⚠ Cached repo (gitId ${existing.gitId}) is not on this backend — ` +
|
|
381
394
|
`re-registering it here.`
|
|
382
395
|
));
|
|
383
396
|
watchState.remove(targetDir);
|
|
@@ -407,7 +420,7 @@ async function init(pathArg, opts = {}) {
|
|
|
407
420
|
fileHashes: {},
|
|
408
421
|
lastSyncedAt: null,
|
|
409
422
|
}, currentApiUrl);
|
|
410
|
-
console.log(`\n
|
|
423
|
+
console.log(`\n ${step("●")} Registered repo for upload (gitId ${color.dim(gitId)}).`);
|
|
411
424
|
}
|
|
412
425
|
|
|
413
426
|
// UPLOAD STEP — skipped entirely when reusing a repo that is ALREADY mapped to a
|
|
@@ -419,7 +432,7 @@ async function init(pathArg, opts = {}) {
|
|
|
419
432
|
|
|
420
433
|
// Build the manifest, surface oversize files (the 256 KiB server cap skips
|
|
421
434
|
// them), then ask the server which files to include.
|
|
422
|
-
process.stdout.write(`\n
|
|
435
|
+
process.stdout.write(`\n${nestPad()} Scanning ${color.cyan(targetDir)}...`);
|
|
423
436
|
const manifest = folderSync.buildManifest(targetDir);
|
|
424
437
|
process.stdout.write(` ${manifest.length} files found.\n`);
|
|
425
438
|
const filterResult = await agentApi.filterManifest(gitId, manifest);
|
|
@@ -430,7 +443,7 @@ async function init(pathArg, opts = {}) {
|
|
|
430
443
|
const excludedCount = filterResult.excluded_count || filterResult.excludedCount || 0;
|
|
431
444
|
const includePatterns = filterResult.include_patterns || filterResult.includePatterns || [];
|
|
432
445
|
const excludePatterns = filterResult.exclude_patterns || filterResult.excludePatterns || [];
|
|
433
|
-
console.log(`
|
|
446
|
+
console.log(` ${ok("Will sync:")} ${includedCount} files ${warn("Excluded:")} ${excludedCount} files`);
|
|
434
447
|
|
|
435
448
|
// Push include/exclude patterns to the server so the build pipeline respects
|
|
436
449
|
// them. module_path "" (root) — there is no build-file-detected module yet.
|
|
@@ -442,18 +455,18 @@ async function init(pathArg, opts = {}) {
|
|
|
442
455
|
excluded: excludePatterns,
|
|
443
456
|
});
|
|
444
457
|
} catch (err) {
|
|
445
|
-
console.log(warn(`
|
|
458
|
+
console.log(warn(` ⚠ Could not save patterns to server: ${err.message}`));
|
|
446
459
|
}
|
|
447
460
|
}
|
|
448
461
|
watchState.update(targetDir, { includePatterns, excludePatterns });
|
|
449
462
|
|
|
450
|
-
console.log(`\n
|
|
463
|
+
console.log(`\n ${substep("1/3")} Uploading ${includedPaths.length} files (branch ${color.cyan(defaultBranch || "detached HEAD")})...`);
|
|
451
464
|
// Reuse the sizes the manifest already computed so the aggregate guard does not re-stat every file.
|
|
452
465
|
const sizes = {};
|
|
453
466
|
for (const m of manifest) sizes[m.path] = m.size_bytes;
|
|
454
467
|
await folderSync.doInitialSync(gitId, targetDir, includedPaths, { sizes });
|
|
455
468
|
} else {
|
|
456
|
-
console.log(`
|
|
469
|
+
console.log(` ${substep("1/3")} ${ok("Skipping upload — the server already has this repo's files.")}`);
|
|
457
470
|
}
|
|
458
471
|
|
|
459
472
|
let project;
|
|
@@ -462,13 +475,13 @@ async function init(pathArg, opts = {}) {
|
|
|
462
475
|
// same git-mappings endpoint `niro edit --add-repo` uses. Do NOT create a
|
|
463
476
|
// project. The discovered env vars come back via the preview fetch below,
|
|
464
477
|
// not from this response (POST git-mappings returns the updated project).
|
|
465
|
-
console.log(`
|
|
478
|
+
console.log(` ${substep("2/3")} Adding repo to existing project "${color.bold(projectId)}" and scanning for environment variables...`);
|
|
466
479
|
project = await api.post(
|
|
467
480
|
`/api/projects/${encodeURIComponent(projectId)}/git-mappings`,
|
|
468
481
|
{ [gitId]: {} }
|
|
469
482
|
);
|
|
470
483
|
} else {
|
|
471
|
-
console.log(`
|
|
484
|
+
console.log(` ${substep("2/3")} Creating project "${color.bold(projectId)}" and scanning for environment variables...`);
|
|
472
485
|
// Send the chosen name as `alias` (account-scoped, duplicates across
|
|
473
486
|
// accounts allowed by design) — never as `project_id`. Letting the server
|
|
474
487
|
// assign the opaque project_id avoids colliding with an unrelated
|
|
@@ -488,13 +501,13 @@ async function init(pathArg, opts = {}) {
|
|
|
488
501
|
// tools / `niro build` defaults) follow the most recent one.
|
|
489
502
|
if (existing && existing.projectId && existing.projectId !== projectId) {
|
|
490
503
|
console.log(warn(
|
|
491
|
-
`
|
|
504
|
+
` ⚠ This folder was pinned to "${existing.projectId}"; it stays mapped there, ` +
|
|
492
505
|
`but is now also attached to "${projectId}" and re-pinned to it.`
|
|
493
506
|
));
|
|
494
507
|
}
|
|
495
508
|
const { path: pinPath } = niroProjectFile.write(repoRoot, projectId, { overwrite: true });
|
|
496
509
|
watchState.update(targetDir, { projectId });
|
|
497
|
-
console.log(`
|
|
510
|
+
console.log(` ${ok("✓")} Pinned project to ${color.dim(pinPath)}`);
|
|
498
511
|
|
|
499
512
|
let envVars = extractEnvVars(project, gitId);
|
|
500
513
|
// EXISTING mode attaches via POST /git-mappings, whose response may not carry the
|
|
@@ -518,9 +531,9 @@ async function init(pathArg, opts = {}) {
|
|
|
518
531
|
}
|
|
519
532
|
let filledVars = envVars;
|
|
520
533
|
if (envVars.length === 0) {
|
|
521
|
-
console.log("
|
|
534
|
+
console.log(" No env vars detected in this repo — skipping.");
|
|
522
535
|
} else if (opts.defaults) {
|
|
523
|
-
console.log(`
|
|
536
|
+
console.log(` Found ${envVars.length} env var(s); leaving all at defaults (--defaults).`);
|
|
524
537
|
} else if (prompt.isNonInteractive()) {
|
|
525
538
|
console.error(fail(
|
|
526
539
|
`Found ${envVars.length} env var(s) that need values and no terminal is available to prompt.\n` +
|
|
@@ -529,7 +542,7 @@ async function init(pathArg, opts = {}) {
|
|
|
529
542
|
process.exitCode = 1;
|
|
530
543
|
return;
|
|
531
544
|
} else {
|
|
532
|
-
console.log(`
|
|
545
|
+
console.log(` Found ${envVars.length} env var(s). When a var appears in several files, pick which file's value to use.`);
|
|
533
546
|
// Show, per var, the candidate value each committed file carries (source-file
|
|
534
547
|
// parity with the web UI). Reuse the preview fetched above when available; if a
|
|
535
548
|
// fresh fetch fails, fall back to a plain prompt rather than blocking onboarding.
|
|
@@ -542,7 +555,7 @@ async function init(pathArg, opts = {}) {
|
|
|
542
555
|
activePropertiesFiles = preview.activePropertiesFiles;
|
|
543
556
|
} catch (err) {
|
|
544
557
|
varsByFile = {};
|
|
545
|
-
console.log(warn(`
|
|
558
|
+
console.log(warn(` ⚠ Could not load env-var source files (${err.message}) — prompting without candidates.`));
|
|
546
559
|
}
|
|
547
560
|
}
|
|
548
561
|
|
|
@@ -569,9 +582,9 @@ async function init(pathArg, opts = {}) {
|
|
|
569
582
|
}
|
|
570
583
|
|
|
571
584
|
if (opts.defaults) {
|
|
572
|
-
console.log("
|
|
585
|
+
console.log(" Skipping indexing-scope review (--defaults).");
|
|
573
586
|
} else if (prompt.isNonInteractive()) {
|
|
574
|
-
console.log("
|
|
587
|
+
console.log(" Skipping indexing-scope review (no terminal to prompt; using detected defaults).");
|
|
575
588
|
} else {
|
|
576
589
|
await reviewIndexingScope(gitId);
|
|
577
590
|
}
|
|
@@ -582,7 +595,7 @@ async function init(pathArg, opts = {}) {
|
|
|
582
595
|
const quietPeriodSeconds = parseQuietPeriod(opts.quietPeriod, 30);
|
|
583
596
|
await saveAutoBuild(projectId, autoBuildFlag, quietPeriodSeconds);
|
|
584
597
|
} else if (opts.defaults) {
|
|
585
|
-
console.log("
|
|
598
|
+
console.log(" Leaving auto-build disabled (--defaults).");
|
|
586
599
|
} else if (prompt.isNonInteractive()) {
|
|
587
600
|
console.error(fail(
|
|
588
601
|
"Auto-build setting is required and no terminal is available to prompt.\n" +
|
|
@@ -609,12 +622,12 @@ async function init(pathArg, opts = {}) {
|
|
|
609
622
|
process.exitCode = 1;
|
|
610
623
|
return;
|
|
611
624
|
} else {
|
|
612
|
-
shouldBuild = await prompt.confirm("
|
|
625
|
+
shouldBuild = await prompt.confirm(" Trigger a full index build now?", true);
|
|
613
626
|
}
|
|
614
627
|
if (!shouldBuild) {
|
|
615
|
-
console.log(`
|
|
628
|
+
console.log(` ${warn("⚠")} Skipping build. Run ${color.cyan("niro build")} when you're ready.`);
|
|
616
629
|
} else {
|
|
617
|
-
console.log(`
|
|
630
|
+
console.log(` ${substep("3/3")} Triggering full index build...`);
|
|
618
631
|
await api.post(
|
|
619
632
|
`/api/graph/${encodeURIComponent(projectId)}/build`,
|
|
620
633
|
{},
|
|
@@ -622,7 +635,7 @@ async function init(pathArg, opts = {}) {
|
|
|
622
635
|
);
|
|
623
636
|
|
|
624
637
|
if (opts.watch === false) {
|
|
625
|
-
console.log(`
|
|
638
|
+
console.log(` Build triggered. Run ${color.cyan("niro status")} to check progress.`);
|
|
626
639
|
} else {
|
|
627
640
|
await watchStatus(projectId);
|
|
628
641
|
}
|
|
@@ -634,12 +647,20 @@ async function init(pathArg, opts = {}) {
|
|
|
634
647
|
// The closing banner must not read as an all-clear when auto-sync is dead —
|
|
635
648
|
// customers were only discovering the missing daemon after a reboot.
|
|
636
649
|
if (daemon.ok) {
|
|
637
|
-
|
|
650
|
+
if (nested) {
|
|
651
|
+
console.log(`\n ${ok("✓")} Indexed. Niro MCP tools launched from this repo will use this project.`);
|
|
652
|
+
} else {
|
|
653
|
+
console.log(`\n${ok("✓ Done.")} Niro MCP tools launched from this repo will now use this project automatically.`);
|
|
654
|
+
}
|
|
638
655
|
} else {
|
|
639
656
|
console.log(`\n${warn("✓ Done, with 1 warning:")} auto-sync is OFF — this folder will not stay in sync (see above for the fix).`);
|
|
640
657
|
console.log(`Niro MCP tools launched from this repo will still use this project automatically.`);
|
|
641
658
|
}
|
|
642
|
-
|
|
659
|
+
// The wizard prints its own closing screen, and step 5 IS the mcp install this line points
|
|
660
|
+
// at, so saying it here contradicts what happens two seconds later.
|
|
661
|
+
if (!nested) {
|
|
662
|
+
console.log(`Next: ${color.cyan("niro status")}, or ${color.cyan("niro mcp install")} to configure Claude Code / Cursor / Windsurf.`);
|
|
663
|
+
}
|
|
643
664
|
}
|
|
644
665
|
|
|
645
666
|
/**
|
|
@@ -654,7 +675,7 @@ async function init(pathArg, opts = {}) {
|
|
|
654
675
|
async function requireProceed(msg, opts = {}) {
|
|
655
676
|
if (opts.force) return true;
|
|
656
677
|
if (prompt.isNonInteractive()) {
|
|
657
|
-
console.error(fail("
|
|
678
|
+
console.error(fail(" Pass --force to proceed past this warning."));
|
|
658
679
|
return false;
|
|
659
680
|
}
|
|
660
681
|
return await prompt.confirm(msg, false);
|
|
@@ -797,7 +818,7 @@ async function fetchExistingProjects(opts) {
|
|
|
797
818
|
const data = await api.get("/api/projects", { apiUrl: opts.apiUrl });
|
|
798
819
|
return projectsToRows(data);
|
|
799
820
|
} catch (err) {
|
|
800
|
-
console.log(warn(`
|
|
821
|
+
console.log(warn(` ⚠ Could not load your existing projects (${err.message}) — creating a new project.`));
|
|
801
822
|
return [];
|
|
802
823
|
}
|
|
803
824
|
}
|
|
@@ -807,13 +828,13 @@ async function fetchExistingProjects(opts) {
|
|
|
807
828
|
* "NEW", an existing project_id, or null when the answer is invalid.
|
|
808
829
|
*/
|
|
809
830
|
async function chooseTarget(rows) {
|
|
810
|
-
console.log(`\n
|
|
811
|
-
console.log(`
|
|
831
|
+
console.log(`\n ${color.bold("New project, or add to an existing one?")}`);
|
|
832
|
+
console.log(` [n] create a new project`);
|
|
812
833
|
rows.forEach((r, i) => {
|
|
813
834
|
const label = r.alias ? `${r.alias} (${color.dim(r.project_id)})` : color.dim(r.project_id);
|
|
814
|
-
console.log(`
|
|
835
|
+
console.log(` [${i + 1}] ${label}`);
|
|
815
836
|
});
|
|
816
|
-
const answer = (await prompt.ask(`
|
|
837
|
+
const answer = (await prompt.ask(` Choose 'n' for new, or 1-${rows.length} to add to an existing project`, "n")).trim();
|
|
817
838
|
if (!answer || answer.toLowerCase() === "n") return "NEW";
|
|
818
839
|
const idx = Number.parseInt(answer, 10) - 1;
|
|
819
840
|
if (Number.isInteger(idx) && idx >= 0 && idx < rows.length) {
|
|
@@ -882,13 +903,13 @@ async function resolveAvailableProjectName({ defaultName, opts }) {
|
|
|
882
903
|
|
|
883
904
|
let candidate = defaultName;
|
|
884
905
|
while (true) {
|
|
885
|
-
const answer = await prompt.ask("Project name", candidate);
|
|
906
|
+
const answer = await prompt.ask(" Project name", candidate);
|
|
886
907
|
if (!answer) {
|
|
887
|
-
console.error(fail("Project name is required."));
|
|
908
|
+
console.error(fail(" Project name is required."));
|
|
888
909
|
return null;
|
|
889
910
|
}
|
|
890
911
|
if (!(await aliasTaken(answer, opts))) return answer;
|
|
891
|
-
console.log(`
|
|
912
|
+
console.log(` ${warn("⚠")} "${color.bold(answer)}" already exists on this account — pick another.`);
|
|
892
913
|
candidate = answer;
|
|
893
914
|
}
|
|
894
915
|
}
|
|
@@ -913,18 +934,18 @@ async function reviewIndexingScope(gitId) {
|
|
|
913
934
|
try {
|
|
914
935
|
details = await api.get(`/api/git/${encodeURIComponent(gitId)}`);
|
|
915
936
|
} catch (err) {
|
|
916
|
-
console.log(`
|
|
937
|
+
console.log(` Could not fetch project resolutions (${err.message}) — skipping scope review.`);
|
|
917
938
|
return;
|
|
918
939
|
}
|
|
919
940
|
const resolutions = (details && (details.project_resolutions || details.projectResolutions)) || {};
|
|
920
941
|
const modulePaths = Object.keys(resolutions);
|
|
921
942
|
if (modulePaths.length === 0) {
|
|
922
|
-
console.log("
|
|
943
|
+
console.log(" No module resolutions detected — skipping scope review.");
|
|
923
944
|
return;
|
|
924
945
|
}
|
|
925
946
|
|
|
926
|
-
console.log(`\n
|
|
927
|
-
console.log(color.dim(`
|
|
947
|
+
console.log(`\n ${color.bold("Review indexing scope")} (${modulePaths.length} module(s) detected).`);
|
|
948
|
+
console.log(color.dim(` Glob patterns use ** for recursive match. Press Enter to keep the detected set.`));
|
|
928
949
|
|
|
929
950
|
for (const modulePath of modulePaths) {
|
|
930
951
|
const resolution = resolutions[modulePath] || {};
|
|
@@ -933,12 +954,12 @@ async function reviewIndexingScope(gitId) {
|
|
|
933
954
|
const pkg = resolution.package_name || resolution.packageName;
|
|
934
955
|
const label = pkg ? `${modulePath} (${color.dim(pkg)})` : modulePath;
|
|
935
956
|
|
|
936
|
-
console.log(`\n
|
|
937
|
-
console.log(`
|
|
938
|
-
console.log(`
|
|
957
|
+
console.log(`\n ${color.bold("[" + label + "]")}`);
|
|
958
|
+
console.log(` ${color.green("Included")}: ${formatList(includeDirs)}`);
|
|
959
|
+
console.log(` ${color.yellow("Excluded")}: ${formatList(excluded)}`);
|
|
939
960
|
|
|
940
|
-
const addedIncludes = await promptGlobList("
|
|
941
|
-
const addedExcludes = await promptGlobList("
|
|
961
|
+
const addedIncludes = await promptGlobList(" Add inclusion patterns", GLOB_INCLUDE_EXAMPLES);
|
|
962
|
+
const addedExcludes = await promptGlobList(" Add exclusion patterns", GLOB_EXCLUDE_EXAMPLES);
|
|
942
963
|
|
|
943
964
|
if (addedIncludes.length === 0 && addedExcludes.length === 0) continue;
|
|
944
965
|
|
|
@@ -951,9 +972,9 @@ async function reviewIndexingScope(gitId) {
|
|
|
951
972
|
include_dirs: nextIncludes,
|
|
952
973
|
excluded: nextExcluded,
|
|
953
974
|
});
|
|
954
|
-
console.log(`
|
|
975
|
+
console.log(` ${ok("✓")} Updated.`);
|
|
955
976
|
} catch (err) {
|
|
956
|
-
console.log(`
|
|
977
|
+
console.log(` ${fail("✗")} Failed to update scope: ${err.message}`);
|
|
957
978
|
}
|
|
958
979
|
}
|
|
959
980
|
}
|
|
@@ -968,10 +989,10 @@ async function promptGlobList(label, exampleHint) {
|
|
|
968
989
|
}
|
|
969
990
|
|
|
970
991
|
async function promptAutoBuild(projectId) {
|
|
971
|
-
const enabled = await prompt.confirm("
|
|
992
|
+
const enabled = await prompt.confirm(" Enable auto-build when files change?", false);
|
|
972
993
|
let quietPeriodSeconds = 30;
|
|
973
994
|
if (enabled) {
|
|
974
|
-
const raw = await prompt.ask("
|
|
995
|
+
const raw = await prompt.ask(" Quiet period in seconds before rebuild", "30");
|
|
975
996
|
const parsed = Number.parseInt(raw, 10);
|
|
976
997
|
if (Number.isFinite(parsed) && parsed >= 0) quietPeriodSeconds = parsed;
|
|
977
998
|
}
|
|
@@ -985,9 +1006,9 @@ async function saveAutoBuild(projectId, enabled, quietPeriodSeconds) {
|
|
|
985
1006
|
quiet_period_seconds: quietPeriodSeconds,
|
|
986
1007
|
});
|
|
987
1008
|
const statusLabel = enabled ? ok(`ON (quiet ${quietPeriodSeconds}s)`) : color.dim("off");
|
|
988
|
-
console.log(`
|
|
1009
|
+
console.log(` Auto-build: ${statusLabel}.`);
|
|
989
1010
|
} catch (err) {
|
|
990
|
-
console.log(`
|
|
1011
|
+
console.log(` ${fail("✗")} Failed to save auto-build setting: ${err.message}`);
|
|
991
1012
|
}
|
|
992
1013
|
}
|
|
993
1014
|
|
|
@@ -1066,13 +1087,13 @@ async function promptActivePropertyFiles(varsByFile, currentActive) {
|
|
|
1066
1087
|
: null; // null => all active
|
|
1067
1088
|
const isActive = (f) => activeSet == null || activeSet.has(f);
|
|
1068
1089
|
|
|
1069
|
-
console.log(`\n
|
|
1090
|
+
console.log(`\n ${color.bold("Property files Niro found")} (their values become env-var defaults):`);
|
|
1070
1091
|
files.forEach((f, i) => {
|
|
1071
1092
|
const state = isActive(f) ? ok("[active]") : warn("[ignored]");
|
|
1072
|
-
console.log(`
|
|
1093
|
+
console.log(` [${i + 1}] ${f} ${state}`);
|
|
1073
1094
|
});
|
|
1074
1095
|
const answer = (await prompt.ask(
|
|
1075
|
-
"
|
|
1096
|
+
" Enter to use ALL, or list numbers to IGNORE (e.g. 2,3)",
|
|
1076
1097
|
""
|
|
1077
1098
|
)).trim();
|
|
1078
1099
|
|
|
@@ -1089,18 +1110,18 @@ async function promptActivePropertyFiles(varsByFile, currentActive) {
|
|
|
1089
1110
|
);
|
|
1090
1111
|
const active = files.filter((_f, i) => !ignoreIdx.has(i));
|
|
1091
1112
|
if (active.length === files.length) {
|
|
1092
|
-
console.log(color.dim("
|
|
1113
|
+
console.log(color.dim(" No valid numbers recognised — keeping all property files active."));
|
|
1093
1114
|
return files.slice();
|
|
1094
1115
|
}
|
|
1095
1116
|
if (active.length === 0) {
|
|
1096
1117
|
// Ignoring every file isn't a durable state (an empty active set reads back as
|
|
1097
1118
|
// "all active"). Keep all; the user can still leave values blank and set them
|
|
1098
1119
|
// later with `niro edit --set-env`.
|
|
1099
|
-
console.log(warn("
|
|
1120
|
+
console.log(warn(" Can't ignore every property file — keeping all active."));
|
|
1100
1121
|
return files.slice();
|
|
1101
1122
|
}
|
|
1102
1123
|
const ignored = files.filter((_f, i) => ignoreIdx.has(i));
|
|
1103
|
-
console.log(`
|
|
1124
|
+
console.log(` ${warn("Ignoring")}: ${ignored.join(", ")}`);
|
|
1104
1125
|
return active;
|
|
1105
1126
|
}
|
|
1106
1127
|
|
|
@@ -1132,24 +1153,24 @@ async function promptEnvVars(envVars, varsByFile = {}, activeFiles = null) {
|
|
|
1132
1153
|
const candidates = buildCandidates(varsByFile, key, v, activeFiles);
|
|
1133
1154
|
|
|
1134
1155
|
if (candidates.length > 1) {
|
|
1135
|
-
console.log(`\n
|
|
1156
|
+
console.log(`\n ${color.bold(key)}${currentSource ? color.dim(` (current: ${currentSource})`) : ""}`);
|
|
1136
1157
|
candidates.forEach((c, i) => {
|
|
1137
1158
|
// Mask when the key looks secret OR the value embeds credentials.
|
|
1138
1159
|
const hide = secret || looksSecretValue(c.value);
|
|
1139
1160
|
const shown = c.value == null ? "(empty)" : (hide ? "****" : c.value);
|
|
1140
1161
|
const marker = (c.is_current || c.is_default) ? color.green(" [current default]") : "";
|
|
1141
|
-
console.log(`
|
|
1162
|
+
console.log(` [${i + 1}] ${shown} ${color.dim("from " + c.source_file)}${marker}`);
|
|
1142
1163
|
});
|
|
1143
|
-
console.log(`
|
|
1144
|
-
const choice = (await prompt.ask(`
|
|
1164
|
+
console.log(` [c] enter a custom value`);
|
|
1165
|
+
const choice = (await prompt.ask(` Choose 1-${candidates.length}, c, or Enter to keep current`, "")).trim();
|
|
1145
1166
|
if (!choice) {
|
|
1146
1167
|
out.push({ ...v });
|
|
1147
1168
|
continue;
|
|
1148
1169
|
}
|
|
1149
1170
|
if (choice.toLowerCase() === "c") {
|
|
1150
1171
|
const answer = secret
|
|
1151
|
-
? await prompt.askHidden(`
|
|
1152
|
-
: await prompt.ask(`
|
|
1172
|
+
? await prompt.askHidden(` Value for ${key}`)
|
|
1173
|
+
: await prompt.ask(` Value for ${key}`, "");
|
|
1153
1174
|
out.push({ ...v, user_value: answer || null });
|
|
1154
1175
|
continue;
|
|
1155
1176
|
}
|
|
@@ -1170,10 +1191,10 @@ async function promptEnvVars(envVars, varsByFile = {}, activeFiles = null) {
|
|
|
1170
1191
|
let hint = currentSource ? ` (from ${currentSource})` : "";
|
|
1171
1192
|
if (!currentValue) hint += " (no default — please enter a value)";
|
|
1172
1193
|
if (secret) {
|
|
1173
|
-
const answer = await prompt.askHidden(`
|
|
1194
|
+
const answer = await prompt.askHidden(` ${key}${hint} (hidden, Enter to keep current)`);
|
|
1174
1195
|
out.push({ ...v, user_value: answer || (v.user_value || null) });
|
|
1175
1196
|
} else {
|
|
1176
|
-
const answer = await prompt.ask(`
|
|
1197
|
+
const answer = await prompt.ask(` ${key}${hint}`, currentValue);
|
|
1177
1198
|
out.push({ ...v, user_value: answer || null });
|
|
1178
1199
|
}
|
|
1179
1200
|
}
|