@rafiqui987/markode 0.4.0 → 0.4.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 -0
  2. package/dist/cli.js +115 -66
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -382,12 +382,18 @@ markode features # which modules are on, with what each one does
382
382
  markode features live off # turn a module on or off (same as Settings → Features)
383
383
  markode translate # choose languages to review the docs in (also: markode --translate)
384
384
  markode link # only CLAUDE.md or only AGENTS.md? add the other one, pointing to it
385
+ markode update # install the latest version now (see below)
385
386
  markode -v # version
386
387
  ```
387
388
 
388
389
  To keep it installed: `npm install -g @rafiqui987/markode` (or `pnpm add -g @rafiqui987/markode`).
389
390
  Installing it also makes the Claude Code hooks faster than running through `npx`.
390
391
 
392
+ **A new version right away.** pnpm (and npm with `min-release-age`) wait a day before installing a
393
+ version just published. `markode update` asks the npm registry for the latest version and installs it
394
+ now, with the same package manager that installed markode (`--pnpm` or `--npm` to choose);
395
+ `markode update --check` only says whether there is one.
396
+
391
397
  ## For AI agents
392
398
 
393
399
  The AI agent module installs the `markode` skill where the project's agents read skills
package/dist/cli.js CHANGED
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/cli/index.ts
4
- import fs21 from "fs";
5
- import path21 from "path";
4
+ import fs22 from "fs";
5
+ import path22 from "path";
6
6
 
7
7
  // src/core/changes.ts
8
8
  import crypto from "crypto";
@@ -1889,12 +1889,43 @@ async function ensureServer(root2, cli2, port) {
1889
1889
  }
1890
1890
  var editorUrl = (port, doc) => `http://localhost:${port}/${doc ? `open?path=${encodeURIComponent(doc).replace(/%2F/g, "/")}` : ""}`;
1891
1891
 
1892
- // src/core/wait.ts
1892
+ // src/core/update.ts
1893
+ import { spawnSync as spawnSync2 } from "child_process";
1893
1894
  import fs18 from "fs";
1894
1895
  import path18 from "path";
1896
+ var PKG = JSON.parse(fs18.readFileSync(path18.join(PKG_ROOT, "package.json"), "utf8"));
1897
+ var CURRENT_VERSION = PKG.version;
1898
+ function installer() {
1899
+ const dir = PKG_ROOT.split(path18.sep).join("/");
1900
+ if (dir.includes("/_npx/")) return "npx";
1901
+ if (!dir.includes("/node_modules/")) return null;
1902
+ return dir.includes("/pnpm/") || dir.includes("/.pnpm/") ? "pnpm" : "npm";
1903
+ }
1904
+ async function latestVersion() {
1905
+ const res = await fetch(`https://registry.npmjs.org/${PKG.name}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
1906
+ if (!res.ok) throw new Error(`The npm registry answered ${res.status}`);
1907
+ return (await res.json()).version;
1908
+ }
1909
+ function isNewer(a, b) {
1910
+ const pa = a.split(".").map((n) => parseInt(n, 10));
1911
+ const pb = b.split(".").map((n) => parseInt(n, 10));
1912
+ for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
1913
+ return false;
1914
+ }
1915
+ function installCommand2(via, version) {
1916
+ const spec = `${PKG.name}@${version}`;
1917
+ return via === "pnpm" ? ["pnpm", "add", "-g", spec, "--config.minimum-release-age=0"] : ["npm", "install", "-g", spec, "--min-release-age=0", "--prefer-online"];
1918
+ }
1919
+ function runInstall(cmd) {
1920
+ return spawnSync2(cmd[0], cmd.slice(1), { stdio: "inherit", shell: process.platform === "win32" }).status === 0;
1921
+ }
1922
+
1923
+ // src/core/wait.ts
1924
+ import fs19 from "fs";
1925
+ import path19 from "path";
1895
1926
  import YAML5 from "yaml";
1896
- var waitsDir = (root2) => path18.join(stateDir(root2), "waits");
1897
- var pidFile = (root2, rel) => path18.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
1927
+ var waitsDir = (root2) => path19.join(stateDir(root2), "waits");
1928
+ var pidFile = (root2, rel) => path19.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
1898
1929
  var frontmatter2 = (text3) => {
1899
1930
  try {
1900
1931
  return YAML5.parse(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "", { logLevel: "error" }) ?? {};
@@ -1913,27 +1944,27 @@ var isAlive2 = (pid) => {
1913
1944
  function waitingOn(root2) {
1914
1945
  let names2 = [];
1915
1946
  try {
1916
- names2 = fs18.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1947
+ names2 = fs19.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1917
1948
  } catch {
1918
1949
  }
1919
1950
  return names2.flatMap((f) => {
1920
- const pid = Number(fs18.readFileSync(path18.join(waitsDir(root2), f), "utf8"));
1951
+ const pid = Number(fs19.readFileSync(path19.join(waitsDir(root2), f), "utf8"));
1921
1952
  return isAlive2(pid) ? [decodeURIComponent(f.slice(0, -4))] : [];
1922
1953
  });
1923
1954
  }
1924
1955
  function waitForAnswer(root2, rel) {
1925
- const abs = path18.join(root2, rel);
1926
- fs18.mkdirSync(waitsDir(root2), { recursive: true });
1956
+ const abs = path19.join(root2, rel);
1957
+ fs19.mkdirSync(waitsDir(root2), { recursive: true });
1927
1958
  assertInside(pidFile(root2, rel));
1928
- fs18.writeFileSync(pidFile(root2, rel), String(process.pid));
1929
- const cleanup = () => fs18.rmSync(pidFile(root2, rel), { force: true });
1959
+ fs19.writeFileSync(pidFile(root2, rel), String(process.pid));
1960
+ const cleanup = () => fs19.rmSync(pidFile(root2, rel), { force: true });
1930
1961
  process.once("exit", cleanup);
1931
1962
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => process.exit(130));
1932
1963
  return new Promise((resolve) => {
1933
1964
  const poll = () => {
1934
1965
  let text3;
1935
1966
  try {
1936
- text3 = fs18.readFileSync(abs, "utf8");
1967
+ text3 = fs19.readFileSync(abs, "utf8");
1937
1968
  } catch {
1938
1969
  return resolve("gone");
1939
1970
  }
@@ -1947,7 +1978,7 @@ function waitForAnswer(root2, rel) {
1947
1978
  }
1948
1979
  var COMMENT = /^\s*<!-- (comment|reply) by (.+?), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\]|\\.)*)")?:\s?([\s\S]*?)\s*-->\s*$/;
1949
1980
  function answerSummary(root2, rel) {
1950
- const text3 = fs18.readFileSync(path18.join(root2, rel), "utf8");
1981
+ const text3 = fs19.readFileSync(path19.join(root2, rel), "utf8");
1951
1982
  const fm = frontmatter2(text3);
1952
1983
  const body = text3.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
1953
1984
  const lines = body.split("\n");
@@ -2022,9 +2053,9 @@ function answerSummary(root2, rel) {
2022
2053
 
2023
2054
  // src/server/index.ts
2024
2055
  import crypto3 from "crypto";
2025
- import fs19 from "fs";
2056
+ import fs20 from "fs";
2026
2057
  import http from "http";
2027
- import path19 from "path";
2058
+ import path20 from "path";
2028
2059
  import { watch } from "chokidar";
2029
2060
  import open from "open";
2030
2061
  import pc from "picocolors";
@@ -2092,7 +2123,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2092
2123
  if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
2093
2124
  const abs = safeResolve(root2, rel);
2094
2125
  if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
2095
- if (!fs19.existsSync(abs)) throw new HttpError(404, "Not found");
2126
+ if (!fs20.existsSync(abs)) throw new HttpError(404, "Not found");
2096
2127
  return abs;
2097
2128
  };
2098
2129
  const translationFile = (lang, rel) => {
@@ -2104,7 +2135,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2104
2135
  "GET /api/info": () => {
2105
2136
  const c = config();
2106
2137
  return {
2107
- name: path19.basename(root2),
2138
+ name: path20.basename(root2),
2108
2139
  mainDoc: c.mainDoc ?? null,
2109
2140
  sourceLang: sourceLang(),
2110
2141
  langs: langs().filter((l) => l !== sourceLang()),
@@ -2131,7 +2162,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2131
2162
  }
2132
2163
  if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
2133
2164
  if (!c.mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
2134
- applyRulesToFile(path19.join(root2, c.mainDoc), void 0, body.sourceLang);
2165
+ applyRulesToFile(path20.join(root2, c.mainDoc), void 0, body.sourceLang);
2135
2166
  }
2136
2167
  writeConfig(root2, c);
2137
2168
  return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
@@ -2162,10 +2193,10 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2162
2193
  "GET /api/file": (url3) => {
2163
2194
  const rel = mdPath(url3.searchParams.get("path"));
2164
2195
  const l = viewLang(url3.searchParams.get("lang"));
2165
- const source = fs19.readFileSync(safeResolve(root2, rel), "utf8");
2196
+ const source = fs20.readFileSync(safeResolve(root2, rel), "utf8");
2166
2197
  if (l) {
2167
2198
  const tPath = translationFile(l, rel);
2168
- if (fs19.existsSync(tPath)) return { path: rel, content: fs19.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
2199
+ if (fs20.existsSync(tPath)) return { path: rel, content: fs20.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
2169
2200
  return { path: rel, content: source, target: "source", status: "missing" };
2170
2201
  }
2171
2202
  return { path: rel, content: source, target: "source", status: null };
@@ -2174,19 +2205,19 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2174
2205
  const rel = mdPath(body.path);
2175
2206
  const l = viewLang(body.lang);
2176
2207
  const file = body.target === "translation" && l ? translationFile(l, rel) : safeResolve(root2, rel);
2177
- fs19.mkdirSync(path19.dirname(file), { recursive: true });
2208
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
2178
2209
  const content = String(body.content ?? "");
2179
- if (!l && featureState(root2).agent) logSave(root2, rel, fs19.existsSync(file) ? fs19.readFileSync(file, "utf8") : "", content);
2180
- fs19.writeFileSync(file, content);
2210
+ if (!l && featureState(root2).agent) logSave(root2, rel, fs20.existsSync(file) ? fs20.readFileSync(file, "utf8") : "", content);
2211
+ fs20.writeFileSync(file, content);
2181
2212
  return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
2182
2213
  },
2183
2214
  "POST /api/file": (_, body) => {
2184
2215
  const rel = mdPath(body.path);
2185
2216
  const file = safeResolve(root2, rel);
2186
- if (fs19.existsSync(file)) throw new HttpError(409, "File already exists");
2187
- fs19.mkdirSync(path19.dirname(file), { recursive: true });
2188
- const title = path19.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
2189
- fs19.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
2217
+ if (fs20.existsSync(file)) throw new HttpError(409, "File already exists");
2218
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
2219
+ const title = path20.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
2220
+ fs20.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
2190
2221
 
2191
2222
  `);
2192
2223
  return { ok: true, path: rel };
@@ -2196,20 +2227,20 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2196
2227
  if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
2197
2228
  const dir = safeResolve(root2, body.path);
2198
2229
  if (dir === root2) throw new HttpError(400, "Expected a path");
2199
- if (fs19.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
2200
- fs19.mkdirSync(dir, { recursive: true });
2201
- return { ok: true, path: toPosix(path19.relative(root2, dir)) };
2230
+ if (fs20.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
2231
+ fs20.mkdirSync(dir, { recursive: true });
2232
+ return { ok: true, path: toPosix(path20.relative(root2, dir)) };
2202
2233
  },
2203
2234
  "GET /api/delete-preview": (url3) => {
2204
2235
  const abs = deletable(url3.searchParams.get("path"));
2205
- if (!fs19.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
2206
- const files = fs19.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
2236
+ if (!fs20.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
2237
+ const files = fs20.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
2207
2238
  return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
2208
2239
  },
2209
2240
  "DELETE /api/path": (url3) => {
2210
- const rel = toPosix(path19.relative(root2, deletable(url3.searchParams.get("path"))));
2211
- fs19.rmSync(safeResolve(root2, rel), { recursive: true });
2212
- for (const l of config().langs) fs19.rmSync(translationFile(l, rel), { recursive: true, force: true });
2241
+ const rel = toPosix(path20.relative(root2, deletable(url3.searchParams.get("path"))));
2242
+ fs20.rmSync(safeResolve(root2, rel), { recursive: true });
2243
+ for (const l of config().langs) fs20.rmSync(translationFile(l, rel), { recursive: true, force: true });
2213
2244
  return { ok: true };
2214
2245
  },
2215
2246
  "POST /api/ignore": (_, body) => {
@@ -2226,7 +2257,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2226
2257
  },
2227
2258
  "GET /api/rules": () => {
2228
2259
  const mainDoc = readConfig(root2)?.mainDoc;
2229
- const text3 = mainDoc && fs19.existsSync(path19.join(root2, mainDoc)) ? fs19.readFileSync(path19.join(root2, mainDoc), "utf8") : "";
2260
+ const text3 = mainDoc && fs20.existsSync(path20.join(root2, mainDoc)) ? fs20.readFileSync(path20.join(root2, mainDoc), "utf8") : "";
2230
2261
  return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
2231
2262
  },
2232
2263
  "PUT /api/rules": (_, body) => saveRule(body),
@@ -2242,12 +2273,12 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2242
2273
  },
2243
2274
  "GET /api/skills/file": (url3) => {
2244
2275
  const skill = getSkill(root2, url3.searchParams.get("key"));
2245
- return { path: skill.key, content: fs19.readFileSync(path19.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
2276
+ return { path: skill.key, content: fs20.readFileSync(path20.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
2246
2277
  },
2247
2278
  "PUT /api/skills/file": (_, body) => {
2248
2279
  const skill = getSkill(root2, body.key);
2249
2280
  if (skill.managed) throw new HttpError(403, "This skill is managed by the app that installed it");
2250
- fs19.writeFileSync(path19.join(skill.key, "SKILL.md"), String(body.content ?? ""));
2281
+ fs20.writeFileSync(path20.join(skill.key, "SKILL.md"), String(body.content ?? ""));
2251
2282
  return { ok: true, status: null };
2252
2283
  },
2253
2284
  "POST /api/skills/install": (_, body) => {
@@ -2260,7 +2291,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2260
2291
  "POST /api/rules/apply": (_, body) => {
2261
2292
  const mainDoc = readConfig(root2)?.mainDoc;
2262
2293
  if (!mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
2263
- applyRulesToFile(path19.join(root2, mainDoc), body.ids ?? []);
2294
+ applyRulesToFile(path20.join(root2, mainDoc), body.ids ?? []);
2264
2295
  return { ok: true };
2265
2296
  }
2266
2297
  };
@@ -2295,13 +2326,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2295
2326
  res.writeHead(404).end();
2296
2327
  return;
2297
2328
  }
2298
- const type = IMAGE_MIME[path19.extname(file).toLowerCase()];
2299
- if (!type || !fs19.existsSync(file) || !fs19.statSync(file).isFile()) {
2329
+ const type = IMAGE_MIME[path20.extname(file).toLowerCase()];
2330
+ if (!type || !fs20.existsSync(file) || !fs20.statSync(file).isFile()) {
2300
2331
  res.writeHead(404).end();
2301
2332
  return;
2302
2333
  }
2303
2334
  res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox; frame-ancestors 'none'" });
2304
- fs19.createReadStream(file).pipe(res);
2335
+ fs20.createReadStream(file).pipe(res);
2305
2336
  }
2306
2337
  function openDoc(rel, res) {
2307
2338
  let doc;
@@ -2335,16 +2366,16 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2335
2366
  // Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
2336
2367
  ignorePermissionErrors: true,
2337
2368
  ignored: (p3, stats) => {
2338
- const rel = toPosix(path19.relative(root2, p3));
2369
+ const rel = toPosix(path20.relative(root2, p3));
2339
2370
  if (rel === ".markode") return false;
2340
2371
  if (rel.startsWith(".markode/")) return !rel.startsWith(".markode/i18n") && rel !== ".markode/state.json" && rel !== ".markode/config.json";
2341
2372
  return skips(rel, stats ? stats.isDirectory() : void 0);
2342
2373
  }
2343
2374
  }).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
2344
- const abs = path19.resolve(file);
2375
+ const abs = path20.resolve(file);
2345
2376
  if (!abs.endsWith(".md") && !abs.endsWith(".json") && !abs.endsWith(".markodeignore") && event !== "addDir" && event !== "unlinkDir") return;
2346
- let rel = toPosix(path19.relative(root2, abs));
2347
- const i18n = toPosix(path19.relative(root2, path19.join(stateDir(root2), "i18n")));
2377
+ let rel = toPosix(path20.relative(root2, abs));
2378
+ const i18n = toPosix(path20.relative(root2, path20.join(stateDir(root2), "i18n")));
2348
2379
  if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
2349
2380
  broadcast({ type: "fs", event, path: rel });
2350
2381
  });
@@ -2353,7 +2384,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2353
2384
  console.log(`
2354
2385
  ${pc.bold("markode")} ${pc.dim("\u2192")} ${pc.cyan(url2)} ${pc.dim(root2)}
2355
2386
  `);
2356
- if (!fs19.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
2387
+ if (!fs20.existsSync(WEB_DIR)) console.log(pc.yellow(" Web UI not built: run `pnpm build`, or `pnpm dev` and open http://localhost:4320\n"));
2357
2388
  else if (openBrowser) open(url2).catch(() => {
2358
2389
  });
2359
2390
  }
@@ -2373,19 +2404,19 @@ function listen(server, port) {
2373
2404
  });
2374
2405
  }
2375
2406
  function serveStatic(pathname, res) {
2376
- let file = path19.join(WEB_DIR, path19.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
2377
- if (!file.startsWith(WEB_DIR) || !fs19.existsSync(file) || fs19.statSync(file).isDirectory()) file = path19.join(WEB_DIR, "index.html");
2378
- if (!fs19.existsSync(file)) {
2407
+ let file = path20.join(WEB_DIR, path20.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
2408
+ if (!file.startsWith(WEB_DIR) || !fs20.existsSync(file) || fs20.statSync(file).isDirectory()) file = path20.join(WEB_DIR, "index.html");
2409
+ if (!fs20.existsSync(file)) {
2379
2410
  res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
2380
2411
  return;
2381
2412
  }
2382
- res.writeHead(200, { "content-type": MIME[path19.extname(file)] ?? "application/octet-stream", "content-security-policy": "frame-ancestors 'none'" });
2383
- fs19.createReadStream(file).pipe(res);
2413
+ res.writeHead(200, { "content-type": MIME[path20.extname(file)] ?? "application/octet-stream", "content-security-policy": "frame-ancestors 'none'" });
2414
+ fs20.createReadStream(file).pipe(res);
2384
2415
  }
2385
2416
 
2386
2417
  // src/cli/init.ts
2387
- import fs20 from "fs";
2388
- import path20 from "path";
2418
+ import fs21 from "fs";
2419
+ import path21 from "path";
2389
2420
  import * as p from "@clack/prompts";
2390
2421
  import pc2 from "picocolors";
2391
2422
  function check(value) {
@@ -2396,12 +2427,12 @@ function check(value) {
2396
2427
  return value;
2397
2428
  }
2398
2429
  async function offerClaudeLink(root2, d) {
2399
- if (fs20.existsSync(path20.join(root2, "CLAUDE.md"))) return;
2430
+ if (fs21.existsSync(path21.join(root2, "CLAUDE.md"))) return;
2400
2431
  const link2 = d ? d.mainDoc === "CLAUDE.md" : check(await p.confirm({ message: "Claude Code reads CLAUDE.md, not AGENTS.md. Add a CLAUDE.md that imports AGENTS.md?", initialValue: true }));
2401
2432
  if (link2 && linkAgentsDoc(root2)) p.log.success("Created CLAUDE.md (imports AGENTS.md)");
2402
2433
  }
2403
2434
  async function chooseMainDoc(root2, d) {
2404
- const existing = MAIN_DOCS.filter((d2) => fs20.existsSync(path20.join(root2, d2)) && !isPointer(fs20.readFileSync(path20.join(root2, d2), "utf8"), d2));
2435
+ const existing = MAIN_DOCS.filter((d2) => fs21.existsSync(path21.join(root2, d2)) && !isPointer(fs21.readFileSync(path21.join(root2, d2), "utf8"), d2));
2405
2436
  if (d) {
2406
2437
  if (existing.length === 1 && existing[0] === "AGENTS.md") await offerClaudeLink(root2, d);
2407
2438
  if (existing.length) return existing.includes(d.mainDoc) ? d.mainDoc : existing[0];
@@ -2518,7 +2549,7 @@ async function chooseAreas(root2) {
2518
2549
  return areas;
2519
2550
  }
2520
2551
  async function chooseKind(root2, dir) {
2521
- const guess = guessKind(path20.join(root2, dir));
2552
+ const guess = guessKind(path21.join(root2, dir));
2522
2553
  if (!guess.unclear) return guess.kind;
2523
2554
  const where = dir ? `${dir}/` : "this project";
2524
2555
  const seen = guess.signals.length ? `found: ${guess.signals.join(", ")}` : "no code yet";
@@ -2531,7 +2562,7 @@ async function chooseKind(root2, dir) {
2531
2562
  );
2532
2563
  }
2533
2564
  async function askProfile(root2, dir, kind) {
2534
- const base = detectProfile(path20.join(root2, dir), kind);
2565
+ const base = detectProfile(path21.join(root2, dir), kind);
2535
2566
  if (kind === "app") return base;
2536
2567
  const where = dir ? ` (${dir}/)` : "";
2537
2568
  if (kind === "library") return { kind, published: check(await p.confirm({ message: `Is the library${where} published (npm, PyPI\u2026)?`, initialValue: base.published })) };
@@ -2608,7 +2639,7 @@ async function chooseWorkflow(root2, mainDoc, previous, quiet) {
2608
2639
  const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, ""));
2609
2640
  const created = scaffoldMethod(root2, mainDoc, areas);
2610
2641
  installMethodSkills(root2);
2611
- applyWorkflowToFile(path20.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
2642
+ applyWorkflowToFile(path21.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
2612
2643
  const big = !areas.length && flattenFiles(scanTree(root2)).length > 40;
2613
2644
  const layout = areas.length ? "structured" : !big ? "general" : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
2614
2645
  activateProposals(root2, layout, mainDoc);
@@ -2673,10 +2704,10 @@ async function runInit(root2, plan2) {
2673
2704
  let workflow = previous?.workflow ?? "none";
2674
2705
  if (has2("agent")) {
2675
2706
  mainDoc = await chooseMainDoc(root2, d);
2676
- const mainFile = path20.join(root2, mainDoc);
2707
+ const mainFile = path21.join(root2, mainDoc);
2677
2708
  if (has2("dashboard")) workflow = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
2678
2709
  if (workflow === "openspec" || workflow === "speckit") applyWorkflowToFile(mainFile, workflow);
2679
- const mainText = fs20.readFileSync(mainFile, "utf8");
2710
+ const mainText = fs21.readFileSync(mainFile, "utf8");
2680
2711
  const sourceLang = d ? SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang : check(await p.select({ message: SOURCE_LANG_QUESTION, options: LANG_OPTIONS, initialValue: SOURCE_LANG_MARK.test(mainText) ? sourceLangOf(mainText) : detectedLang }));
2681
2712
  const rules = listRules();
2682
2713
  const applied = appliedRuleIds(mainText);
@@ -2691,7 +2722,7 @@ async function runInit(root2, plan2) {
2691
2722
  required: false
2692
2723
  })
2693
2724
  );
2694
- for (const dir of targets) applyRulesToFile(contentDoc(path20.join(root2, dir)), selected, sourceLang);
2725
+ for (const dir of targets) applyRulesToFile(contentDoc(path21.join(root2, dir)), selected, sourceLang);
2695
2726
  }
2696
2727
  installAgentSkill(root2);
2697
2728
  const proposals = proposalsState(root2);
@@ -2848,6 +2879,8 @@ Usage
2848
2879
  markode changes [--json] what the user edited or commented in markode since the agent was last told
2849
2880
  markode changes --start a new agent session: what happened before is old news
2850
2881
  markode changes --hook <event> the same, as Claude Code hook output (silent when nothing changed)
2882
+ markode update [--check] install the latest version now, without the day npm/pnpm wait for new releases
2883
+ markode update --pnpm|--npm the same, choosing the package manager to install it with
2851
2884
 
2852
2885
  For the agent's /markode translate:
2853
2886
  markode translate --status [--json] translations + files not in the source language
@@ -2873,18 +2906,20 @@ var option = (name) => {
2873
2906
  var command = args[0] && !args[0].startsWith("-") ? args[0] : "start";
2874
2907
  var root = process.env.MARKODE_ROOT ?? process.cwd();
2875
2908
  async function main() {
2876
- if (fs21.existsSync(root)) setProjectRoot(root);
2909
+ if (fs22.existsSync(root)) setProjectRoot(root);
2877
2910
  if (flag("--help") || flag("-h")) return console.log(HELP);
2878
- if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs21.readFileSync(path21.join(PKG_ROOT, "package.json"), "utf8")).version);
2911
+ if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs22.readFileSync(path22.join(PKG_ROOT, "package.json"), "utf8")).version);
2879
2912
  const translation = flag("--status") || flag("--mark") || flag("--keep");
2880
2913
  if ((command === "sync" || command === "translate") && translation) return sync();
2881
2914
  if (command === "translate" || flag("--translate")) return runTranslate(root);
2915
+ if (["check", "url", "link"].includes(command) || command === "changes" && flag("--start")) refreshInstalled(root);
2882
2916
  if (command === "check") return check3();
2883
2917
  if (command === "wait") return wait();
2884
2918
  if (command === "link") return link();
2885
2919
  if (command === "changes") return changes();
2886
2920
  if (command === "url") return url();
2887
2921
  if (command === "stop") return stop();
2922
+ if (command === "update") return update();
2888
2923
  if (command === "features") return features();
2889
2924
  if (command === "setup") {
2890
2925
  await runSetup(false, flag("--custom"));
@@ -2972,7 +3007,7 @@ function link() {
2972
3007
  }
2973
3008
  async function url() {
2974
3009
  const doc = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
2975
- if (doc && (!doc.toLowerCase().endsWith(".md") || !fs21.existsSync(path21.join(root, doc)))) {
3010
+ if (doc && (!doc.toLowerCase().endsWith(".md") || !fs22.existsSync(path22.join(root, doc)))) {
2976
3011
  console.error(`Usage: markode url [<doc.md>] (relative to ${root})`);
2977
3012
  process.exitCode = 1;
2978
3013
  return;
@@ -2986,6 +3021,20 @@ function stop() {
2986
3021
  process.kill(running.pid);
2987
3022
  console.log(`Stopped the editor on port ${running.port}.`);
2988
3023
  }
3024
+ async function update() {
3025
+ const latest = await latestVersion();
3026
+ if (!isNewer(latest, CURRENT_VERSION)) return console.log(`markode ${CURRENT_VERSION} is the latest version.`);
3027
+ console.log(`markode ${latest} is out (you have ${CURRENT_VERSION}).`);
3028
+ if (flag("--check")) return;
3029
+ const via = flag("--pnpm") ? "pnpm" : flag("--npm") ? "npm" : installer();
3030
+ if (via === "npx") return console.log(`Running through npx: use \`npx -y @rafiqui987/markode@${latest}\` once to fetch it, or install it with \`markode update --pnpm\` (or --npm).`);
3031
+ if (!via) return console.log("This copy runs from a clone of the repository: pull and build it, or install it with `markode update --pnpm` (or --npm).");
3032
+ const cmd = installCommand2(via, latest);
3033
+ console.log(`$ ${cmd.join(" ")}`);
3034
+ if (runInstall(cmd)) return console.log(`Updated to ${latest}. A running editor keeps the old version until \`markode stop\`.`);
3035
+ console.error("The install failed; the output above says why.");
3036
+ process.exitCode = 1;
3037
+ }
2989
3038
  function changes() {
2990
3039
  if (flag("--start")) return startSession(root);
2991
3040
  const list = takeChanges(root);
@@ -3000,7 +3049,7 @@ function changes() {
3000
3049
  }
3001
3050
  async function wait() {
3002
3051
  const rel = args[1];
3003
- if (!rel || !fs21.existsSync(path21.join(root, rel))) {
3052
+ if (!rel || !fs22.existsSync(path22.join(root, rel))) {
3004
3053
  console.error(`Usage: markode wait <proposal.md> (relative to ${root})`);
3005
3054
  process.exitCode = 1;
3006
3055
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rafiqui987/markode",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Visual editor for CLAUDE.md, AGENTS.md and /docs — humans edit comfortably, the AI keeps reading plain Markdown.",
5
5
  "keywords": [
6
6
  "markdown",