@rafiqui987/markode 0.4.0 → 0.4.2

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 +129 -74
  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";
@@ -1834,9 +1834,42 @@ function keepLanguage(root2, rel) {
1834
1834
 
1835
1835
  // src/core/server.ts
1836
1836
  import { spawn } from "child_process";
1837
+ import fs18 from "fs";
1838
+ import path18 from "path";
1839
+
1840
+ // src/core/update.ts
1841
+ import { spawnSync as spawnSync2 } from "child_process";
1837
1842
  import fs17 from "fs";
1838
1843
  import path17 from "path";
1839
- var serverFile = (root2) => path17.join(stateDir(root2), "server.json");
1844
+ var PKG = JSON.parse(fs17.readFileSync(path17.join(PKG_ROOT, "package.json"), "utf8"));
1845
+ var CURRENT_VERSION = PKG.version;
1846
+ function installer() {
1847
+ const dir = PKG_ROOT.split(path17.sep).join("/");
1848
+ if (dir.includes("/_npx/")) return "npx";
1849
+ if (!dir.includes("/node_modules/")) return null;
1850
+ return dir.includes("/pnpm/") || dir.includes("/.pnpm/") ? "pnpm" : "npm";
1851
+ }
1852
+ async function latestVersion() {
1853
+ const res = await fetch(`https://registry.npmjs.org/${PKG.name}/latest`, { headers: { accept: "application/json" }, signal: AbortSignal.timeout(15e3) });
1854
+ if (!res.ok) throw new Error(`The npm registry answered ${res.status}`);
1855
+ return (await res.json()).version;
1856
+ }
1857
+ function isNewer(a, b) {
1858
+ const pa = a.split(".").map((n) => parseInt(n, 10));
1859
+ const pb = b.split(".").map((n) => parseInt(n, 10));
1860
+ for (let i = 0; i < 3; i++) if ((pa[i] ?? 0) !== (pb[i] ?? 0)) return (pa[i] ?? 0) > (pb[i] ?? 0);
1861
+ return false;
1862
+ }
1863
+ function installCommand2(via, version) {
1864
+ const spec = `${PKG.name}@${version}`;
1865
+ return via === "pnpm" ? ["pnpm", "add", "-g", spec, "--config.minimum-release-age=0"] : ["npm", "install", "-g", spec, "--min-release-age=0", "--prefer-online"];
1866
+ }
1867
+ function runInstall(cmd) {
1868
+ return spawnSync2(cmd[0], cmd.slice(1), { stdio: "inherit", shell: process.platform === "win32" }).status === 0;
1869
+ }
1870
+
1871
+ // src/core/server.ts
1872
+ var serverFile = (root2) => path18.join(stateDir(root2), "server.json");
1840
1873
  var isAlive = (pid) => {
1841
1874
  try {
1842
1875
  process.kill(pid, 0);
@@ -1847,18 +1880,18 @@ var isAlive = (pid) => {
1847
1880
  };
1848
1881
  function recordServer(root2, port) {
1849
1882
  const file = serverFile(root2);
1850
- fs17.mkdirSync(path17.dirname(file), { recursive: true });
1883
+ fs18.mkdirSync(path18.dirname(file), { recursive: true });
1851
1884
  assertInside(file);
1852
- fs17.writeFileSync(file, JSON.stringify({ port, pid: process.pid }));
1885
+ fs18.writeFileSync(file, JSON.stringify({ port, pid: process.pid, version: CURRENT_VERSION }));
1853
1886
  process.once("exit", () => {
1854
- if (runningServer(root2)?.pid === process.pid) fs17.rmSync(file, { force: true });
1887
+ if (runningServer(root2)?.pid === process.pid) fs18.rmSync(file, { force: true });
1855
1888
  });
1856
1889
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => process.exit(130));
1857
1890
  }
1858
1891
  function runningServer(root2) {
1859
1892
  try {
1860
- const s = JSON.parse(fs17.readFileSync(serverFile(root2), "utf8"));
1861
- return Number.isInteger(s.port) && Number.isInteger(s.pid) && isAlive(s.pid) ? { port: s.port, pid: s.pid } : null;
1893
+ const s = JSON.parse(fs18.readFileSync(serverFile(root2), "utf8"));
1894
+ return Number.isInteger(s.port) && Number.isInteger(s.pid) && isAlive(s.pid) ? { port: s.port, pid: s.pid, version: s.version } : null;
1862
1895
  } catch {
1863
1896
  return null;
1864
1897
  }
@@ -1872,7 +1905,11 @@ async function responds(port) {
1872
1905
  }
1873
1906
  async function ensureServer(root2, cli2, port) {
1874
1907
  const running = runningServer(root2);
1875
- if (running && await responds(running.port)) return running;
1908
+ if (running && running.version !== CURRENT_VERSION) {
1909
+ process.kill(running.pid);
1910
+ for (let i = 0; i < 50 && isAlive(running.pid); i++) await new Promise((r) => setTimeout(r, 100));
1911
+ port = running.port;
1912
+ } else if (running && await responds(running.port)) return running;
1876
1913
  const child = spawn(process.execPath, [...process.execArgv, cli2, "serve", "--no-open", "--port", String(port)], {
1877
1914
  cwd: root2,
1878
1915
  env: { ...process.env, MARKODE_ROOT: root2 },
@@ -1890,11 +1927,11 @@ async function ensureServer(root2, cli2, port) {
1890
1927
  var editorUrl = (port, doc) => `http://localhost:${port}/${doc ? `open?path=${encodeURIComponent(doc).replace(/%2F/g, "/")}` : ""}`;
1891
1928
 
1892
1929
  // src/core/wait.ts
1893
- import fs18 from "fs";
1894
- import path18 from "path";
1930
+ import fs19 from "fs";
1931
+ import path19 from "path";
1895
1932
  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`);
1933
+ var waitsDir = (root2) => path19.join(stateDir(root2), "waits");
1934
+ var pidFile = (root2, rel) => path19.join(waitsDir(root2), `${encodeURIComponent(rel)}.pid`);
1898
1935
  var frontmatter2 = (text3) => {
1899
1936
  try {
1900
1937
  return YAML5.parse(/^---\r?\n([\s\S]*?)\r?\n---/.exec(text3)?.[1] ?? "", { logLevel: "error" }) ?? {};
@@ -1913,27 +1950,27 @@ var isAlive2 = (pid) => {
1913
1950
  function waitingOn(root2) {
1914
1951
  let names2 = [];
1915
1952
  try {
1916
- names2 = fs18.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1953
+ names2 = fs19.readdirSync(waitsDir(root2)).filter((f) => f.endsWith(".pid"));
1917
1954
  } catch {
1918
1955
  }
1919
1956
  return names2.flatMap((f) => {
1920
- const pid = Number(fs18.readFileSync(path18.join(waitsDir(root2), f), "utf8"));
1957
+ const pid = Number(fs19.readFileSync(path19.join(waitsDir(root2), f), "utf8"));
1921
1958
  return isAlive2(pid) ? [decodeURIComponent(f.slice(0, -4))] : [];
1922
1959
  });
1923
1960
  }
1924
1961
  function waitForAnswer(root2, rel) {
1925
- const abs = path18.join(root2, rel);
1926
- fs18.mkdirSync(waitsDir(root2), { recursive: true });
1962
+ const abs = path19.join(root2, rel);
1963
+ fs19.mkdirSync(waitsDir(root2), { recursive: true });
1927
1964
  assertInside(pidFile(root2, rel));
1928
- fs18.writeFileSync(pidFile(root2, rel), String(process.pid));
1929
- const cleanup = () => fs18.rmSync(pidFile(root2, rel), { force: true });
1965
+ fs19.writeFileSync(pidFile(root2, rel), String(process.pid));
1966
+ const cleanup = () => fs19.rmSync(pidFile(root2, rel), { force: true });
1930
1967
  process.once("exit", cleanup);
1931
1968
  for (const sig of ["SIGINT", "SIGTERM", "SIGHUP"]) process.once(sig, () => process.exit(130));
1932
1969
  return new Promise((resolve) => {
1933
1970
  const poll = () => {
1934
1971
  let text3;
1935
1972
  try {
1936
- text3 = fs18.readFileSync(abs, "utf8");
1973
+ text3 = fs19.readFileSync(abs, "utf8");
1937
1974
  } catch {
1938
1975
  return resolve("gone");
1939
1976
  }
@@ -1947,7 +1984,7 @@ function waitForAnswer(root2, rel) {
1947
1984
  }
1948
1985
  var COMMENT = /^\s*<!-- (comment|reply) by (.+?), (\d{4}-\d{2}-\d{2})(?:, on "((?:[^"\\]|\\.)*)")?:\s?([\s\S]*?)\s*-->\s*$/;
1949
1986
  function answerSummary(root2, rel) {
1950
- const text3 = fs18.readFileSync(path18.join(root2, rel), "utf8");
1987
+ const text3 = fs19.readFileSync(path19.join(root2, rel), "utf8");
1951
1988
  const fm = frontmatter2(text3);
1952
1989
  const body = text3.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, "");
1953
1990
  const lines = body.split("\n");
@@ -2022,9 +2059,9 @@ function answerSummary(root2, rel) {
2022
2059
 
2023
2060
  // src/server/index.ts
2024
2061
  import crypto3 from "crypto";
2025
- import fs19 from "fs";
2062
+ import fs20 from "fs";
2026
2063
  import http from "http";
2027
- import path19 from "path";
2064
+ import path20 from "path";
2028
2065
  import { watch } from "chokidar";
2029
2066
  import open from "open";
2030
2067
  import pc from "picocolors";
@@ -2092,7 +2129,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2092
2129
  if (typeof rel !== "string" || !rel.trim()) throw new HttpError(400, "Expected a path");
2093
2130
  const abs = safeResolve(root2, rel);
2094
2131
  if (abs === root2) throw new HttpError(400, "Refusing to delete the project root");
2095
- if (!fs19.existsSync(abs)) throw new HttpError(404, "Not found");
2132
+ if (!fs20.existsSync(abs)) throw new HttpError(404, "Not found");
2096
2133
  return abs;
2097
2134
  };
2098
2135
  const translationFile = (lang, rel) => {
@@ -2104,7 +2141,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2104
2141
  "GET /api/info": () => {
2105
2142
  const c = config();
2106
2143
  return {
2107
- name: path19.basename(root2),
2144
+ name: path20.basename(root2),
2108
2145
  mainDoc: c.mainDoc ?? null,
2109
2146
  sourceLang: sourceLang(),
2110
2147
  langs: langs().filter((l) => l !== sourceLang()),
@@ -2131,7 +2168,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2131
2168
  }
2132
2169
  if (isLang(body.sourceLang) && body.sourceLang !== sourceLang()) {
2133
2170
  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);
2171
+ applyRulesToFile(path20.join(root2, c.mainDoc), void 0, body.sourceLang);
2135
2172
  }
2136
2173
  writeConfig(root2, c);
2137
2174
  return { ...c, langs: c.langs.filter((l) => l !== sourceLang()) };
@@ -2162,10 +2199,10 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2162
2199
  "GET /api/file": (url3) => {
2163
2200
  const rel = mdPath(url3.searchParams.get("path"));
2164
2201
  const l = viewLang(url3.searchParams.get("lang"));
2165
- const source = fs19.readFileSync(safeResolve(root2, rel), "utf8");
2202
+ const source = fs20.readFileSync(safeResolve(root2, rel), "utf8");
2166
2203
  if (l) {
2167
2204
  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) };
2205
+ if (fs20.existsSync(tPath)) return { path: rel, content: fs20.readFileSync(tPath, "utf8"), target: "translation", status: fileStatus(root2, l, rel) };
2169
2206
  return { path: rel, content: source, target: "source", status: "missing" };
2170
2207
  }
2171
2208
  return { path: rel, content: source, target: "source", status: null };
@@ -2174,19 +2211,19 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2174
2211
  const rel = mdPath(body.path);
2175
2212
  const l = viewLang(body.lang);
2176
2213
  const file = body.target === "translation" && l ? translationFile(l, rel) : safeResolve(root2, rel);
2177
- fs19.mkdirSync(path19.dirname(file), { recursive: true });
2214
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
2178
2215
  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);
2216
+ if (!l && featureState(root2).agent) logSave(root2, rel, fs20.existsSync(file) ? fs20.readFileSync(file, "utf8") : "", content);
2217
+ fs20.writeFileSync(file, content);
2181
2218
  return { ok: true, status: l ? fileStatus(root2, l, rel) : null };
2182
2219
  },
2183
2220
  "POST /api/file": (_, body) => {
2184
2221
  const rel = mdPath(body.path);
2185
2222
  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)}
2223
+ if (fs20.existsSync(file)) throw new HttpError(409, "File already exists");
2224
+ fs20.mkdirSync(path20.dirname(file), { recursive: true });
2225
+ const title = path20.basename(rel, ".md").replace(/^\d+[-_]/, "").replace(/[-_]/g, " ");
2226
+ fs20.writeFileSync(file, `# ${title.charAt(0).toUpperCase() + title.slice(1)}
2190
2227
 
2191
2228
  `);
2192
2229
  return { ok: true, path: rel };
@@ -2196,20 +2233,20 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2196
2233
  if (typeof body.path !== "string" || !body.path.trim()) throw new HttpError(400, "Expected a path");
2197
2234
  const dir = safeResolve(root2, body.path);
2198
2235
  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)) };
2236
+ if (fs20.existsSync(dir)) throw new HttpError(409, "A file or folder with that name already exists");
2237
+ fs20.mkdirSync(dir, { recursive: true });
2238
+ return { ok: true, path: toPosix(path20.relative(root2, dir)) };
2202
2239
  },
2203
2240
  "GET /api/delete-preview": (url3) => {
2204
2241
  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());
2242
+ if (!fs20.statSync(abs).isDirectory()) return { kind: "file", files: 1, md: 1 };
2243
+ const files = fs20.readdirSync(abs, { recursive: true, withFileTypes: true }).filter((e) => e.isFile());
2207
2244
  return { kind: "dir", files: files.length, md: files.filter((e) => e.name.toLowerCase().endsWith(".md")).length };
2208
2245
  },
2209
2246
  "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 });
2247
+ const rel = toPosix(path20.relative(root2, deletable(url3.searchParams.get("path"))));
2248
+ fs20.rmSync(safeResolve(root2, rel), { recursive: true });
2249
+ for (const l of config().langs) fs20.rmSync(translationFile(l, rel), { recursive: true, force: true });
2213
2250
  return { ok: true };
2214
2251
  },
2215
2252
  "POST /api/ignore": (_, body) => {
@@ -2226,7 +2263,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2226
2263
  },
2227
2264
  "GET /api/rules": () => {
2228
2265
  const mainDoc = readConfig(root2)?.mainDoc;
2229
- const text3 = mainDoc && fs19.existsSync(path19.join(root2, mainDoc)) ? fs19.readFileSync(path19.join(root2, mainDoc), "utf8") : "";
2266
+ const text3 = mainDoc && fs20.existsSync(path20.join(root2, mainDoc)) ? fs20.readFileSync(path20.join(root2, mainDoc), "utf8") : "";
2230
2267
  return { rules: listRules(), applied: appliedRuleIds(text3), mainDoc: mainDoc ?? null };
2231
2268
  },
2232
2269
  "PUT /api/rules": (_, body) => saveRule(body),
@@ -2242,12 +2279,12 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2242
2279
  },
2243
2280
  "GET /api/skills/file": (url3) => {
2244
2281
  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 };
2282
+ return { path: skill.key, content: fs20.readFileSync(path20.join(skill.key, "SKILL.md"), "utf8"), target: "source", status: null };
2246
2283
  },
2247
2284
  "PUT /api/skills/file": (_, body) => {
2248
2285
  const skill = getSkill(root2, body.key);
2249
2286
  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 ?? ""));
2287
+ fs20.writeFileSync(path20.join(skill.key, "SKILL.md"), String(body.content ?? ""));
2251
2288
  return { ok: true, status: null };
2252
2289
  },
2253
2290
  "POST /api/skills/install": (_, body) => {
@@ -2260,7 +2297,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2260
2297
  "POST /api/rules/apply": (_, body) => {
2261
2298
  const mainDoc = readConfig(root2)?.mainDoc;
2262
2299
  if (!mainDoc) throw new HttpError(400, "No main document configured; run `markode init`");
2263
- applyRulesToFile(path19.join(root2, mainDoc), body.ids ?? []);
2300
+ applyRulesToFile(path20.join(root2, mainDoc), body.ids ?? []);
2264
2301
  return { ok: true };
2265
2302
  }
2266
2303
  };
@@ -2295,13 +2332,13 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2295
2332
  res.writeHead(404).end();
2296
2333
  return;
2297
2334
  }
2298
- const type = IMAGE_MIME[path19.extname(file).toLowerCase()];
2299
- if (!type || !fs19.existsSync(file) || !fs19.statSync(file).isFile()) {
2335
+ const type = IMAGE_MIME[path20.extname(file).toLowerCase()];
2336
+ if (!type || !fs20.existsSync(file) || !fs20.statSync(file).isFile()) {
2300
2337
  res.writeHead(404).end();
2301
2338
  return;
2302
2339
  }
2303
2340
  res.writeHead(200, { "content-type": type, "cache-control": "no-cache", "content-security-policy": "sandbox; frame-ancestors 'none'" });
2304
- fs19.createReadStream(file).pipe(res);
2341
+ fs20.createReadStream(file).pipe(res);
2305
2342
  }
2306
2343
  function openDoc(rel, res) {
2307
2344
  let doc;
@@ -2335,16 +2372,16 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2335
2372
  // Unreadable folders (e.g. caches owned by another user) are skipped instead of failing.
2336
2373
  ignorePermissionErrors: true,
2337
2374
  ignored: (p3, stats) => {
2338
- const rel = toPosix(path19.relative(root2, p3));
2375
+ const rel = toPosix(path20.relative(root2, p3));
2339
2376
  if (rel === ".markode") return false;
2340
2377
  if (rel.startsWith(".markode/")) return !rel.startsWith(".markode/i18n") && rel !== ".markode/state.json" && rel !== ".markode/config.json";
2341
2378
  return skips(rel, stats ? stats.isDirectory() : void 0);
2342
2379
  }
2343
2380
  }).on("error", (err) => console.warn(pc.yellow(` file watcher: ${err.message}`))).on("all", (event, file) => {
2344
- const abs = path19.resolve(file);
2381
+ const abs = path20.resolve(file);
2345
2382
  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")));
2383
+ let rel = toPosix(path20.relative(root2, abs));
2384
+ const i18n = toPosix(path20.relative(root2, path20.join(stateDir(root2), "i18n")));
2348
2385
  if (rel.startsWith(i18n + "/")) rel = rel.split("/").slice(3).join("/");
2349
2386
  broadcast({ type: "fs", event, path: rel });
2350
2387
  });
@@ -2353,7 +2390,7 @@ async function startServer({ root: root2, port, open: openBrowser }) {
2353
2390
  console.log(`
2354
2391
  ${pc.bold("markode")} ${pc.dim("\u2192")} ${pc.cyan(url2)} ${pc.dim(root2)}
2355
2392
  `);
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"));
2393
+ 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
2394
  else if (openBrowser) open(url2).catch(() => {
2358
2395
  });
2359
2396
  }
@@ -2373,19 +2410,19 @@ function listen(server, port) {
2373
2410
  });
2374
2411
  }
2375
2412
  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)) {
2413
+ let file = path20.join(WEB_DIR, path20.normalize(pathname).replace(/^(\.\.[/\\])+/, ""));
2414
+ if (!file.startsWith(WEB_DIR) || !fs20.existsSync(file) || fs20.statSync(file).isDirectory()) file = path20.join(WEB_DIR, "index.html");
2415
+ if (!fs20.existsSync(file)) {
2379
2416
  res.writeHead(404).end("Web UI not built. Run `pnpm build`.");
2380
2417
  return;
2381
2418
  }
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);
2419
+ res.writeHead(200, { "content-type": MIME[path20.extname(file)] ?? "application/octet-stream", "content-security-policy": "frame-ancestors 'none'" });
2420
+ fs20.createReadStream(file).pipe(res);
2384
2421
  }
2385
2422
 
2386
2423
  // src/cli/init.ts
2387
- import fs20 from "fs";
2388
- import path20 from "path";
2424
+ import fs21 from "fs";
2425
+ import path21 from "path";
2389
2426
  import * as p from "@clack/prompts";
2390
2427
  import pc2 from "picocolors";
2391
2428
  function check(value) {
@@ -2396,12 +2433,12 @@ function check(value) {
2396
2433
  return value;
2397
2434
  }
2398
2435
  async function offerClaudeLink(root2, d) {
2399
- if (fs20.existsSync(path20.join(root2, "CLAUDE.md"))) return;
2436
+ if (fs21.existsSync(path21.join(root2, "CLAUDE.md"))) return;
2400
2437
  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
2438
  if (link2 && linkAgentsDoc(root2)) p.log.success("Created CLAUDE.md (imports AGENTS.md)");
2402
2439
  }
2403
2440
  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));
2441
+ const existing = MAIN_DOCS.filter((d2) => fs21.existsSync(path21.join(root2, d2)) && !isPointer(fs21.readFileSync(path21.join(root2, d2), "utf8"), d2));
2405
2442
  if (d) {
2406
2443
  if (existing.length === 1 && existing[0] === "AGENTS.md") await offerClaudeLink(root2, d);
2407
2444
  if (existing.length) return existing.includes(d.mainDoc) ? d.mainDoc : existing[0];
@@ -2518,7 +2555,7 @@ async function chooseAreas(root2) {
2518
2555
  return areas;
2519
2556
  }
2520
2557
  async function chooseKind(root2, dir) {
2521
- const guess = guessKind(path20.join(root2, dir));
2558
+ const guess = guessKind(path21.join(root2, dir));
2522
2559
  if (!guess.unclear) return guess.kind;
2523
2560
  const where = dir ? `${dir}/` : "this project";
2524
2561
  const seen = guess.signals.length ? `found: ${guess.signals.join(", ")}` : "no code yet";
@@ -2531,7 +2568,7 @@ async function chooseKind(root2, dir) {
2531
2568
  );
2532
2569
  }
2533
2570
  async function askProfile(root2, dir, kind) {
2534
- const base = detectProfile(path20.join(root2, dir), kind);
2571
+ const base = detectProfile(path21.join(root2, dir), kind);
2535
2572
  if (kind === "app") return base;
2536
2573
  const where = dir ? ` (${dir}/)` : "";
2537
2574
  if (kind === "library") return { kind, published: check(await p.confirm({ message: `Is the library${where} published (npm, PyPI\u2026)?`, initialValue: base.published })) };
@@ -2608,7 +2645,7 @@ async function chooseWorkflow(root2, mainDoc, previous, quiet) {
2608
2645
  const profile = areas.length ? void 0 : await askProfile(root2, "", await chooseKind(root2, ""));
2609
2646
  const created = scaffoldMethod(root2, mainDoc, areas);
2610
2647
  installMethodSkills(root2);
2611
- applyWorkflowToFile(path20.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
2648
+ applyWorkflowToFile(path21.join(root2, mainDoc), "markode", methodSection(areas, root2, { profile, contracts, existing }));
2612
2649
  const big = !areas.length && flattenFiles(scanTree(root2)).length > 40;
2613
2650
  const layout = areas.length ? "structured" : !big ? "general" : check(await p.select({ message: PROPOSALS_QUESTION, options: PROPOSAL_OPTIONS }));
2614
2651
  activateProposals(root2, layout, mainDoc);
@@ -2673,10 +2710,10 @@ async function runInit(root2, plan2) {
2673
2710
  let workflow = previous?.workflow ?? "none";
2674
2711
  if (has2("agent")) {
2675
2712
  mainDoc = await chooseMainDoc(root2, d);
2676
- const mainFile = path20.join(root2, mainDoc);
2713
+ const mainFile = path21.join(root2, mainDoc);
2677
2714
  if (has2("dashboard")) workflow = await chooseWorkflow(root2, mainDoc, previous?.workflow, d);
2678
2715
  if (workflow === "openspec" || workflow === "speckit") applyWorkflowToFile(mainFile, workflow);
2679
- const mainText = fs20.readFileSync(mainFile, "utf8");
2716
+ const mainText = fs21.readFileSync(mainFile, "utf8");
2680
2717
  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
2718
  const rules = listRules();
2682
2719
  const applied = appliedRuleIds(mainText);
@@ -2691,7 +2728,7 @@ async function runInit(root2, plan2) {
2691
2728
  required: false
2692
2729
  })
2693
2730
  );
2694
- for (const dir of targets) applyRulesToFile(contentDoc(path20.join(root2, dir)), selected, sourceLang);
2731
+ for (const dir of targets) applyRulesToFile(contentDoc(path21.join(root2, dir)), selected, sourceLang);
2695
2732
  }
2696
2733
  installAgentSkill(root2);
2697
2734
  const proposals = proposalsState(root2);
@@ -2848,6 +2885,8 @@ Usage
2848
2885
  markode changes [--json] what the user edited or commented in markode since the agent was last told
2849
2886
  markode changes --start a new agent session: what happened before is old news
2850
2887
  markode changes --hook <event> the same, as Claude Code hook output (silent when nothing changed)
2888
+ markode update [--check] install the latest version now, without the day npm/pnpm wait for new releases
2889
+ markode update --pnpm|--npm the same, choosing the package manager to install it with
2851
2890
 
2852
2891
  For the agent's /markode translate:
2853
2892
  markode translate --status [--json] translations + files not in the source language
@@ -2873,18 +2912,20 @@ var option = (name) => {
2873
2912
  var command = args[0] && !args[0].startsWith("-") ? args[0] : "start";
2874
2913
  var root = process.env.MARKODE_ROOT ?? process.cwd();
2875
2914
  async function main() {
2876
- if (fs21.existsSync(root)) setProjectRoot(root);
2915
+ if (fs22.existsSync(root)) setProjectRoot(root);
2877
2916
  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);
2917
+ if (flag("--version") || flag("-v")) return console.log(JSON.parse(fs22.readFileSync(path22.join(PKG_ROOT, "package.json"), "utf8")).version);
2879
2918
  const translation = flag("--status") || flag("--mark") || flag("--keep");
2880
2919
  if ((command === "sync" || command === "translate") && translation) return sync();
2881
2920
  if (command === "translate" || flag("--translate")) return runTranslate(root);
2921
+ if (["check", "url", "link"].includes(command) || command === "changes" && flag("--start")) refreshInstalled(root);
2882
2922
  if (command === "check") return check3();
2883
2923
  if (command === "wait") return wait();
2884
2924
  if (command === "link") return link();
2885
2925
  if (command === "changes") return changes();
2886
2926
  if (command === "url") return url();
2887
2927
  if (command === "stop") return stop();
2928
+ if (command === "update") return update();
2888
2929
  if (command === "features") return features();
2889
2930
  if (command === "setup") {
2890
2931
  await runSetup(false, flag("--custom"));
@@ -2972,7 +3013,7 @@ function link() {
2972
3013
  }
2973
3014
  async function url() {
2974
3015
  const doc = args[1] && !args[1].startsWith("-") ? args[1] : void 0;
2975
- if (doc && (!doc.toLowerCase().endsWith(".md") || !fs21.existsSync(path21.join(root, doc)))) {
3016
+ if (doc && (!doc.toLowerCase().endsWith(".md") || !fs22.existsSync(path22.join(root, doc)))) {
2976
3017
  console.error(`Usage: markode url [<doc.md>] (relative to ${root})`);
2977
3018
  process.exitCode = 1;
2978
3019
  return;
@@ -2986,6 +3027,20 @@ function stop() {
2986
3027
  process.kill(running.pid);
2987
3028
  console.log(`Stopped the editor on port ${running.port}.`);
2988
3029
  }
3030
+ async function update() {
3031
+ const latest = await latestVersion();
3032
+ if (!isNewer(latest, CURRENT_VERSION)) return console.log(`markode ${CURRENT_VERSION} is the latest version.`);
3033
+ console.log(`markode ${latest} is out (you have ${CURRENT_VERSION}).`);
3034
+ if (flag("--check")) return;
3035
+ const via = flag("--pnpm") ? "pnpm" : flag("--npm") ? "npm" : installer();
3036
+ 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).`);
3037
+ 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).");
3038
+ const cmd = installCommand2(via, latest);
3039
+ console.log(`$ ${cmd.join(" ")}`);
3040
+ if (runInstall(cmd)) return console.log(`Updated to ${latest}. The next \`markode url\` (or /markode) restarts a running editor with it.`);
3041
+ console.error("The install failed; the output above says why.");
3042
+ process.exitCode = 1;
3043
+ }
2989
3044
  function changes() {
2990
3045
  if (flag("--start")) return startSession(root);
2991
3046
  const list = takeChanges(root);
@@ -3000,7 +3055,7 @@ function changes() {
3000
3055
  }
3001
3056
  async function wait() {
3002
3057
  const rel = args[1];
3003
- if (!rel || !fs21.existsSync(path21.join(root, rel))) {
3058
+ if (!rel || !fs22.existsSync(path22.join(root, rel))) {
3004
3059
  console.error(`Usage: markode wait <proposal.md> (relative to ${root})`);
3005
3060
  process.exitCode = 1;
3006
3061
  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.2",
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",