@nolto/cli 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.
Files changed (3) hide show
  1. package/README.md +6 -3
  2. package/dist/index.js +95 -21
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -45,7 +45,7 @@ nolto whoami # Show resolved auth/config state and project count
45
45
  ```bash
46
46
  nolto link <projectId> # Write or update nolto.json at the repository root
47
47
  nolto link --show # Show the current binding, source, and repository identity
48
- nolto link --rebind # Rebind the project to this repository (owner only)
48
+ nolto link --rebind [--yes] # Rebind the project to this repository (owner only)
49
49
  nolto link --unlink # Remove projectId from nolto.json
50
50
  ```
51
51
 
@@ -53,8 +53,11 @@ Commit `nolto.json` so everyone working in the repository targets the same Nolto
53
53
  project. A Nolto project is bound to the first repository identity it syncs from.
54
54
  The identity is the normalized `origin` remote, or a machine ID plus path when no
55
55
  remote is available. Syncing from a different repository fails with
56
- `409 repo_mismatch`. An owner can run `nolto link --rebind` in the correct
57
- repository to change the binding, at most once every seven days.
56
+ `409 repo_mismatch`. Before rebinding, run `nolto link --show` and confirm that
57
+ the project ID is intended for this repository. An owner can then run
58
+ `nolto link --rebind` in the correct repository to change the binding, at most
59
+ once every seven days. The command shows the project name and asks for
60
+ confirmation; pass `--yes` (`-y`) to skip the prompt.
58
61
 
59
62
  CLI versions older than 0.8.0 do not send a repository identity and receive
60
63
  `426`; run `nolto update` before syncing.
package/dist/index.js CHANGED
@@ -56,7 +56,7 @@ function mapHttpStatusToCliError(status, opts = {}) {
56
56
  return new CliError(
57
57
  opts.serverMessage ?? "This project is bound to a different repository.",
58
58
  2,
59
- "Owner: run `nolto link --rebind` inside the repository that should own this project.",
59
+ "Check that nolto.json points to the project for this repository (nolto link --show). Owner: run `nolto link --rebind` inside the repository that should own this project.",
60
60
  status
61
61
  );
62
62
  }
@@ -374,7 +374,7 @@ function createHttpClient(opts) {
374
374
  import { Command } from "commander";
375
375
 
376
376
  // src/commands/init.ts
377
- import readline from "readline/promises";
377
+ import readline2 from "readline/promises";
378
378
  import { createRequire } from "module";
379
379
  import { fileURLToPath as fileURLToPath2 } from "url";
380
380
  import os3 from "os";
@@ -382,6 +382,7 @@ import path9 from "path";
382
382
  import fs from "fs";
383
383
 
384
384
  // src/commands/link.ts
385
+ import readline from "readline/promises";
385
386
  import os2 from "os";
386
387
  import path5 from "path";
387
388
  import { statSync as statSync2 } from "fs";
@@ -819,14 +820,51 @@ async function handleShow(deps, projectBindingPath, mode2) {
819
820
  }
820
821
  }
821
822
  }
822
- async function handleRebind(deps, projectId, root, mode2) {
823
+ async function handleRebind(deps, projectId, root, mode2, yes) {
823
824
  if (!UUID_RE.test(projectId)) {
824
825
  throw new CliError(
825
826
  `Invalid project ID: "${projectId}". Must be a UUID (e.g. 00000000-0000-0000-0000-000000000001).`,
826
827
  2
827
828
  );
828
829
  }
830
+ const result = await deps.http.get("/api/projects");
831
+ const projects = Array.isArray(result.projects) ? result.projects : [];
832
+ const project = projects.find((candidate) => candidate.id === projectId);
833
+ if (project == null) {
834
+ throw new CliError(
835
+ `Project ${projectId} is not in your accessible projects. Check nolto.json (nolto link --show) before rebinding.`,
836
+ 2
837
+ );
838
+ }
829
839
  const repoIdentity = await makeRepoIdentityResolver(deps)(root);
840
+ if (mode2 !== "json") {
841
+ process.stdout.write(`Project : ${project.name} (${project.id})
842
+ `);
843
+ process.stdout.write(`Bound to : ${project.repoIdentity ?? "not bound"}
844
+ `);
845
+ process.stdout.write(`Rebind to : ${repoIdentity.kind}:${repoIdentity.value}
846
+ `);
847
+ }
848
+ if (!yes) {
849
+ if (mode2 === "json") {
850
+ throw new CliError(
851
+ "--rebind requires confirmation. Pass --yes to skip the prompt in --json mode.",
852
+ 2
853
+ );
854
+ }
855
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
856
+ let confirmed = false;
857
+ try {
858
+ const answer = await rl.question(`Rebind "${project.name}" to this repository? [y/N] `);
859
+ confirmed = answer.trim().toLowerCase() === "y";
860
+ } finally {
861
+ rl.close();
862
+ }
863
+ if (!confirmed) {
864
+ process.stdout.write("Aborted.\n");
865
+ return;
866
+ }
867
+ }
830
868
  await deps.http.post(`/api/projects/${projectId}/repo-binding`, { repoIdentity });
831
869
  if (mode2 === "json") {
832
870
  printResult({ rebound: true, projectId, repoIdentity }, mode2);
@@ -945,7 +983,7 @@ Commit nolto.json to share the binding with your team.
945
983
  function register(program, deps) {
946
984
  const cmd = program.command("link [projectId]").description(
947
985
  "Bind this repository to a Nolto project.\nWrites nolto.json at the repo root. Commit it to share the binding with your team.\n\nExamples:\n nolto link <uuid> Write / update nolto.json\n nolto link --show Show the current binding\n nolto link --rebind Rebind the project to this repository\n nolto link --unlink Remove the projectId from nolto.json"
948
- ).option("--show", "Show the current repo binding (path + projectId + source)").option("--rebind", "Rebind the project to this repository (owner only, once per 7 days)").option("--unlink", "Remove the projectId key from nolto.json");
986
+ ).option("--show", "Show the current repo binding (path + projectId + source)").option("--rebind", "Rebind the project to this repository (owner only, once per 7 days)").option("-y, --yes", "Skip the rebind confirmation prompt").option("--unlink", "Remove the projectId key from nolto.json");
949
987
  cmd.action(async (projectId) => {
950
988
  const { output } = deps;
951
989
  const projectBindingPath = deps.repoBinding?.path ?? deps.projectBindingPath ?? null;
@@ -973,7 +1011,7 @@ function register(program, deps) {
973
1011
  }
974
1012
  const startDir = resolveStartDir(process.env, process.cwd());
975
1013
  const root = findRepoRoot(startDir).root;
976
- await handleRebind(deps, effectiveProjectId, root, mode2);
1014
+ await handleRebind(deps, effectiveProjectId, root, mode2, cmd.opts()["yes"] === true);
977
1015
  return;
978
1016
  }
979
1017
  if (projectId == null || projectId.trim().length === 0) {
@@ -1203,10 +1241,27 @@ async function pickProject(rl, http, projects, promptText) {
1203
1241
  }
1204
1242
  return void 0;
1205
1243
  }
1244
+ async function selectRepoProject(rl, http, projects, defaultProjectId) {
1245
+ const defaultProject = defaultProjectId == null ? void 0 : projects.find((project) => project.id === defaultProjectId);
1246
+ if (defaultProject != null) {
1247
+ const useDefault = await rl.question(
1248
+ `Use "${defaultProject.name}" (${defaultProject.id}) for this repository? [Y/n] `
1249
+ );
1250
+ if (useDefault.trim().toLowerCase() !== "n") {
1251
+ return defaultProject;
1252
+ }
1253
+ }
1254
+ return pickProject(
1255
+ rl,
1256
+ http,
1257
+ projects,
1258
+ "Project number for this repository, 'c' to create new (or Enter to skip): "
1259
+ );
1260
+ }
1206
1261
  function register2(program, deps) {
1207
1262
  program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
1208
1263
  const configPath = deps.configPath;
1209
- const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
1264
+ const rl = readline2.createInterface({ input: process.stdin, output: process.stdout });
1210
1265
  try {
1211
1266
  let configureGlobal = opts.force === true;
1212
1267
  if (!configureGlobal) {
@@ -1262,6 +1317,9 @@ function register2(program, deps) {
1262
1317
  );
1263
1318
  defaultProjectId = selected?.id;
1264
1319
  defaultProjectName = selected?.name;
1320
+ if (selected != null && !projects.some((project) => project.id === selected.id)) {
1321
+ projects = [...projects, selected];
1322
+ }
1265
1323
  await saveConfigFile(configPath, {
1266
1324
  token,
1267
1325
  baseUrl: baseUrl !== DEFAULT_BASE_URL ? baseUrl : void 0,
@@ -1298,19 +1356,35 @@ Saved ${configPath}
1298
1356
  return;
1299
1357
  }
1300
1358
  const existingBinding = deps.repoBinding?.error == null ? deps.repoBinding?.binding ?? null : null;
1301
- let repoProject = existingBinding != null ? { id: existingBinding.projectId, name: path9.basename(root) } : defaultProjectId != null ? { id: defaultProjectId, name: defaultProjectName ?? path9.basename(root) } : void 0;
1302
- if (repoProject == null) {
1303
- http ??= createHttpClient({ baseUrl, token, version: getCliVersion() });
1304
- if (projects == null) {
1305
- const result = await http.get("/api/projects");
1306
- projects = Array.isArray(result.projects) ? result.projects : [];
1359
+ const repoHttp = http ?? deps.http;
1360
+ if (projects == null) {
1361
+ const result = await repoHttp.get("/api/projects");
1362
+ projects = Array.isArray(result.projects) ? result.projects : [];
1363
+ }
1364
+ let keepExistingBinding = false;
1365
+ let repoProject;
1366
+ if (existingBinding != null) {
1367
+ const boundProject = projects.find(
1368
+ (project) => project.id === existingBinding.projectId
1369
+ );
1370
+ if (boundProject != null) {
1371
+ process.stdout.write(
1372
+ `Existing binding: nolto.json \u2192 "${boundProject.name}" (${boundProject.id})
1373
+ `
1374
+ );
1375
+ } else {
1376
+ process.stderr.write(
1377
+ `Warning: nolto.json points to project ${existingBinding.projectId}, which is not in your accessible projects.
1378
+ `
1379
+ );
1307
1380
  }
1308
- repoProject = await pickProject(
1309
- rl,
1310
- http,
1311
- projects,
1312
- "Project number for this repository, 'c' to create new (or Enter to skip): "
1381
+ const keep = await rl.question(
1382
+ boundProject != null ? "Keep this binding? [Y/n] " : "Keep this binding? [y/N] "
1313
1383
  );
1384
+ keepExistingBinding = boundProject != null ? keep.trim().toLowerCase() !== "n" : keep.trim().toLowerCase() === "y";
1385
+ repoProject = keepExistingBinding ? boundProject ?? { id: existingBinding.projectId, name: path9.basename(root) } : await selectRepoProject(rl, repoHttp, projects, defaultProjectId);
1386
+ } else {
1387
+ repoProject = await selectRepoProject(rl, repoHttp, projects, defaultProjectId);
1314
1388
  }
1315
1389
  if (repoProject == null) {
1316
1390
  process.stdout.write("Skipped repo setup (no project selected).\n");
@@ -1323,8 +1397,8 @@ Set up this repository (${root}) for roadmap sync? [Y/n] `);
1323
1397
  return;
1324
1398
  }
1325
1399
  const bindingPath = deps.repoBinding?.path ?? path9.join(root, "nolto.json");
1326
- if (existingBinding != null) {
1327
- process.stdout.write(`binding: kept ${bindingPath} (${existingBinding.projectId})
1400
+ if (keepExistingBinding) {
1401
+ process.stdout.write(`binding: kept ${bindingPath} (${repoProject.id})
1328
1402
  `);
1329
1403
  } else {
1330
1404
  if (deps.repoBinding?.error != null) {
@@ -1407,7 +1481,7 @@ async function promptHidden(rl, prompt) {
1407
1481
  }
1408
1482
 
1409
1483
  // src/commands/login.ts
1410
- import readline2 from "readline/promises";
1484
+ import readline3 from "readline/promises";
1411
1485
 
1412
1486
  // src/login-poll.ts
1413
1487
  async function pollUntilToken(opts) {
@@ -1473,7 +1547,7 @@ function register3(program, deps) {
1473
1547
  } catch {
1474
1548
  }
1475
1549
  if (existing?.token) {
1476
- const rl = readline2.createInterface({
1550
+ const rl = readline3.createInterface({
1477
1551
  input: process.stdin,
1478
1552
  output: process.stdout
1479
1553
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolto/cli",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "description": "CLI for syncing repository roadmaps with Nolto.",
5
5
  "license": "MIT",
6
6
  "type": "module",