@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.
- package/README.md +6 -0
- package/dist/cli.js +129 -74
- 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
|
|
5
|
-
import
|
|
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
|
|
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
|
-
|
|
1883
|
+
fs18.mkdirSync(path18.dirname(file), { recursive: true });
|
|
1851
1884
|
assertInside(file);
|
|
1852
|
-
|
|
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)
|
|
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(
|
|
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 &&
|
|
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
|
|
1894
|
-
import
|
|
1930
|
+
import fs19 from "fs";
|
|
1931
|
+
import path19 from "path";
|
|
1895
1932
|
import YAML5 from "yaml";
|
|
1896
|
-
var waitsDir = (root2) =>
|
|
1897
|
-
var pidFile = (root2, rel) =>
|
|
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 =
|
|
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(
|
|
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 =
|
|
1926
|
-
|
|
1962
|
+
const abs = path19.join(root2, rel);
|
|
1963
|
+
fs19.mkdirSync(waitsDir(root2), { recursive: true });
|
|
1927
1964
|
assertInside(pidFile(root2, rel));
|
|
1928
|
-
|
|
1929
|
-
const cleanup = () =>
|
|
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 =
|
|
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 =
|
|
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
|
|
2062
|
+
import fs20 from "fs";
|
|
2026
2063
|
import http from "http";
|
|
2027
|
-
import
|
|
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 (!
|
|
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:
|
|
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(
|
|
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 =
|
|
2202
|
+
const source = fs20.readFileSync(safeResolve(root2, rel), "utf8");
|
|
2166
2203
|
if (l) {
|
|
2167
2204
|
const tPath = translationFile(l, rel);
|
|
2168
|
-
if (
|
|
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
|
-
|
|
2214
|
+
fs20.mkdirSync(path20.dirname(file), { recursive: true });
|
|
2178
2215
|
const content = String(body.content ?? "");
|
|
2179
|
-
if (!l && featureState(root2).agent) logSave(root2, rel,
|
|
2180
|
-
|
|
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 (
|
|
2187
|
-
|
|
2188
|
-
const title =
|
|
2189
|
-
|
|
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 (
|
|
2200
|
-
|
|
2201
|
-
return { ok: true, path: toPosix(
|
|
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 (!
|
|
2206
|
-
const files =
|
|
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(
|
|
2211
|
-
|
|
2212
|
-
for (const l of config().langs)
|
|
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 &&
|
|
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:
|
|
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
|
-
|
|
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(
|
|
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[
|
|
2299
|
-
if (!type || !
|
|
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
|
-
|
|
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(
|
|
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 =
|
|
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(
|
|
2347
|
-
const i18n = toPosix(
|
|
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 (!
|
|
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 =
|
|
2377
|
-
if (!file.startsWith(WEB_DIR) || !
|
|
2378
|
-
if (!
|
|
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[
|
|
2383
|
-
|
|
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
|
|
2388
|
-
import
|
|
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 (
|
|
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) =>
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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 =
|
|
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(
|
|
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 (
|
|
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(
|
|
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") || !
|
|
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 || !
|
|
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