@nolto/cli 0.8.0 → 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 +6 -1
- package/dist/index.js +263 -147
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -21,6 +21,8 @@ nolto init
|
|
|
21
21
|
`nolto init` configures authentication, selects or creates a project, writes the
|
|
22
22
|
repo-local `nolto.json` binding, installs the bundled `roadmap-progress` skill,
|
|
23
23
|
and creates `.nolto/roadmaps/<slug>.json` when needed.
|
|
24
|
+
Re-running it inside a repository keeps the saved token and only sets up that
|
|
25
|
+
repository; use `--force` to reconfigure the global settings.
|
|
24
26
|
|
|
25
27
|
For browser-based authentication without the rest of the repository setup:
|
|
26
28
|
|
|
@@ -68,10 +70,13 @@ nolto watch --install-service # Install the Linux systemd user service
|
|
|
68
70
|
`sync` reads every `.nolto/roadmaps/*.json` file in filename order, uses each
|
|
69
71
|
filename stem as its slug, validates schema v2, follows any linked plan Markdown
|
|
70
72
|
paths, and sends an idempotent full upsert to the bound project. A legacy
|
|
71
|
-
`.roadmap/roadmap.json` is migrated
|
|
73
|
+
`.roadmap/roadmap.json` is migrated by `nolto sync` when no canonical roadmap
|
|
74
|
+
exists; `nolto watch` only warns and never migrates.
|
|
72
75
|
|
|
73
76
|
`watch` uses the repository registry maintained by `nolto init`. Missing registered
|
|
74
77
|
repositories are skipped with a warning.
|
|
78
|
+
The CLI warns when an installed `roadmap-progress` skill version differs from the
|
|
79
|
+
CLI version; run `nolto init` in that repository to refresh it.
|
|
75
80
|
|
|
76
81
|
## Configuration
|
|
77
82
|
|
package/dist/index.js
CHANGED
|
@@ -377,10 +377,12 @@ import { Command } from "commander";
|
|
|
377
377
|
import readline from "readline/promises";
|
|
378
378
|
import { createRequire } from "module";
|
|
379
379
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
380
|
+
import os3 from "os";
|
|
380
381
|
import path8 from "path";
|
|
381
382
|
import fs from "fs";
|
|
382
383
|
|
|
383
384
|
// src/commands/link.ts
|
|
385
|
+
import os2 from "os";
|
|
384
386
|
import path5 from "path";
|
|
385
387
|
import { statSync as statSync2 } from "fs";
|
|
386
388
|
|
|
@@ -771,6 +773,9 @@ function findRepoRoot(startDir, hasGit = dirHasGit) {
|
|
|
771
773
|
}
|
|
772
774
|
return { root: startDir, foundGit: false };
|
|
773
775
|
}
|
|
776
|
+
function isHomeDirectory(dir, home = os2.homedir()) {
|
|
777
|
+
return path5.resolve(dir) === path5.resolve(home);
|
|
778
|
+
}
|
|
774
779
|
async function handleShow(deps, projectBindingPath, mode2) {
|
|
775
780
|
const startDir = resolveStartDir(process.env, process.cwd());
|
|
776
781
|
const root = findRepoRoot(startDir).root;
|
|
@@ -906,12 +911,17 @@ Proceeding anyway \u2014 verify the ID is correct.
|
|
|
906
911
|
await writeRepoBinding(root, projectId);
|
|
907
912
|
const writtenPath = path5.join(root, "nolto.json");
|
|
908
913
|
let registryAdded = false;
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
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}
|
|
914
923
|
`);
|
|
924
|
+
}
|
|
915
925
|
}
|
|
916
926
|
if (mode2 === "json") {
|
|
917
927
|
printResult({
|
|
@@ -1004,6 +1014,30 @@ function resolveSkillSourceDir() {
|
|
|
1004
1014
|
throw new Error("Bundled roadmap-progress skill not found. Reinstall @nolto/cli.");
|
|
1005
1015
|
}
|
|
1006
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
|
+
}
|
|
1007
1041
|
async function installSkill(args) {
|
|
1008
1042
|
const targetDir = path6.join(args.skillsParentDir, "roadmap-progress");
|
|
1009
1043
|
const markerPath = path6.join(targetDir, VERSION_MARKER);
|
|
@@ -1090,161 +1124,212 @@ function getCliVersion() {
|
|
|
1090
1124
|
}
|
|
1091
1125
|
return "0.0.0";
|
|
1092
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
|
+
}
|
|
1093
1165
|
function register2(program, deps) {
|
|
1094
1166
|
program.command("init").description("Interactive setup: configure token, base URL, and default project.").option("--force", "Overwrite existing config without prompting").action(async (opts) => {
|
|
1095
1167
|
const configPath = deps.configPath;
|
|
1096
|
-
if (!opts.force) {
|
|
1097
|
-
let existing = null;
|
|
1098
|
-
try {
|
|
1099
|
-
existing = await loadConfigFile(configPath);
|
|
1100
|
-
} catch {
|
|
1101
|
-
}
|
|
1102
|
-
if (existing != null) {
|
|
1103
|
-
const rl2 = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1104
|
-
try {
|
|
1105
|
-
const answer = await rl2.question(`Config already exists at ${configPath}. Overwrite? [y/N] `);
|
|
1106
|
-
if (answer.trim().toLowerCase() !== "y") {
|
|
1107
|
-
process.stdout.write("Cancelled.\n");
|
|
1108
|
-
return;
|
|
1109
|
-
}
|
|
1110
|
-
} finally {
|
|
1111
|
-
rl2.close();
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1114
|
-
}
|
|
1115
1168
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
1116
|
-
let token = "";
|
|
1117
1169
|
try {
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
let projects = [];
|
|
1133
|
-
try {
|
|
1134
|
-
const result = await http.get("/api/projects");
|
|
1135
|
-
projects = Array.isArray(result.projects) ? result.projects : [];
|
|
1136
|
-
} catch (err) {
|
|
1137
|
-
if (err instanceof CliError && err.exitCode === 3) {
|
|
1138
|
-
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";
|
|
1139
1184
|
}
|
|
1140
|
-
throw err;
|
|
1141
1185
|
}
|
|
1142
|
-
let
|
|
1186
|
+
let token = deps.settings.token ?? "";
|
|
1187
|
+
let baseUrl = deps.settings.baseUrl;
|
|
1188
|
+
let defaultProjectId = deps.settings.defaultProjectId;
|
|
1143
1189
|
let defaultProjectName;
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
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()
|
|
1149
1206
|
});
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
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;
|
|
1159
1215
|
}
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
1216
|
+
const selected = await pickProject(
|
|
1217
|
+
rl,
|
|
1218
|
+
http,
|
|
1219
|
+
projects,
|
|
1220
|
+
"Default project number, 'c' to create new (or Enter to skip): "
|
|
1163
1221
|
);
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
if (!isNaN(num) && num >= 1 && num <= projects.length) {
|
|
1174
|
-
defaultProjectId = projects[num - 1].id;
|
|
1175
|
-
defaultProjectName = projects[num - 1].name;
|
|
1176
|
-
}
|
|
1177
|
-
}
|
|
1178
|
-
await saveConfigFile(configPath, {
|
|
1179
|
-
token,
|
|
1180
|
-
baseUrl: baseUrl !== DEFAULT_BASE_URL ? baseUrl : void 0,
|
|
1181
|
-
defaultProjectId
|
|
1182
|
-
});
|
|
1183
|
-
const projectDisplay = defaultProjectId != null ? `${defaultProjectName ?? ""} (${defaultProjectId})` : "not set";
|
|
1184
|
-
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(`
|
|
1185
1231
|
Saved ${configPath}
|
|
1186
1232
|
`);
|
|
1187
|
-
|
|
1233
|
+
process.stdout.write(`baseUrl: ${baseUrl}
|
|
1188
1234
|
`);
|
|
1189
|
-
|
|
1235
|
+
process.stdout.write(`token: ${maskToken(token)} (verified)
|
|
1190
1236
|
`);
|
|
1191
|
-
|
|
1237
|
+
process.stdout.write(`defaultProject: ${projectDisplay}
|
|
1192
1238
|
`);
|
|
1193
|
-
|
|
1194
|
-
const
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
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(`
|
|
1198
1279
|
Set up this repository (${root}) for roadmap sync? [Y/n] `);
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
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.
|
|
1203
1292
|
`
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1293
|
+
);
|
|
1294
|
+
}
|
|
1295
|
+
await writeRepoBinding(root, repoProject.id);
|
|
1296
|
+
process.stdout.write(`binding: wrote ${path8.join(root, "nolto.json")}
|
|
1208
1297
|
`);
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
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}
|
|
1217
1307
|
`);
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
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}
|
|
1226
1316
|
`);
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
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}
|
|
1234
1324
|
` : `roadmap: exists ${scaffold.path}
|
|
1235
1325
|
`
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1326
|
+
);
|
|
1327
|
+
const registryResult = await addRepoToRegistry(getRegistryPath(process.env), root);
|
|
1328
|
+
process.stdout.write(
|
|
1329
|
+
registryResult.added ? `watch registry: added ${root}
|
|
1240
1330
|
` : `watch registry: already registered
|
|
1241
1331
|
`
|
|
1242
|
-
|
|
1243
|
-
}
|
|
1244
|
-
} else {
|
|
1245
|
-
process.stdout.write("\nNo git repository found here \u2014 skipped repo setup. Run `nolto init` inside a repo to set up roadmap sync.\n");
|
|
1246
|
-
}
|
|
1247
|
-
}
|
|
1332
|
+
);
|
|
1248
1333
|
} finally {
|
|
1249
1334
|
rl.close();
|
|
1250
1335
|
}
|
|
@@ -1600,6 +1685,12 @@ async function syncRepo(args, io) {
|
|
|
1600
1685
|
);
|
|
1601
1686
|
}
|
|
1602
1687
|
} else if (io.fileExists(legacyPath)) {
|
|
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
|
+
}
|
|
1603
1694
|
const migrationSlug = binding?.roadmapSlug ?? slugifyProjectId(path10.basename(args.root));
|
|
1604
1695
|
roadmapFiles = [
|
|
1605
1696
|
await migrateLegacyRoadmap({
|
|
@@ -1659,8 +1750,11 @@ function register5(program, deps) {
|
|
|
1659
1750
|
throw new CliError("No git repository found. Run inside a repo set up with `nolto init`.", 2);
|
|
1660
1751
|
}
|
|
1661
1752
|
const http = deps.http;
|
|
1753
|
+
const warn = (line) => {
|
|
1754
|
+
process.stderr.write("Warning: " + line + "\n");
|
|
1755
|
+
};
|
|
1662
1756
|
const response = await syncRepo(
|
|
1663
|
-
{ root, defaultProjectId: deps.settings.defaultProjectId },
|
|
1757
|
+
{ root, defaultProjectId: deps.settings.defaultProjectId, migrateLegacy: true },
|
|
1664
1758
|
{
|
|
1665
1759
|
readFile: (p) => readFile5(p, "utf8"),
|
|
1666
1760
|
fileExists: (p) => existsSync3(p),
|
|
@@ -1673,16 +1767,28 @@ function register5(program, deps) {
|
|
|
1673
1767
|
repoIdentity: makeRepoIdentityResolver(deps),
|
|
1674
1768
|
http,
|
|
1675
1769
|
log: (line) => process.stdout.write(line + "\n"),
|
|
1676
|
-
warn
|
|
1770
|
+
warn
|
|
1677
1771
|
}
|
|
1678
1772
|
);
|
|
1679
|
-
let registryAdded = false;
|
|
1680
1773
|
try {
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
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}
|
|
1685
1790
|
`);
|
|
1791
|
+
}
|
|
1686
1792
|
}
|
|
1687
1793
|
if (deps.output.mode === "json") {
|
|
1688
1794
|
const { planAbsPaths: _planAbsPaths, ...publicResponse } = response;
|
|
@@ -1804,7 +1910,7 @@ var RepoWatch = class {
|
|
|
1804
1910
|
|
|
1805
1911
|
// src/service-install.ts
|
|
1806
1912
|
import path11 from "path";
|
|
1807
|
-
import
|
|
1913
|
+
import os4 from "os";
|
|
1808
1914
|
function buildUnitFile(args) {
|
|
1809
1915
|
return [
|
|
1810
1916
|
"[Unit]",
|
|
@@ -1823,7 +1929,7 @@ function buildUnitFile(args) {
|
|
|
1823
1929
|
}
|
|
1824
1930
|
function getUnitPath(env) {
|
|
1825
1931
|
const xdg = env["XDG_CONFIG_HOME"];
|
|
1826
|
-
const base = xdg != null && xdg.length > 0 ? xdg : path11.join(
|
|
1932
|
+
const base = xdg != null && xdg.length > 0 ? xdg : path11.join(os4.homedir(), ".config");
|
|
1827
1933
|
return path11.join(base, "systemd", "user", "nolto-watch.service");
|
|
1828
1934
|
}
|
|
1829
1935
|
async function installServiceWith(deps) {
|
|
@@ -1976,10 +2082,15 @@ function register6(program, deps) {
|
|
|
1976
2082
|
const startRepo = (root) => {
|
|
1977
2083
|
const roadmapsPath = path12.join(root, ".nolto", "roadmaps");
|
|
1978
2084
|
const legacyRoadmapPath = path12.join(root, ".roadmap", "roadmap.json");
|
|
2085
|
+
const warn = (line) => {
|
|
2086
|
+
process.stderr.write(`Warning: [${path12.basename(root)}] ${line}
|
|
2087
|
+
`);
|
|
2088
|
+
};
|
|
1979
2089
|
const watcher = chokidar.watch([roadmapsPath, legacyRoadmapPath], { ignoreInitial: true });
|
|
1980
2090
|
const repoWatch = new RepoWatch(root, {
|
|
1981
2091
|
sync: () => syncRepo(
|
|
1982
|
-
|
|
2092
|
+
// #316: watch must warn about legacy roadmaps without migrating them.
|
|
2093
|
+
{ root, defaultProjectId: deps.settings.defaultProjectId, migrateLegacy: false },
|
|
1983
2094
|
{
|
|
1984
2095
|
readFile: (p) => readFile6(p, "utf8"),
|
|
1985
2096
|
fileExists: (p) => existsSync4(p),
|
|
@@ -1993,8 +2104,7 @@ function register6(program, deps) {
|
|
|
1993
2104
|
http,
|
|
1994
2105
|
log: (line) => process.stdout.write(`[${path12.basename(root)}] ${line}
|
|
1995
2106
|
`),
|
|
1996
|
-
warn
|
|
1997
|
-
`)
|
|
2107
|
+
warn
|
|
1998
2108
|
}
|
|
1999
2109
|
),
|
|
2000
2110
|
watcher,
|
|
@@ -2005,6 +2115,12 @@ function register6(program, deps) {
|
|
|
2005
2115
|
warn: (line) => process.stderr.write(line + "\n")
|
|
2006
2116
|
});
|
|
2007
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
|
+
});
|
|
2008
2124
|
void repoWatch.flush();
|
|
2009
2125
|
return { stop: () => watcher.close() };
|
|
2010
2126
|
};
|