mellos-mapping 0.24.0 → 0.26.0
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 +82 -12
- package/README.zh-CN.md +69 -10
- package/dist/hook-session-start.mjs +69 -13
- package/dist/native/LICENSES.txt +443 -0
- package/dist/native/darwin-arm64/fs-native-extensions.node +0 -0
- package/dist/native/darwin-x64/fs-native-extensions.node +0 -0
- package/dist/native/linux-arm64/fs-native-extensions.node +0 -0
- package/dist/native/linux-x64/fs-native-extensions.node +0 -0
- package/dist/native/win32-arm64/fs-native-extensions.node +0 -0
- package/dist/native/win32-x64/fs-native-extensions.node +0 -0
- package/dist/native-lock.cjs +346 -0
- package/dist/omp-extension.mjs +359 -0
- package/dist/server.mjs +199 -139
- package/dist/terminal-worker.mjs +116 -74
- package/dist/watch.mjs +125 -83
- package/dist/web.mjs +170 -113
- package/docs/codex.md +4 -1
- package/docs/locking.md +115 -0
- package/docs/map-api.md +29 -13
- package/lib/store/migration.js +19 -12
- package/lib/store/native-lock.d.ts +5 -0
- package/lib/store/native-lock.js +27 -0
- package/lib/store/transaction.d.ts +9 -2
- package/lib/store/transaction.js +79 -50
- package/package.json +11 -3
- package/scripts/install-mmap-command.mjs +179 -13
package/dist/web.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { createRequire } from 'node:module'; const require =
|
|
2
|
+
import { createRequire as __mellosCreateRequire } from 'node:module'; const require = __mellosCreateRequire(import.meta.url);
|
|
3
3
|
var __create = Object.create;
|
|
4
4
|
var __defProp = Object.defineProperty;
|
|
5
5
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -2271,7 +2271,7 @@ var require_websocket = __commonJS({
|
|
|
2271
2271
|
var http = __require("http");
|
|
2272
2272
|
var net = __require("net");
|
|
2273
2273
|
var tls = __require("tls");
|
|
2274
|
-
var { randomBytes: randomBytes2, createHash:
|
|
2274
|
+
var { randomBytes: randomBytes2, createHash: createHash3 } = __require("crypto");
|
|
2275
2275
|
var { Duplex, Readable } = __require("stream");
|
|
2276
2276
|
var { URL: URL2 } = __require("url");
|
|
2277
2277
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
@@ -2939,7 +2939,7 @@ var require_websocket = __commonJS({
|
|
|
2939
2939
|
abortHandshake(websocket, socket, "Invalid Upgrade header");
|
|
2940
2940
|
return;
|
|
2941
2941
|
}
|
|
2942
|
-
const digest =
|
|
2942
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
2943
2943
|
if (res.headers["sec-websocket-accept"] !== digest) {
|
|
2944
2944
|
abortHandshake(websocket, socket, "Invalid Sec-WebSocket-Accept header");
|
|
2945
2945
|
return;
|
|
@@ -3308,7 +3308,7 @@ var require_websocket_server = __commonJS({
|
|
|
3308
3308
|
var EventEmitter = __require("events");
|
|
3309
3309
|
var http = __require("http");
|
|
3310
3310
|
var { Duplex } = __require("stream");
|
|
3311
|
-
var { createHash:
|
|
3311
|
+
var { createHash: createHash3 } = __require("crypto");
|
|
3312
3312
|
var extension2 = require_extension();
|
|
3313
3313
|
var PerMessageDeflate2 = require_permessage_deflate();
|
|
3314
3314
|
var subprotocol2 = require_subprotocol();
|
|
@@ -3615,7 +3615,7 @@ var require_websocket_server = __commonJS({
|
|
|
3615
3615
|
);
|
|
3616
3616
|
}
|
|
3617
3617
|
if (this._state > RUNNING) return abortHandshake(socket, 503);
|
|
3618
|
-
const digest =
|
|
3618
|
+
const digest = createHash3("sha1").update(key + GUID).digest("base64");
|
|
3619
3619
|
const headers = [
|
|
3620
3620
|
"HTTP/1.1 101 Switching Protocols",
|
|
3621
3621
|
"Upgrade: websocket",
|
|
@@ -3703,9 +3703,9 @@ var require_websocket_server = __commonJS({
|
|
|
3703
3703
|
});
|
|
3704
3704
|
|
|
3705
3705
|
// src/web/cli.ts
|
|
3706
|
-
import { existsSync as
|
|
3706
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync as realpathSync4, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
|
|
3707
3707
|
import { dirname as dirname9, join as join7, resolve as resolve2 } from "node:path";
|
|
3708
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3708
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
3709
3709
|
import { homedir } from "node:os";
|
|
3710
3710
|
|
|
3711
3711
|
// src/domain/types.ts
|
|
@@ -4299,33 +4299,38 @@ function stripBom(text) {
|
|
|
4299
4299
|
}
|
|
4300
4300
|
|
|
4301
4301
|
// src/store/migration.ts
|
|
4302
|
-
import { dirname as
|
|
4303
|
-
var LEGACY_STATE_FILE_RELATIVE_PATH = join2(".claude", "mellos-mapping.json");
|
|
4302
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
4304
4303
|
|
|
4305
|
-
// src/store/
|
|
4306
|
-
import {
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4304
|
+
// src/store/transaction.ts
|
|
4305
|
+
import { closeSync, fstatSync, lstatSync, mkdirSync as mkdirSync2, openSync, realpathSync } from "node:fs";
|
|
4306
|
+
import { basename as basename2, dirname as dirname3, join as join2 } from "node:path";
|
|
4307
|
+
|
|
4308
|
+
// src/store/native-lock.ts
|
|
4309
|
+
import { existsSync as existsSync2 } from "node:fs";
|
|
4310
|
+
import { createRequire } from "node:module";
|
|
4311
|
+
import { fileURLToPath } from "node:url";
|
|
4312
|
+
var loaded;
|
|
4313
|
+
function nativeLock() {
|
|
4314
|
+
if (loaded) return loaded;
|
|
4315
|
+
if (Number(process.versions.napi ?? 0) < 9) {
|
|
4316
|
+
throw new Error("Project locks require Node-API 9 (Node 18.17+ or 20.3+).");
|
|
4315
4317
|
}
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
|
|
4318
|
+
const candidates = [
|
|
4319
|
+
new URL("./native-lock.cjs", import.meta.url),
|
|
4320
|
+
new URL("../../dist/native-lock.cjs", import.meta.url)
|
|
4321
|
+
];
|
|
4322
|
+
const entry = candidates.find((candidate) => existsSync2(candidate));
|
|
4323
|
+
if (!entry) throw new Error("The installed package is missing dist/native-lock.cjs; reinstall the complete package.");
|
|
4324
|
+
const backend = createRequire(import.meta.url)(fileURLToPath(entry));
|
|
4325
|
+
if (typeof backend.tryLock !== "function" || typeof backend.unlock !== "function") {
|
|
4326
|
+
throw new Error("The installed native lock backend is invalid.");
|
|
4321
4327
|
}
|
|
4322
|
-
|
|
4328
|
+
loaded = backend;
|
|
4329
|
+
return backend;
|
|
4323
4330
|
}
|
|
4324
4331
|
|
|
4325
4332
|
// src/store/transaction.ts
|
|
4326
|
-
|
|
4327
|
-
import { dirname as dirname4, join as join3 } from "node:path";
|
|
4328
|
-
import { randomUUID, createHash } from "node:crypto";
|
|
4333
|
+
var STORE_LOCK_PROTOCOL = "os-file-v1";
|
|
4329
4334
|
var LedgerError = class extends Error {
|
|
4330
4335
|
constructor(code, message, details = {}) {
|
|
4331
4336
|
super(message);
|
|
@@ -4334,72 +4339,112 @@ var LedgerError = class extends Error {
|
|
|
4334
4339
|
}
|
|
4335
4340
|
};
|
|
4336
4341
|
function storeDirectory(file) {
|
|
4337
|
-
const dir =
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
function deadOwner(lock) {
|
|
4342
|
+
const dir = dirname3(file);
|
|
4343
|
+
if (basename2(dir).toLowerCase() === "pages") return dirname3(dir);
|
|
4344
|
+
let canonical = dir;
|
|
4341
4345
|
try {
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
4351
|
-
|
|
4346
|
+
canonical = realpathSync.native(dir);
|
|
4347
|
+
} catch (error) {
|
|
4348
|
+
if (error.code !== "ENOENT") throw error;
|
|
4349
|
+
}
|
|
4350
|
+
return basename2(canonical).toLowerCase() === "pages" ? dirname3(canonical) : canonical;
|
|
4351
|
+
}
|
|
4352
|
+
function checkLockPath(lock) {
|
|
4353
|
+
const entry = lstatSync(lock, { throwIfNoEntry: false });
|
|
4354
|
+
if (entry?.isDirectory()) {
|
|
4355
|
+
throw new LedgerError(
|
|
4356
|
+
"LOCK_MIGRATION_REQUIRED",
|
|
4357
|
+
`Legacy directory lock at ${lock}. Stop all old MCP servers, viewers and watchers for this project, then move that directory aside for inspection and retry. Never remove the new regular lock file.`,
|
|
4358
|
+
{ path: lock }
|
|
4359
|
+
);
|
|
4360
|
+
}
|
|
4361
|
+
if (entry && !entry.isFile()) {
|
|
4362
|
+
throw new LedgerError("LOCK_UNAVAILABLE", `Project lock must be a regular file, not a symlink or special file: ${lock}`, { path: lock });
|
|
4352
4363
|
}
|
|
4353
4364
|
}
|
|
4354
4365
|
function withStoreLock(file, action) {
|
|
4366
|
+
let backend;
|
|
4367
|
+
try {
|
|
4368
|
+
backend = nativeLock();
|
|
4369
|
+
} catch (error) {
|
|
4370
|
+
throw new LedgerError("LOCK_UNAVAILABLE", `Cannot load the operating-system lock backend: ${error instanceof Error ? error.message : String(error)}`);
|
|
4371
|
+
}
|
|
4355
4372
|
const dir = storeDirectory(file);
|
|
4356
4373
|
mkdirSync2(dir, { recursive: true });
|
|
4357
|
-
const lock =
|
|
4358
|
-
|
|
4359
|
-
|
|
4374
|
+
const lock = join2(dir, ".write-lock");
|
|
4375
|
+
checkLockPath(lock);
|
|
4376
|
+
let fd;
|
|
4360
4377
|
try {
|
|
4361
|
-
|
|
4378
|
+
fd = openSync(lock, "a+", 384);
|
|
4362
4379
|
} catch (error) {
|
|
4363
|
-
|
|
4364
|
-
|
|
4365
|
-
try {
|
|
4366
|
-
mkdirSync2(join3(lock, ".reap"));
|
|
4367
|
-
} catch {
|
|
4368
|
-
throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
|
|
4369
|
-
}
|
|
4370
|
-
if (!deadOwner(lock)) {
|
|
4371
|
-
rmSync3(join3(lock, ".reap"), { recursive: true, force: true });
|
|
4372
|
-
throw new LedgerError("BUSY", "Writer ownership changed.");
|
|
4373
|
-
}
|
|
4374
|
-
rmSync3(lock, { recursive: true });
|
|
4375
|
-
try {
|
|
4376
|
-
mkdirSync2(lock);
|
|
4377
|
-
} catch {
|
|
4378
|
-
throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
|
|
4379
|
-
}
|
|
4380
|
+
checkLockPath(lock);
|
|
4381
|
+
throw new LedgerError("LOCK_UNAVAILABLE", `Cannot open project lock ${lock}: ${error instanceof Error ? error.message : String(error)}`, { path: lock });
|
|
4380
4382
|
}
|
|
4381
|
-
let
|
|
4383
|
+
let acquired = false;
|
|
4382
4384
|
try {
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
+
checkLockPath(lock);
|
|
4386
|
+
if (!fstatSync(fd).isFile()) throw new LedgerError("LOCK_UNAVAILABLE", `Project lock is not a regular file: ${lock}`);
|
|
4387
|
+
try {
|
|
4388
|
+
acquired = backend.tryLock(fd);
|
|
4389
|
+
} catch (error) {
|
|
4390
|
+
throw new LedgerError("LOCK_UNAVAILABLE", `Cannot acquire project lock ${lock}: ${error instanceof Error ? error.message : String(error)}`, { path: lock });
|
|
4391
|
+
}
|
|
4392
|
+
if (!acquired) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes.`);
|
|
4385
4393
|
return action();
|
|
4386
4394
|
} finally {
|
|
4387
4395
|
try {
|
|
4388
|
-
if (
|
|
4396
|
+
if (acquired) backend.unlock(fd);
|
|
4389
4397
|
} catch {
|
|
4398
|
+
warnLockCleanup(`Could not explicitly unlock ${lock}; closing its file handle.`);
|
|
4399
|
+
} finally {
|
|
4400
|
+
try {
|
|
4401
|
+
closeSync(fd);
|
|
4402
|
+
} catch {
|
|
4403
|
+
warnLockCleanup(`Could not close the project lock handle for ${lock}; restart this process before retrying writes.`);
|
|
4404
|
+
}
|
|
4390
4405
|
}
|
|
4391
4406
|
}
|
|
4392
4407
|
}
|
|
4408
|
+
function warnLockCleanup(message) {
|
|
4409
|
+
try {
|
|
4410
|
+
process.stderr.write(`mellos-mapping: ${message}
|
|
4411
|
+
`);
|
|
4412
|
+
} catch {
|
|
4413
|
+
}
|
|
4414
|
+
}
|
|
4415
|
+
|
|
4416
|
+
// src/store/migration.ts
|
|
4417
|
+
var LEGACY_STATE_FILE_RELATIVE_PATH = join3(".claude", "mellos-mapping.json");
|
|
4418
|
+
|
|
4419
|
+
// src/store/maps.ts
|
|
4420
|
+
import { readFileSync } from "node:fs";
|
|
4421
|
+
function loadMapFile(path) {
|
|
4422
|
+
let text;
|
|
4423
|
+
try {
|
|
4424
|
+
text = readFileSync(path, "utf8");
|
|
4425
|
+
} catch (e) {
|
|
4426
|
+
const code = e.code;
|
|
4427
|
+
if (code === "ENOENT") return err({ kind: "not-found", path });
|
|
4428
|
+
throw e;
|
|
4429
|
+
}
|
|
4430
|
+
let raw;
|
|
4431
|
+
try {
|
|
4432
|
+
raw = JSON.parse(stripBom(text));
|
|
4433
|
+
} catch (e) {
|
|
4434
|
+
return err({ kind: "malformed-json", path, detail: e.message });
|
|
4435
|
+
}
|
|
4436
|
+
return parseMap(raw, path);
|
|
4437
|
+
}
|
|
4393
4438
|
|
|
4394
4439
|
// src/web/launcher.ts
|
|
4395
4440
|
import { spawn } from "node:child_process";
|
|
4396
|
-
import { existsSync as
|
|
4441
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
4397
4442
|
import { dirname as dirname6, join as join4 } from "node:path";
|
|
4398
4443
|
|
|
4399
4444
|
// src/web/source.ts
|
|
4400
|
-
import { createHash
|
|
4445
|
+
import { createHash } from "node:crypto";
|
|
4401
4446
|
import { statSync } from "node:fs";
|
|
4402
|
-
import { basename as
|
|
4447
|
+
import { basename as basename3, dirname as dirname5 } from "node:path";
|
|
4403
4448
|
function readWebSnapshot(defaultFile) {
|
|
4404
4449
|
const pages = listPageFiles(defaultFile).map((file) => {
|
|
4405
4450
|
const id = pageIdOfFile(defaultFile, file) ?? "";
|
|
@@ -4414,41 +4459,46 @@ function readWebSnapshot(defaultFile) {
|
|
|
4414
4459
|
}
|
|
4415
4460
|
});
|
|
4416
4461
|
if (pages.length === 0) pages.push({ id: "", title: "\u7B49\u5F85\u7B2C\u4E00\u5F20\u5730\u56FE", modified: 0, map: EMPTY_MAP });
|
|
4417
|
-
const value = { project:
|
|
4418
|
-
return { revision:
|
|
4462
|
+
const value = { project: basename3(dirname5(dirname5(defaultFile))), pages };
|
|
4463
|
+
return { revision: createHash("sha256").update(JSON.stringify(value)).digest("hex"), value };
|
|
4419
4464
|
}
|
|
4420
4465
|
|
|
4421
4466
|
// src/web/launcher.ts
|
|
4422
4467
|
var webRuntimeFile = (defaultFile) => join4(dirname6(defaultFile), "web", "server.json");
|
|
4423
|
-
async function
|
|
4468
|
+
async function runningWebRuntime(defaultFile) {
|
|
4424
4469
|
try {
|
|
4425
|
-
const info = JSON.parse(
|
|
4470
|
+
const info = JSON.parse(readFileSync2(webRuntimeFile(defaultFile), "utf8"));
|
|
4426
4471
|
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || !/^[a-f0-9]{48}$/.test(info.token)) return void 0;
|
|
4427
4472
|
const url = `http://127.0.0.1:${info.port}/${info.token}/`;
|
|
4428
4473
|
const response = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(700) });
|
|
4429
|
-
if (response.ok
|
|
4474
|
+
if (response.ok) {
|
|
4475
|
+
const health = await response.json();
|
|
4476
|
+
if (health.file === defaultFile) return { url, health };
|
|
4477
|
+
}
|
|
4430
4478
|
} catch {
|
|
4431
4479
|
}
|
|
4432
4480
|
return void 0;
|
|
4433
4481
|
}
|
|
4482
|
+
async function runningWebUrl(defaultFile) {
|
|
4483
|
+
return (await runningWebRuntime(defaultFile))?.url;
|
|
4484
|
+
}
|
|
4434
4485
|
async function openWebPreview(defaultFile, entry, page, terminal = false) {
|
|
4435
4486
|
if (page !== void 0 && (!ID_RULE.test(page) || !readWebSnapshot(defaultFile).value.pages.some((p) => p.id === page))) throw new Error(`No map page named "${page}".`);
|
|
4436
|
-
let
|
|
4437
|
-
if (
|
|
4438
|
-
const
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
4487
|
+
let runtime = await runningWebRuntime(defaultFile);
|
|
4488
|
+
if (runtime) {
|
|
4489
|
+
const { url: url2, health } = runtime;
|
|
4490
|
+
if (health.lockProtocol === STORE_LOCK_PROTOCOL && (!health.formats?.includes(2) || terminal && !health.surfaces?.includes("web-terminal"))) {
|
|
4491
|
+
await fetch(`${url2}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
4442
4492
|
const deadline = Date.now() + 3e3;
|
|
4443
4493
|
while (await runningWebUrl(defaultFile)) {
|
|
4444
4494
|
if (Date.now() > deadline) throw new Error("Old web viewer is still stopping. Retry opening the map.");
|
|
4445
4495
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4446
4496
|
}
|
|
4447
|
-
|
|
4497
|
+
runtime = void 0;
|
|
4448
4498
|
}
|
|
4449
4499
|
}
|
|
4450
|
-
if (!
|
|
4451
|
-
if (!
|
|
4500
|
+
if (!runtime) {
|
|
4501
|
+
if (!existsSync3(entry)) throw new Error(`Web runtime missing: ${entry}. Run npm run build or reinstall the plugin.`);
|
|
4452
4502
|
const child = spawn(process.execPath, [entry, "--serve", defaultFile], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
4453
4503
|
let failure;
|
|
4454
4504
|
child.on("error", (error) => {
|
|
@@ -4456,13 +4506,20 @@ async function openWebPreview(defaultFile, entry, page, terminal = false) {
|
|
|
4456
4506
|
});
|
|
4457
4507
|
child.unref();
|
|
4458
4508
|
const deadline = Date.now() + 8e3;
|
|
4459
|
-
while (!
|
|
4509
|
+
while (!runtime && Date.now() < deadline) {
|
|
4460
4510
|
if (failure) throw failure;
|
|
4461
4511
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4462
|
-
|
|
4512
|
+
runtime = await runningWebRuntime(defaultFile);
|
|
4463
4513
|
}
|
|
4464
|
-
if (!
|
|
4514
|
+
if (!runtime) throw new Error("Web preview did not start. Run the web CLI directly to inspect the error.");
|
|
4465
4515
|
}
|
|
4516
|
+
if (runtime.health.lockProtocol !== STORE_LOCK_PROTOCOL) {
|
|
4517
|
+
throw new LedgerError(
|
|
4518
|
+
"LOCK_MIGRATION_REQUIRED",
|
|
4519
|
+
"The running web viewer uses an older storage-lock protocol. Stop the viewer with mellos-mapping-web <project-directory> --stop, then reopen it using the updated runtime."
|
|
4520
|
+
);
|
|
4521
|
+
}
|
|
4522
|
+
const { url } = runtime;
|
|
4466
4523
|
const query = new URLSearchParams();
|
|
4467
4524
|
if (terminal) query.set("view", "terminal");
|
|
4468
4525
|
if (page !== void 0) query.set("page", page);
|
|
@@ -4474,8 +4531,8 @@ import { randomBytes } from "node:crypto";
|
|
|
4474
4531
|
import { createServer } from "node:http";
|
|
4475
4532
|
|
|
4476
4533
|
// src/preview/publisher.ts
|
|
4477
|
-
import { createHash as
|
|
4478
|
-
import { existsSync as
|
|
4534
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
4535
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, readdirSync as readdirSync2, realpathSync as realpathSync2, rmdirSync } from "node:fs";
|
|
4479
4536
|
import { dirname as dirname7, join as join5, resolve } from "node:path";
|
|
4480
4537
|
|
|
4481
4538
|
// src/semantics/vocabulary.ts
|
|
@@ -5201,7 +5258,7 @@ function previewFile(defaultFile, page) {
|
|
|
5201
5258
|
}
|
|
5202
5259
|
function save(path, contents) {
|
|
5203
5260
|
try {
|
|
5204
|
-
if (
|
|
5261
|
+
if (readFileSync3(path, "utf8") === contents) return;
|
|
5205
5262
|
} catch (error) {
|
|
5206
5263
|
if (error.code !== "ENOENT") throw error;
|
|
5207
5264
|
}
|
|
@@ -5210,8 +5267,8 @@ function save(path, contents) {
|
|
|
5210
5267
|
}
|
|
5211
5268
|
function ownedDirectory(path) {
|
|
5212
5269
|
mkdirSync3(path, { recursive: true });
|
|
5213
|
-
const expected = join5(
|
|
5214
|
-
const actual =
|
|
5270
|
+
const expected = join5(realpathSync2(dirname7(path)), path.slice(dirname7(path).length + 1));
|
|
5271
|
+
const actual = realpathSync2(path);
|
|
5215
5272
|
if (process.platform === "win32" ? actual.toLowerCase() !== expected.toLowerCase() : actual !== expected) {
|
|
5216
5273
|
throw new Error(`Preview directory redirects outside its parent: ${path}`);
|
|
5217
5274
|
}
|
|
@@ -5233,7 +5290,7 @@ function acquireLock(directory) {
|
|
|
5233
5290
|
function createPreviewPublisher(defaultFile) {
|
|
5234
5291
|
const directory = previewDirectory(defaultFile);
|
|
5235
5292
|
const enabledFile = join5(directory, ENABLED);
|
|
5236
|
-
const enabled = () =>
|
|
5293
|
+
const enabled = () => existsSync4(enabledFile);
|
|
5237
5294
|
const refresh = (page) => {
|
|
5238
5295
|
try {
|
|
5239
5296
|
const path = previewFile(defaultFile, page);
|
|
@@ -5244,9 +5301,9 @@ function createPreviewPublisher(defaultFile) {
|
|
|
5244
5301
|
for (const source of listPageFiles(defaultFile)) {
|
|
5245
5302
|
const slug = pageIdOfFile(defaultFile, source);
|
|
5246
5303
|
if (slug !== void 0 && !ID_RULE.test(slug)) throw new Error(`Invalid map page filename: ${source}`);
|
|
5247
|
-
const
|
|
5248
|
-
if (!
|
|
5249
|
-
pages.push({ page: slug, map:
|
|
5304
|
+
const loaded2 = loadMapFile(source);
|
|
5305
|
+
if (!loaded2.ok) throw new Error(describeStoreError(loaded2.error));
|
|
5306
|
+
pages.push({ page: slug, map: loaded2.value });
|
|
5250
5307
|
}
|
|
5251
5308
|
if (page !== void 0 && !pages.some((p) => p.page === page)) return err(`No map page named "${page}".`);
|
|
5252
5309
|
if (page === void 0 && !pages.some((p) => p.page === void 0)) pages.unshift({ page: void 0, map: EMPTY_MAP });
|
|
@@ -5255,7 +5312,7 @@ function createPreviewPublisher(defaultFile) {
|
|
|
5255
5312
|
const present = /* @__PURE__ */ new Set();
|
|
5256
5313
|
for (const item of pages) {
|
|
5257
5314
|
const svg = renderMapSvg(item.map);
|
|
5258
|
-
const digest =
|
|
5315
|
+
const digest = createHash2("sha256").update(svg).digest("hex");
|
|
5259
5316
|
const image = `images/${digest}.svg`;
|
|
5260
5317
|
save(join5(images, `${digest}.svg`), svg);
|
|
5261
5318
|
const filename = documentName(item.page);
|
|
@@ -5441,15 +5498,15 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
5441
5498
|
import { win32 } from "node:path";
|
|
5442
5499
|
|
|
5443
5500
|
// src/support/star-reminder.ts
|
|
5444
|
-
import { existsSync as
|
|
5445
|
-
import { basename as
|
|
5501
|
+
import { existsSync as existsSync5, readFileSync as readFileSync4, realpathSync as realpathSync3 } from "node:fs";
|
|
5502
|
+
import { basename as basename4, dirname as dirname8, join as join6 } from "node:path";
|
|
5446
5503
|
var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
|
|
5447
5504
|
var DAY_MS = 864e5;
|
|
5448
5505
|
function isNpmInstallation(entry) {
|
|
5449
5506
|
try {
|
|
5450
|
-
const root = dirname8(dirname8(
|
|
5451
|
-
if (
|
|
5452
|
-
const pkg = JSON.parse(
|
|
5507
|
+
const root = dirname8(dirname8(realpathSync3(entry)));
|
|
5508
|
+
if (basename4(dirname8(root)).toLowerCase() !== "node_modules" || existsSync5(join6(root, ".git")) || existsSync5(join6(root, ".codex-plugin")) || existsSync5(join6(root, ".claude-plugin"))) return false;
|
|
5509
|
+
const pkg = JSON.parse(readFileSync4(join6(root, "package.json"), "utf8"));
|
|
5453
5510
|
return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
|
|
5454
5511
|
} catch {
|
|
5455
5512
|
return false;
|
|
@@ -5479,7 +5536,7 @@ function createStarReminder(options) {
|
|
|
5479
5536
|
const day = Math.floor(now / DAY_MS);
|
|
5480
5537
|
let state;
|
|
5481
5538
|
try {
|
|
5482
|
-
const saved2 = JSON.parse(
|
|
5539
|
+
const saved2 = JSON.parse(readFileSync4(file, "utf8"));
|
|
5483
5540
|
if (!validState(saved2)) return false;
|
|
5484
5541
|
state = saved2;
|
|
5485
5542
|
} catch (error) {
|
|
@@ -5595,7 +5652,7 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5595
5652
|
}
|
|
5596
5653
|
}
|
|
5597
5654
|
if (req.method === "GET" && route === "api/health") {
|
|
5598
|
-
send(res, 200, JSON.stringify({ file: defaultFile, pid: process.pid, formats: [1, 2], surfaces: terminalService ? ["web", "web-terminal"] : ["web"] }));
|
|
5655
|
+
send(res, 200, JSON.stringify({ file: defaultFile, pid: process.pid, formats: [1, 2], lockProtocol: STORE_LOCK_PROTOCOL, surfaces: terminalService ? ["web", "web-terminal"] : ["web"] }));
|
|
5599
5656
|
return;
|
|
5600
5657
|
}
|
|
5601
5658
|
if (req.method === "POST" && route === "api/star-reminder/open") {
|
|
@@ -5714,23 +5771,23 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5714
5771
|
|
|
5715
5772
|
// src/web/cli.ts
|
|
5716
5773
|
async function runWeb(args) {
|
|
5717
|
-
const entry =
|
|
5774
|
+
const entry = fileURLToPath2(import.meta.url);
|
|
5718
5775
|
if (args[0] === "--serve" && args.length === 2) {
|
|
5719
5776
|
const file2 = resolve2(args[1]);
|
|
5720
5777
|
if (await runningWebUrl(file2)) return;
|
|
5721
5778
|
const directory = dirname9(webRuntimeFile(file2));
|
|
5722
5779
|
mkdirSync4(directory, { recursive: true });
|
|
5723
|
-
if (
|
|
5780
|
+
if (realpathSync4(directory).toLowerCase() !== join7(realpathSync4(dirname9(directory)), "web").toLowerCase()) throw new Error("Redirected web runtime directory");
|
|
5724
5781
|
const assets = join7(dirname9(entry), "web");
|
|
5725
5782
|
let service;
|
|
5726
5783
|
service = await startWebService(file2, {
|
|
5727
|
-
html:
|
|
5728
|
-
javascript:
|
|
5729
|
-
css:
|
|
5730
|
-
terminal: { html:
|
|
5784
|
+
html: readFileSync5(join7(assets, "index.html"), "utf8"),
|
|
5785
|
+
javascript: readFileSync5(join7(assets, "app.js"), "utf8"),
|
|
5786
|
+
css: readFileSync5(join7(assets, "app.css"), "utf8"),
|
|
5787
|
+
terminal: { html: readFileSync5(join7(assets, "terminal.html"), "utf8"), javascript: readFileSync5(join7(assets, "terminal.js"), "utf8"), css: readFileSync5(join7(assets, "terminal.css"), "utf8"), xtermCss: readFileSync5(join7(assets, "xterm.css"), "utf8") }
|
|
5731
5788
|
}, { starReminder: createStarReminder({ entry, userBase: homedir(), env: process.env }), terminalWorker: join7(dirname9(entry), "terminal-worker.mjs"), onClose: () => {
|
|
5732
5789
|
try {
|
|
5733
|
-
if (JSON.parse(
|
|
5790
|
+
if (JSON.parse(readFileSync5(webRuntimeFile(file2), "utf8")).token === service.token) rmSync3(webRuntimeFile(file2));
|
|
5734
5791
|
} catch {
|
|
5735
5792
|
}
|
|
5736
5793
|
} });
|
|
@@ -5761,7 +5818,7 @@ async function runWeb(args) {
|
|
|
5761
5818
|
else throw new Error(usage);
|
|
5762
5819
|
}
|
|
5763
5820
|
const project = resolve2(args[0]);
|
|
5764
|
-
if (!
|
|
5821
|
+
if (!existsSync6(project) || !statSync2(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
|
|
5765
5822
|
const file = join7(project, STATE_FILE_RELATIVE_PATH);
|
|
5766
5823
|
if (stop) {
|
|
5767
5824
|
const url = await runningWebUrl(file);
|
|
@@ -5771,7 +5828,7 @@ async function runWeb(args) {
|
|
|
5771
5828
|
}
|
|
5772
5829
|
console.log(JSON.stringify({ surface: terminal ? "web-terminal" : "web", url: await openWebPreview(file, entry, page, terminal), visibility: "unconfirmed" }));
|
|
5773
5830
|
}
|
|
5774
|
-
if (process.argv[1] &&
|
|
5831
|
+
if (process.argv[1] && existsSync6(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync4(process.argv[1])).href) {
|
|
5775
5832
|
runWeb(process.argv.slice(2)).catch((error) => {
|
|
5776
5833
|
console.error(String(error));
|
|
5777
5834
|
process.exitCode = 1;
|
package/docs/codex.md
CHANGED
|
@@ -6,7 +6,10 @@ has its own skill and includes no Claude SessionStart hook or MCP configuration.
|
|
|
6
6
|
|
|
7
7
|
## Install and update
|
|
8
8
|
|
|
9
|
-
Requires Node.js 18+ and a Codex CLI with plugin
|
|
9
|
+
Requires Node.js 18.17+ on the 18.x line, or 20.3+, and a Codex CLI with plugin
|
|
10
|
+
commands (tested with 0.153.4). Supported platforms are Windows 10+, macOS 13.0+
|
|
11
|
+
and Linux with glibc 2.28+, each on x64 or arm64. The OS must also meet the selected
|
|
12
|
+
Node.js version's requirements. Native lock bindings ship with the package.
|
|
10
13
|
From the `chatgpt-app` branch run `node install.mjs`; from `main` run
|
|
11
14
|
`node install.mjs chatgpt-app`. Committed bundles need no build or npm dependencies.
|
|
12
15
|
|
package/docs/locking.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Project locking
|
|
2
|
+
|
|
3
|
+
Map writes have three separate guarantees: atomic file replacement keeps a
|
|
4
|
+
reader from seeing half a saved map, a project lock serializes cooperating
|
|
5
|
+
writers, and an optional revision check rejects edits computed from stale
|
|
6
|
+
data. None of these replaces the others.
|
|
7
|
+
|
|
8
|
+
## One stable lock for every page
|
|
9
|
+
|
|
10
|
+
The default page and all named pages use the same fixed regular file,
|
|
11
|
+
`.mellos/.write-lock`, for their project.
|
|
12
|
+
`withStoreLock` acquires an OS exclusive lock without waiting before reading
|
|
13
|
+
the current graph. MCP graph mutations, HTTP viewer mutations and watcher
|
|
14
|
+
mutations all use this boundary. A competing writer receives `BUSY` and can
|
|
15
|
+
retry after the active operation completes; acquisition does not start a
|
|
16
|
+
background poll or queue.
|
|
17
|
+
|
|
18
|
+
The lock file is independent of the map JSON files. A successful map save
|
|
19
|
+
replaces its target through a private sibling temporary file, while the lock
|
|
20
|
+
file keeps the same identity. Do not delete, rename or replace the lock file:
|
|
21
|
+
doing so can leave processes locking different files for the same project.
|
|
22
|
+
Keeping the file after an operation is deliberate; its presence is not proof
|
|
23
|
+
that a writer is active.
|
|
24
|
+
|
|
25
|
+
Normal completion and errors release the lock. When the owning process exits,
|
|
26
|
+
including a crash, the OS releases its lock. The new protocol does not use
|
|
27
|
+
directory creation, an `owner.json` PID, a `.reap` directory, or elapsed time
|
|
28
|
+
to decide ownership or reclaim a lock. Failure to acquire the required OS lock
|
|
29
|
+
must not cause a write to continue without it.
|
|
30
|
+
|
|
31
|
+
## A lock does not validate an earlier read
|
|
32
|
+
|
|
33
|
+
Each operation loads the current map, checks a supplied `expectedRevision`,
|
|
34
|
+
applies its changes and saves while holding the project lock. This prevents
|
|
35
|
+
two cooperating writers from both saving independently loaded copies at once.
|
|
36
|
+
The lock does not remain held across separate MCP read and write calls.
|
|
37
|
+
|
|
38
|
+
When an edit depends on data returned by `mmap_read`, include that page's
|
|
39
|
+
revision in the write. If another operation changed the map in between, the
|
|
40
|
+
write returns `CONFLICT` without applying the edit. Read again, reconsider the
|
|
41
|
+
change, and submit it with the new revision. Use `expectedRevision: "absent"`
|
|
42
|
+
when declaring a page that must not already exist.
|
|
43
|
+
|
|
44
|
+
Omitting `expectedRevision` preserves compatibility, but skips that comparison.
|
|
45
|
+
For example, if two sessions read the same `context`, revise different parts,
|
|
46
|
+
and then write their complete objects without a revision, the later write
|
|
47
|
+
replaces the earlier one. The project lock serializes both writes; it cannot
|
|
48
|
+
infer which fields the caller intended to preserve.
|
|
49
|
+
|
|
50
|
+
## Scope
|
|
51
|
+
|
|
52
|
+
The lock is a contract among participating writers. Hand edits, direct calls
|
|
53
|
+
to low-level `saveMapFile`, and older running processes do not acquire this
|
|
54
|
+
new lock. A filesystem must support the OS lock used by the runtime; file
|
|
55
|
+
replacement alone is not an alternative locking protocol.
|
|
56
|
+
|
|
57
|
+
The bundled native bindings require Node.js `^18.17.0 || >=20.3.0` (N-API 9).
|
|
58
|
+
Releases include bindings for Windows 10+ (Server 2016+ on x64), macOS 13.0+
|
|
59
|
+
and Linux with glibc 2.28+, each on x64 and arm64; users do not compile them
|
|
60
|
+
during installation. A missing binding or unsupported platform returns
|
|
61
|
+
`LOCK_UNAVAILABLE` instead of falling back to directory locks or continuing
|
|
62
|
+
without a lock.
|
|
63
|
+
|
|
64
|
+
macOS 13.0 is the deployment target recorded in both shipped Mach-O addons.
|
|
65
|
+
The OS must also meet the selected Node.js version's requirements: for example,
|
|
66
|
+
[Node.js 24 requires macOS 13.5+](https://github.com/nodejs/node/blob/v24.0.0/BUILDING.md#platform-list).
|
|
67
|
+
The Linux addons require only `libc.so.6`, with maximum symbol versions
|
|
68
|
+
`GLIBC_2.14` on x64 and `GLIBC_2.17` on arm64; they have no `GLIBCXX` or `CXXABI`
|
|
69
|
+
dependency. The higher glibc 2.28 baseline comes from the supported official
|
|
70
|
+
Node.js binaries, which also require kernel 4.18+ and libstdc++ providing
|
|
71
|
+
`GLIBCXX_3.4.25`; see the [Node.js 18.17 platform requirements](https://github.com/nodejs/node/blob/v18.17.0/BUILDING.md#platform-list).
|
|
72
|
+
The Windows addons import system `KERNEL32.dll` and `ntdll.dll`; the supported
|
|
73
|
+
Windows floor follows Node.js, rather than the older PE header version.
|
|
74
|
+
|
|
75
|
+
Pages organize separate efforts, but share the project lock. This does not
|
|
76
|
+
turn legacy `mmap_remove {pages:[...]}` into a multi-page transaction: that API
|
|
77
|
+
still reports partial deletion and does not accept `expectedRevision`. Use
|
|
78
|
+
per-page `deletePage` for revision-checked deletion. See the
|
|
79
|
+
[persistent-map API guide](map-api.md) for the data contract.
|
|
80
|
+
|
|
81
|
+
## Upgrade from directory locks
|
|
82
|
+
|
|
83
|
+
Earlier runtimes used a directory named `.mellos/.write-lock`, containing
|
|
84
|
+
`owner.json` and sometimes `.reap`. New writers use a regular file at that same
|
|
85
|
+
path. If they find a directory there, they return `LOCK_MIGRATION_REQUIRED`.
|
|
86
|
+
They do not reclaim it automatically, even when the recorded PID appears dead
|
|
87
|
+
or its timestamp is old.
|
|
88
|
+
|
|
89
|
+
1. Stop every old MCP server, HTTP viewer service and native watcher using this
|
|
90
|
+
project. Updating files or opening a new browser tab does not stop those
|
|
91
|
+
processes. Stop and restart existing HTTP services explicitly; there is no
|
|
92
|
+
hot upgrade of the locking protocol.
|
|
93
|
+
New clients check the viewer's health response for `lockProtocol:
|
|
94
|
+
"os-file-v1"`; an older service returns a migration error on reuse rather
|
|
95
|
+
than silently providing a viewer with different write semantics. Use
|
|
96
|
+
`mellos-mapping-web <project-directory> --stop` before reopening it.
|
|
97
|
+
2. Inspect `.mellos/.write-lock`. If it is a **directory**, and all old writers
|
|
98
|
+
have stopped, move that directory to a separate backup location. Preserve
|
|
99
|
+
it for diagnosis. If it is already a **regular file**, leave it in place:
|
|
100
|
+
it belongs to the new protocol and must not be deleted or renamed. If the
|
|
101
|
+
path is absent, no old directory needs moving.
|
|
102
|
+
3. Start the updated runtime and retry the operation. It creates the permanent
|
|
103
|
+
regular file if absent, acquires the OS lock and then performs the write.
|
|
104
|
+
Reconnect viewer tabs using the URL returned by the restarted service.
|
|
105
|
+
|
|
106
|
+
Using the same path prevents old and new writers from silently choosing
|
|
107
|
+
different locks. The new runtime's atomic file creation competes with the old
|
|
108
|
+
runtime's directory creation. If the old directory wins, the new runtime
|
|
109
|
+
requires migration. If the regular file wins, an old writer cannot create its
|
|
110
|
+
directory or read `owner.json` beneath the file and returns `BUSY` instead of
|
|
111
|
+
writing. Do not remove the new file to make an old client proceed; upgrade and
|
|
112
|
+
restart that client.
|
|
113
|
+
|
|
114
|
+
Do not automate this migration by deleting everything named `.write-lock`.
|
|
115
|
+
The old directory and the new permanent file require different treatment.
|