@nolto/cli 0.7.1 → 0.8.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -3
- package/dist/index.js +771 -426
- package/package.json +1 -1
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,13 @@ 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 os3 from "os";
|
|
381
|
+
import path8 from "path";
|
|
349
382
|
import fs from "fs";
|
|
350
383
|
|
|
351
384
|
// src/commands/link.ts
|
|
352
|
-
import
|
|
385
|
+
import os2 from "os";
|
|
386
|
+
import path5 from "path";
|
|
353
387
|
import { statSync as statSync2 } from "fs";
|
|
354
388
|
|
|
355
389
|
// src/output.ts
|
|
@@ -403,6 +437,316 @@ function formatValue(value) {
|
|
|
403
437
|
return JSON.stringify(value, null, 2);
|
|
404
438
|
}
|
|
405
439
|
|
|
440
|
+
// src/registry.ts
|
|
441
|
+
import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, rename, unlink } from "fs/promises";
|
|
442
|
+
import { randomUUID } from "crypto";
|
|
443
|
+
import path2 from "path";
|
|
444
|
+
import { z as z2 } from "zod";
|
|
445
|
+
var registrySchema = z2.object({
|
|
446
|
+
schemaVersion: z2.literal(1),
|
|
447
|
+
repos: z2.array(z2.object({ root: z2.string().min(1) }).passthrough())
|
|
448
|
+
}).passthrough();
|
|
449
|
+
function getRegistryPath(env) {
|
|
450
|
+
return path2.join(getConfigDir(env), "registry.json");
|
|
451
|
+
}
|
|
452
|
+
async function loadRegistry(filePath) {
|
|
453
|
+
let raw;
|
|
454
|
+
try {
|
|
455
|
+
raw = await readFile2(filePath, "utf8");
|
|
456
|
+
} catch (err) {
|
|
457
|
+
const code = err != null && typeof err === "object" && "code" in err ? err.code : "";
|
|
458
|
+
if (code === "ENOENT") {
|
|
459
|
+
return { schemaVersion: 1, repos: [] };
|
|
460
|
+
}
|
|
461
|
+
throw new CliError(`Cannot read registry: ${filePath}: ${String(err)}`, 2);
|
|
462
|
+
}
|
|
463
|
+
let parsed;
|
|
464
|
+
try {
|
|
465
|
+
parsed = JSON.parse(raw);
|
|
466
|
+
} catch {
|
|
467
|
+
throw new CliError(`Malformed JSON in ${filePath}. Fix or remove the file.`, 2);
|
|
468
|
+
}
|
|
469
|
+
const result = registrySchema.safeParse(parsed);
|
|
470
|
+
if (!result.success) {
|
|
471
|
+
const issue = result.error.issues[0];
|
|
472
|
+
const fieldPath = issue?.path.join(".") ?? "";
|
|
473
|
+
throw new CliError(
|
|
474
|
+
`Invalid registry at ${filePath}: ${fieldPath.length > 0 ? `field "${fieldPath}" \u2014 ` : ""}${issue?.message ?? "validation failed"}`,
|
|
475
|
+
2
|
|
476
|
+
);
|
|
477
|
+
}
|
|
478
|
+
return result.data;
|
|
479
|
+
}
|
|
480
|
+
async function addRepoToRegistry(filePath, repoRoot) {
|
|
481
|
+
const registry = await loadRegistry(filePath);
|
|
482
|
+
if (registry.repos.some((repo) => repo.root === repoRoot)) {
|
|
483
|
+
return { added: false, registry };
|
|
484
|
+
}
|
|
485
|
+
const updated = { ...registry, repos: [...registry.repos, { root: repoRoot }] };
|
|
486
|
+
await mkdir2(path2.dirname(filePath), { recursive: true, mode: 448 });
|
|
487
|
+
const tempPath = `${filePath}.tmp-${process.pid}-${randomUUID()}`;
|
|
488
|
+
try {
|
|
489
|
+
await writeFile2(tempPath, JSON.stringify(updated, null, 2) + "\n", "utf8");
|
|
490
|
+
await rename(tempPath, filePath);
|
|
491
|
+
} catch (err) {
|
|
492
|
+
await unlink(tempPath).catch(() => void 0);
|
|
493
|
+
throw err;
|
|
494
|
+
}
|
|
495
|
+
return { added: true, registry: updated };
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
// src/repo-identity.ts
|
|
499
|
+
import { execFile } from "child_process";
|
|
500
|
+
import { realpathSync } from "fs";
|
|
501
|
+
import path4 from "path";
|
|
502
|
+
|
|
503
|
+
// ../roadmap-schema/src/repo-identity.ts
|
|
504
|
+
var HOSTED_LOWERCASE_PATH = /* @__PURE__ */ new Set(["github.com", "gitlab.com", "bitbucket.org"]);
|
|
505
|
+
function normalizeRemote(raw) {
|
|
506
|
+
let s = raw.trim();
|
|
507
|
+
if (s.length === 0) return null;
|
|
508
|
+
if (s.startsWith("file://") || s.startsWith("/") || s.startsWith(".") || /^[A-Za-z]:[\\/]/.test(s)) return null;
|
|
509
|
+
const scheme = /^[a-z][a-z0-9+.-]*:\/\//i.exec(s);
|
|
510
|
+
const hadScheme = scheme !== null;
|
|
511
|
+
if (scheme !== null) s = s.slice(scheme[0].length);
|
|
512
|
+
const authorityEnd = s.indexOf("/");
|
|
513
|
+
const lastAt = s.lastIndexOf("@", authorityEnd === -1 ? s.length - 1 : authorityEnd - 1);
|
|
514
|
+
if (lastAt !== -1) s = s.slice(lastAt + 1);
|
|
515
|
+
const scpLike = hadScheme ? null : /^([^/:]+):(.+)$/.exec(s);
|
|
516
|
+
if (scpLike !== null) {
|
|
517
|
+
s = `${scpLike[1]}/${scpLike[2]}`;
|
|
518
|
+
}
|
|
519
|
+
const firstSlash = s.indexOf("/");
|
|
520
|
+
if (firstSlash <= 0) return null;
|
|
521
|
+
let host = s.slice(0, firstSlash).toLowerCase();
|
|
522
|
+
let path16 = s.slice(firstSlash + 1);
|
|
523
|
+
if (hadScheme) host = host.replace(/:\d+$/, "");
|
|
524
|
+
path16 = path16.replace(/\/+/g, "/").replace(/^\/+/, "").replace(/\/+$/, "").replace(/\.git$/i, "").replace(/\/+$/, "");
|
|
525
|
+
if (path16.length === 0) return null;
|
|
526
|
+
if (HOSTED_LOWERCASE_PATH.has(host)) path16 = path16.toLowerCase();
|
|
527
|
+
return `${host}/${path16}`;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// ../roadmap-schema/src/index.ts
|
|
531
|
+
var STATUSES = /* @__PURE__ */ new Set(["todo", "in-progress", "done", "blocked"]);
|
|
532
|
+
var ID_PATTERN = /^[a-z0-9][a-z0-9._-]*$/;
|
|
533
|
+
var ALLOWED_KEYS = {
|
|
534
|
+
roadmap: /* @__PURE__ */ new Set(["schemaVersion", "project", "updatedAt", "currentTaskId", "summary", "phases"]),
|
|
535
|
+
project: /* @__PURE__ */ new Set(["id", "name", "repository"]),
|
|
536
|
+
phase: /* @__PURE__ */ new Set(["id", "title", "status", "plan", "tasks"]),
|
|
537
|
+
task: /* @__PURE__ */ new Set(["id", "title", "status", "startedAt", "completedAt", "note", "dependsOn", "plan"])
|
|
538
|
+
};
|
|
539
|
+
function isRecord(value) {
|
|
540
|
+
return value != null && typeof value === "object" && !Array.isArray(value);
|
|
541
|
+
}
|
|
542
|
+
function checkKeys(value, allowed, at, errors) {
|
|
543
|
+
if (!isRecord(value)) return;
|
|
544
|
+
for (const key of Object.keys(value)) {
|
|
545
|
+
if (!allowed.has(key)) errors.push(`${at} contains unsupported property "${key}".`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function checkPlan(value, at, errors) {
|
|
549
|
+
if (value === void 0) return;
|
|
550
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
551
|
+
errors.push(`${at}.plan must be a non-empty string.`);
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
function derivePhaseStatus(phase) {
|
|
555
|
+
if (phase.tasks.length > 0 && phase.tasks.every((task) => task.status === "done")) return "done";
|
|
556
|
+
if (phase.tasks.some((task) => task.status === "in-progress")) return "in-progress";
|
|
557
|
+
if (phase.tasks.some((task) => task.status === "blocked")) return "blocked";
|
|
558
|
+
if (phase.tasks.some((task) => task.status === "done")) return "in-progress";
|
|
559
|
+
return "todo";
|
|
560
|
+
}
|
|
561
|
+
function validateRoadmap(value) {
|
|
562
|
+
const errors = [];
|
|
563
|
+
const warnings = [];
|
|
564
|
+
if (!isRecord(value)) return { errors: ["Root must be an object."], warnings };
|
|
565
|
+
checkKeys(value, ALLOWED_KEYS.roadmap, "roadmap", errors);
|
|
566
|
+
if (value["schemaVersion"] !== 1 && value["schemaVersion"] !== 2) {
|
|
567
|
+
errors.push("schemaVersion must be 1 or 2.");
|
|
568
|
+
} else if (value["schemaVersion"] === 1) {
|
|
569
|
+
warnings.push("schemaVersion 1 is legacy; the next roadmap-progress mutation migrates this file to 2.");
|
|
570
|
+
}
|
|
571
|
+
const project = value["project"];
|
|
572
|
+
checkKeys(project, ALLOWED_KEYS.project, "project", errors);
|
|
573
|
+
const projectRecord = isRecord(project) ? project : {};
|
|
574
|
+
if (!ID_PATTERN.test(String(projectRecord["id"] ?? ""))) errors.push("project.id is invalid.");
|
|
575
|
+
if (projectRecord["name"] == null || projectRecord["name"] === "") errors.push("project.name is required.");
|
|
576
|
+
if (projectRecord["repository"] == null || projectRecord["repository"] === "") errors.push("project.repository is required.");
|
|
577
|
+
if (Number.isNaN(Date.parse(String(value["updatedAt"])))) errors.push("updatedAt must be a valid date-time.");
|
|
578
|
+
if (typeof value["summary"] !== "string") errors.push("summary must be a string.");
|
|
579
|
+
if (!Array.isArray(value["phases"])) errors.push("phases must be an array.");
|
|
580
|
+
const allIds = /* @__PURE__ */ new Set();
|
|
581
|
+
const tasks = /* @__PURE__ */ new Map();
|
|
582
|
+
const phases = Array.isArray(value["phases"]) ? value["phases"] : [];
|
|
583
|
+
for (const [phaseIndex, rawPhase] of phases.entries()) {
|
|
584
|
+
const at = `phases[${phaseIndex}]`;
|
|
585
|
+
checkKeys(rawPhase, ALLOWED_KEYS.phase, at, errors);
|
|
586
|
+
const phase = isRecord(rawPhase) ? rawPhase : {};
|
|
587
|
+
const phaseId = String(phase["id"] ?? "");
|
|
588
|
+
if (!ID_PATTERN.test(phaseId)) errors.push(`${at}.id is invalid.`);
|
|
589
|
+
if (allIds.has(phaseId)) errors.push(`Duplicate id "${phaseId}".`);
|
|
590
|
+
allIds.add(phaseId);
|
|
591
|
+
if (phase["title"] == null || phase["title"] === "") errors.push(`${at}.title is required.`);
|
|
592
|
+
if (!STATUSES.has(String(phase["status"]))) errors.push(`${at}.status is invalid.`);
|
|
593
|
+
checkPlan(phase["plan"], at, errors);
|
|
594
|
+
if (!Array.isArray(phase["tasks"])) errors.push(`${at}.tasks must be an array.`);
|
|
595
|
+
const rawTasks = Array.isArray(phase["tasks"]) ? phase["tasks"] : [];
|
|
596
|
+
for (const [taskIndex, rawTask] of rawTasks.entries()) {
|
|
597
|
+
const taskAt = `${at}.tasks[${taskIndex}]`;
|
|
598
|
+
checkKeys(rawTask, ALLOWED_KEYS.task, taskAt, errors);
|
|
599
|
+
const task = isRecord(rawTask) ? rawTask : {};
|
|
600
|
+
const taskId = String(task["id"] ?? "");
|
|
601
|
+
if (!ID_PATTERN.test(taskId)) errors.push(`${taskAt}.id is invalid.`);
|
|
602
|
+
if (allIds.has(taskId)) errors.push(`Duplicate id "${taskId}".`);
|
|
603
|
+
allIds.add(taskId);
|
|
604
|
+
tasks.set(taskId, task);
|
|
605
|
+
if (task["title"] == null || task["title"] === "") errors.push(`${taskAt}.title is required.`);
|
|
606
|
+
if (!STATUSES.has(String(task["status"]))) errors.push(`${taskAt}.status is invalid.`);
|
|
607
|
+
checkPlan(task["plan"], taskAt, errors);
|
|
608
|
+
if (task["status"] === "done" && task["completedAt"] == null) warnings.push(`${taskId} is done without completedAt.`);
|
|
609
|
+
if (task["status"] === "in-progress" && task["startedAt"] == null) warnings.push(`${taskId} is in-progress without startedAt.`);
|
|
610
|
+
if (task["dependsOn"] !== void 0 && !Array.isArray(task["dependsOn"])) errors.push(`${taskAt}.dependsOn must be an array.`);
|
|
611
|
+
}
|
|
612
|
+
if (Array.isArray(phase["tasks"]) && STATUSES.has(String(phase["status"]))) {
|
|
613
|
+
const expected = derivePhaseStatus({ ...phase, tasks: rawTasks });
|
|
614
|
+
if (phase["status"] !== expected) warnings.push(`${phaseId} status is ${String(phase["status"])}; task states derive ${expected}.`);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
for (const task of tasks.values()) {
|
|
618
|
+
for (const dependency of task.dependsOn ?? []) {
|
|
619
|
+
if (!tasks.has(dependency)) warnings.push(`${task.id} depends on unknown task ${dependency}.`);
|
|
620
|
+
if (task.id === dependency) errors.push(`${task.id} cannot depend on itself.`);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
const currentTaskId = value["currentTaskId"];
|
|
624
|
+
if (currentTaskId !== null && currentTaskId !== void 0) {
|
|
625
|
+
const current = tasks.get(String(currentTaskId));
|
|
626
|
+
if (current == null) errors.push(`currentTaskId ${String(currentTaskId)} does not exist.`);
|
|
627
|
+
else if (current.status !== "in-progress") warnings.push(`currentTaskId ${String(currentTaskId)} is not in-progress.`);
|
|
628
|
+
}
|
|
629
|
+
return { errors, warnings };
|
|
630
|
+
}
|
|
631
|
+
function deriveTaskStats(roadmap) {
|
|
632
|
+
let done = 0;
|
|
633
|
+
let blocked = 0;
|
|
634
|
+
let inProgress = 0;
|
|
635
|
+
let todo = 0;
|
|
636
|
+
for (const phase of roadmap.phases) {
|
|
637
|
+
for (const task of phase.tasks) {
|
|
638
|
+
if (task.status === "done") done += 1;
|
|
639
|
+
else if (task.status === "blocked") blocked += 1;
|
|
640
|
+
else if (task.status === "in-progress") inProgress += 1;
|
|
641
|
+
else todo += 1;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
const total = done + blocked + inProgress + todo;
|
|
645
|
+
return {
|
|
646
|
+
total,
|
|
647
|
+
done,
|
|
648
|
+
blocked,
|
|
649
|
+
inProgress,
|
|
650
|
+
todo,
|
|
651
|
+
progressPct: total > 0 ? Math.round(done / total * 100) : 0
|
|
652
|
+
};
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// src/machine-id.ts
|
|
656
|
+
import { chmod, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "fs/promises";
|
|
657
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
658
|
+
import path3 from "path";
|
|
659
|
+
var inFlightByConfigDir = /* @__PURE__ */ new Map();
|
|
660
|
+
function parseMachineId(raw, file) {
|
|
661
|
+
let parsed;
|
|
662
|
+
try {
|
|
663
|
+
parsed = JSON.parse(raw);
|
|
664
|
+
} catch {
|
|
665
|
+
throw new CliError(`Malformed JSON in ${file}. Remove the file to regenerate it.`, 2);
|
|
666
|
+
}
|
|
667
|
+
const id = parsed?.machineId;
|
|
668
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
669
|
+
throw new CliError(`Invalid ${file}: missing machineId. Remove the file to regenerate it.`, 2);
|
|
670
|
+
}
|
|
671
|
+
return id;
|
|
672
|
+
}
|
|
673
|
+
async function loadOrCreate(configDir) {
|
|
674
|
+
const file = path3.join(configDir, "machine.json");
|
|
675
|
+
let raw = null;
|
|
676
|
+
try {
|
|
677
|
+
raw = await readFile3(file, "utf8");
|
|
678
|
+
} catch (err) {
|
|
679
|
+
const code = err.code;
|
|
680
|
+
if (code !== "ENOENT") {
|
|
681
|
+
throw new CliError(`Cannot read ${file}: ${String(err)}`, 2);
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
if (raw !== null) {
|
|
685
|
+
return parseMachineId(raw, file);
|
|
686
|
+
}
|
|
687
|
+
const machineId = randomUUID2();
|
|
688
|
+
await mkdir3(configDir, { recursive: true, mode: 448 });
|
|
689
|
+
try {
|
|
690
|
+
await writeFile3(file, `${JSON.stringify({ machineId }, null, 2)}
|
|
691
|
+
`, {
|
|
692
|
+
flag: "wx",
|
|
693
|
+
mode: 384
|
|
694
|
+
});
|
|
695
|
+
} catch (err) {
|
|
696
|
+
if (err.code === "EEXIST") {
|
|
697
|
+
return parseMachineId(await readFile3(file, "utf8"), file);
|
|
698
|
+
}
|
|
699
|
+
throw new CliError(`Cannot write ${file}: ${String(err)}`, 2);
|
|
700
|
+
}
|
|
701
|
+
await chmod(file, 384);
|
|
702
|
+
return machineId;
|
|
703
|
+
}
|
|
704
|
+
function loadOrCreateMachineId(configDir) {
|
|
705
|
+
const key = path3.resolve(configDir);
|
|
706
|
+
const existing = inFlightByConfigDir.get(key);
|
|
707
|
+
if (existing !== void 0) return existing;
|
|
708
|
+
const pending = loadOrCreate(key).finally(() => {
|
|
709
|
+
if (inFlightByConfigDir.get(key) === pending) inFlightByConfigDir.delete(key);
|
|
710
|
+
});
|
|
711
|
+
inFlightByConfigDir.set(key, pending);
|
|
712
|
+
return pending;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// src/repo-identity.ts
|
|
716
|
+
function defaultGetRemoteUrl(root) {
|
|
717
|
+
return new Promise((resolve) => {
|
|
718
|
+
execFile(
|
|
719
|
+
"git",
|
|
720
|
+
["-C", root, "remote", "get-url", "origin"],
|
|
721
|
+
{ timeout: 5e3 },
|
|
722
|
+
(err, stdout) => {
|
|
723
|
+
if (err) return resolve(null);
|
|
724
|
+
const line = stdout.split(/\r?\n/)[0]?.trim() ?? "";
|
|
725
|
+
resolve(line.length > 0 ? line : null);
|
|
726
|
+
}
|
|
727
|
+
);
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
async function resolveRepoIdentity(root, deps) {
|
|
731
|
+
const remote = await deps.getRemoteUrl(root);
|
|
732
|
+
const normalized = remote === null ? null : normalizeRemote(remote);
|
|
733
|
+
if (normalized !== null) return { kind: "remote", value: normalized };
|
|
734
|
+
const machineId = await deps.machineId();
|
|
735
|
+
let canonicalRoot;
|
|
736
|
+
try {
|
|
737
|
+
canonicalRoot = realpathSync(root);
|
|
738
|
+
} catch {
|
|
739
|
+
canonicalRoot = path4.resolve(root);
|
|
740
|
+
}
|
|
741
|
+
return { kind: "local", value: `${machineId}:${canonicalRoot}` };
|
|
742
|
+
}
|
|
743
|
+
function makeRepoIdentityResolver(deps) {
|
|
744
|
+
return deps.repoIdentity ?? ((root) => resolveRepoIdentity(root, {
|
|
745
|
+
getRemoteUrl: defaultGetRemoteUrl,
|
|
746
|
+
machineId: () => loadOrCreateMachineId(getConfigDir(process.env))
|
|
747
|
+
}));
|
|
748
|
+
}
|
|
749
|
+
|
|
406
750
|
// src/commands/link.ts
|
|
407
751
|
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
752
|
function resolveStartDir(env, cwd) {
|
|
@@ -411,7 +755,7 @@ function resolveStartDir(env, cwd) {
|
|
|
411
755
|
}
|
|
412
756
|
function dirHasGit(dir) {
|
|
413
757
|
try {
|
|
414
|
-
statSync2(
|
|
758
|
+
statSync2(path5.join(dir, ".git"));
|
|
415
759
|
return true;
|
|
416
760
|
} catch {
|
|
417
761
|
return false;
|
|
@@ -423,18 +767,26 @@ function findRepoRoot(startDir, hasGit = dirHasGit) {
|
|
|
423
767
|
if (hasGit(current)) {
|
|
424
768
|
return { root: current, foundGit: true };
|
|
425
769
|
}
|
|
426
|
-
const parent =
|
|
770
|
+
const parent = path5.dirname(current);
|
|
427
771
|
if (parent === current) break;
|
|
428
772
|
current = parent;
|
|
429
773
|
}
|
|
430
774
|
return { root: startDir, foundGit: false };
|
|
431
775
|
}
|
|
776
|
+
function isHomeDirectory(dir, home = os2.homedir()) {
|
|
777
|
+
return path5.resolve(dir) === path5.resolve(home);
|
|
778
|
+
}
|
|
432
779
|
async function handleShow(deps, projectBindingPath, mode2) {
|
|
780
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
781
|
+
const root = findRepoRoot(startDir).root;
|
|
782
|
+
const repoIdentity = await makeRepoIdentityResolver(deps)(root);
|
|
433
783
|
if (projectBindingPath == null) {
|
|
434
784
|
if (mode2 === "json") {
|
|
435
|
-
printResult({ bound: false, projectBindingPath: null }, mode2);
|
|
785
|
+
printResult({ bound: false, projectBindingPath: null, repoIdentity }, mode2);
|
|
436
786
|
} else {
|
|
437
787
|
process.stdout.write("No nolto.json binding found in this directory tree.\n");
|
|
788
|
+
process.stdout.write(`repoIdentity : ${repoIdentity.kind}:${repoIdentity.value}
|
|
789
|
+
`);
|
|
438
790
|
}
|
|
439
791
|
return;
|
|
440
792
|
}
|
|
@@ -447,7 +799,8 @@ async function handleShow(deps, projectBindingPath, mode2) {
|
|
|
447
799
|
bound: binding != null,
|
|
448
800
|
projectId: binding?.projectId ?? null,
|
|
449
801
|
projectBindingPath,
|
|
450
|
-
source: deps.settings.source.project === "repo" ? "repo" : "file"
|
|
802
|
+
source: deps.settings.source.project === "repo" ? "repo" : "file",
|
|
803
|
+
repoIdentity
|
|
451
804
|
}, mode2);
|
|
452
805
|
} else {
|
|
453
806
|
if (binding == null) {
|
|
@@ -460,15 +813,33 @@ async function handleShow(deps, projectBindingPath, mode2) {
|
|
|
460
813
|
`);
|
|
461
814
|
const active = deps.settings.source.project === "repo" ? "repo (active)" : "repo (not active \u2014 overridden)";
|
|
462
815
|
process.stdout.write(`source : ${active}
|
|
816
|
+
`);
|
|
817
|
+
process.stdout.write(`repoIdentity : ${repoIdentity.kind}:${repoIdentity.value}
|
|
463
818
|
`);
|
|
464
819
|
}
|
|
465
820
|
}
|
|
466
821
|
}
|
|
822
|
+
async function handleRebind(deps, projectId, root, mode2) {
|
|
823
|
+
if (!UUID_RE.test(projectId)) {
|
|
824
|
+
throw new CliError(
|
|
825
|
+
`Invalid project ID: "${projectId}". Must be a UUID (e.g. 00000000-0000-0000-0000-000000000001).`,
|
|
826
|
+
2
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
const repoIdentity = await makeRepoIdentityResolver(deps)(root);
|
|
830
|
+
await deps.http.post(`/api/projects/${projectId}/repo-binding`, { repoIdentity });
|
|
831
|
+
if (mode2 === "json") {
|
|
832
|
+
printResult({ rebound: true, projectId, repoIdentity }, mode2);
|
|
833
|
+
} else {
|
|
834
|
+
process.stdout.write(`Rebound project ${projectId} to ${repoIdentity.kind}:${repoIdentity.value}.
|
|
835
|
+
`);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
467
838
|
async function handleUnlink(projectBindingPath, mode2) {
|
|
468
|
-
const { readFile:
|
|
839
|
+
const { readFile: readFile8, writeFile: writeFile7, chmod: chmod2 } = await import("fs/promises");
|
|
469
840
|
let existing = {};
|
|
470
841
|
try {
|
|
471
|
-
const raw = await
|
|
842
|
+
const raw = await readFile8(projectBindingPath, "utf8");
|
|
472
843
|
const parsed = JSON.parse(raw);
|
|
473
844
|
if (parsed == null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
474
845
|
throw new CliError(
|
|
@@ -483,8 +854,8 @@ async function handleUnlink(projectBindingPath, mode2) {
|
|
|
483
854
|
}
|
|
484
855
|
const { projectId: _removed, ...rest } = existing;
|
|
485
856
|
void _removed;
|
|
486
|
-
await
|
|
487
|
-
await
|
|
857
|
+
await writeFile7(projectBindingPath, JSON.stringify(rest, null, 2) + "\n", { mode: 420 });
|
|
858
|
+
await chmod2(projectBindingPath, 420);
|
|
488
859
|
if (mode2 === "json") {
|
|
489
860
|
printResult({ unlinked: true, projectBindingPath }, mode2);
|
|
490
861
|
} else {
|
|
@@ -538,21 +909,43 @@ Proceeding anyway \u2014 verify the ID is correct.
|
|
|
538
909
|
);
|
|
539
910
|
}
|
|
540
911
|
await writeRepoBinding(root, projectId);
|
|
541
|
-
const writtenPath =
|
|
912
|
+
const writtenPath = path5.join(root, "nolto.json");
|
|
913
|
+
let registryAdded = false;
|
|
914
|
+
if (isHomeDirectory(root)) {
|
|
915
|
+
process.stderr.write(`Warning: refusing to add home directory ${root} to the watch registry.
|
|
916
|
+
`);
|
|
917
|
+
} else {
|
|
918
|
+
try {
|
|
919
|
+
registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
|
|
920
|
+
} catch (err) {
|
|
921
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
922
|
+
process.stderr.write(`Warning: could not update watch registry: ${message}
|
|
923
|
+
`);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
542
926
|
if (mode2 === "json") {
|
|
543
|
-
printResult({
|
|
927
|
+
printResult({
|
|
928
|
+
linked: true,
|
|
929
|
+
projectId,
|
|
930
|
+
projectBindingPath: writtenPath,
|
|
931
|
+
registryAdded
|
|
932
|
+
}, mode2);
|
|
544
933
|
} else {
|
|
545
934
|
process.stdout.write(
|
|
546
935
|
`Linked this repo to project ${projectId} (wrote ${writtenPath}).
|
|
547
936
|
Commit nolto.json to share the binding with your team.
|
|
548
937
|
`
|
|
549
938
|
);
|
|
939
|
+
if (registryAdded) {
|
|
940
|
+
process.stdout.write(`watch registry: added ${root}
|
|
941
|
+
`);
|
|
942
|
+
}
|
|
550
943
|
}
|
|
551
944
|
}
|
|
552
945
|
function register(program, deps) {
|
|
553
946
|
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");
|
|
947
|
+
"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");
|
|
556
949
|
cmd.action(async (projectId) => {
|
|
557
950
|
const { output } = deps;
|
|
558
951
|
const projectBindingPath = deps.repoBinding?.path ?? deps.projectBindingPath ?? null;
|
|
@@ -572,12 +965,23 @@ function register(program, deps) {
|
|
|
572
965
|
await handleUnlink(projectBindingPath, mode2);
|
|
573
966
|
return;
|
|
574
967
|
}
|
|
968
|
+
if (cmd.opts()["rebind"]) {
|
|
969
|
+
const boundProjectId = deps.repoBinding?.binding?.projectId;
|
|
970
|
+
const effectiveProjectId = projectId ?? boundProjectId;
|
|
971
|
+
if (effectiveProjectId == null) {
|
|
972
|
+
throw new CliError("No project binding. Run `nolto link <projectId>` first.", 2);
|
|
973
|
+
}
|
|
974
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
975
|
+
const root = findRepoRoot(startDir).root;
|
|
976
|
+
await handleRebind(deps, effectiveProjectId, root, mode2);
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
575
979
|
if (projectId == null || projectId.trim().length === 0) {
|
|
576
980
|
if (bindingError != null) {
|
|
577
981
|
throw invalidBindingError(projectBindingPath, bindingError);
|
|
578
982
|
}
|
|
579
983
|
throw new CliError(
|
|
580
|
-
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --unlink to remove it.",
|
|
984
|
+
"Usage: nolto link <projectId> (provide a UUID)\nOr use --show to view the current binding, --rebind to rebind it, --unlink to remove it.",
|
|
581
985
|
2
|
|
582
986
|
);
|
|
583
987
|
}
|
|
@@ -592,113 +996,87 @@ function invalidBindingError(projectBindingPath, error) {
|
|
|
592
996
|
}
|
|
593
997
|
|
|
594
998
|
// src/skill-install.ts
|
|
595
|
-
import { cp, mkdir as
|
|
999
|
+
import { cp, mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, rm } from "fs/promises";
|
|
596
1000
|
import { existsSync } from "fs";
|
|
597
|
-
import
|
|
1001
|
+
import path6 from "path";
|
|
598
1002
|
import { fileURLToPath } from "url";
|
|
599
|
-
var __dirname =
|
|
1003
|
+
var __dirname = path6.dirname(fileURLToPath(import.meta.url));
|
|
600
1004
|
function resolveSkillSourceDir() {
|
|
601
1005
|
const candidates = [
|
|
602
|
-
|
|
1006
|
+
path6.resolve(__dirname, "skill/roadmap-progress"),
|
|
603
1007
|
// bundled: dist/skill/...
|
|
604
|
-
|
|
1008
|
+
path6.resolve(__dirname, "../../../skills/roadmap-progress")
|
|
605
1009
|
// source: <repo>/skills/...
|
|
606
1010
|
];
|
|
607
1011
|
for (const candidate of candidates) {
|
|
608
|
-
if (existsSync(
|
|
1012
|
+
if (existsSync(path6.join(candidate, "SKILL.md"))) return candidate;
|
|
609
1013
|
}
|
|
610
1014
|
throw new Error("Bundled roadmap-progress skill not found. Reinstall @nolto/cli.");
|
|
611
1015
|
}
|
|
612
1016
|
var VERSION_MARKER = ".nolto-skill-version";
|
|
1017
|
+
async function checkSkillVersionDrift(repoRoot, cliVersion) {
|
|
1018
|
+
const skillDirs = [
|
|
1019
|
+
path6.join(repoRoot, ".claude", "skills", "roadmap-progress"),
|
|
1020
|
+
path6.join(repoRoot, ".agents", "skills", "roadmap-progress")
|
|
1021
|
+
];
|
|
1022
|
+
const drifted = [];
|
|
1023
|
+
for (const dir of skillDirs) {
|
|
1024
|
+
if (!existsSync(dir)) continue;
|
|
1025
|
+
let installed;
|
|
1026
|
+
try {
|
|
1027
|
+
const marker = (await readFile4(path6.join(dir, VERSION_MARKER), "utf8")).trim();
|
|
1028
|
+
installed = marker.length > 0 ? marker : null;
|
|
1029
|
+
} catch {
|
|
1030
|
+
installed = null;
|
|
1031
|
+
}
|
|
1032
|
+
if (installed !== cliVersion) drifted.push({ dir, installed });
|
|
1033
|
+
}
|
|
1034
|
+
return drifted;
|
|
1035
|
+
}
|
|
1036
|
+
function formatSkillVersionDriftWarning(repoRoot, cliVersion, drifted) {
|
|
1037
|
+
const dirs = drifted.map(({ dir }) => path6.relative(repoRoot, dir).split(path6.sep).join("/")).join(", ");
|
|
1038
|
+
const installed = [...new Set(drifted.map((entry) => entry.installed ?? "unknown"))].join(", ");
|
|
1039
|
+
return `roadmap-progress skill is outdated in ${dirs} (installed ${installed}, CLI ${cliVersion}). Run \`nolto init\` in this repo to update it.`;
|
|
1040
|
+
}
|
|
613
1041
|
async function installSkill(args) {
|
|
614
|
-
const targetDir =
|
|
615
|
-
const markerPath =
|
|
1042
|
+
const targetDir = path6.join(args.skillsParentDir, "roadmap-progress");
|
|
1043
|
+
const markerPath = path6.join(targetDir, VERSION_MARKER);
|
|
616
1044
|
const dirExists = existsSync(targetDir);
|
|
617
1045
|
let installedVersion = null;
|
|
618
1046
|
if (dirExists) {
|
|
619
1047
|
try {
|
|
620
|
-
installedVersion = (await
|
|
1048
|
+
installedVersion = (await readFile4(markerPath, "utf8")).trim();
|
|
621
1049
|
} catch {
|
|
622
1050
|
installedVersion = null;
|
|
623
1051
|
}
|
|
624
1052
|
}
|
|
625
|
-
if (dirExists && installedVersion === args.version && args.force !== true) {
|
|
626
|
-
return { action: "skipped", targetDir };
|
|
627
|
-
}
|
|
628
|
-
await rm(targetDir, { recursive: true, force: true });
|
|
629
|
-
await
|
|
630
|
-
await cp(args.sourceDir, targetDir, { recursive: true });
|
|
631
|
-
await
|
|
632
|
-
return { action: dirExists ? "updated" : "installed", targetDir };
|
|
633
|
-
}
|
|
634
|
-
|
|
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 };
|
|
1053
|
+
if (dirExists && installedVersion === args.version && args.force !== true) {
|
|
1054
|
+
return { action: "skipped", targetDir };
|
|
1055
|
+
}
|
|
1056
|
+
await rm(targetDir, { recursive: true, force: true });
|
|
1057
|
+
await mkdir4(args.skillsParentDir, { recursive: true });
|
|
1058
|
+
await cp(args.sourceDir, targetDir, { recursive: true });
|
|
1059
|
+
await writeFile4(markerPath, args.version + "\n", "utf8");
|
|
1060
|
+
return { action: dirExists ? "updated" : "installed", targetDir };
|
|
683
1061
|
}
|
|
684
1062
|
|
|
685
1063
|
// src/roadmap-scaffold.ts
|
|
686
|
-
import { mkdir as
|
|
1064
|
+
import { mkdir as mkdir5, readdir, writeFile as writeFile5 } from "fs/promises";
|
|
687
1065
|
import { existsSync as existsSync2 } from "fs";
|
|
688
|
-
import
|
|
1066
|
+
import path7 from "path";
|
|
689
1067
|
function slugifyProjectId(name) {
|
|
690
1068
|
const slug = name.toLowerCase().replace(/[^a-z0-9.-]+/g, "-").replace(/^[^a-z0-9]+/, "").replace(/[-_.]+$/, "");
|
|
691
1069
|
return slug.length > 0 ? slug : "project";
|
|
692
1070
|
}
|
|
693
1071
|
async function scaffoldRoadmap(args) {
|
|
694
|
-
const repoBasename =
|
|
695
|
-
const dir =
|
|
696
|
-
const filePath =
|
|
697
|
-
const legacyPath =
|
|
1072
|
+
const repoBasename = path7.basename(args.repoRoot);
|
|
1073
|
+
const dir = path7.join(args.repoRoot, ".nolto", "roadmaps");
|
|
1074
|
+
const filePath = path7.join(dir, `${slugifyProjectId(repoBasename)}.json`);
|
|
1075
|
+
const legacyPath = path7.join(args.repoRoot, ".roadmap", "roadmap.json");
|
|
698
1076
|
try {
|
|
699
1077
|
const existing = (await readdir(dir)).sort();
|
|
700
1078
|
if (existing.length > 0) {
|
|
701
|
-
return { created: false, path:
|
|
1079
|
+
return { created: false, path: path7.join(dir, existing[0]) };
|
|
702
1080
|
}
|
|
703
1081
|
} catch (err) {
|
|
704
1082
|
if (err == null || typeof err !== "object" || !("code" in err) || err.code !== "ENOENT") {
|
|
@@ -720,19 +1098,19 @@ async function scaffoldRoadmap(args) {
|
|
|
720
1098
|
summary: "",
|
|
721
1099
|
phases: []
|
|
722
1100
|
};
|
|
723
|
-
await
|
|
724
|
-
await
|
|
1101
|
+
await mkdir5(dir, { recursive: true });
|
|
1102
|
+
await writeFile5(filePath, JSON.stringify(roadmap, null, 2) + "\n", "utf8");
|
|
725
1103
|
return { created: true, path: filePath };
|
|
726
1104
|
}
|
|
727
1105
|
|
|
728
1106
|
// src/commands/init.ts
|
|
729
|
-
var __dirname2 =
|
|
1107
|
+
var __dirname2 = path8.dirname(fileURLToPath2(import.meta.url));
|
|
730
1108
|
var _require = createRequire(import.meta.url);
|
|
731
1109
|
function getCliVersion() {
|
|
732
1110
|
const candidates = [
|
|
733
|
-
|
|
1111
|
+
path8.resolve(__dirname2, "../package.json"),
|
|
734
1112
|
// bundled: dist/../package.json
|
|
735
|
-
|
|
1113
|
+
path8.resolve(__dirname2, "../../package.json")
|
|
736
1114
|
// source: src/commands/../../package.json
|
|
737
1115
|
];
|
|
738
1116
|
for (const pkgPath of candidates) {
|
|
@@ -746,160 +1124,212 @@ function getCliVersion() {
|
|
|
746
1124
|
}
|
|
747
1125
|
return "0.0.0";
|
|
748
1126
|
}
|
|
1127
|
+
async function pickProject(rl, http, projects, promptText) {
|
|
1128
|
+
if (projects.length > 0) {
|
|
1129
|
+
process.stdout.write("\nProjects:\n");
|
|
1130
|
+
projects.forEach((project, index) => {
|
|
1131
|
+
process.stdout.write(` (${index + 1}) ${project.name} \u2014 ${project.id}
|
|
1132
|
+
`);
|
|
1133
|
+
});
|
|
1134
|
+
} else {
|
|
1135
|
+
process.stdout.write("\nNo projects yet.\n");
|
|
1136
|
+
}
|
|
1137
|
+
const pick = await rl.question(promptText);
|
|
1138
|
+
const trimmedPick = pick.trim().toLowerCase();
|
|
1139
|
+
if (trimmedPick === "c") {
|
|
1140
|
+
const name = await rl.question("New project name: ");
|
|
1141
|
+
if (name.trim().length === 0) {
|
|
1142
|
+
throw new CliError("Project name is required.", 2);
|
|
1143
|
+
}
|
|
1144
|
+
const created = await http.post(
|
|
1145
|
+
"/api/projects",
|
|
1146
|
+
{ name: name.trim() }
|
|
1147
|
+
);
|
|
1148
|
+
if (created.project?.id == null) {
|
|
1149
|
+
throw new CliError("Project API did not return a project id.", 2);
|
|
1150
|
+
}
|
|
1151
|
+
const project = {
|
|
1152
|
+
id: created.project.id,
|
|
1153
|
+
name: created.project.name ?? name.trim()
|
|
1154
|
+
};
|
|
1155
|
+
process.stdout.write(`Created project ${project.name} (${project.id})
|
|
1156
|
+
`);
|
|
1157
|
+
return project;
|
|
1158
|
+
}
|
|
1159
|
+
const num = parseInt(trimmedPick, 10);
|
|
1160
|
+
if (!isNaN(num) && num >= 1 && num <= projects.length) {
|
|
1161
|
+
return projects[num - 1];
|
|
1162
|
+
}
|
|
1163
|
+
return void 0;
|
|
1164
|
+
}
|
|
749
1165
|
function register2(program, deps) {
|
|
750
1166
|
program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
|
|
751
1167
|
const configPath = deps.configPath;
|
|
752
|
-
if (!opts.force) {
|
|
753
|
-
let existing = null;
|
|
754
|
-
try {
|
|
755
|
-
existing = await loadConfigFile(configPath);
|
|
756
|
-
} catch {
|
|
757
|
-
}
|
|
758
|
-
if (existing != null) {
|
|
759
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
760
|
-
try {
|
|
761
|
-
const answer = await rl2.question(`Config already exists at ${configPath}. Overwrite? [y/N] `);
|
|
762
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
763
|
-
process.stdout.write("Cancelled.\n");
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
} finally {
|
|
767
|
-
rl2.close();
|
|
768
|
-
}
|
|
769
|
-
}
|
|
770
|
-
}
|
|
771
1168
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
772
|
-
let token = "";
|
|
773
1169
|
try {
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
let projects = [];
|
|
789
|
-
try {
|
|
790
|
-
const result = await http.get("/api/projects");
|
|
791
|
-
projects = Array.isArray(result.projects) ? result.projects : [];
|
|
792
|
-
} catch (err) {
|
|
793
|
-
if (err instanceof CliError && err.exitCode === 3) {
|
|
794
|
-
throw new CliError(`Token rejected by ${baseUrl}`, 3, "Check that your token is valid and has not been revoked.");
|
|
1170
|
+
let configureGlobal = opts.force === true;
|
|
1171
|
+
if (!configureGlobal) {
|
|
1172
|
+
let existing = null;
|
|
1173
|
+
try {
|
|
1174
|
+
existing = await loadConfigFile(configPath);
|
|
1175
|
+
} catch {
|
|
1176
|
+
}
|
|
1177
|
+
if (existing == null || deps.settings.token == null) {
|
|
1178
|
+
configureGlobal = true;
|
|
1179
|
+
} else {
|
|
1180
|
+
const answer = await rl.question(
|
|
1181
|
+
`Config already exists at ${configPath}. Reconfigure token and base URL? [y/N] `
|
|
1182
|
+
);
|
|
1183
|
+
configureGlobal = answer.trim().toLowerCase() === "y";
|
|
795
1184
|
}
|
|
796
|
-
throw err;
|
|
797
1185
|
}
|
|
798
|
-
let
|
|
1186
|
+
let token = deps.settings.token ?? "";
|
|
1187
|
+
let baseUrl = deps.settings.baseUrl;
|
|
1188
|
+
let defaultProjectId = deps.settings.defaultProjectId;
|
|
799
1189
|
let defaultProjectName;
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
1190
|
+
let http;
|
|
1191
|
+
let projects;
|
|
1192
|
+
if (configureGlobal) {
|
|
1193
|
+
const rawUrl = await rl.question(`Base URL [${DEFAULT_BASE_URL}]: `);
|
|
1194
|
+
baseUrl = rawUrl.trim() || DEFAULT_BASE_URL;
|
|
1195
|
+
const currentToken = deps.settings.token;
|
|
1196
|
+
const tokenPrompt = currentToken != null ? "API token [press Enter to keep current token]: " : "API token: ";
|
|
1197
|
+
const enteredToken = await promptHidden(rl, tokenPrompt);
|
|
1198
|
+
token = enteredToken.length > 0 ? enteredToken : currentToken ?? "";
|
|
1199
|
+
if (token.length === 0) {
|
|
1200
|
+
throw new CliError("Token is required.", 2);
|
|
1201
|
+
}
|
|
1202
|
+
http = createHttpClient({
|
|
1203
|
+
baseUrl,
|
|
1204
|
+
token,
|
|
1205
|
+
version: getCliVersion()
|
|
805
1206
|
});
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
throw new CliError("Project name is required.", 2);
|
|
1207
|
+
try {
|
|
1208
|
+
const result = await http.get("/api/projects");
|
|
1209
|
+
projects = Array.isArray(result.projects) ? result.projects : [];
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
if (err instanceof CliError && err.exitCode === 3) {
|
|
1212
|
+
throw new CliError(`Token rejected by ${baseUrl}`, 3, "Check that your token is valid and has not been revoked.");
|
|
1213
|
+
}
|
|
1214
|
+
throw err;
|
|
815
1215
|
}
|
|
816
|
-
const
|
|
817
|
-
|
|
818
|
-
|
|
1216
|
+
const selected = await pickProject(
|
|
1217
|
+
rl,
|
|
1218
|
+
http,
|
|
1219
|
+
projects,
|
|
1220
|
+
"Default project number, 'c' to create new (or Enter to skip): "
|
|
819
1221
|
);
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
if (!isNaN(num) && num >= 1 && num <= projects.length) {
|
|
830
|
-
defaultProjectId = projects[num - 1].id;
|
|
831
|
-
defaultProjectName = projects[num - 1].name;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
await saveConfigFile(configPath, {
|
|
835
|
-
token,
|
|
836
|
-
baseUrl: baseUrl !== DEFAULT_BASE_URL ? baseUrl : void 0,
|
|
837
|
-
defaultProjectId
|
|
838
|
-
});
|
|
839
|
-
const projectDisplay = defaultProjectId != null ? `${defaultProjectName ?? ""} (${defaultProjectId})` : "not set";
|
|
840
|
-
process.stdout.write(`
|
|
1222
|
+
defaultProjectId = selected?.id;
|
|
1223
|
+
defaultProjectName = selected?.name;
|
|
1224
|
+
await saveConfigFile(configPath, {
|
|
1225
|
+
token,
|
|
1226
|
+
baseUrl: baseUrl !== DEFAULT_BASE_URL ? baseUrl : void 0,
|
|
1227
|
+
defaultProjectId
|
|
1228
|
+
});
|
|
1229
|
+
const projectDisplay = defaultProjectId != null ? `${defaultProjectName ?? ""} (${defaultProjectId})` : "not set";
|
|
1230
|
+
process.stdout.write(`
|
|
841
1231
|
Saved ${configPath}
|
|
842
1232
|
`);
|
|
843
|
-
|
|
1233
|
+
process.stdout.write(`baseUrl: ${baseUrl}
|
|
844
1234
|
`);
|
|
845
|
-
|
|
1235
|
+
process.stdout.write(`token: ${maskToken(token)} (verified)
|
|
846
1236
|
`);
|
|
847
|
-
|
|
1237
|
+
process.stdout.write(`defaultProject: ${projectDisplay}
|
|
848
1238
|
`);
|
|
849
|
-
|
|
850
|
-
const
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
1239
|
+
} else {
|
|
1240
|
+
const projectDisplay = defaultProjectId ?? "not set";
|
|
1241
|
+
process.stdout.write(
|
|
1242
|
+
`Using existing config: baseUrl=${baseUrl}, token=${maskToken(token)}, defaultProject=${projectDisplay}
|
|
1243
|
+
`
|
|
1244
|
+
);
|
|
1245
|
+
}
|
|
1246
|
+
const startDir = resolveStartDir(process.env, process.cwd());
|
|
1247
|
+
const { root, foundGit } = findRepoRoot(startDir);
|
|
1248
|
+
if (!foundGit) {
|
|
1249
|
+
process.stdout.write("\nNo git repository found here \u2014 skipped repo setup. Run `nolto init` inside a repo to set up roadmap sync.\n");
|
|
1250
|
+
return;
|
|
1251
|
+
}
|
|
1252
|
+
if (isHomeDirectory(root)) {
|
|
1253
|
+
process.stderr.write(
|
|
1254
|
+
`Refusing to set up your home directory as a repository root (found ${path8.join(os3.homedir(), ".git")}). Run nolto init inside a project repository.
|
|
1255
|
+
`
|
|
1256
|
+
);
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
const existingBinding = deps.repoBinding?.error == null ? deps.repoBinding?.binding ?? null : null;
|
|
1260
|
+
let repoProject = existingBinding != null ? { id: existingBinding.projectId, name: path8.basename(root) } : defaultProjectId != null ? { id: defaultProjectId, name: defaultProjectName ?? path8.basename(root) } : void 0;
|
|
1261
|
+
if (repoProject == null) {
|
|
1262
|
+
http ??= createHttpClient({ baseUrl, token, version: getCliVersion() });
|
|
1263
|
+
if (projects == null) {
|
|
1264
|
+
const result = await http.get("/api/projects");
|
|
1265
|
+
projects = Array.isArray(result.projects) ? result.projects : [];
|
|
1266
|
+
}
|
|
1267
|
+
repoProject = await pickProject(
|
|
1268
|
+
rl,
|
|
1269
|
+
http,
|
|
1270
|
+
projects,
|
|
1271
|
+
"Project number for this repository, 'c' to create new (or Enter to skip): "
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
if (repoProject == null) {
|
|
1275
|
+
process.stdout.write("Skipped repo setup (no project selected).\n");
|
|
1276
|
+
return;
|
|
1277
|
+
}
|
|
1278
|
+
const setup = await rl.question(`
|
|
854
1279
|
Set up this repository (${root}) for roadmap sync? [Y/n] `);
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
1280
|
+
if (setup.trim().toLowerCase() === "n") {
|
|
1281
|
+
process.stdout.write("Skipped repo setup.\n");
|
|
1282
|
+
return;
|
|
1283
|
+
}
|
|
1284
|
+
const bindingPath = deps.repoBinding?.path ?? path8.join(root, "nolto.json");
|
|
1285
|
+
if (existingBinding != null) {
|
|
1286
|
+
process.stdout.write(`binding: kept ${bindingPath} (${existingBinding.projectId})
|
|
1287
|
+
`);
|
|
1288
|
+
} else {
|
|
1289
|
+
if (deps.repoBinding?.error != null) {
|
|
1290
|
+
process.stderr.write(
|
|
1291
|
+
`Warning: existing nolto.json is invalid (${deps.repoBinding.error.message}). Overwriting it to repair.
|
|
859
1292
|
`
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
await writeRepoBinding(root, repoProject.id);
|
|
1296
|
+
process.stdout.write(`binding: wrote ${path8.join(root, "nolto.json")}
|
|
864
1297
|
`);
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
1298
|
+
}
|
|
1299
|
+
const sourceDir = resolveSkillSourceDir();
|
|
1300
|
+
const version = getCliVersion();
|
|
1301
|
+
const claudeInstall = await installSkill({
|
|
1302
|
+
skillsParentDir: path8.join(root, ".claude", "skills"),
|
|
1303
|
+
sourceDir,
|
|
1304
|
+
version
|
|
1305
|
+
});
|
|
1306
|
+
process.stdout.write(`skill (claude): ${claudeInstall.action} ${claudeInstall.targetDir}
|
|
873
1307
|
`);
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
1308
|
+
const usesAgentsTooling = fs.existsSync(path8.join(root, ".agents")) || fs.existsSync(path8.join(root, ".codex")) || fs.existsSync(path8.join(root, "AGENTS.md"));
|
|
1309
|
+
if (usesAgentsTooling) {
|
|
1310
|
+
const agentsInstall = await installSkill({
|
|
1311
|
+
skillsParentDir: path8.join(root, ".agents", "skills"),
|
|
1312
|
+
sourceDir,
|
|
1313
|
+
version
|
|
1314
|
+
});
|
|
1315
|
+
process.stdout.write(`skill (agents): ${agentsInstall.action} ${agentsInstall.targetDir}
|
|
881
1316
|
`);
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
1317
|
+
}
|
|
1318
|
+
const scaffold = await scaffoldRoadmap({
|
|
1319
|
+
repoRoot: root,
|
|
1320
|
+
projectName: repoProject.name
|
|
1321
|
+
});
|
|
1322
|
+
process.stdout.write(
|
|
1323
|
+
scaffold.created ? `roadmap: created ${scaffold.path}
|
|
889
1324
|
` : `roadmap: exists ${scaffold.path}
|
|
890
1325
|
`
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
1326
|
+
);
|
|
1327
|
+
const registryResult = await addRepoToRegistry(getRegistryPath(process.env), root);
|
|
1328
|
+
process.stdout.write(
|
|
1329
|
+
registryResult.added ? `watch registry: added ${root}
|
|
895
1330
|
` : `watch registry: already registered
|
|
896
1331
|
`
|
|
897
|
-
|
|
898
|
-
}
|
|
899
|
-
} else {
|
|
900
|
-
process.stdout.write("\nNo git repository found here \u2014 skipped repo setup. Run `nolto init` inside a repo to set up roadmap sync.\n");
|
|
901
|
-
}
|
|
902
|
-
}
|
|
1332
|
+
);
|
|
903
1333
|
} finally {
|
|
904
1334
|
rl.close();
|
|
905
1335
|
}
|
|
@@ -1119,142 +1549,15 @@ function register4(program, deps) {
|
|
|
1119
1549
|
}
|
|
1120
1550
|
|
|
1121
1551
|
// src/commands/sync.ts
|
|
1122
|
-
import { copyFile, mkdir as
|
|
1552
|
+
import { copyFile, mkdir as mkdir6, readFile as readFile5, readdir as readdir2, rename as rename2, rmdir, unlink as unlink2 } from "fs/promises";
|
|
1123
1553
|
import { existsSync as existsSync3 } from "fs";
|
|
1124
1554
|
|
|
1125
1555
|
// src/sync-repo.ts
|
|
1126
|
-
import
|
|
1556
|
+
import path10 from "path";
|
|
1127
1557
|
|
|
1128
1558
|
// src/sync-core.ts
|
|
1129
1559
|
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
|
|
1560
|
+
import path9 from "path";
|
|
1258
1561
|
function sha256Hex(content) {
|
|
1259
1562
|
return "sha256:" + createHash("sha256").update(content, "utf8").digest("hex");
|
|
1260
1563
|
}
|
|
@@ -1275,10 +1578,10 @@ function collectPlanRefs(roadmap) {
|
|
|
1275
1578
|
}
|
|
1276
1579
|
return refs;
|
|
1277
1580
|
}
|
|
1278
|
-
async function loadValidRoadmap(filePath,
|
|
1581
|
+
async function loadValidRoadmap(filePath, readFile8) {
|
|
1279
1582
|
let raw;
|
|
1280
1583
|
try {
|
|
1281
|
-
raw = await
|
|
1584
|
+
raw = await readFile8(filePath);
|
|
1282
1585
|
} catch {
|
|
1283
1586
|
throw new CliError(`No roadmap found at ${filePath}. Run \`nolto init\` first.`, 2);
|
|
1284
1587
|
}
|
|
@@ -1301,7 +1604,7 @@ async function loadValidRoadmap(filePath, readFile7) {
|
|
|
1301
1604
|
async function buildSyncBody(args) {
|
|
1302
1605
|
const planDocuments = [];
|
|
1303
1606
|
for (const ref of collectPlanRefs(args.roadmap)) {
|
|
1304
|
-
const absolute =
|
|
1607
|
+
const absolute = path9.join(args.repoRoot, ref.path);
|
|
1305
1608
|
if (!args.deps.fileExists(absolute)) {
|
|
1306
1609
|
args.deps.warn(`plan file not found, skipping: ${ref.path}`);
|
|
1307
1610
|
continue;
|
|
@@ -1314,12 +1617,13 @@ async function buildSyncBody(args) {
|
|
|
1314
1617
|
contentHash: sha256Hex(content)
|
|
1315
1618
|
});
|
|
1316
1619
|
}
|
|
1317
|
-
return { roadmap: args.roadmap, planDocuments };
|
|
1620
|
+
return { roadmap: args.roadmap, planDocuments, repoIdentity: args.repoIdentity };
|
|
1318
1621
|
}
|
|
1319
1622
|
async function runSync(args, deps) {
|
|
1320
1623
|
const body = await buildSyncBody({
|
|
1321
1624
|
roadmap: args.roadmap,
|
|
1322
1625
|
repoRoot: args.repoRoot,
|
|
1626
|
+
repoIdentity: args.repoIdentity,
|
|
1323
1627
|
deps
|
|
1324
1628
|
});
|
|
1325
1629
|
const response = await deps.http.put(
|
|
@@ -1347,7 +1651,7 @@ async function listRoadmapFiles(roadmapsDir, io) {
|
|
|
1347
1651
|
}
|
|
1348
1652
|
}
|
|
1349
1653
|
async function migrateLegacyRoadmap(args) {
|
|
1350
|
-
const targetPath =
|
|
1654
|
+
const targetPath = path10.join(args.roadmapsDir, `${args.slug}.json`);
|
|
1351
1655
|
await args.io.mkdir(args.roadmapsDir);
|
|
1352
1656
|
try {
|
|
1353
1657
|
await args.io.rename(args.legacyPath, targetPath);
|
|
@@ -1356,7 +1660,7 @@ async function migrateLegacyRoadmap(args) {
|
|
|
1356
1660
|
await args.io.unlink(args.legacyPath);
|
|
1357
1661
|
}
|
|
1358
1662
|
try {
|
|
1359
|
-
await args.io.rmdir(
|
|
1663
|
+
await args.io.rmdir(path10.dirname(args.legacyPath));
|
|
1360
1664
|
} catch {
|
|
1361
1665
|
}
|
|
1362
1666
|
args.io.log(
|
|
@@ -1365,14 +1669,14 @@ async function migrateLegacyRoadmap(args) {
|
|
|
1365
1669
|
return `${args.slug}.json`;
|
|
1366
1670
|
}
|
|
1367
1671
|
async function syncRepo(args, io) {
|
|
1368
|
-
const bindingPath =
|
|
1672
|
+
const bindingPath = path10.join(args.root, "nolto.json");
|
|
1369
1673
|
const binding = await loadRepoBinding(bindingPath);
|
|
1370
1674
|
const projectId = binding?.projectId ?? args.defaultProjectId;
|
|
1371
1675
|
if (projectId == null) {
|
|
1372
1676
|
throw new CliError("No project binding. Run `nolto init` or `nolto link <projectId>`.", 2);
|
|
1373
1677
|
}
|
|
1374
|
-
const roadmapsDir =
|
|
1375
|
-
const legacyPath =
|
|
1678
|
+
const roadmapsDir = path10.join(args.root, ".nolto", "roadmaps");
|
|
1679
|
+
const legacyPath = path10.join(args.root, ".roadmap", "roadmap.json");
|
|
1376
1680
|
let roadmapFiles = await listRoadmapFiles(roadmapsDir, io);
|
|
1377
1681
|
if (roadmapFiles.length > 0) {
|
|
1378
1682
|
if (io.fileExists(legacyPath)) {
|
|
@@ -1381,7 +1685,13 @@ async function syncRepo(args, io) {
|
|
|
1381
1685
|
);
|
|
1382
1686
|
}
|
|
1383
1687
|
} else if (io.fileExists(legacyPath)) {
|
|
1384
|
-
|
|
1688
|
+
if (args.migrateLegacy !== true) {
|
|
1689
|
+
io.warn(
|
|
1690
|
+
"legacy .roadmap/roadmap.json found \u2014 run `nolto sync` once to migrate it to .nolto/roadmaps/ (watch does not migrate automatically)"
|
|
1691
|
+
);
|
|
1692
|
+
return { results: [], planAbsPaths: [] };
|
|
1693
|
+
}
|
|
1694
|
+
const migrationSlug = binding?.roadmapSlug ?? slugifyProjectId(path10.basename(args.root));
|
|
1385
1695
|
roadmapFiles = [
|
|
1386
1696
|
await migrateLegacyRoadmap({
|
|
1387
1697
|
slug: migrationSlug,
|
|
@@ -1405,21 +1715,22 @@ async function syncRepo(args, io) {
|
|
|
1405
1715
|
2
|
|
1406
1716
|
);
|
|
1407
1717
|
}
|
|
1408
|
-
const filePath =
|
|
1718
|
+
const filePath = path10.join(roadmapsDir, fileName);
|
|
1409
1719
|
return { slug, roadmap: await loadValidRoadmap(filePath, io.readFile) };
|
|
1410
1720
|
})
|
|
1411
1721
|
);
|
|
1412
1722
|
const planAbsPaths = /* @__PURE__ */ new Set();
|
|
1413
1723
|
for (const { roadmap } of roadmaps) {
|
|
1414
1724
|
for (const ref of collectPlanRefs(roadmap)) {
|
|
1415
|
-
planAbsPaths.add(
|
|
1725
|
+
planAbsPaths.add(path10.join(args.root, ref.path));
|
|
1416
1726
|
}
|
|
1417
1727
|
}
|
|
1418
1728
|
const results = [];
|
|
1729
|
+
const repoIdentity = await io.repoIdentity(args.root);
|
|
1419
1730
|
for (const { slug, roadmap } of roadmaps) {
|
|
1420
1731
|
results.push(
|
|
1421
1732
|
await runSync(
|
|
1422
|
-
{ repoRoot: args.root, projectId, slug, roadmap },
|
|
1733
|
+
{ repoRoot: args.root, projectId, slug, roadmap, repoIdentity },
|
|
1423
1734
|
{ http: io.http, readFile: io.readFile, fileExists: io.fileExists, log: io.log, warn: io.warn }
|
|
1424
1735
|
)
|
|
1425
1736
|
);
|
|
@@ -1438,38 +1749,61 @@ function register5(program, deps) {
|
|
|
1438
1749
|
if (!foundGit) {
|
|
1439
1750
|
throw new CliError("No git repository found. Run inside a repo set up with `nolto init`.", 2);
|
|
1440
1751
|
}
|
|
1441
|
-
const http =
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
});
|
|
1752
|
+
const http = deps.http;
|
|
1753
|
+
const warn = (line) => {
|
|
1754
|
+
process.stderr.write("Warning: " + line + "\n");
|
|
1755
|
+
};
|
|
1446
1756
|
const response = await syncRepo(
|
|
1447
|
-
{ root, defaultProjectId: deps.settings.defaultProjectId },
|
|
1757
|
+
{ root, defaultProjectId: deps.settings.defaultProjectId, migrateLegacy: true },
|
|
1448
1758
|
{
|
|
1449
|
-
readFile: (p) =>
|
|
1759
|
+
readFile: (p) => readFile5(p, "utf8"),
|
|
1450
1760
|
fileExists: (p) => existsSync3(p),
|
|
1451
1761
|
listDir: (p) => readdir2(p),
|
|
1452
|
-
rename,
|
|
1762
|
+
rename: rename2,
|
|
1453
1763
|
copyFile,
|
|
1454
|
-
mkdir: (p) =>
|
|
1455
|
-
unlink,
|
|
1764
|
+
mkdir: (p) => mkdir6(p, { recursive: true }).then(() => void 0),
|
|
1765
|
+
unlink: unlink2,
|
|
1456
1766
|
rmdir,
|
|
1767
|
+
repoIdentity: makeRepoIdentityResolver(deps),
|
|
1457
1768
|
http,
|
|
1458
1769
|
log: (line) => process.stdout.write(line + "\n"),
|
|
1459
|
-
warn
|
|
1770
|
+
warn
|
|
1460
1771
|
}
|
|
1461
1772
|
);
|
|
1773
|
+
try {
|
|
1774
|
+
const drifted = await checkSkillVersionDrift(root, deps.version);
|
|
1775
|
+
if (drifted.length > 0) {
|
|
1776
|
+
warn(formatSkillVersionDriftWarning(root, deps.version, drifted));
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
}
|
|
1780
|
+
let registryAdded = false;
|
|
1781
|
+
if (isHomeDirectory(root)) {
|
|
1782
|
+
process.stderr.write(`Warning: refusing to add home directory ${root} to the watch registry.
|
|
1783
|
+
`);
|
|
1784
|
+
} else {
|
|
1785
|
+
try {
|
|
1786
|
+
registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
|
|
1787
|
+
} catch (err) {
|
|
1788
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1789
|
+
process.stderr.write(`Warning: could not update watch registry: ${message}
|
|
1790
|
+
`);
|
|
1791
|
+
}
|
|
1792
|
+
}
|
|
1462
1793
|
if (deps.output.mode === "json") {
|
|
1463
1794
|
const { planAbsPaths: _planAbsPaths, ...publicResponse } = response;
|
|
1464
|
-
printResult(publicResponse, "json");
|
|
1795
|
+
printResult({ ...publicResponse, registryAdded }, "json");
|
|
1796
|
+
} else if (registryAdded) {
|
|
1797
|
+
process.stdout.write(`watch registry: added ${root}
|
|
1798
|
+
`);
|
|
1465
1799
|
}
|
|
1466
1800
|
});
|
|
1467
1801
|
}
|
|
1468
1802
|
|
|
1469
1803
|
// src/commands/watch.ts
|
|
1470
|
-
import { copyFile as copyFile2, mkdir as
|
|
1804
|
+
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
1805
|
import { existsSync as existsSync4 } from "fs";
|
|
1472
|
-
import
|
|
1806
|
+
import path12 from "path";
|
|
1473
1807
|
import chokidar from "chokidar";
|
|
1474
1808
|
|
|
1475
1809
|
// src/watch-core.ts
|
|
@@ -1575,8 +1909,8 @@ var RepoWatch = class {
|
|
|
1575
1909
|
};
|
|
1576
1910
|
|
|
1577
1911
|
// src/service-install.ts
|
|
1578
|
-
import
|
|
1579
|
-
import
|
|
1912
|
+
import path11 from "path";
|
|
1913
|
+
import os4 from "os";
|
|
1580
1914
|
function buildUnitFile(args) {
|
|
1581
1915
|
return [
|
|
1582
1916
|
"[Unit]",
|
|
@@ -1595,8 +1929,8 @@ function buildUnitFile(args) {
|
|
|
1595
1929
|
}
|
|
1596
1930
|
function getUnitPath(env) {
|
|
1597
1931
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
1598
|
-
const base = xdg != null && xdg.length > 0 ? xdg :
|
|
1599
|
-
return
|
|
1932
|
+
const base = xdg != null && xdg.length > 0 ? xdg : path11.join(os4.homedir(), ".config");
|
|
1933
|
+
return path11.join(base, "systemd", "user", "nolto-watch.service");
|
|
1600
1934
|
}
|
|
1601
1935
|
async function installServiceWith(deps) {
|
|
1602
1936
|
if (deps.platform !== "linux") {
|
|
@@ -1607,7 +1941,7 @@ async function installServiceWith(deps) {
|
|
|
1607
1941
|
);
|
|
1608
1942
|
}
|
|
1609
1943
|
const unitPath = getUnitPath(deps.env);
|
|
1610
|
-
await deps.mkdir(
|
|
1944
|
+
await deps.mkdir(path11.dirname(unitPath));
|
|
1611
1945
|
await deps.writeFile(unitPath, buildUnitFile({ nodePath: deps.nodePath, scriptPath: deps.scriptPath }));
|
|
1612
1946
|
deps.log(`Wrote ${unitPath}`);
|
|
1613
1947
|
const reload = await deps.exec(["systemctl", "--user", "daemon-reload"]);
|
|
@@ -1621,18 +1955,18 @@ async function installServiceWith(deps) {
|
|
|
1621
1955
|
deps.log("Service nolto-watch enabled and started. Logs: journalctl --user -u nolto-watch -f");
|
|
1622
1956
|
}
|
|
1623
1957
|
async function installService() {
|
|
1624
|
-
const { writeFile:
|
|
1625
|
-
const { execFile:
|
|
1958
|
+
const { writeFile: writeFile7, mkdir: mkdir9 } = await import("fs/promises");
|
|
1959
|
+
const { execFile: execFile3 } = await import("child_process");
|
|
1626
1960
|
const { promisify: promisify2 } = await import("util");
|
|
1627
|
-
const execFileAsync = promisify2(
|
|
1961
|
+
const execFileAsync = promisify2(execFile3);
|
|
1628
1962
|
await installServiceWith({
|
|
1629
1963
|
platform: process.platform,
|
|
1630
1964
|
env: process.env,
|
|
1631
1965
|
nodePath: process.execPath,
|
|
1632
|
-
scriptPath:
|
|
1633
|
-
writeFile: (p, content) =>
|
|
1966
|
+
scriptPath: path11.resolve(process.argv[1] ?? ""),
|
|
1967
|
+
writeFile: (p, content) => writeFile7(p, content, "utf8"),
|
|
1634
1968
|
mkdir: async (p) => {
|
|
1635
|
-
await
|
|
1969
|
+
await mkdir9(p, { recursive: true });
|
|
1636
1970
|
},
|
|
1637
1971
|
exec: async (cmd) => {
|
|
1638
1972
|
try {
|
|
@@ -1686,14 +2020,14 @@ async function uninstallServiceWith(deps) {
|
|
|
1686
2020
|
deps.log("Service nolto-watch stopped, disabled, and removed.");
|
|
1687
2021
|
}
|
|
1688
2022
|
async function uninstallService() {
|
|
1689
|
-
const { unlink:
|
|
1690
|
-
const { execFile:
|
|
2023
|
+
const { unlink: unlink4 } = await import("fs/promises");
|
|
2024
|
+
const { execFile: execFile3 } = await import("child_process");
|
|
1691
2025
|
const { promisify: promisify2 } = await import("util");
|
|
1692
|
-
const execFileAsync = promisify2(
|
|
2026
|
+
const execFileAsync = promisify2(execFile3);
|
|
1693
2027
|
await uninstallServiceWith({
|
|
1694
2028
|
platform: process.platform,
|
|
1695
2029
|
env: process.env,
|
|
1696
|
-
unlink:
|
|
2030
|
+
unlink: unlink4,
|
|
1697
2031
|
exec: async (cmd) => {
|
|
1698
2032
|
try {
|
|
1699
2033
|
await execFileAsync(cmd[0], cmd.slice(1));
|
|
@@ -1746,26 +2080,31 @@ function register6(program, deps) {
|
|
|
1746
2080
|
token: deps.settings.token
|
|
1747
2081
|
});
|
|
1748
2082
|
const startRepo = (root) => {
|
|
1749
|
-
const roadmapsPath =
|
|
1750
|
-
const legacyRoadmapPath =
|
|
2083
|
+
const roadmapsPath = path12.join(root, ".nolto", "roadmaps");
|
|
2084
|
+
const legacyRoadmapPath = path12.join(root, ".roadmap", "roadmap.json");
|
|
2085
|
+
const warn = (line) => {
|
|
2086
|
+
process.stderr.write(`Warning: [${path12.basename(root)}] ${line}
|
|
2087
|
+
`);
|
|
2088
|
+
};
|
|
1751
2089
|
const watcher = chokidar.watch([roadmapsPath, legacyRoadmapPath], { ignoreInitial: true });
|
|
1752
2090
|
const repoWatch = new RepoWatch(root, {
|
|
1753
2091
|
sync: () => syncRepo(
|
|
1754
|
-
|
|
2092
|
+
// #316: watch must warn about legacy roadmaps without migrating them.
|
|
2093
|
+
{ root, defaultProjectId: deps.settings.defaultProjectId, migrateLegacy: false },
|
|
1755
2094
|
{
|
|
1756
|
-
readFile: (p) =>
|
|
2095
|
+
readFile: (p) => readFile6(p, "utf8"),
|
|
1757
2096
|
fileExists: (p) => existsSync4(p),
|
|
1758
2097
|
listDir: (p) => readdir3(p),
|
|
1759
|
-
rename:
|
|
2098
|
+
rename: rename3,
|
|
1760
2099
|
copyFile: copyFile2,
|
|
1761
|
-
mkdir: (p) =>
|
|
1762
|
-
unlink:
|
|
2100
|
+
mkdir: (p) => mkdir7(p, { recursive: true }).then(() => void 0),
|
|
2101
|
+
unlink: unlink3,
|
|
1763
2102
|
rmdir: rmdir2,
|
|
2103
|
+
repoIdentity: makeRepoIdentityResolver(deps),
|
|
1764
2104
|
http,
|
|
1765
|
-
log: (line) => process.stdout.write(`[${
|
|
2105
|
+
log: (line) => process.stdout.write(`[${path12.basename(root)}] ${line}
|
|
1766
2106
|
`),
|
|
1767
|
-
warn
|
|
1768
|
-
`)
|
|
2107
|
+
warn
|
|
1769
2108
|
}
|
|
1770
2109
|
),
|
|
1771
2110
|
watcher,
|
|
@@ -1776,6 +2115,12 @@ function register6(program, deps) {
|
|
|
1776
2115
|
warn: (line) => process.stderr.write(line + "\n")
|
|
1777
2116
|
});
|
|
1778
2117
|
watcher.on("all", (_event, filePath) => repoWatch.handleEvent(filePath));
|
|
2118
|
+
void checkSkillVersionDrift(root, deps.version).then((drifted) => {
|
|
2119
|
+
if (drifted.length > 0) {
|
|
2120
|
+
warn(formatSkillVersionDriftWarning(root, deps.version, drifted));
|
|
2121
|
+
}
|
|
2122
|
+
}).catch(() => {
|
|
2123
|
+
});
|
|
1779
2124
|
void repoWatch.flush();
|
|
1780
2125
|
return { stop: () => watcher.close() };
|
|
1781
2126
|
};
|
|
@@ -1820,18 +2165,18 @@ function register6(program, deps) {
|
|
|
1820
2165
|
}
|
|
1821
2166
|
|
|
1822
2167
|
// src/update-cli.ts
|
|
1823
|
-
import { execFile } from "child_process";
|
|
2168
|
+
import { execFile as execFile2 } from "child_process";
|
|
1824
2169
|
import { existsSync as existsSync5 } from "fs";
|
|
1825
2170
|
import { realpath } from "fs/promises";
|
|
1826
2171
|
import { createRequire as createRequire2 } from "module";
|
|
1827
|
-
import
|
|
2172
|
+
import path14 from "path";
|
|
1828
2173
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
1829
2174
|
import { promisify } from "util";
|
|
1830
2175
|
|
|
1831
2176
|
// src/update-notifier.ts
|
|
1832
|
-
import { readFile as
|
|
2177
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir8 } from "fs/promises";
|
|
1833
2178
|
import https from "https";
|
|
1834
|
-
import
|
|
2179
|
+
import path13 from "path";
|
|
1835
2180
|
var PACKAGE = "@nolto/cli";
|
|
1836
2181
|
var CACHE_FILE = "update-check.json";
|
|
1837
2182
|
var CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
@@ -1889,9 +2234,9 @@ function fetchLatestFromRegistry(timeoutMs = REQUEST_TIMEOUT_MS, opts = {}) {
|
|
|
1889
2234
|
});
|
|
1890
2235
|
}
|
|
1891
2236
|
async function writeUpdateCache(cachePath, now, latest) {
|
|
1892
|
-
await
|
|
2237
|
+
await mkdir8(path13.dirname(cachePath), { recursive: true });
|
|
1893
2238
|
const payload = { checkedAt: now, latest };
|
|
1894
|
-
await
|
|
2239
|
+
await writeFile6(cachePath, JSON.stringify(payload), { mode: 384 });
|
|
1895
2240
|
}
|
|
1896
2241
|
async function refreshCache(cachePath, now, fetchLatest) {
|
|
1897
2242
|
const latest = await fetchLatest();
|
|
@@ -1900,10 +2245,10 @@ async function refreshCache(cachePath, now, fetchLatest) {
|
|
|
1900
2245
|
}
|
|
1901
2246
|
async function checkForUpdate(opts) {
|
|
1902
2247
|
if (isDisabled(opts.env)) return null;
|
|
1903
|
-
const cachePath =
|
|
2248
|
+
const cachePath = path13.join(opts.configDir, CACHE_FILE);
|
|
1904
2249
|
let cache = {};
|
|
1905
2250
|
try {
|
|
1906
|
-
cache = JSON.parse(await
|
|
2251
|
+
cache = JSON.parse(await readFile7(cachePath, "utf8"));
|
|
1907
2252
|
} catch {
|
|
1908
2253
|
}
|
|
1909
2254
|
if (typeof cache.checkedAt !== "number" || opts.now - cache.checkedAt > CACHE_TTL_MS) {
|
|
@@ -2008,7 +2353,7 @@ async function updateCliWith(deps) {
|
|
|
2008
2353
|
}
|
|
2009
2354
|
deps.log(`Updated ${PACKAGE2} ${deps.currentVersion} \u2192 ${latest}.`);
|
|
2010
2355
|
await deps.writeCache(
|
|
2011
|
-
|
|
2356
|
+
path14.join(deps.configDir, UPDATE_CACHE_FILE),
|
|
2012
2357
|
deps.now,
|
|
2013
2358
|
latest
|
|
2014
2359
|
).catch(() => void 0);
|
|
@@ -2039,17 +2384,17 @@ async function updateCliWith(deps) {
|
|
|
2039
2384
|
};
|
|
2040
2385
|
}
|
|
2041
2386
|
function getCurrentVersion() {
|
|
2042
|
-
const dirname =
|
|
2387
|
+
const dirname = path14.dirname(fileURLToPath3(import.meta.url));
|
|
2043
2388
|
const require3 = createRequire2(import.meta.url);
|
|
2044
2389
|
try {
|
|
2045
|
-
const pkg = require3(
|
|
2390
|
+
const pkg = require3(path14.resolve(dirname, "../package.json"));
|
|
2046
2391
|
return pkg.version ?? "0.0.0";
|
|
2047
2392
|
} catch {
|
|
2048
2393
|
return "0.0.0";
|
|
2049
2394
|
}
|
|
2050
2395
|
}
|
|
2051
2396
|
async function updateCli(opts = {}) {
|
|
2052
|
-
const execFileAsync = promisify(
|
|
2397
|
+
const execFileAsync = promisify(execFile2);
|
|
2053
2398
|
const scriptPath = await realpath(process.argv[1] ?? "");
|
|
2054
2399
|
return updateCliWith({
|
|
2055
2400
|
currentVersion: getCurrentVersion(),
|
|
@@ -2117,11 +2462,11 @@ function buildProgram(deps) {
|
|
|
2117
2462
|
}
|
|
2118
2463
|
|
|
2119
2464
|
// src/index.ts
|
|
2120
|
-
var __dirname3 =
|
|
2465
|
+
var __dirname3 = path15.dirname(fileURLToPath4(import.meta.url));
|
|
2121
2466
|
var require2 = createRequire3(import.meta.url);
|
|
2122
2467
|
function getVersion() {
|
|
2123
2468
|
try {
|
|
2124
|
-
const pkgPath =
|
|
2469
|
+
const pkgPath = path15.resolve(__dirname3, "../package.json");
|
|
2125
2470
|
const pkg = require2(pkgPath);
|
|
2126
2471
|
return pkg.version ?? "0.0.0";
|
|
2127
2472
|
} catch {
|