@nolto/cli 0.7.0 → 0.8.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 +10 -2
- package/dist/index.js +526 -294
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,12 +42,20 @@ nolto whoami # Show resolved auth/config state and project count
|
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
44
|
nolto link <projectId> # Write or update nolto.json at the repository root
|
|
45
|
-
nolto link --show # Show the current binding and
|
|
45
|
+
nolto link --show # Show the current binding, source, and repository identity
|
|
46
|
+
nolto link --rebind # Rebind the project to this repository (owner only)
|
|
46
47
|
nolto link --unlink # Remove projectId from nolto.json
|
|
47
48
|
```
|
|
48
49
|
|
|
49
50
|
Commit `nolto.json` so everyone working in the repository targets the same Nolto
|
|
50
|
-
project.
|
|
51
|
+
project. A Nolto project is bound to the first repository identity it syncs from.
|
|
52
|
+
The identity is the normalized `origin` remote, or a machine ID plus path when no
|
|
53
|
+
remote is available. Syncing from a different repository fails with
|
|
54
|
+
`409 repo_mismatch`. An owner can run `nolto link --rebind` in the correct
|
|
55
|
+
repository to change the binding, at most once every seven days.
|
|
56
|
+
|
|
57
|
+
CLI versions older than 0.8.0 do not send a repository identity and receive
|
|
58
|
+
`426`; run `nolto update` before syncing.
|
|
51
59
|
|
|
52
60
|
### Roadmap Sync
|
|
53
61
|
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { createRequire as createRequire3 } from "module";
|
|
5
5
|
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
6
|
-
import
|
|
6
|
+
import path15 from "path";
|
|
7
7
|
import { CommanderError } from "commander";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
@@ -36,6 +36,38 @@ function mapHttpStatusToCliError(status, opts = {}) {
|
|
|
36
36
|
status
|
|
37
37
|
);
|
|
38
38
|
}
|
|
39
|
+
if (status === 426) {
|
|
40
|
+
return new CliError(
|
|
41
|
+
opts.serverMessage ?? "Nolto CLI is outdated.",
|
|
42
|
+
2,
|
|
43
|
+
"Run `nolto update`, then retry.",
|
|
44
|
+
status
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
if (status === 409 && opts.serverError === "repo_unbound") {
|
|
48
|
+
return new CliError(
|
|
49
|
+
opts.serverMessage ?? "This project is not bound to a repository.",
|
|
50
|
+
2,
|
|
51
|
+
"Ask the project owner to run nolto sync in the correct repository first.",
|
|
52
|
+
status
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
if (status === 409 && opts.serverError === "repo_mismatch") {
|
|
56
|
+
return new CliError(
|
|
57
|
+
opts.serverMessage ?? "This project is bound to a different repository.",
|
|
58
|
+
2,
|
|
59
|
+
"Owner: run `nolto link --rebind` inside the repository that should own this project.",
|
|
60
|
+
status
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
if (status === 429 && opts.serverError === "rebind_cooldown") {
|
|
64
|
+
return new CliError(
|
|
65
|
+
opts.serverMessage ?? "Rebind cooldown active.",
|
|
66
|
+
4,
|
|
67
|
+
void 0,
|
|
68
|
+
status
|
|
69
|
+
);
|
|
70
|
+
}
|
|
39
71
|
if (status === 402) {
|
|
40
72
|
const hint = opts.upgradeUrl != null ? `Upgrade: ${opts.upgradeUrl}` : void 0;
|
|
41
73
|
return new CliError(
|
|
@@ -157,9 +189,9 @@ async function mergeJsonFile(filePath, patch) {
|
|
|
157
189
|
}
|
|
158
190
|
}
|
|
159
191
|
const merged = { ...existing, ...patch };
|
|
160
|
-
const { chmod } = await import("fs/promises");
|
|
192
|
+
const { chmod: chmod2 } = await import("fs/promises");
|
|
161
193
|
await writeFile(filePath, JSON.stringify(merged, null, 2) + "\n", { mode: 420 });
|
|
162
|
-
await
|
|
194
|
+
await chmod2(filePath, 420);
|
|
163
195
|
}
|
|
164
196
|
async function writeRepoBinding(root, projectId) {
|
|
165
197
|
await mergeJsonFile(path.join(root, "nolto.json"), { projectId });
|
|
@@ -282,11 +314,11 @@ function maskToken(token) {
|
|
|
282
314
|
function createHttpClient(opts) {
|
|
283
315
|
const { baseUrl, version, token } = opts;
|
|
284
316
|
const base = baseUrl.replace(/\/+$/, "");
|
|
285
|
-
async function request(method,
|
|
286
|
-
if (!
|
|
287
|
-
throw new CliError(`HTTP client path must start with /api/, got: ${
|
|
317
|
+
async function request(method, path16, body) {
|
|
318
|
+
if (!path16.startsWith("/api/")) {
|
|
319
|
+
throw new CliError(`HTTP client path must start with /api/, got: ${path16}`, 2);
|
|
288
320
|
}
|
|
289
|
-
const url = `${base}${
|
|
321
|
+
const url = `${base}${path16}`;
|
|
290
322
|
const headers = {
|
|
291
323
|
"Content-Type": "application/json",
|
|
292
324
|
"User-Agent": `${CLI_USER_AGENT_NAME}/${version}`
|
|
@@ -345,11 +377,11 @@ import { Command } from "commander";
|
|
|
345
377
|
import readline from "readline/promises";
|
|
346
378
|
import { createRequire } from "module";
|
|
347
379
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
348
|
-
import
|
|
380
|
+
import path8 from "path";
|
|
349
381
|
import fs from "fs";
|
|
350
382
|
|
|
351
383
|
// src/commands/link.ts
|
|
352
|
-
import
|
|
384
|
+
import path5 from "path";
|
|
353
385
|
import { statSync as statSync2 } from "fs";
|
|
354
386
|
|
|
355
387
|
// src/output.ts
|
|
@@ -403,6 +435,316 @@ function formatValue(value) {
|
|
|
403
435
|
return JSON.stringify(value, null, 2);
|
|
404
436
|
}
|
|
405
437
|
|
|
438
|
+
// src/registry.ts
|
|
439
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rename, unlink } from "fs/promises";
|
|
440
|
+
import { randomUUID } from "crypto";
|
|
441
|
+
import path2 from "path";
|
|
442
|
+
import { z as z2 } from "zod";
|
|
443
|
+
var registrySchema = z2.object({
|
|
444
|
+
schemaVersion: z2.literal(1),
|
|
445
|
+
repos: z2.array(z2.object({ root: z2.string().min(1) }).passthrough())
|
|
446
|
+
}).passthrough();
|
|
447
|
+
function getRegistryPath(env) {
|
|
448
|
+
return path2.join(getConfigDir(env), "registry.json");
|
|
449
|
+
}
|
|
450
|
+
async function loadRegistry(filePath) {
|
|
451
|
+
let raw;
|
|
452
|
+
try {
|
|
453
|
+
raw = await readFile2(filePath, "utf8");
|
|
454
|
+
} catch (err) {
|
|
455
|
+
const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
|
|
456
|
+
if (code === "ENOENT") {
|
|
457
|
+
return { schemaVersion: 1, repos: [] };
|
|
458
|
+
}
|
|
459
|
+
throw new CliError(`Cannot read registry: ${filePath}: ${String(err)}`, 2);
|
|
460
|
+
}
|
|
461
|
+
let parsed;
|
|
462
|
+
try {
|
|
463
|
+
parsed = JSON.parse(raw);
|
|
464
|
+
} catch {
|
|
465
|
+
throw new CliError(`Malformed JSON in ${filePath}. Fix or remove the file.`, 2);
|
|
466
|
+
}
|
|
467
|
+
const result = registrySchema.safeParse(parsed);
|
|
468
|
+
if (!result.success) {
|
|
469
|
+
const issue = result.error.issues[0];
|
|
470
|
+
const fieldPath = issue?.path.join(".") ?? "";
|
|
471
|
+
throw new CliError(
|
|
472
|
+
`Invalid registry at ${filePath}: ${fieldPath.length > 0 ? `field "${fieldPath}" \u2014 ` : ""}${issue?.message ?? "validation failed"}`,
|
|
473
|
+
2
|
|
474
|
+
);
|
|
475
|
+
}
|
|
476
|
+
return result.data;
|
|
477
|
+
}
|
|
478
|
+
async function addRepoToRegistry(filePath, repoRoot) {
|
|
479
|
+
const registry = await loadRegistry(filePath);
|
|
480
|
+
if (registry.repos.some((repo) => repo.root === repoRoot)) {
|
|
481
|
+
return { added: false, registry };
|
|
482
|
+
}
|
|
483
|
+
const updated = { ...registry, repos: [...registry.repos, { root: repoRoot }] };
|
|
484
|
+
await mkdir2(path2.dirname(filePath), { recursive: true, mode: 448 });
|
|
485
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
486
|
+
try {
|
|
487
|
+
await writeFile2(tempPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
488
|
+
await rename(tempPath, filePath);
|
|
489
|
+
} catch (err) {
|
|
490
|
+
await unlink(tempPath).catch(() => void 0);
|
|
491
|
+
throw err;
|
|
492
|
+
}
|
|
493
|
+
return { added: true, registry: updated };
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// src/repo-identity.ts
|
|
497
|
+
import { execFile } from "child_process";
|
|
498
|
+
import { realpathSync } from "fs";
|
|
499
|
+
import path4 from "path";
|
|
500
|
+
|
|
501
|
+
// ../roadmap-schema/src/repo-identity.ts
|
|
502
|
+
var HOSTED_LOWERCASE_PATH = /* @__PURE__ */ new Set(["github.com", "gitlab.com", "bitbucket.org"]);
|
|
503
|
+
function normalizeRemote(raw) {
|
|
504
|
+
let s = raw.trim();
|
|
505
|
+
if (s.length === 0) return null;
|
|
506
|
+
if (s.startsWith("file://") || s.startsWith("/") || s.startsWith(".") || /^[A-Za-z]:[\\/]/.test(s)) return null;
|
|
507
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(s);
|
|
508
|
+
const hadScheme = scheme !== null;
|
|
509
|
+
if (scheme !== null) s = s.slice(scheme[0].length);
|
|
510
|
+
const authorityEnd = s.indexOf("/");
|
|
511
|
+
const lastAt = s.lastIndexOf("@", authorityEnd === -1 ? s.length - 1 : authorityEnd - 1);
|
|
512
|
+
if (lastAt !== -1) s = s.slice(lastAt + 1);
|
|
513
|
+
const scpLike = hadScheme ? null : /^([^/:]+):(.+)$/.exec(s);
|
|
514
|
+
if (scpLike !== null) {
|
|
515
|
+
s = `${scpLike[1]}/${scpLike[2]}`;
|
|
516
|
+
}
|
|
517
|
+
const firstSlash = s.indexOf("/");
|
|
518
|
+
if (firstSlash <= 0) return null;
|
|
519
|
+
let host = s.slice(0, firstSlash).toLowerCase();
|
|
520
|
+
let path16 = s.slice(firstSlash + 1);
|
|
521
|
+
if (hadScheme) host = host.replace(/:\d+$/, "");
|
|
522
|
+
path16 = path16.replace(/\/+/g, "/").replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
523
|
+
if (path16.length === 0) return null;
|
|
524
|
+
if (HOSTED_LOWERCASE_PATH.has(host)) path16 = path16.toLowerCase();
|
|
525
|
+
return `${host}/${path16}`;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
// ../roadmap-schema/src/index.ts
|
|
529
|
+
var STATUSES = /* @__PURE__ */ new Set(["todo", "in-progress", "done", "blocked"]);
|
|
530
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
531
|
+
var ALLOWED_KEYS = {
|
|
532
|
+
roadmap: /* @__PURE__ */ new Set(["schemaVersion", "project", "updatedAt", "currentTaskId", "summary", "phases"]),
|
|
533
|
+
project: /* @__PURE__ */ new Set(["id", "name", "repository"]),
|
|
534
|
+
phase: /* @__PURE__ */ new Set(["id", "title", "status", "plan", "tasks"]),
|
|
535
|
+
task: /* @__PURE__ */ new Set(["id", "title", "status", "startedAt", "completedAt", "note", "dependsOn", "plan"])
|
|
536
|
+
};
|
|
537
|
+
function isRecord(value) {
|
|
538
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
539
|
+
}
|
|
540
|
+
function checkKeys(value, allowed, at, errors) {
|
|
541
|
+
if (!isRecord(value)) return;
|
|
542
|
+
for (const key of Object.keys(value)) {
|
|
543
|
+
if (!allowed.has(key)) errors.push(`${at} contains unsupported property "${key}".`);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
function checkPlan(value, at, errors) {
|
|
547
|
+
if (value === void 0) return;
|
|
548
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
549
|
+
errors.push(`${at}.plan must be a non-empty string.`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
function derivePhaseStatus(phase) {
|
|
553
|
+
if (phase.tasks.length > 0 && phase.tasks.every((task) => task.status === "done")) return "done";
|
|
554
|
+
if (phase.tasks.some((task) => task.status === "in-progress")) return "in-progress";
|
|
555
|
+
if (phase.tasks.some((task) => task.status === "blocked")) return "blocked";
|
|
556
|
+
if (phase.tasks.some((task) => task.status === "done")) return "in-progress";
|
|
557
|
+
return "todo";
|
|
558
|
+
}
|
|
559
|
+
function validateRoadmap(value) {
|
|
560
|
+
const errors = [];
|
|
561
|
+
const warnings = [];
|
|
562
|
+
if (!isRecord(value)) return { errors: ["Root must be an object."], warnings };
|
|
563
|
+
checkKeys(value, ALLOWED_KEYS.roadmap, "roadmap", errors);
|
|
564
|
+
if (value["schemaVersion"] !== 1 && value["schemaVersion"] !== 2) {
|
|
565
|
+
errors.push("schemaVersion must be 1 or 2.");
|
|
566
|
+
} else if (value["schemaVersion"] === 1) {
|
|
567
|
+
warnings.push("schemaVersion 1 is legacy; the next roadmap-progress mutation migrates this file to 2.");
|
|
568
|
+
}
|
|
569
|
+
const project = value["project"];
|
|
570
|
+
checkKeys(project, ALLOWED_KEYS.project, "project", errors);
|
|
571
|
+
const projectRecord = isRecord(project) ? project : {};
|
|
572
|
+
if (!ID_PATTERN.test(String(projectRecord["id"] ?? ""))) errors.push("project.id is invalid.");
|
|
573
|
+
if (projectRecord["name"] == null || projectRecord["name"] === "") errors.push("project.name is required.");
|
|
574
|
+
if (projectRecord["repository"] == null || projectRecord["repository"] === "") errors.push("project.repository is required.");
|
|
575
|
+
if (Number.isNaN(Date.parse(String(value["updatedAt"])))) errors.push("updatedAt must be a valid date-time.");
|
|
576
|
+
if (typeof value["summary"] !== "string") errors.push("summary must be a string.");
|
|
577
|
+
if (!Array.isArray(value["phases"])) errors.push("phases must be an array.");
|
|
578
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
579
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
580
|
+
const phases = Array.isArray(value["phases"]) ? value["phases"] : [];
|
|
581
|
+
for (const [phaseIndex, rawPhase] of phases.entries()) {
|
|
582
|
+
const at = `phases[${phaseIndex}]`;
|
|
583
|
+
checkKeys(rawPhase, ALLOWED_KEYS.phase, at, errors);
|
|
584
|
+
const phase = isRecord(rawPhase) ? rawPhase : {};
|
|
585
|
+
const phaseId = String(phase["id"] ?? "");
|
|
586
|
+
if (!ID_PATTERN.test(phaseId)) errors.push(`${at}.id is invalid.`);
|
|
587
|
+
if (allIds.has(phaseId)) errors.push(`Duplicate id "${phaseId}".`);
|
|
588
|
+
allIds.add(phaseId);
|
|
589
|
+
if (phase["title"] == null || phase["title"] === "") errors.push(`${at}.title is required.`);
|
|
590
|
+
if (!STATUSES.has(String(phase["status"]))) errors.push(`${at}.status is invalid.`);
|
|
591
|
+
checkPlan(phase["plan"], at, errors);
|
|
592
|
+
if (!Array.isArray(phase["tasks"])) errors.push(`${at}.tasks must be an array.`);
|
|
593
|
+
const rawTasks = Array.isArray(phase["tasks"]) ? phase["tasks"] : [];
|
|
594
|
+
for (const [taskIndex, rawTask] of rawTasks.entries()) {
|
|
595
|
+
const taskAt = `${at}.tasks[${taskIndex}]`;
|
|
596
|
+
checkKeys(rawTask, ALLOWED_KEYS.task, taskAt, errors);
|
|
597
|
+
const task = isRecord(rawTask) ? rawTask : {};
|
|
598
|
+
const taskId = String(task["id"] ?? "");
|
|
599
|
+
if (!ID_PATTERN.test(taskId)) errors.push(`${taskAt}.id is invalid.`);
|
|
600
|
+
if (allIds.has(taskId)) errors.push(`Duplicate id "${taskId}".`);
|
|
601
|
+
allIds.add(taskId);
|
|
602
|
+
tasks.set(taskId, task);
|
|
603
|
+
if (task["title"] == null || task["title"] === "") errors.push(`${taskAt}.title is required.`);
|
|
604
|
+
if (!STATUSES.has(String(task["status"]))) errors.push(`${taskAt}.status is invalid.`);
|
|
605
|
+
checkPlan(task["plan"], taskAt, errors);
|
|
606
|
+
if (task["status"] === "done" && task["completedAt"] == null) warnings.push(`${taskId} is done without completedAt.`);
|
|
607
|
+
if (task["status"] === "in-progress" && task["startedAt"] == null) warnings.push(`${taskId} is in-progress without startedAt.`);
|
|
608
|
+
if (task["dependsOn"] !== void 0 && !Array.isArray(task["dependsOn"])) errors.push(`${taskAt}.dependsOn must be an array.`);
|
|
609
|
+
}
|
|
610
|
+
if (Array.isArray(phase["tasks"]) && STATUSES.has(String(phase["status"]))) {
|
|
611
|
+
const expected = derivePhaseStatus({ ...phase, tasks: rawTasks });
|
|
612
|
+
if (phase["status"] !== expected) warnings.push(`${phaseId} status is ${String(phase["status"])}; task states derive ${expected}.`);
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
for (const task of tasks.values()) {
|
|
616
|
+
for (const dependency of task.dependsOn ?? []) {
|
|
617
|
+
if (!tasks.has(dependency)) warnings.push(`${task.id} depends on unknown task ${dependency}.`);
|
|
618
|
+
if (task.id === dependency) errors.push(`${task.id} cannot depend on itself.`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
const currentTaskId = value["currentTaskId"];
|
|
622
|
+
if (currentTaskId !== null && currentTaskId !== void 0) {
|
|
623
|
+
const current = tasks.get(String(currentTaskId));
|
|
624
|
+
if (current == null) errors.push(`currentTaskId ${String(currentTaskId)} does not exist.`);
|
|
625
|
+
else if (current.status !== "in-progress") warnings.push(`currentTaskId ${String(currentTaskId)} is not in-progress.`);
|
|
626
|
+
}
|
|
627
|
+
return { errors, warnings };
|
|
628
|
+
}
|
|
629
|
+
function deriveTaskStats(roadmap) {
|
|
630
|
+
let done = 0;
|
|
631
|
+
let blocked = 0;
|
|
632
|
+
let inProgress = 0;
|
|
633
|
+
let todo = 0;
|
|
634
|
+
for (const phase of roadmap.phases) {
|
|
635
|
+
for (const task of phase.tasks) {
|
|
636
|
+
if (task.status === "done") done += 1;
|
|
637
|
+
else if (task.status === "blocked") blocked += 1;
|
|
638
|
+
else if (task.status === "in-progress") inProgress += 1;
|
|
639
|
+
else todo += 1;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const total = done + blocked + inProgress + todo;
|
|
643
|
+
return {
|
|
644
|
+
total,
|
|
645
|
+
done,
|
|
646
|
+
blocked,
|
|
647
|
+
inProgress,
|
|
648
|
+
todo,
|
|
649
|
+
progressPct: total > 0 ? Math.round(done / total * 100) : 0
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/machine-id.ts
|
|
654
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
655
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
656
|
+
import path3 from "path";
|
|
657
|
+
var inFlightByConfigDir = /* @__PURE__ */ new Map();
|
|
658
|
+
function parseMachineId(raw, file) {
|
|
659
|
+
let parsed;
|
|
660
|
+
try {
|
|
661
|
+
parsed = JSON.parse(raw);
|
|
662
|
+
} catch {
|
|
663
|
+
throw new CliError(`Malformed JSON in ${file}. Remove the file to regenerate it.`, 2);
|
|
664
|
+
}
|
|
665
|
+
const id = parsed?.machineId;
|
|
666
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
667
|
+
throw new CliError(`Invalid ${file}: missing machineId. Remove the file to regenerate it.`, 2);
|
|
668
|
+
}
|
|
669
|
+
return id;
|
|
670
|
+
}
|
|
671
|
+
async function loadOrCreate(configDir) {
|
|
672
|
+
const file = path3.join(configDir, "machine.json");
|
|
673
|
+
let raw = null;
|
|
674
|
+
try {
|
|
675
|
+
raw = await readFile3(file, "utf8");
|
|
676
|
+
} catch (err) {
|
|
677
|
+
const code = err.code;
|
|
678
|
+
if (code !== "ENOENT") {
|
|
679
|
+
throw new CliError(`Cannot read ${file}: ${String(err)}`, 2);
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
if (raw !== null) {
|
|
683
|
+
return parseMachineId(raw, file);
|
|
684
|
+
}
|
|
685
|
+
const machineId = randomUUID2();
|
|
686
|
+
await mkdir3(configDir, { recursive: true, mode: 448 });
|
|
687
|
+
try {
|
|
688
|
+
await writeFile3(file, `${JSON.stringify({ machineId }, null, 2)}
|
|
689
|
+
`, {
|
|
690
|
+
flag: "wx",
|
|
691
|
+
mode: 384
|
|
692
|
+
});
|
|
693
|
+
} catch (err) {
|
|
694
|
+
if (err.code === "EEXIST") {
|
|
695
|
+
return parseMachineId(await readFile3(file, "utf8"), file);
|
|
696
|
+
}
|
|
697
|
+
throw new CliError(`Cannot write ${file}: ${String(err)}`, 2);
|
|
698
|
+
}
|
|
699
|
+
await chmod(file, 384);
|
|
700
|
+
return machineId;
|
|
701
|
+
}
|
|
702
|
+
function loadOrCreateMachineId(configDir) {
|
|
703
|
+
const key = path3.resolve(configDir);
|
|
704
|
+
const existing = inFlightByConfigDir.get(key);
|
|
705
|
+
if (existing !== void 0) return existing;
|
|
706
|
+
const pending = loadOrCreate(key).finally(() => {
|
|
707
|
+
if (inFlightByConfigDir.get(key) === pending) inFlightByConfigDir.delete(key);
|
|
708
|
+
});
|
|
709
|
+
inFlightByConfigDir.set(key, pending);
|
|
710
|
+
return pending;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// src/repo-identity.ts
|
|
714
|
+
function defaultGetRemoteUrl(root) {
|
|
715
|
+
return new Promise((resolve) => {
|
|
716
|
+
execFile(
|
|
717
|
+
"git",
|
|
718
|
+
["-C", root, "remote", "get-url", "origin"],
|
|
719
|
+
{ timeout: 5e3 },
|
|
720
|
+
(err, stdout) => {
|
|
721
|
+
if (err) return resolve(null);
|
|
722
|
+
const line = stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
723
|
+
resolve(line.length > 0 ? line : null);
|
|
724
|
+
}
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
async function resolveRepoIdentity(root, deps) {
|
|
729
|
+
const remote = await deps.getRemoteUrl(root);
|
|
730
|
+
const normalized = remote === null ? null : normalizeRemote(remote);
|
|
731
|
+
if (normalized !== null) return { kind: "remote", value: normalized };
|
|
732
|
+
const machineId = await deps.machineId();
|
|
733
|
+
let canonicalRoot;
|
|
734
|
+
try {
|
|
735
|
+
canonicalRoot = realpathSync(root);
|
|
736
|
+
} catch {
|
|
737
|
+
canonicalRoot = path4.resolve(root);
|
|
738
|
+
}
|
|
739
|
+
return { kind: "local", value: `${machineId}:${canonicalRoot}` };
|
|
740
|
+
}
|
|
741
|
+
function makeRepoIdentityResolver(deps) {
|
|
742
|
+
return deps.repoIdentity ?? ((root) => resolveRepoIdentity(root, {
|
|
743
|
+
getRemoteUrl: defaultGetRemoteUrl,
|
|
744
|
+
machineId: () => loadOrCreateMachineId(getConfigDir(process.env))
|
|
745
|
+
}));
|
|
746
|
+
}
|
|
747
|
+
|
|
406
748
|
// src/commands/link.ts
|
|
407
749
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
408
750
|
function resolveStartDir(env, cwd) {
|
|
@@ -411,7 +753,7 @@ function resolveStartDir(env, cwd) {
|
|
|
411
753
|
}
|
|
412
754
|
function dirHasGit(dir) {
|
|
413
755
|
try {
|
|
414
|
-
statSync2(
|
|
756
|
+
statSync2(path5.join(dir, ".git"));
|
|
415
757
|
return true;
|
|
416
758
|
} catch {
|
|
417
759
|
return false;
|
|
@@ -423,18 +765,23 @@ function findRepoRoot(startDir, hasGit = dirHasGit) {
|
|
|
423
765
|
if (hasGit(current)) {
|
|
424
766
|
return { root: current, foundGit: true };
|
|
425
767
|
}
|
|
426
|
-
const parent =
|
|
768
|
+
const parent = path5.dirname(current);
|
|
427
769
|
if (parent === current) break;
|
|
428
770
|
current = parent;
|
|
429
771
|
}
|
|
430
772
|
return { root: startDir, foundGit: false };
|
|
431
773
|
}
|
|
432
774
|
async function handleShow(deps, projectBindingPath, mode2) {
|
|
775
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
776
|
+
const root = findRepoRoot(startDir).root;
|
|
777
|
+
const repoIdentity = await makeRepoIdentityResolver(deps)(root);
|
|
433
778
|
if (projectBindingPath == null) {
|
|
434
779
|
if (mode2 === "json") {
|
|
435
|
-
printResult({ bound: false, projectBindingPath: null }, mode2);
|
|
780
|
+
printResult({ bound: false, projectBindingPath: null, repoIdentity }, mode2);
|
|
436
781
|
} else {
|
|
437
782
|
process.stdout.write("No nolto.json binding found in this directory tree.\n");
|
|
783
|
+
process.stdout.write(`repoIdentity : ${repoIdentity.kind}:${repoIdentity.value}
|
|
784
|
+
`);
|
|
438
785
|
}
|
|
439
786
|
return;
|
|
440
787
|
}
|
|
@@ -447,7 +794,8 @@ async function handleShow(deps, projectBindingPath, mode2) {
|
|
|
447
794
|
bound: binding != null,
|
|
448
795
|
projectId: binding?.projectId ?? null,
|
|
449
796
|
projectBindingPath,
|
|
450
|
-
source: deps.settings.source.project === "repo" ? "repo" : "file"
|
|
797
|
+
source: deps.settings.source.project === "repo" ? "repo" : "file",
|
|
798
|
+
repoIdentity
|
|
451
799
|
}, mode2);
|
|
452
800
|
} else {
|
|
453
801
|
if (binding == null) {
|
|
@@ -460,15 +808,33 @@ async function handleShow(deps, projectBindingPath, mode2) {
|
|
|
460
808
|
`);
|
|
461
809
|
const active = deps.settings.source.project === "repo" ? "repo (active)" : "repo (not active \u2014 overridden)";
|
|
462
810
|
process.stdout.write(`source : ${active}
|
|
811
|
+
`);
|
|
812
|
+
process.stdout.write(`repoIdentity : ${repoIdentity.kind}:${repoIdentity.value}
|
|
463
813
|
`);
|
|
464
814
|
}
|
|
465
815
|
}
|
|
466
816
|
}
|
|
817
|
+
async function handleRebind(deps, projectId, root, mode2) {
|
|
818
|
+
if (!UUID_RE.test(projectId)) {
|
|
819
|
+
throw new CliError(
|
|
820
|
+
`Invalid project ID: "${projectId}". Must be a UUID (e.g. 00000000-0000-0000-0000-000000000001).`,
|
|
821
|
+
2
|
|
822
|
+
);
|
|
823
|
+
}
|
|
824
|
+
const repoIdentity = await makeRepoIdentityResolver(deps)(root);
|
|
825
|
+
await deps.http.post(`/api/projects/${projectId}/repo-binding`, { repoIdentity });
|
|
826
|
+
if (mode2 === "json") {
|
|
827
|
+
printResult({ rebound: true, projectId, repoIdentity }, mode2);
|
|
828
|
+
} else {
|
|
829
|
+
process.stdout.write(`Rebound project ${projectId} to ${repoIdentity.kind}:${repoIdentity.value}.
|
|
830
|
+
`);
|
|
831
|
+
}
|
|
832
|
+
}
|
|
467
833
|
async function handleUnlink(projectBindingPath, mode2) {
|
|
468
|
-
const { readFile:
|
|
834
|
+
const { readFile: readFile8, writeFile: writeFile7, chmod: chmod2 } = await import("fs/promises");
|
|
469
835
|
let existing = {};
|
|
470
836
|
try {
|
|
471
|
-
const raw = await
|
|
837
|
+
const raw = await readFile8(projectBindingPath, "utf8");
|
|
472
838
|
const parsed = JSON.parse(raw);
|
|
473
839
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
474
840
|
throw new CliError(
|
|
@@ -483,8 +849,8 @@ async function handleUnlink(projectBindingPath, mode2) {
|
|
|
483
849
|
}
|
|
484
850
|
const { projectId: _removed, ...rest } = existing;
|
|
485
851
|
void _removed;
|
|
486
|
-
await
|
|
487
|
-
await
|
|
852
|
+
await writeFile7(projectBindingPath, JSON.stringify(rest, null, 2) + "\n", { mode: 420 });
|
|
853
|
+
await chmod2(projectBindingPath, 420);
|
|
488
854
|
if (mode2 === "json") {
|
|
489
855
|
printResult({ unlinked: true, projectBindingPath }, mode2);
|
|
490
856
|
} else {
|
|
@@ -538,21 +904,38 @@ Proceeding anyway \u2014 verify the ID is correct.
|
|
|
538
904
|
);
|
|
539
905
|
}
|
|
540
906
|
await writeRepoBinding(root, projectId);
|
|
541
|
-
const writtenPath =
|
|
907
|
+
const writtenPath = path5.join(root, "nolto.json");
|
|
908
|
+
let registryAdded = false;
|
|
909
|
+
try {
|
|
910
|
+
registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
|
|
911
|
+
} catch (err) {
|
|
912
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
913
|
+
process.stderr.write(`Warning: could not update watch registry: ${message}
|
|
914
|
+
`);
|
|
915
|
+
}
|
|
542
916
|
if (mode2 === "json") {
|
|
543
|
-
printResult({
|
|
917
|
+
printResult({
|
|
918
|
+
linked: true,
|
|
919
|
+
projectId,
|
|
920
|
+
projectBindingPath: writtenPath,
|
|
921
|
+
registryAdded
|
|
922
|
+
}, mode2);
|
|
544
923
|
} else {
|
|
545
924
|
process.stdout.write(
|
|
546
925
|
`Linked this repo to project ${projectId} (wrote ${writtenPath}).
|
|
547
926
|
Commit nolto.json to share the binding with your team.
|
|
548
927
|
`
|
|
549
928
|
);
|
|
929
|
+
if (registryAdded) {
|
|
930
|
+
process.stdout.write(`watch registry: added ${root}
|
|
931
|
+
`);
|
|
932
|
+
}
|
|
550
933
|
}
|
|
551
934
|
}
|
|
552
935
|
function register(program, deps) {
|
|
553
936
|
const cmd = program.command("link [projectId]").description(
|
|
554
|
-
"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 --unlink Remove the projectId from nolto.json"
|
|
555
|
-
).option("--show", "Show the current repo binding (path + projectId + source)").option("--unlink", "Remove the projectId key from nolto.json");
|
|
937
|
+
"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"
|
|
938
|
+
).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");
|
|
556
939
|
cmd.action(async (projectId) => {
|
|
557
940
|
const { output } = deps;
|
|
558
941
|
const projectBindingPath = deps.repoBinding?.path ?? deps.projectBindingPath ?? null;
|
|
@@ -572,12 +955,23 @@ function register(program, deps) {
|
|
|
572
955
|
await handleUnlink(projectBindingPath, mode2);
|
|
573
956
|
return;
|
|
574
957
|
}
|
|
958
|
+
if (cmd.opts()["rebind"]) {
|
|
959
|
+
const boundProjectId = deps.repoBinding?.binding?.projectId;
|
|
960
|
+
const effectiveProjectId = projectId ?? boundProjectId;
|
|
961
|
+
if (effectiveProjectId == null) {
|
|
962
|
+
throw new CliError("No project binding. Run `nolto link <projectId>` first.", 2);
|
|
963
|
+
}
|
|
964
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
965
|
+
const root = findRepoRoot(startDir).root;
|
|
966
|
+
await handleRebind(deps, effectiveProjectId, root, mode2);
|
|
967
|
+
return;
|
|
968
|
+
}
|
|
575
969
|
if (projectId == null || projectId.trim().length === 0) {
|
|
576
970
|
if (bindingError != null) {
|
|
577
971
|
throw invalidBindingError(projectBindingPath, bindingError);
|
|
578
972
|
}
|
|
579
973
|
throw new CliError(
|
|
580
|
-
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --unlink to remove it.",
|
|
974
|
+
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --rebind to rebind it, --unlink to remove it.",
|
|
581
975
|
2
|
|
582
976
|
);
|
|
583
977
|
}
|
|
@@ -592,32 +986,32 @@ function invalidBindingError(projectBindingPath, error) {
|
|
|
592
986
|
}
|
|
593
987
|
|
|
594
988
|
// src/skill-install.ts
|
|
595
|
-
import { cp, mkdir as
|
|
989
|
+
import { cp, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, rm } from "fs/promises";
|
|
596
990
|
import { existsSync } from "fs";
|
|
597
|
-
import
|
|
991
|
+
import path6 from "path";
|
|
598
992
|
import { fileURLToPath } from "url";
|
|
599
|
-
var __dirname =
|
|
993
|
+
var __dirname = path6.dirname(fileURLToPath(import.meta.url));
|
|
600
994
|
function resolveSkillSourceDir() {
|
|
601
995
|
const candidates = [
|
|
602
|
-
|
|
996
|
+
path6.resolve(__dirname, "skill/roadmap-progress"),
|
|
603
997
|
// bundled: dist/skill/...
|
|
604
|
-
|
|
998
|
+
path6.resolve(__dirname, "../../../skills/roadmap-progress")
|
|
605
999
|
// source: <repo>/skills/...
|
|
606
1000
|
];
|
|
607
1001
|
for (const candidate of candidates) {
|
|
608
|
-
if (existsSync(
|
|
1002
|
+
if (existsSync(path6.join(candidate, "SKILL.md"))) return candidate;
|
|
609
1003
|
}
|
|
610
1004
|
throw new Error("Bundled roadmap-progress skill not found. Reinstall @nolto/cli.");
|
|
611
1005
|
}
|
|
612
1006
|
var VERSION_MARKER = ".nolto-skill-version";
|
|
613
1007
|
async function installSkill(args) {
|
|
614
|
-
const targetDir =
|
|
615
|
-
const markerPath =
|
|
1008
|
+
const targetDir = path6.join(args.skillsParentDir, "roadmap-progress");
|
|
1009
|
+
const markerPath = path6.join(targetDir, VERSION_MARKER);
|
|
616
1010
|
const dirExists = existsSync(targetDir);
|
|
617
1011
|
let installedVersion = null;
|
|
618
1012
|
if (dirExists) {
|
|
619
1013
|
try {
|
|
620
|
-
installedVersion = (await
|
|
1014
|
+
installedVersion = (await readFile4(markerPath, "utf8")).trim();
|
|
621
1015
|
} catch {
|
|
622
1016
|
installedVersion = null;
|
|
623
1017
|
}
|
|
@@ -626,79 +1020,29 @@ async function installSkill(args) {
|
|
|
626
1020
|
return { action: "skipped", targetDir };
|
|
627
1021
|
}
|
|
628
1022
|
await rm(targetDir, { recursive: true, force: true });
|
|
629
|
-
await
|
|
1023
|
+
await mkdir4(args.skillsParentDir, { recursive: true });
|
|
630
1024
|
await cp(args.sourceDir, targetDir, { recursive: true });
|
|
631
|
-
await
|
|
1025
|
+
await writeFile4(markerPath, args.version + "\n", "utf8");
|
|
632
1026
|
return { action: dirExists ? "updated" : "installed", targetDir };
|
|
633
1027
|
}
|
|
634
1028
|
|
|
635
|
-
// src/registry.ts
|
|
636
|
-
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir3 } from "fs/promises";
|
|
637
|
-
import path4 from "path";
|
|
638
|
-
import { z as z2 } from "zod";
|
|
639
|
-
var registrySchema = z2.object({
|
|
640
|
-
schemaVersion: z2.literal(1),
|
|
641
|
-
repos: z2.array(z2.object({ root: z2.string().min(1) }).passthrough())
|
|
642
|
-
}).passthrough();
|
|
643
|
-
function getRegistryPath(env) {
|
|
644
|
-
return path4.join(getConfigDir(env), "registry.json");
|
|
645
|
-
}
|
|
646
|
-
async function loadRegistry(filePath) {
|
|
647
|
-
let raw;
|
|
648
|
-
try {
|
|
649
|
-
raw = await readFile3(filePath, "utf8");
|
|
650
|
-
} catch (err) {
|
|
651
|
-
const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
|
|
652
|
-
if (code === "ENOENT") {
|
|
653
|
-
return { schemaVersion: 1, repos: [] };
|
|
654
|
-
}
|
|
655
|
-
throw new CliError(`Cannot read registry: ${filePath}: ${String(err)}`, 2);
|
|
656
|
-
}
|
|
657
|
-
let parsed;
|
|
658
|
-
try {
|
|
659
|
-
parsed = JSON.parse(raw);
|
|
660
|
-
} catch {
|
|
661
|
-
throw new CliError(`Malformed JSON in ${filePath}. Fix or remove the file.`, 2);
|
|
662
|
-
}
|
|
663
|
-
const result = registrySchema.safeParse(parsed);
|
|
664
|
-
if (!result.success) {
|
|
665
|
-
const issue = result.error.issues[0];
|
|
666
|
-
const fieldPath = issue?.path.join(".") ?? "";
|
|
667
|
-
throw new CliError(
|
|
668
|
-
`Invalid registry at ${filePath}: ${fieldPath.length > 0 ? `field "${fieldPath}" \u2014 ` : ""}${issue?.message ?? "validation failed"}`,
|
|
669
|
-
2
|
|
670
|
-
);
|
|
671
|
-
}
|
|
672
|
-
return result.data;
|
|
673
|
-
}
|
|
674
|
-
async function addRepoToRegistry(filePath, repoRoot) {
|
|
675
|
-
const registry = await loadRegistry(filePath);
|
|
676
|
-
if (registry.repos.some((repo) => repo.root === repoRoot)) {
|
|
677
|
-
return { added: false, registry };
|
|
678
|
-
}
|
|
679
|
-
const updated = { ...registry, repos: [...registry.repos, { root: repoRoot }] };
|
|
680
|
-
await mkdir3(path4.dirname(filePath), { recursive: true, mode: 448 });
|
|
681
|
-
await writeFile3(filePath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
682
|
-
return { added: true, registry: updated };
|
|
683
|
-
}
|
|
684
|
-
|
|
685
1029
|
// src/roadmap-scaffold.ts
|
|
686
|
-
import { mkdir as
|
|
1030
|
+
import { mkdir as mkdir5, readdir, writeFile as writeFile5 } from "fs/promises";
|
|
687
1031
|
import { existsSync as existsSync2 } from "fs";
|
|
688
|
-
import
|
|
1032
|
+
import path7 from "path";
|
|
689
1033
|
function slugifyProjectId(name) {
|
|
690
1034
|
const slug = name.toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[-_.]+$/, "");
|
|
691
1035
|
return slug.length > 0 ? slug : "project";
|
|
692
1036
|
}
|
|
693
1037
|
async function scaffoldRoadmap(args) {
|
|
694
|
-
const repoBasename =
|
|
695
|
-
const dir =
|
|
696
|
-
const filePath =
|
|
697
|
-
const legacyPath =
|
|
1038
|
+
const repoBasename = path7.basename(args.repoRoot);
|
|
1039
|
+
const dir = path7.join(args.repoRoot, ".nolto", "roadmaps");
|
|
1040
|
+
const filePath = path7.join(dir, `${slugifyProjectId(repoBasename)}.json`);
|
|
1041
|
+
const legacyPath = path7.join(args.repoRoot, ".roadmap", "roadmap.json");
|
|
698
1042
|
try {
|
|
699
1043
|
const existing = (await readdir(dir)).sort();
|
|
700
1044
|
if (existing.length > 0) {
|
|
701
|
-
return { created: false, path:
|
|
1045
|
+
return { created: false, path: path7.join(dir, existing[0]) };
|
|
702
1046
|
}
|
|
703
1047
|
} catch (err) {
|
|
704
1048
|
if (err == null || typeof err !== "object" || !("code" in err) || err.code !== "ENOENT") {
|
|
@@ -720,19 +1064,19 @@ async function scaffoldRoadmap(args) {
|
|
|
720
1064
|
summary: "",
|
|
721
1065
|
phases: []
|
|
722
1066
|
};
|
|
723
|
-
await
|
|
724
|
-
await
|
|
1067
|
+
await mkdir5(dir, { recursive: true });
|
|
1068
|
+
await writeFile5(filePath, JSON.stringify(roadmap, null, 2) + "\n", "utf8");
|
|
725
1069
|
return { created: true, path: filePath };
|
|
726
1070
|
}
|
|
727
1071
|
|
|
728
1072
|
// src/commands/init.ts
|
|
729
|
-
var __dirname2 =
|
|
1073
|
+
var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
730
1074
|
var _require = createRequire(import.meta.url);
|
|
731
1075
|
function getCliVersion() {
|
|
732
1076
|
const candidates = [
|
|
733
|
-
|
|
1077
|
+
path8.resolve(__dirname2, "../package.json"),
|
|
734
1078
|
// bundled: dist/../package.json
|
|
735
|
-
|
|
1079
|
+
path8.resolve(__dirname2, "../../package.json")
|
|
736
1080
|
// source: src/commands/../../package.json
|
|
737
1081
|
];
|
|
738
1082
|
for (const pkgPath of candidates) {
|
|
@@ -860,20 +1204,21 @@ Set up this repository (${root}) for roadmap sync? [Y/n] `);
|
|
|
860
1204
|
);
|
|
861
1205
|
}
|
|
862
1206
|
await writeRepoBinding(root, defaultProjectId);
|
|
863
|
-
process.stdout.write(`binding: wrote ${
|
|
1207
|
+
process.stdout.write(`binding: wrote ${path8.join(root, "nolto.json")}
|
|
864
1208
|
`);
|
|
865
1209
|
const sourceDir = resolveSkillSourceDir();
|
|
866
1210
|
const version = getCliVersion();
|
|
867
1211
|
const claudeInstall = await installSkill({
|
|
868
|
-
skillsParentDir:
|
|
1212
|
+
skillsParentDir: path8.join(root, ".claude", "skills"),
|
|
869
1213
|
sourceDir,
|
|
870
1214
|
version
|
|
871
1215
|
});
|
|
872
1216
|
process.stdout.write(`skill (claude): ${claudeInstall.action} ${claudeInstall.targetDir}
|
|
873
1217
|
`);
|
|
874
|
-
|
|
1218
|
+
const usesAgentsTooling = fs.existsSync(path8.join(root, ".agents")) || fs.existsSync(path8.join(root, ".codex")) || fs.existsSync(path8.join(root, "AGENTS.md"));
|
|
1219
|
+
if (usesAgentsTooling) {
|
|
875
1220
|
const agentsInstall = await installSkill({
|
|
876
|
-
skillsParentDir:
|
|
1221
|
+
skillsParentDir: path8.join(root, ".agents", "skills"),
|
|
877
1222
|
sourceDir,
|
|
878
1223
|
version
|
|
879
1224
|
});
|
|
@@ -882,7 +1227,7 @@ Set up this repository (${root}) for roadmap sync? [Y/n] `);
|
|
|
882
1227
|
}
|
|
883
1228
|
const scaffold = await scaffoldRoadmap({
|
|
884
1229
|
repoRoot: root,
|
|
885
|
-
projectName: defaultProjectName ??
|
|
1230
|
+
projectName: defaultProjectName ?? path8.basename(root)
|
|
886
1231
|
});
|
|
887
1232
|
process.stdout.write(
|
|
888
1233
|
scaffold.created ? `roadmap: created ${scaffold.path}
|
|
@@ -1119,142 +1464,15 @@ function register4(program, deps) {
|
|
|
1119
1464
|
}
|
|
1120
1465
|
|
|
1121
1466
|
// src/commands/sync.ts
|
|
1122
|
-
import { copyFile, mkdir as
|
|
1467
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile5, readdir as readdir2, rename as rename2, rmdir, unlink as unlink2 } from "fs/promises";
|
|
1123
1468
|
import { existsSync as existsSync3 } from "fs";
|
|
1124
1469
|
|
|
1125
1470
|
// src/sync-repo.ts
|
|
1126
|
-
import
|
|
1471
|
+
import path10 from "path";
|
|
1127
1472
|
|
|
1128
1473
|
// src/sync-core.ts
|
|
1129
1474
|
import { createHash } from "crypto";
|
|
1130
|
-
import
|
|
1131
|
-
|
|
1132
|
-
// ../roadmap-schema/src/index.ts
|
|
1133
|
-
var STATUSES = /* @__PURE__ */ new Set(["todo", "in-progress", "done", "blocked"]);
|
|
1134
|
-
var ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
1135
|
-
var ALLOWED_KEYS = {
|
|
1136
|
-
roadmap: /* @__PURE__ */ new Set(["schemaVersion", "project", "updatedAt", "currentTaskId", "summary", "phases"]),
|
|
1137
|
-
project: /* @__PURE__ */ new Set(["id", "name", "repository"]),
|
|
1138
|
-
phase: /* @__PURE__ */ new Set(["id", "title", "status", "plan", "tasks"]),
|
|
1139
|
-
task: /* @__PURE__ */ new Set(["id", "title", "status", "startedAt", "completedAt", "note", "dependsOn", "plan"])
|
|
1140
|
-
};
|
|
1141
|
-
function isRecord(value) {
|
|
1142
|
-
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
1143
|
-
}
|
|
1144
|
-
function checkKeys(value, allowed, at, errors) {
|
|
1145
|
-
if (!isRecord(value)) return;
|
|
1146
|
-
for (const key of Object.keys(value)) {
|
|
1147
|
-
if (!allowed.has(key)) errors.push(`${at} contains unsupported property "${key}".`);
|
|
1148
|
-
}
|
|
1149
|
-
}
|
|
1150
|
-
function checkPlan(value, at, errors) {
|
|
1151
|
-
if (value === void 0) return;
|
|
1152
|
-
if (typeof value !== "string" || value.length === 0) {
|
|
1153
|
-
errors.push(`${at}.plan must be a non-empty string.`);
|
|
1154
|
-
}
|
|
1155
|
-
}
|
|
1156
|
-
function derivePhaseStatus(phase) {
|
|
1157
|
-
if (phase.tasks.length > 0 && phase.tasks.every((task) => task.status === "done")) return "done";
|
|
1158
|
-
if (phase.tasks.some((task) => task.status === "in-progress")) return "in-progress";
|
|
1159
|
-
if (phase.tasks.some((task) => task.status === "blocked")) return "blocked";
|
|
1160
|
-
if (phase.tasks.some((task) => task.status === "done")) return "in-progress";
|
|
1161
|
-
return "todo";
|
|
1162
|
-
}
|
|
1163
|
-
function validateRoadmap(value) {
|
|
1164
|
-
const errors = [];
|
|
1165
|
-
const warnings = [];
|
|
1166
|
-
if (!isRecord(value)) return { errors: ["Root must be an object."], warnings };
|
|
1167
|
-
checkKeys(value, ALLOWED_KEYS.roadmap, "roadmap", errors);
|
|
1168
|
-
if (value["schemaVersion"] !== 1 && value["schemaVersion"] !== 2) {
|
|
1169
|
-
errors.push("schemaVersion must be 1 or 2.");
|
|
1170
|
-
} else if (value["schemaVersion"] === 1) {
|
|
1171
|
-
warnings.push("schemaVersion 1 is legacy; the next roadmap-progress mutation migrates this file to 2.");
|
|
1172
|
-
}
|
|
1173
|
-
const project = value["project"];
|
|
1174
|
-
checkKeys(project, ALLOWED_KEYS.project, "project", errors);
|
|
1175
|
-
const projectRecord = isRecord(project) ? project : {};
|
|
1176
|
-
if (!ID_PATTERN.test(String(projectRecord["id"] ?? ""))) errors.push("project.id is invalid.");
|
|
1177
|
-
if (projectRecord["name"] == null || projectRecord["name"] === "") errors.push("project.name is required.");
|
|
1178
|
-
if (projectRecord["repository"] == null || projectRecord["repository"] === "") errors.push("project.repository is required.");
|
|
1179
|
-
if (Number.isNaN(Date.parse(String(value["updatedAt"])))) errors.push("updatedAt must be a valid date-time.");
|
|
1180
|
-
if (typeof value["summary"] !== "string") errors.push("summary must be a string.");
|
|
1181
|
-
if (!Array.isArray(value["phases"])) errors.push("phases must be an array.");
|
|
1182
|
-
const allIds = /* @__PURE__ */ new Set();
|
|
1183
|
-
const tasks = /* @__PURE__ */ new Map();
|
|
1184
|
-
const phases = Array.isArray(value["phases"]) ? value["phases"] : [];
|
|
1185
|
-
for (const [phaseIndex, rawPhase] of phases.entries()) {
|
|
1186
|
-
const at = `phases[${phaseIndex}]`;
|
|
1187
|
-
checkKeys(rawPhase, ALLOWED_KEYS.phase, at, errors);
|
|
1188
|
-
const phase = isRecord(rawPhase) ? rawPhase : {};
|
|
1189
|
-
const phaseId = String(phase["id"] ?? "");
|
|
1190
|
-
if (!ID_PATTERN.test(phaseId)) errors.push(`${at}.id is invalid.`);
|
|
1191
|
-
if (allIds.has(phaseId)) errors.push(`Duplicate id "${phaseId}".`);
|
|
1192
|
-
allIds.add(phaseId);
|
|
1193
|
-
if (phase["title"] == null || phase["title"] === "") errors.push(`${at}.title is required.`);
|
|
1194
|
-
if (!STATUSES.has(String(phase["status"]))) errors.push(`${at}.status is invalid.`);
|
|
1195
|
-
checkPlan(phase["plan"], at, errors);
|
|
1196
|
-
if (!Array.isArray(phase["tasks"])) errors.push(`${at}.tasks must be an array.`);
|
|
1197
|
-
const rawTasks = Array.isArray(phase["tasks"]) ? phase["tasks"] : [];
|
|
1198
|
-
for (const [taskIndex, rawTask] of rawTasks.entries()) {
|
|
1199
|
-
const taskAt = `${at}.tasks[${taskIndex}]`;
|
|
1200
|
-
checkKeys(rawTask, ALLOWED_KEYS.task, taskAt, errors);
|
|
1201
|
-
const task = isRecord(rawTask) ? rawTask : {};
|
|
1202
|
-
const taskId = String(task["id"] ?? "");
|
|
1203
|
-
if (!ID_PATTERN.test(taskId)) errors.push(`${taskAt}.id is invalid.`);
|
|
1204
|
-
if (allIds.has(taskId)) errors.push(`Duplicate id "${taskId}".`);
|
|
1205
|
-
allIds.add(taskId);
|
|
1206
|
-
tasks.set(taskId, task);
|
|
1207
|
-
if (task["title"] == null || task["title"] === "") errors.push(`${taskAt}.title is required.`);
|
|
1208
|
-
if (!STATUSES.has(String(task["status"]))) errors.push(`${taskAt}.status is invalid.`);
|
|
1209
|
-
checkPlan(task["plan"], taskAt, errors);
|
|
1210
|
-
if (task["status"] === "done" && task["completedAt"] == null) warnings.push(`${taskId} is done without completedAt.`);
|
|
1211
|
-
if (task["status"] === "in-progress" && task["startedAt"] == null) warnings.push(`${taskId} is in-progress without startedAt.`);
|
|
1212
|
-
if (task["dependsOn"] !== void 0 && !Array.isArray(task["dependsOn"])) errors.push(`${taskAt}.dependsOn must be an array.`);
|
|
1213
|
-
}
|
|
1214
|
-
if (Array.isArray(phase["tasks"]) && STATUSES.has(String(phase["status"]))) {
|
|
1215
|
-
const expected = derivePhaseStatus({ ...phase, tasks: rawTasks });
|
|
1216
|
-
if (phase["status"] !== expected) warnings.push(`${phaseId} status is ${String(phase["status"])}; task states derive ${expected}.`);
|
|
1217
|
-
}
|
|
1218
|
-
}
|
|
1219
|
-
for (const task of tasks.values()) {
|
|
1220
|
-
for (const dependency of task.dependsOn ?? []) {
|
|
1221
|
-
if (!tasks.has(dependency)) warnings.push(`${task.id} depends on unknown task ${dependency}.`);
|
|
1222
|
-
if (task.id === dependency) errors.push(`${task.id} cannot depend on itself.`);
|
|
1223
|
-
}
|
|
1224
|
-
}
|
|
1225
|
-
const currentTaskId = value["currentTaskId"];
|
|
1226
|
-
if (currentTaskId !== null && currentTaskId !== void 0) {
|
|
1227
|
-
const current = tasks.get(String(currentTaskId));
|
|
1228
|
-
if (current == null) errors.push(`currentTaskId ${String(currentTaskId)} does not exist.`);
|
|
1229
|
-
else if (current.status !== "in-progress") warnings.push(`currentTaskId ${String(currentTaskId)} is not in-progress.`);
|
|
1230
|
-
}
|
|
1231
|
-
return { errors, warnings };
|
|
1232
|
-
}
|
|
1233
|
-
function deriveTaskStats(roadmap) {
|
|
1234
|
-
let done = 0;
|
|
1235
|
-
let blocked = 0;
|
|
1236
|
-
let inProgress = 0;
|
|
1237
|
-
let todo = 0;
|
|
1238
|
-
for (const phase of roadmap.phases) {
|
|
1239
|
-
for (const task of phase.tasks) {
|
|
1240
|
-
if (task.status === "done") done += 1;
|
|
1241
|
-
else if (task.status === "blocked") blocked += 1;
|
|
1242
|
-
else if (task.status === "in-progress") inProgress += 1;
|
|
1243
|
-
else todo += 1;
|
|
1244
|
-
}
|
|
1245
|
-
}
|
|
1246
|
-
const total = done + blocked + inProgress + todo;
|
|
1247
|
-
return {
|
|
1248
|
-
total,
|
|
1249
|
-
done,
|
|
1250
|
-
blocked,
|
|
1251
|
-
inProgress,
|
|
1252
|
-
todo,
|
|
1253
|
-
progressPct: total > 0 ? Math.round(done / total * 100) : 0
|
|
1254
|
-
};
|
|
1255
|
-
}
|
|
1256
|
-
|
|
1257
|
-
// src/sync-core.ts
|
|
1475
|
+
import path9 from "path";
|
|
1258
1476
|
function sha256Hex(content) {
|
|
1259
1477
|
return "sha256:" + createHash("sha256").update(content, "utf8").digest("hex");
|
|
1260
1478
|
}
|
|
@@ -1275,10 +1493,10 @@ function collectPlanRefs(roadmap) {
|
|
|
1275
1493
|
}
|
|
1276
1494
|
return refs;
|
|
1277
1495
|
}
|
|
1278
|
-
async function loadValidRoadmap(filePath,
|
|
1496
|
+
async function loadValidRoadmap(filePath, readFile8) {
|
|
1279
1497
|
let raw;
|
|
1280
1498
|
try {
|
|
1281
|
-
raw = await
|
|
1499
|
+
raw = await readFile8(filePath);
|
|
1282
1500
|
} catch {
|
|
1283
1501
|
throw new CliError(`No roadmap found at ${filePath}. Run \`nolto init\` first.`, 2);
|
|
1284
1502
|
}
|
|
@@ -1301,7 +1519,7 @@ async function loadValidRoadmap(filePath, readFile7) {
|
|
|
1301
1519
|
async function buildSyncBody(args) {
|
|
1302
1520
|
const planDocuments = [];
|
|
1303
1521
|
for (const ref of collectPlanRefs(args.roadmap)) {
|
|
1304
|
-
const absolute =
|
|
1522
|
+
const absolute = path9.join(args.repoRoot, ref.path);
|
|
1305
1523
|
if (!args.deps.fileExists(absolute)) {
|
|
1306
1524
|
args.deps.warn(`plan file not found, skipping: ${ref.path}`);
|
|
1307
1525
|
continue;
|
|
@@ -1314,12 +1532,13 @@ async function buildSyncBody(args) {
|
|
|
1314
1532
|
contentHash: sha256Hex(content)
|
|
1315
1533
|
});
|
|
1316
1534
|
}
|
|
1317
|
-
return { roadmap: args.roadmap, planDocuments };
|
|
1535
|
+
return { roadmap: args.roadmap, planDocuments, repoIdentity: args.repoIdentity };
|
|
1318
1536
|
}
|
|
1319
1537
|
async function runSync(args, deps) {
|
|
1320
1538
|
const body = await buildSyncBody({
|
|
1321
1539
|
roadmap: args.roadmap,
|
|
1322
1540
|
repoRoot: args.repoRoot,
|
|
1541
|
+
repoIdentity: args.repoIdentity,
|
|
1323
1542
|
deps
|
|
1324
1543
|
});
|
|
1325
1544
|
const response = await deps.http.put(
|
|
@@ -1347,7 +1566,7 @@ async function listRoadmapFiles(roadmapsDir, io) {
|
|
|
1347
1566
|
}
|
|
1348
1567
|
}
|
|
1349
1568
|
async function migrateLegacyRoadmap(args) {
|
|
1350
|
-
const targetPath =
|
|
1569
|
+
const targetPath = path10.join(args.roadmapsDir, `${args.slug}.json`);
|
|
1351
1570
|
await args.io.mkdir(args.roadmapsDir);
|
|
1352
1571
|
try {
|
|
1353
1572
|
await args.io.rename(args.legacyPath, targetPath);
|
|
@@ -1356,7 +1575,7 @@ async function migrateLegacyRoadmap(args) {
|
|
|
1356
1575
|
await args.io.unlink(args.legacyPath);
|
|
1357
1576
|
}
|
|
1358
1577
|
try {
|
|
1359
|
-
await args.io.rmdir(
|
|
1578
|
+
await args.io.rmdir(path10.dirname(args.legacyPath));
|
|
1360
1579
|
} catch {
|
|
1361
1580
|
}
|
|
1362
1581
|
args.io.log(
|
|
@@ -1365,14 +1584,14 @@ async function migrateLegacyRoadmap(args) {
|
|
|
1365
1584
|
return `${args.slug}.json`;
|
|
1366
1585
|
}
|
|
1367
1586
|
async function syncRepo(args, io) {
|
|
1368
|
-
const bindingPath =
|
|
1587
|
+
const bindingPath = path10.join(args.root, "nolto.json");
|
|
1369
1588
|
const binding = await loadRepoBinding(bindingPath);
|
|
1370
1589
|
const projectId = binding?.projectId ?? args.defaultProjectId;
|
|
1371
1590
|
if (projectId == null) {
|
|
1372
1591
|
throw new CliError("No project binding. Run `nolto init` or `nolto link <projectId>`.", 2);
|
|
1373
1592
|
}
|
|
1374
|
-
const roadmapsDir =
|
|
1375
|
-
const legacyPath =
|
|
1593
|
+
const roadmapsDir = path10.join(args.root, ".nolto", "roadmaps");
|
|
1594
|
+
const legacyPath = path10.join(args.root, ".roadmap", "roadmap.json");
|
|
1376
1595
|
let roadmapFiles = await listRoadmapFiles(roadmapsDir, io);
|
|
1377
1596
|
if (roadmapFiles.length > 0) {
|
|
1378
1597
|
if (io.fileExists(legacyPath)) {
|
|
@@ -1381,7 +1600,7 @@ async function syncRepo(args, io) {
|
|
|
1381
1600
|
);
|
|
1382
1601
|
}
|
|
1383
1602
|
} else if (io.fileExists(legacyPath)) {
|
|
1384
|
-
const migrationSlug = binding?.roadmapSlug ?? slugifyProjectId(
|
|
1603
|
+
const migrationSlug = binding?.roadmapSlug ?? slugifyProjectId(path10.basename(args.root));
|
|
1385
1604
|
roadmapFiles = [
|
|
1386
1605
|
await migrateLegacyRoadmap({
|
|
1387
1606
|
slug: migrationSlug,
|
|
@@ -1405,21 +1624,22 @@ async function syncRepo(args, io) {
|
|
|
1405
1624
|
2
|
|
1406
1625
|
);
|
|
1407
1626
|
}
|
|
1408
|
-
const filePath =
|
|
1627
|
+
const filePath = path10.join(roadmapsDir, fileName);
|
|
1409
1628
|
return { slug, roadmap: await loadValidRoadmap(filePath, io.readFile) };
|
|
1410
1629
|
})
|
|
1411
1630
|
);
|
|
1412
1631
|
const planAbsPaths = /* @__PURE__ */ new Set();
|
|
1413
1632
|
for (const { roadmap } of roadmaps) {
|
|
1414
1633
|
for (const ref of collectPlanRefs(roadmap)) {
|
|
1415
|
-
planAbsPaths.add(
|
|
1634
|
+
planAbsPaths.add(path10.join(args.root, ref.path));
|
|
1416
1635
|
}
|
|
1417
1636
|
}
|
|
1418
1637
|
const results = [];
|
|
1638
|
+
const repoIdentity = await io.repoIdentity(args.root);
|
|
1419
1639
|
for (const { slug, roadmap } of roadmaps) {
|
|
1420
1640
|
results.push(
|
|
1421
1641
|
await runSync(
|
|
1422
|
-
{ repoRoot: args.root, projectId, slug, roadmap },
|
|
1642
|
+
{ repoRoot: args.root, projectId, slug, roadmap, repoIdentity },
|
|
1423
1643
|
{ http: io.http, readFile: io.readFile, fileExists: io.fileExists, log: io.log, warn: io.warn }
|
|
1424
1644
|
)
|
|
1425
1645
|
);
|
|
@@ -1438,38 +1658,46 @@ function register5(program, deps) {
|
|
|
1438
1658
|
if (!foundGit) {
|
|
1439
1659
|
throw new CliError("No git repository found. Run inside a repo set up with `nolto init`.", 2);
|
|
1440
1660
|
}
|
|
1441
|
-
const http =
|
|
1442
|
-
baseUrl: deps.settings.baseUrl,
|
|
1443
|
-
version: deps.version,
|
|
1444
|
-
token: deps.settings.token
|
|
1445
|
-
});
|
|
1661
|
+
const http = deps.http;
|
|
1446
1662
|
const response = await syncRepo(
|
|
1447
1663
|
{ root, defaultProjectId: deps.settings.defaultProjectId },
|
|
1448
1664
|
{
|
|
1449
|
-
readFile: (p) =>
|
|
1665
|
+
readFile: (p) => readFile5(p, "utf8"),
|
|
1450
1666
|
fileExists: (p) => existsSync3(p),
|
|
1451
1667
|
listDir: (p) => readdir2(p),
|
|
1452
|
-
rename,
|
|
1668
|
+
rename: rename2,
|
|
1453
1669
|
copyFile,
|
|
1454
|
-
mkdir: (p) =>
|
|
1455
|
-
unlink,
|
|
1670
|
+
mkdir: (p) => mkdir6(p, { recursive: true }).then(() => void 0),
|
|
1671
|
+
unlink: unlink2,
|
|
1456
1672
|
rmdir,
|
|
1673
|
+
repoIdentity: makeRepoIdentityResolver(deps),
|
|
1457
1674
|
http,
|
|
1458
1675
|
log: (line) => process.stdout.write(line + "\n"),
|
|
1459
1676
|
warn: (line) => process.stderr.write("Warning: " + line + "\n")
|
|
1460
1677
|
}
|
|
1461
1678
|
);
|
|
1679
|
+
let registryAdded = false;
|
|
1680
|
+
try {
|
|
1681
|
+
registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
|
|
1682
|
+
} catch (err) {
|
|
1683
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1684
|
+
process.stderr.write(`Warning: could not update watch registry: ${message}
|
|
1685
|
+
`);
|
|
1686
|
+
}
|
|
1462
1687
|
if (deps.output.mode === "json") {
|
|
1463
1688
|
const { planAbsPaths: _planAbsPaths, ...publicResponse } = response;
|
|
1464
|
-
printResult(publicResponse, "json");
|
|
1689
|
+
printResult({ ...publicResponse, registryAdded }, "json");
|
|
1690
|
+
} else if (registryAdded) {
|
|
1691
|
+
process.stdout.write(`watch registry: added ${root}
|
|
1692
|
+
`);
|
|
1465
1693
|
}
|
|
1466
1694
|
});
|
|
1467
1695
|
}
|
|
1468
1696
|
|
|
1469
1697
|
// src/commands/watch.ts
|
|
1470
|
-
import { copyFile as copyFile2, mkdir as
|
|
1698
|
+
import { copyFile as copyFile2, mkdir as mkdir7, readFile as readFile6, readdir as readdir3, rename as rename3, rmdir as rmdir2, unlink as unlink3 } from "fs/promises";
|
|
1471
1699
|
import { existsSync as existsSync4 } from "fs";
|
|
1472
|
-
import
|
|
1700
|
+
import path12 from "path";
|
|
1473
1701
|
import chokidar from "chokidar";
|
|
1474
1702
|
|
|
1475
1703
|
// src/watch-core.ts
|
|
@@ -1575,7 +1803,7 @@ var RepoWatch = class {
|
|
|
1575
1803
|
};
|
|
1576
1804
|
|
|
1577
1805
|
// src/service-install.ts
|
|
1578
|
-
import
|
|
1806
|
+
import path11 from "path";
|
|
1579
1807
|
import os2 from "os";
|
|
1580
1808
|
function buildUnitFile(args) {
|
|
1581
1809
|
return [
|
|
@@ -1595,8 +1823,8 @@ function buildUnitFile(args) {
|
|
|
1595
1823
|
}
|
|
1596
1824
|
function getUnitPath(env) {
|
|
1597
1825
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
1598
|
-
const base = xdg != null && xdg.length > 0 ? xdg :
|
|
1599
|
-
return
|
|
1826
|
+
const base = xdg != null && xdg.length > 0 ? xdg : path11.join(os2.homedir(), ".config");
|
|
1827
|
+
return path11.join(base, "systemd", "user", "nolto-watch.service");
|
|
1600
1828
|
}
|
|
1601
1829
|
async function installServiceWith(deps) {
|
|
1602
1830
|
if (deps.platform !== "linux") {
|
|
@@ -1607,7 +1835,7 @@ async function installServiceWith(deps) {
|
|
|
1607
1835
|
);
|
|
1608
1836
|
}
|
|
1609
1837
|
const unitPath = getUnitPath(deps.env);
|
|
1610
|
-
await deps.mkdir(
|
|
1838
|
+
await deps.mkdir(path11.dirname(unitPath));
|
|
1611
1839
|
await deps.writeFile(unitPath, buildUnitFile({ nodePath: deps.nodePath, scriptPath: deps.scriptPath }));
|
|
1612
1840
|
deps.log(`Wrote ${unitPath}`);
|
|
1613
1841
|
const reload = await deps.exec(["systemctl", "--user", "daemon-reload"]);
|
|
@@ -1621,18 +1849,18 @@ async function installServiceWith(deps) {
|
|
|
1621
1849
|
deps.log("Service nolto-watch enabled and started. Logs: journalctl --user -u nolto-watch -f");
|
|
1622
1850
|
}
|
|
1623
1851
|
async function installService() {
|
|
1624
|
-
const { writeFile:
|
|
1625
|
-
const { execFile:
|
|
1852
|
+
const { writeFile: writeFile7, mkdir: mkdir9 } = await import("fs/promises");
|
|
1853
|
+
const { execFile: execFile3 } = await import("child_process");
|
|
1626
1854
|
const { promisify: promisify2 } = await import("util");
|
|
1627
|
-
const execFileAsync = promisify2(
|
|
1855
|
+
const execFileAsync = promisify2(execFile3);
|
|
1628
1856
|
await installServiceWith({
|
|
1629
1857
|
platform: process.platform,
|
|
1630
1858
|
env: process.env,
|
|
1631
1859
|
nodePath: process.execPath,
|
|
1632
|
-
scriptPath:
|
|
1633
|
-
writeFile: (p, content) =>
|
|
1860
|
+
scriptPath: path11.resolve(process.argv[1] ?? ""),
|
|
1861
|
+
writeFile: (p, content) => writeFile7(p, content, "utf8"),
|
|
1634
1862
|
mkdir: async (p) => {
|
|
1635
|
-
await
|
|
1863
|
+
await mkdir9(p, { recursive: true });
|
|
1636
1864
|
},
|
|
1637
1865
|
exec: async (cmd) => {
|
|
1638
1866
|
try {
|
|
@@ -1686,14 +1914,14 @@ async function uninstallServiceWith(deps) {
|
|
|
1686
1914
|
deps.log("Service nolto-watch stopped, disabled, and removed.");
|
|
1687
1915
|
}
|
|
1688
1916
|
async function uninstallService() {
|
|
1689
|
-
const { unlink:
|
|
1690
|
-
const { execFile:
|
|
1917
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
1918
|
+
const { execFile: execFile3 } = await import("child_process");
|
|
1691
1919
|
const { promisify: promisify2 } = await import("util");
|
|
1692
|
-
const execFileAsync = promisify2(
|
|
1920
|
+
const execFileAsync = promisify2(execFile3);
|
|
1693
1921
|
await uninstallServiceWith({
|
|
1694
1922
|
platform: process.platform,
|
|
1695
1923
|
env: process.env,
|
|
1696
|
-
unlink:
|
|
1924
|
+
unlink: unlink4,
|
|
1697
1925
|
exec: async (cmd) => {
|
|
1698
1926
|
try {
|
|
1699
1927
|
await execFileAsync(cmd[0], cmd.slice(1));
|
|
@@ -1746,25 +1974,26 @@ function register6(program, deps) {
|
|
|
1746
1974
|
token: deps.settings.token
|
|
1747
1975
|
});
|
|
1748
1976
|
const startRepo = (root) => {
|
|
1749
|
-
const roadmapsPath =
|
|
1750
|
-
const legacyRoadmapPath =
|
|
1977
|
+
const roadmapsPath = path12.join(root, ".nolto", "roadmaps");
|
|
1978
|
+
const legacyRoadmapPath = path12.join(root, ".roadmap", "roadmap.json");
|
|
1751
1979
|
const watcher = chokidar.watch([roadmapsPath, legacyRoadmapPath], { ignoreInitial: true });
|
|
1752
1980
|
const repoWatch = new RepoWatch(root, {
|
|
1753
1981
|
sync: () => syncRepo(
|
|
1754
1982
|
{ root, defaultProjectId: deps.settings.defaultProjectId },
|
|
1755
1983
|
{
|
|
1756
|
-
readFile: (p) =>
|
|
1984
|
+
readFile: (p) => readFile6(p, "utf8"),
|
|
1757
1985
|
fileExists: (p) => existsSync4(p),
|
|
1758
1986
|
listDir: (p) => readdir3(p),
|
|
1759
|
-
rename:
|
|
1987
|
+
rename: rename3,
|
|
1760
1988
|
copyFile: copyFile2,
|
|
1761
|
-
mkdir: (p) =>
|
|
1762
|
-
unlink:
|
|
1989
|
+
mkdir: (p) => mkdir7(p, { recursive: true }).then(() => void 0),
|
|
1990
|
+
unlink: unlink3,
|
|
1763
1991
|
rmdir: rmdir2,
|
|
1992
|
+
repoIdentity: makeRepoIdentityResolver(deps),
|
|
1764
1993
|
http,
|
|
1765
|
-
log: (line) => process.stdout.write(`[${
|
|
1994
|
+
log: (line) => process.stdout.write(`[${path12.basename(root)}] ${line}
|
|
1766
1995
|
`),
|
|
1767
|
-
warn: (line) => process.stderr.write(`Warning: [${
|
|
1996
|
+
warn: (line) => process.stderr.write(`Warning: [${path12.basename(root)}] ${line}
|
|
1768
1997
|
`)
|
|
1769
1998
|
}
|
|
1770
1999
|
),
|
|
@@ -1820,18 +2049,18 @@ function register6(program, deps) {
|
|
|
1820
2049
|
}
|
|
1821
2050
|
|
|
1822
2051
|
// src/update-cli.ts
|
|
1823
|
-
import { execFile } from "child_process";
|
|
2052
|
+
import { execFile as execFile2 } from "child_process";
|
|
1824
2053
|
import { existsSync as existsSync5 } from "fs";
|
|
1825
2054
|
import { realpath } from "fs/promises";
|
|
1826
2055
|
import { createRequire as createRequire2 } from "module";
|
|
1827
|
-
import
|
|
2056
|
+
import path14 from "path";
|
|
1828
2057
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1829
2058
|
import { promisify } from "util";
|
|
1830
2059
|
|
|
1831
2060
|
// src/update-notifier.ts
|
|
1832
|
-
import { readFile as
|
|
2061
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir8 } from "fs/promises";
|
|
1833
2062
|
import https from "https";
|
|
1834
|
-
import
|
|
2063
|
+
import path13 from "path";
|
|
1835
2064
|
var PACKAGE = "@nolto/cli";
|
|
1836
2065
|
var CACHE_FILE = "update-check.json";
|
|
1837
2066
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1855,7 +2084,7 @@ Update available: ${current} \u2192 ${latest} \xB7 run \`nolto update\`
|
|
|
1855
2084
|
function isDisabled(env) {
|
|
1856
2085
|
return env["NO_UPDATE_NOTIFIER"] === "1" || env["NODE_ENV"] === "test" || Boolean(env["CI"]);
|
|
1857
2086
|
}
|
|
1858
|
-
function fetchLatestFromRegistry(timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
2087
|
+
function fetchLatestFromRegistry(timeoutMs = REQUEST_TIMEOUT_MS, opts = {}) {
|
|
1859
2088
|
return new Promise((resolve) => {
|
|
1860
2089
|
const req = https.get(
|
|
1861
2090
|
`https://registry.npmjs.org/${PACKAGE}/latest`,
|
|
@@ -1878,7 +2107,9 @@ function fetchLatestFromRegistry(timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
1878
2107
|
});
|
|
1879
2108
|
}
|
|
1880
2109
|
);
|
|
1881
|
-
|
|
2110
|
+
if (opts.unrefSocket ?? true) {
|
|
2111
|
+
req.on("socket", (s) => s.unref());
|
|
2112
|
+
}
|
|
1882
2113
|
req.on("timeout", () => {
|
|
1883
2114
|
req.destroy();
|
|
1884
2115
|
resolve(null);
|
|
@@ -1887,9 +2118,9 @@ function fetchLatestFromRegistry(timeoutMs = REQUEST_TIMEOUT_MS) {
|
|
|
1887
2118
|
});
|
|
1888
2119
|
}
|
|
1889
2120
|
async function writeUpdateCache(cachePath, now, latest) {
|
|
1890
|
-
await
|
|
2121
|
+
await mkdir8(path13.dirname(cachePath), { recursive: true });
|
|
1891
2122
|
const payload = { checkedAt: now, latest };
|
|
1892
|
-
await
|
|
2123
|
+
await writeFile6(cachePath, JSON.stringify(payload), { mode: 384 });
|
|
1893
2124
|
}
|
|
1894
2125
|
async function refreshCache(cachePath, now, fetchLatest) {
|
|
1895
2126
|
const latest = await fetchLatest();
|
|
@@ -1898,10 +2129,10 @@ async function refreshCache(cachePath, now, fetchLatest) {
|
|
|
1898
2129
|
}
|
|
1899
2130
|
async function checkForUpdate(opts) {
|
|
1900
2131
|
if (isDisabled(opts.env)) return null;
|
|
1901
|
-
const cachePath =
|
|
2132
|
+
const cachePath = path13.join(opts.configDir, CACHE_FILE);
|
|
1902
2133
|
let cache = {};
|
|
1903
2134
|
try {
|
|
1904
|
-
cache = JSON.parse(await
|
|
2135
|
+
cache = JSON.parse(await readFile7(cachePath, "utf8"));
|
|
1905
2136
|
} catch {
|
|
1906
2137
|
}
|
|
1907
2138
|
if (typeof cache.checkedAt !== "number" || opts.now - cache.checkedAt > CACHE_TTL_MS) {
|
|
@@ -1932,6 +2163,7 @@ async function notifyUpdate(opts) {
|
|
|
1932
2163
|
var PACKAGE2 = "@nolto/cli";
|
|
1933
2164
|
var UPDATE_CACHE_FILE = "update-check.json";
|
|
1934
2165
|
var UPDATE_REQUEST_TIMEOUT_MS = 1e4;
|
|
2166
|
+
var UPDATE_FETCH_OPTIONS = { unrefSocket: false };
|
|
1935
2167
|
function isNpxPath(scriptPath) {
|
|
1936
2168
|
return /[\\/]_npx[\\/]/.test(scriptPath);
|
|
1937
2169
|
}
|
|
@@ -2005,7 +2237,7 @@ async function updateCliWith(deps) {
|
|
|
2005
2237
|
}
|
|
2006
2238
|
deps.log(`Updated ${PACKAGE2} ${deps.currentVersion} \u2192 ${latest}.`);
|
|
2007
2239
|
await deps.writeCache(
|
|
2008
|
-
|
|
2240
|
+
path14.join(deps.configDir, UPDATE_CACHE_FILE),
|
|
2009
2241
|
deps.now,
|
|
2010
2242
|
latest
|
|
2011
2243
|
).catch(() => void 0);
|
|
@@ -2036,17 +2268,17 @@ async function updateCliWith(deps) {
|
|
|
2036
2268
|
};
|
|
2037
2269
|
}
|
|
2038
2270
|
function getCurrentVersion() {
|
|
2039
|
-
const dirname =
|
|
2271
|
+
const dirname = path14.dirname(fileURLToPath3(import.meta.url));
|
|
2040
2272
|
const require3 = createRequire2(import.meta.url);
|
|
2041
2273
|
try {
|
|
2042
|
-
const pkg = require3(
|
|
2274
|
+
const pkg = require3(path14.resolve(dirname, "../package.json"));
|
|
2043
2275
|
return pkg.version ?? "0.0.0";
|
|
2044
2276
|
} catch {
|
|
2045
2277
|
return "0.0.0";
|
|
2046
2278
|
}
|
|
2047
2279
|
}
|
|
2048
2280
|
async function updateCli(opts = {}) {
|
|
2049
|
-
const execFileAsync = promisify(
|
|
2281
|
+
const execFileAsync = promisify(execFile2);
|
|
2050
2282
|
const scriptPath = await realpath(process.argv[1] ?? "");
|
|
2051
2283
|
return updateCliWith({
|
|
2052
2284
|
currentVersion: getCurrentVersion(),
|
|
@@ -2069,7 +2301,7 @@ async function updateCli(opts = {}) {
|
|
|
2069
2301
|
}
|
|
2070
2302
|
},
|
|
2071
2303
|
realpath,
|
|
2072
|
-
fetchLatest: () => fetchLatestFromRegistry(UPDATE_REQUEST_TIMEOUT_MS),
|
|
2304
|
+
fetchLatest: () => fetchLatestFromRegistry(UPDATE_REQUEST_TIMEOUT_MS, UPDATE_FETCH_OPTIONS),
|
|
2073
2305
|
writeCache: writeUpdateCache,
|
|
2074
2306
|
unitExists: existsSync5,
|
|
2075
2307
|
log: opts.quiet === true ? () => void 0 : (line) => process.stdout.write(line + "\n"),
|
|
@@ -2114,11 +2346,11 @@ function buildProgram(deps) {
|
|
|
2114
2346
|
}
|
|
2115
2347
|
|
|
2116
2348
|
// src/index.ts
|
|
2117
|
-
var __dirname3 =
|
|
2349
|
+
var __dirname3 = path15.dirname(fileURLToPath4(import.meta.url));
|
|
2118
2350
|
var require2 = createRequire3(import.meta.url);
|
|
2119
2351
|
function getVersion() {
|
|
2120
2352
|
try {
|
|
2121
|
-
const pkgPath =
|
|
2353
|
+
const pkgPath = path15.resolve(__dirname3, "../package.json");
|
|
2122
2354
|
const pkg = require2(pkgPath);
|
|
2123
2355
|
return pkg.version ?? "0.0.0";
|
|
2124
2356
|
} catch {
|