@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.
- package/README.md +6 -3
- package/dist/index.js +95 -21
- 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
|
|
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`.
|
|
57
|
-
|
|
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
|
|
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 =
|
|
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
|
-
|
|
1302
|
-
if (
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
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
|
-
|
|
1309
|
-
|
|
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 (
|
|
1327
|
-
process.stdout.write(`binding: kept ${bindingPath} (${
|
|
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
|
|
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 =
|
|
1550
|
+
const rl = readline3.createInterface({
|
|
1477
1551
|
input: process.stdin,
|
|
1478
1552
|
output: process.stdout
|
|
1479
1553
|
});
|