@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.
Files changed (3) hide show
  1. package/README.md +6 -1
  2. package/dist/index.js +263 -147
  3. 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 automatically when no canonical roadmap exists.
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
- try {
910
- registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
911
- } catch (err) {
912
- const message = err instanceof Error ? err.message : String(err);
913
- process.stderr.write(`Warning: could not update watch registry: ${message}
914
+ 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
- const rawUrl = await rl.question(`Base URL [${DEFAULT_BASE_URL}]: `);
1119
- const baseUrl = rawUrl.trim() || DEFAULT_BASE_URL;
1120
- const currentToken = deps.settings.token;
1121
- const tokenPrompt = currentToken != null ? "API token [press Enter to keep current token]: " : "API token: ";
1122
- const enteredToken = await promptHidden(rl, tokenPrompt);
1123
- token = enteredToken.length > 0 ? enteredToken : currentToken ?? "";
1124
- if (token.length === 0) {
1125
- throw new CliError("Token is required.", 2);
1126
- }
1127
- const http = createHttpClient({
1128
- baseUrl,
1129
- token,
1130
- version: getCliVersion()
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 defaultProjectId;
1186
+ let token = deps.settings.token ?? "";
1187
+ let baseUrl = deps.settings.baseUrl;
1188
+ let defaultProjectId = deps.settings.defaultProjectId;
1143
1189
  let defaultProjectName;
1144
- if (projects.length > 0) {
1145
- process.stdout.write("\nProjects:\n");
1146
- projects.forEach((p, i) => {
1147
- process.stdout.write(` (${i + 1}) ${p.name} \u2014 ${p.id}
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
- } else {
1151
- process.stdout.write("\nNo projects yet.\n");
1152
- }
1153
- const pick = await rl.question("Default project number, 'c' to create new (or Enter to skip): ");
1154
- const trimmedPick = pick.trim().toLowerCase();
1155
- if (trimmedPick === "c") {
1156
- const name = await rl.question("New project name: ");
1157
- if (name.trim().length === 0) {
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 created = await http.post(
1161
- "/api/projects",
1162
- { name: name.trim() }
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
- if (created.project?.id == null) {
1165
- throw new CliError("Project API did not return a project id.", 2);
1166
- }
1167
- defaultProjectId = created.project.id;
1168
- defaultProjectName = created.project.name ?? name.trim();
1169
- process.stdout.write(`Created project ${defaultProjectName} (${defaultProjectId})
1170
- `);
1171
- } else {
1172
- const num = parseInt(trimmedPick, 10);
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
- process.stdout.write(`baseUrl: ${baseUrl}
1233
+ process.stdout.write(`baseUrl: ${baseUrl}
1188
1234
  `);
1189
- process.stdout.write(`token: ${maskToken(token)} (verified)
1235
+ process.stdout.write(`token: ${maskToken(token)} (verified)
1190
1236
  `);
1191
- process.stdout.write(`defaultProject: ${projectDisplay}
1237
+ process.stdout.write(`defaultProject: ${projectDisplay}
1192
1238
  `);
1193
- if (defaultProjectId != null) {
1194
- const startDir = resolveStartDir(process.env, process.cwd());
1195
- const { root, foundGit } = findRepoRoot(startDir);
1196
- if (foundGit) {
1197
- const setup = await rl.question(`
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
- if (setup.trim().toLowerCase() !== "n") {
1200
- if (deps.repoBinding?.error != null) {
1201
- process.stderr.write(
1202
- `Warning: existing nolto.json is invalid (${deps.repoBinding.error.message}). Overwriting it to repair.
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
- await writeRepoBinding(root, defaultProjectId);
1207
- process.stdout.write(`binding: wrote ${path8.join(root, "nolto.json")}
1293
+ );
1294
+ }
1295
+ await writeRepoBinding(root, repoProject.id);
1296
+ process.stdout.write(`binding: wrote ${path8.join(root, "nolto.json")}
1208
1297
  `);
1209
- const sourceDir = resolveSkillSourceDir();
1210
- const version = getCliVersion();
1211
- const claudeInstall = await installSkill({
1212
- skillsParentDir: path8.join(root, ".claude", "skills"),
1213
- sourceDir,
1214
- version
1215
- });
1216
- process.stdout.write(`skill (claude): ${claudeInstall.action} ${claudeInstall.targetDir}
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
- const usesAgentsTooling = fs.existsSync(path8.join(root, ".agents")) || fs.existsSync(path8.join(root, ".codex")) || fs.existsSync(path8.join(root, "AGENTS.md"));
1219
- if (usesAgentsTooling) {
1220
- const agentsInstall = await installSkill({
1221
- skillsParentDir: path8.join(root, ".agents", "skills"),
1222
- sourceDir,
1223
- version
1224
- });
1225
- process.stdout.write(`skill (agents): ${agentsInstall.action} ${agentsInstall.targetDir}
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
- const scaffold = await scaffoldRoadmap({
1229
- repoRoot: root,
1230
- projectName: defaultProjectName ?? path8.basename(root)
1231
- });
1232
- process.stdout.write(
1233
- scaffold.created ? `roadmap: created ${scaffold.path}
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
- const registryResult = await addRepoToRegistry(getRegistryPath(process.env), root);
1238
- process.stdout.write(
1239
- registryResult.added ? `watch registry: added ${root}
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: (line) => process.stderr.write("Warning: " + line + "\n")
1770
+ warn
1677
1771
  }
1678
1772
  );
1679
- let registryAdded = false;
1680
1773
  try {
1681
- registryAdded = (await addRepoToRegistry(getRegistryPath(process.env), root)).added;
1682
- } catch (err) {
1683
- const message = err instanceof Error ? err.message : String(err);
1684
- process.stderr.write(`Warning: could not update watch registry: ${message}
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 os2 from "os";
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(os2.homedir(), ".config");
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
- { root, defaultProjectId: deps.settings.defaultProjectId },
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: (line) => process.stderr.write(`Warning: [${path12.basename(root)}] ${line}
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
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nolto/cli",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "CLI for syncing repository roadmaps with Nolto.",
5
5
  "license": "MIT",
6
6
  "type": "module",