mellos-mapping 0.23.0 → 0.25.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 +48 -10
- package/README.zh-CN.md +38 -9
- package/dist/hook-session-start.mjs +1 -1
- 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/server.mjs +198 -138
- package/dist/terminal-worker.mjs +117 -74
- package/dist/watch.mjs +223 -100
- package/dist/web/app.css +106 -2
- package/dist/web/app.js +78 -0
- package/dist/web/index.html +1 -1
- package/dist/web/terminal.css +103 -0
- package/dist/web/terminal.html +1 -1
- package/dist/web/terminal.js +78 -0
- package/dist/web.mjs +305 -112
- 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 +6 -3
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,10 @@ var require_websocket_server = __commonJS({
|
|
|
3703
3703
|
});
|
|
3704
3704
|
|
|
3705
3705
|
// src/web/cli.ts
|
|
3706
|
-
import { existsSync as
|
|
3707
|
-
import { dirname as
|
|
3708
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
3706
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync as realpathSync4, rmSync as rmSync3, statSync as statSync2 } from "node:fs";
|
|
3707
|
+
import { dirname as dirname9, join as join7, resolve as resolve2 } from "node:path";
|
|
3708
|
+
import { fileURLToPath as fileURLToPath2, pathToFileURL } from "node:url";
|
|
3709
|
+
import { homedir } from "node:os";
|
|
3709
3710
|
|
|
3710
3711
|
// src/domain/types.ts
|
|
3711
3712
|
var ok = (value) => ({ ok: true, value });
|
|
@@ -4298,33 +4299,38 @@ function stripBom(text) {
|
|
|
4298
4299
|
}
|
|
4299
4300
|
|
|
4300
4301
|
// src/store/migration.ts
|
|
4301
|
-
import { dirname as
|
|
4302
|
-
var LEGACY_STATE_FILE_RELATIVE_PATH = join2(".claude", "mellos-mapping.json");
|
|
4302
|
+
import { dirname as dirname4, join as join3 } from "node:path";
|
|
4303
4303
|
|
|
4304
|
-
// src/store/
|
|
4305
|
-
import {
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
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+).");
|
|
4314
4317
|
}
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
|
|
4319
|
-
|
|
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.");
|
|
4320
4327
|
}
|
|
4321
|
-
|
|
4328
|
+
loaded = backend;
|
|
4329
|
+
return backend;
|
|
4322
4330
|
}
|
|
4323
4331
|
|
|
4324
4332
|
// src/store/transaction.ts
|
|
4325
|
-
|
|
4326
|
-
import { dirname as dirname4, join as join3 } from "node:path";
|
|
4327
|
-
import { randomUUID, createHash } from "node:crypto";
|
|
4333
|
+
var STORE_LOCK_PROTOCOL = "os-file-v1";
|
|
4328
4334
|
var LedgerError = class extends Error {
|
|
4329
4335
|
constructor(code, message, details = {}) {
|
|
4330
4336
|
super(message);
|
|
@@ -4333,72 +4339,112 @@ var LedgerError = class extends Error {
|
|
|
4333
4339
|
}
|
|
4334
4340
|
};
|
|
4335
4341
|
function storeDirectory(file) {
|
|
4336
|
-
const dir =
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
function deadOwner(lock) {
|
|
4342
|
+
const dir = dirname3(file);
|
|
4343
|
+
if (basename2(dir).toLowerCase() === "pages") return dirname3(dir);
|
|
4344
|
+
let canonical = dir;
|
|
4340
4345
|
try {
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
|
|
4348
|
-
|
|
4349
|
-
|
|
4350
|
-
|
|
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 });
|
|
4351
4363
|
}
|
|
4352
4364
|
}
|
|
4353
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
|
+
}
|
|
4354
4372
|
const dir = storeDirectory(file);
|
|
4355
4373
|
mkdirSync2(dir, { recursive: true });
|
|
4356
|
-
const lock =
|
|
4357
|
-
|
|
4358
|
-
|
|
4374
|
+
const lock = join2(dir, ".write-lock");
|
|
4375
|
+
checkLockPath(lock);
|
|
4376
|
+
let fd;
|
|
4359
4377
|
try {
|
|
4360
|
-
|
|
4378
|
+
fd = openSync(lock, "a+", 384);
|
|
4361
4379
|
} catch (error) {
|
|
4362
|
-
|
|
4363
|
-
|
|
4364
|
-
try {
|
|
4365
|
-
mkdirSync2(join3(lock, ".reap"));
|
|
4366
|
-
} catch {
|
|
4367
|
-
throw new LedgerError("BUSY", "Another process is recovering the writer lock.");
|
|
4368
|
-
}
|
|
4369
|
-
if (!deadOwner(lock)) {
|
|
4370
|
-
rmSync3(join3(lock, ".reap"), { recursive: true, force: true });
|
|
4371
|
-
throw new LedgerError("BUSY", "Writer ownership changed.");
|
|
4372
|
-
}
|
|
4373
|
-
rmSync3(lock, { recursive: true });
|
|
4374
|
-
try {
|
|
4375
|
-
mkdirSync2(lock);
|
|
4376
|
-
} catch {
|
|
4377
|
-
throw new LedgerError("BUSY", "Another writer acquired the recovered lock.");
|
|
4378
|
-
}
|
|
4380
|
+
checkLockPath(lock);
|
|
4381
|
+
throw new LedgerError("LOCK_UNAVAILABLE", `Cannot open project lock ${lock}: ${error instanceof Error ? error.message : String(error)}`, { path: lock });
|
|
4379
4382
|
}
|
|
4380
|
-
let
|
|
4383
|
+
let acquired = false;
|
|
4381
4384
|
try {
|
|
4382
|
-
|
|
4383
|
-
|
|
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.`);
|
|
4384
4393
|
return action();
|
|
4385
4394
|
} finally {
|
|
4386
4395
|
try {
|
|
4387
|
-
if (
|
|
4396
|
+
if (acquired) backend.unlock(fd);
|
|
4388
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
|
+
}
|
|
4389
4405
|
}
|
|
4390
4406
|
}
|
|
4391
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
|
+
}
|
|
4392
4438
|
|
|
4393
4439
|
// src/web/launcher.ts
|
|
4394
4440
|
import { spawn } from "node:child_process";
|
|
4395
|
-
import { existsSync as
|
|
4441
|
+
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
|
|
4396
4442
|
import { dirname as dirname6, join as join4 } from "node:path";
|
|
4397
4443
|
|
|
4398
4444
|
// src/web/source.ts
|
|
4399
|
-
import { createHash
|
|
4445
|
+
import { createHash } from "node:crypto";
|
|
4400
4446
|
import { statSync } from "node:fs";
|
|
4401
|
-
import { basename as
|
|
4447
|
+
import { basename as basename3, dirname as dirname5 } from "node:path";
|
|
4402
4448
|
function readWebSnapshot(defaultFile) {
|
|
4403
4449
|
const pages = listPageFiles(defaultFile).map((file) => {
|
|
4404
4450
|
const id = pageIdOfFile(defaultFile, file) ?? "";
|
|
@@ -4413,41 +4459,46 @@ function readWebSnapshot(defaultFile) {
|
|
|
4413
4459
|
}
|
|
4414
4460
|
});
|
|
4415
4461
|
if (pages.length === 0) pages.push({ id: "", title: "\u7B49\u5F85\u7B2C\u4E00\u5F20\u5730\u56FE", modified: 0, map: EMPTY_MAP });
|
|
4416
|
-
const value = { project:
|
|
4417
|
-
return { revision:
|
|
4462
|
+
const value = { project: basename3(dirname5(dirname5(defaultFile))), pages };
|
|
4463
|
+
return { revision: createHash("sha256").update(JSON.stringify(value)).digest("hex"), value };
|
|
4418
4464
|
}
|
|
4419
4465
|
|
|
4420
4466
|
// src/web/launcher.ts
|
|
4421
4467
|
var webRuntimeFile = (defaultFile) => join4(dirname6(defaultFile), "web", "server.json");
|
|
4422
|
-
async function
|
|
4468
|
+
async function runningWebRuntime(defaultFile) {
|
|
4423
4469
|
try {
|
|
4424
|
-
const info = JSON.parse(
|
|
4470
|
+
const info = JSON.parse(readFileSync2(webRuntimeFile(defaultFile), "utf8"));
|
|
4425
4471
|
if (!Number.isInteger(info.port) || info.port < 1 || info.port > 65535 || !/^[a-f0-9]{48}$/.test(info.token)) return void 0;
|
|
4426
4472
|
const url = `http://127.0.0.1:${info.port}/${info.token}/`;
|
|
4427
4473
|
const response = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(700) });
|
|
4428
|
-
if (response.ok
|
|
4474
|
+
if (response.ok) {
|
|
4475
|
+
const health = await response.json();
|
|
4476
|
+
if (health.file === defaultFile) return { url, health };
|
|
4477
|
+
}
|
|
4429
4478
|
} catch {
|
|
4430
4479
|
}
|
|
4431
4480
|
return void 0;
|
|
4432
4481
|
}
|
|
4482
|
+
async function runningWebUrl(defaultFile) {
|
|
4483
|
+
return (await runningWebRuntime(defaultFile))?.url;
|
|
4484
|
+
}
|
|
4433
4485
|
async function openWebPreview(defaultFile, entry, page, terminal = false) {
|
|
4434
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}".`);
|
|
4435
|
-
let
|
|
4436
|
-
if (
|
|
4437
|
-
const
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
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) });
|
|
4441
4492
|
const deadline = Date.now() + 3e3;
|
|
4442
4493
|
while (await runningWebUrl(defaultFile)) {
|
|
4443
4494
|
if (Date.now() > deadline) throw new Error("Old web viewer is still stopping. Retry opening the map.");
|
|
4444
4495
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4445
4496
|
}
|
|
4446
|
-
|
|
4497
|
+
runtime = void 0;
|
|
4447
4498
|
}
|
|
4448
4499
|
}
|
|
4449
|
-
if (!
|
|
4450
|
-
if (!
|
|
4500
|
+
if (!runtime) {
|
|
4501
|
+
if (!existsSync3(entry)) throw new Error(`Web runtime missing: ${entry}. Run npm run build or reinstall the plugin.`);
|
|
4451
4502
|
const child = spawn(process.execPath, [entry, "--serve", defaultFile], { detached: true, windowsHide: true, stdio: "ignore" });
|
|
4452
4503
|
let failure;
|
|
4453
4504
|
child.on("error", (error) => {
|
|
@@ -4455,13 +4506,20 @@ async function openWebPreview(defaultFile, entry, page, terminal = false) {
|
|
|
4455
4506
|
});
|
|
4456
4507
|
child.unref();
|
|
4457
4508
|
const deadline = Date.now() + 8e3;
|
|
4458
|
-
while (!
|
|
4509
|
+
while (!runtime && Date.now() < deadline) {
|
|
4459
4510
|
if (failure) throw failure;
|
|
4460
4511
|
await new Promise((resolve3) => setTimeout(resolve3, 100));
|
|
4461
|
-
|
|
4512
|
+
runtime = await runningWebRuntime(defaultFile);
|
|
4462
4513
|
}
|
|
4463
|
-
if (!
|
|
4514
|
+
if (!runtime) throw new Error("Web preview did not start. Run the web CLI directly to inspect the error.");
|
|
4464
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;
|
|
4465
4523
|
const query = new URLSearchParams();
|
|
4466
4524
|
if (terminal) query.set("view", "terminal");
|
|
4467
4525
|
if (page !== void 0) query.set("page", page);
|
|
@@ -4473,8 +4531,8 @@ import { randomBytes } from "node:crypto";
|
|
|
4473
4531
|
import { createServer } from "node:http";
|
|
4474
4532
|
|
|
4475
4533
|
// src/preview/publisher.ts
|
|
4476
|
-
import { createHash as
|
|
4477
|
-
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";
|
|
4478
4536
|
import { dirname as dirname7, join as join5, resolve } from "node:path";
|
|
4479
4537
|
|
|
4480
4538
|
// src/semantics/vocabulary.ts
|
|
@@ -5200,7 +5258,7 @@ function previewFile(defaultFile, page) {
|
|
|
5200
5258
|
}
|
|
5201
5259
|
function save(path, contents) {
|
|
5202
5260
|
try {
|
|
5203
|
-
if (
|
|
5261
|
+
if (readFileSync3(path, "utf8") === contents) return;
|
|
5204
5262
|
} catch (error) {
|
|
5205
5263
|
if (error.code !== "ENOENT") throw error;
|
|
5206
5264
|
}
|
|
@@ -5209,8 +5267,8 @@ function save(path, contents) {
|
|
|
5209
5267
|
}
|
|
5210
5268
|
function ownedDirectory(path) {
|
|
5211
5269
|
mkdirSync3(path, { recursive: true });
|
|
5212
|
-
const expected = join5(
|
|
5213
|
-
const actual =
|
|
5270
|
+
const expected = join5(realpathSync2(dirname7(path)), path.slice(dirname7(path).length + 1));
|
|
5271
|
+
const actual = realpathSync2(path);
|
|
5214
5272
|
if (process.platform === "win32" ? actual.toLowerCase() !== expected.toLowerCase() : actual !== expected) {
|
|
5215
5273
|
throw new Error(`Preview directory redirects outside its parent: ${path}`);
|
|
5216
5274
|
}
|
|
@@ -5232,7 +5290,7 @@ function acquireLock(directory) {
|
|
|
5232
5290
|
function createPreviewPublisher(defaultFile) {
|
|
5233
5291
|
const directory = previewDirectory(defaultFile);
|
|
5234
5292
|
const enabledFile = join5(directory, ENABLED);
|
|
5235
|
-
const enabled = () =>
|
|
5293
|
+
const enabled = () => existsSync4(enabledFile);
|
|
5236
5294
|
const refresh = (page) => {
|
|
5237
5295
|
try {
|
|
5238
5296
|
const path = previewFile(defaultFile, page);
|
|
@@ -5243,9 +5301,9 @@ function createPreviewPublisher(defaultFile) {
|
|
|
5243
5301
|
for (const source of listPageFiles(defaultFile)) {
|
|
5244
5302
|
const slug = pageIdOfFile(defaultFile, source);
|
|
5245
5303
|
if (slug !== void 0 && !ID_RULE.test(slug)) throw new Error(`Invalid map page filename: ${source}`);
|
|
5246
|
-
const
|
|
5247
|
-
if (!
|
|
5248
|
-
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 });
|
|
5249
5307
|
}
|
|
5250
5308
|
if (page !== void 0 && !pages.some((p) => p.page === page)) return err(`No map page named "${page}".`);
|
|
5251
5309
|
if (page === void 0 && !pages.some((p) => p.page === void 0)) pages.unshift({ page: void 0, map: EMPTY_MAP });
|
|
@@ -5254,7 +5312,7 @@ function createPreviewPublisher(defaultFile) {
|
|
|
5254
5312
|
const present = /* @__PURE__ */ new Set();
|
|
5255
5313
|
for (const item of pages) {
|
|
5256
5314
|
const svg = renderMapSvg(item.map);
|
|
5257
|
-
const digest =
|
|
5315
|
+
const digest = createHash2("sha256").update(svg).digest("hex");
|
|
5258
5316
|
const image = `images/${digest}.svg`;
|
|
5259
5317
|
save(join5(images, `${digest}.svg`), svg);
|
|
5260
5318
|
const filename = documentName(item.page);
|
|
@@ -5435,12 +5493,117 @@ function attachTerminalService(server, options) {
|
|
|
5435
5493
|
};
|
|
5436
5494
|
}
|
|
5437
5495
|
|
|
5496
|
+
// src/support/open-star.ts
|
|
5497
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5498
|
+
import { win32 } from "node:path";
|
|
5499
|
+
|
|
5500
|
+
// src/support/star-reminder.ts
|
|
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";
|
|
5503
|
+
var STAR_URL = "https://github.com/GuangminJu/mellos-mapping";
|
|
5504
|
+
var DAY_MS = 864e5;
|
|
5505
|
+
function isNpmInstallation(entry) {
|
|
5506
|
+
try {
|
|
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"));
|
|
5510
|
+
return pkg.name === "mellos-mapping" && pkg.bin?.mmap === "dist/mmap.mjs";
|
|
5511
|
+
} catch {
|
|
5512
|
+
return false;
|
|
5513
|
+
}
|
|
5514
|
+
}
|
|
5515
|
+
function starReminderFile(userBase) {
|
|
5516
|
+
return join6(userBase, ".mellos", "support", "star-reminder.json");
|
|
5517
|
+
}
|
|
5518
|
+
function enabledFlag(value) {
|
|
5519
|
+
return !!value && value !== "0" && value.toLowerCase() !== "false";
|
|
5520
|
+
}
|
|
5521
|
+
function validState(value) {
|
|
5522
|
+
if (!value || typeof value !== "object") return false;
|
|
5523
|
+
const s = value;
|
|
5524
|
+
return s.version === 1 && Number.isFinite(s.firstSeen) && s.firstSeen >= 0 && Number.isSafeInteger(s.lastDay) && s.lastDay >= Math.floor(s.firstSeen / DAY_MS) && Number.isSafeInteger(s.days) && s.days >= 1 && s.days <= 3 && typeof s.notified === "boolean";
|
|
5525
|
+
}
|
|
5526
|
+
function createStarReminder(options) {
|
|
5527
|
+
const eligible = isNpmInstallation(options.entry) && !enabledFlag(options.env["CI"]) && !enabledFlag(options.env["MELLOS_MAPPING_NO_STAR"]);
|
|
5528
|
+
const file = starReminderFile(options.userBase);
|
|
5529
|
+
return {
|
|
5530
|
+
visit() {
|
|
5531
|
+
if (!eligible) return false;
|
|
5532
|
+
try {
|
|
5533
|
+
return withStoreLock(file, () => {
|
|
5534
|
+
const now = (options.now ?? Date.now)();
|
|
5535
|
+
if (!Number.isFinite(now) || now < 0) return false;
|
|
5536
|
+
const day = Math.floor(now / DAY_MS);
|
|
5537
|
+
let state;
|
|
5538
|
+
try {
|
|
5539
|
+
const saved2 = JSON.parse(readFileSync4(file, "utf8"));
|
|
5540
|
+
if (!validState(saved2)) return false;
|
|
5541
|
+
state = saved2;
|
|
5542
|
+
} catch (error) {
|
|
5543
|
+
if (error.code !== "ENOENT") return false;
|
|
5544
|
+
state = { version: 1, firstSeen: now, lastDay: day, days: 1, notified: false };
|
|
5545
|
+
}
|
|
5546
|
+
if (state.notified || now < state.firstSeen || day < state.lastDay) return false;
|
|
5547
|
+
if (day > state.lastDay) {
|
|
5548
|
+
state.days = Math.min(3, state.days + 1);
|
|
5549
|
+
state.lastDay = day;
|
|
5550
|
+
}
|
|
5551
|
+
const show = state.days >= 3 && now - state.firstSeen >= 7 * DAY_MS;
|
|
5552
|
+
if (show) state.notified = true;
|
|
5553
|
+
const saved = writeAtomic(file, JSON.stringify(state, null, 2) + "\n", 1);
|
|
5554
|
+
return saved.ok && show;
|
|
5555
|
+
});
|
|
5556
|
+
} catch {
|
|
5557
|
+
return false;
|
|
5558
|
+
}
|
|
5559
|
+
}
|
|
5560
|
+
};
|
|
5561
|
+
}
|
|
5562
|
+
|
|
5563
|
+
// src/support/open-star.ts
|
|
5564
|
+
function starBrowserCommand(platform, windowsRoot = "C:\\Windows") {
|
|
5565
|
+
if (platform === "win32") return {
|
|
5566
|
+
file: win32.join(windowsRoot, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"),
|
|
5567
|
+
args: [
|
|
5568
|
+
"-NoLogo",
|
|
5569
|
+
"-NoProfile",
|
|
5570
|
+
"-NonInteractive",
|
|
5571
|
+
"-WindowStyle",
|
|
5572
|
+
"Hidden",
|
|
5573
|
+
"-Command",
|
|
5574
|
+
`$ErrorActionPreference = 'Stop'; Start-Process -FilePath '${STAR_URL}'`
|
|
5575
|
+
]
|
|
5576
|
+
};
|
|
5577
|
+
if (platform === "darwin") return { file: "/usr/bin/open", args: [STAR_URL] };
|
|
5578
|
+
if (platform === "linux") return { file: "xdg-open", args: [STAR_URL] };
|
|
5579
|
+
throw new Error("No supported desktop browser launcher");
|
|
5580
|
+
}
|
|
5581
|
+
function openStarInDefaultBrowser() {
|
|
5582
|
+
const command = starBrowserCommand(process.platform, process.env["SystemRoot"]);
|
|
5583
|
+
return new Promise((resolve3, reject) => {
|
|
5584
|
+
const child = spawn3(command.file, command.args, { windowsHide: true, stdio: "ignore" });
|
|
5585
|
+
let finished = false;
|
|
5586
|
+
const finish = (error) => {
|
|
5587
|
+
if (finished) return;
|
|
5588
|
+
finished = true;
|
|
5589
|
+
clearTimeout(deadline);
|
|
5590
|
+
child.unref();
|
|
5591
|
+
if (error) reject(error);
|
|
5592
|
+
else resolve3();
|
|
5593
|
+
};
|
|
5594
|
+
const deadline = setTimeout(() => finish(), 5e3);
|
|
5595
|
+
child.once("error", (error) => finish(error));
|
|
5596
|
+
child.once("exit", (code) => finish(code === 0 ? void 0 : new Error("Default browser launch failed")));
|
|
5597
|
+
});
|
|
5598
|
+
}
|
|
5599
|
+
|
|
5438
5600
|
// src/web/service.ts
|
|
5439
5601
|
async function startWebService(defaultFile, assets, options = {}) {
|
|
5440
5602
|
const token = options.token ?? randomBytes(24).toString("hex");
|
|
5441
5603
|
if (!/^[a-f0-9]{48}$/.test(token)) throw new Error("Invalid web token");
|
|
5442
5604
|
const prefix = `/${token}/`;
|
|
5443
5605
|
let origin = "", lastRequest = Date.now(), closed = false;
|
|
5606
|
+
let openingStar = false;
|
|
5444
5607
|
const send = (res, code, body, type = "application/json; charset=utf-8") => {
|
|
5445
5608
|
res.writeHead(code, {
|
|
5446
5609
|
"Content-Type": type,
|
|
@@ -5489,7 +5652,37 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5489
5652
|
}
|
|
5490
5653
|
}
|
|
5491
5654
|
if (req.method === "GET" && route === "api/health") {
|
|
5492
|
-
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"] }));
|
|
5656
|
+
return;
|
|
5657
|
+
}
|
|
5658
|
+
if (req.method === "POST" && route === "api/star-reminder/open") {
|
|
5659
|
+
if (req.headers.origin !== origin) {
|
|
5660
|
+
send(res, 403, '{"error":"Origin refused"}');
|
|
5661
|
+
return;
|
|
5662
|
+
}
|
|
5663
|
+
if (req.url !== `${prefix}api/star-reminder/open` || req.headers["transfer-encoding"] || Number(req.headers["content-length"] ?? 0) !== 0) {
|
|
5664
|
+
send(res, 400, '{"error":"No parameters accepted"}');
|
|
5665
|
+
return;
|
|
5666
|
+
}
|
|
5667
|
+
if (openingStar) {
|
|
5668
|
+
send(res, 409, '{"error":"Browser is already opening"}');
|
|
5669
|
+
return;
|
|
5670
|
+
}
|
|
5671
|
+
openingStar = true;
|
|
5672
|
+
void Promise.resolve().then(options.openStar ?? openStarInDefaultBrowser).then(() => {
|
|
5673
|
+
send(res, 200, '{"opened":true}');
|
|
5674
|
+
}, () => {
|
|
5675
|
+
send(res, 503, '{"error":"\u65E0\u6CD5\u6253\u5F00\u9ED8\u8BA4\u6D4F\u89C8\u5668"}');
|
|
5676
|
+
}).finally(() => {
|
|
5677
|
+
openingStar = false;
|
|
5678
|
+
});
|
|
5679
|
+
return;
|
|
5680
|
+
}
|
|
5681
|
+
if (req.method === "POST" && route === "api/star-reminder/visit") {
|
|
5682
|
+
const requested = new URL(req.url, origin).searchParams.get("page");
|
|
5683
|
+
const id = !requested || requested === "_default" ? "" : requested;
|
|
5684
|
+
const hasMap = options.starReminder && readWebSnapshot(defaultFile).value.pages.some((page) => page.id === id && !page.error && (page.map?.nodes.length ?? 0) > 0);
|
|
5685
|
+
send(res, 200, JSON.stringify({ show: !!hasMap && options.starReminder.visit() }));
|
|
5493
5686
|
return;
|
|
5494
5687
|
}
|
|
5495
5688
|
if (req.method === "GET" && route === "api/state") {
|
|
@@ -5578,23 +5771,23 @@ async function startWebService(defaultFile, assets, options = {}) {
|
|
|
5578
5771
|
|
|
5579
5772
|
// src/web/cli.ts
|
|
5580
5773
|
async function runWeb(args) {
|
|
5581
|
-
const entry =
|
|
5774
|
+
const entry = fileURLToPath2(import.meta.url);
|
|
5582
5775
|
if (args[0] === "--serve" && args.length === 2) {
|
|
5583
5776
|
const file2 = resolve2(args[1]);
|
|
5584
5777
|
if (await runningWebUrl(file2)) return;
|
|
5585
|
-
const directory =
|
|
5778
|
+
const directory = dirname9(webRuntimeFile(file2));
|
|
5586
5779
|
mkdirSync4(directory, { recursive: true });
|
|
5587
|
-
if (
|
|
5588
|
-
const assets =
|
|
5780
|
+
if (realpathSync4(directory).toLowerCase() !== join7(realpathSync4(dirname9(directory)), "web").toLowerCase()) throw new Error("Redirected web runtime directory");
|
|
5781
|
+
const assets = join7(dirname9(entry), "web");
|
|
5589
5782
|
let service;
|
|
5590
5783
|
service = await startWebService(file2, {
|
|
5591
|
-
html: readFileSync5(
|
|
5592
|
-
javascript: readFileSync5(
|
|
5593
|
-
css: readFileSync5(
|
|
5594
|
-
terminal: { html: readFileSync5(
|
|
5595
|
-
}, { terminalWorker:
|
|
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") }
|
|
5788
|
+
}, { starReminder: createStarReminder({ entry, userBase: homedir(), env: process.env }), terminalWorker: join7(dirname9(entry), "terminal-worker.mjs"), onClose: () => {
|
|
5596
5789
|
try {
|
|
5597
|
-
if (JSON.parse(readFileSync5(webRuntimeFile(file2), "utf8")).token === service.token)
|
|
5790
|
+
if (JSON.parse(readFileSync5(webRuntimeFile(file2), "utf8")).token === service.token) rmSync3(webRuntimeFile(file2));
|
|
5598
5791
|
} catch {
|
|
5599
5792
|
}
|
|
5600
5793
|
} });
|
|
@@ -5625,8 +5818,8 @@ async function runWeb(args) {
|
|
|
5625
5818
|
else throw new Error(usage);
|
|
5626
5819
|
}
|
|
5627
5820
|
const project = resolve2(args[0]);
|
|
5628
|
-
if (!
|
|
5629
|
-
const file =
|
|
5821
|
+
if (!existsSync6(project) || !statSync2(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
|
|
5822
|
+
const file = join7(project, STATE_FILE_RELATIVE_PATH);
|
|
5630
5823
|
if (stop) {
|
|
5631
5824
|
const url = await runningWebUrl(file);
|
|
5632
5825
|
if (url) await fetch(`${url}api/stop`, { method: "POST", signal: AbortSignal.timeout(2e3) });
|
|
@@ -5635,7 +5828,7 @@ async function runWeb(args) {
|
|
|
5635
5828
|
}
|
|
5636
5829
|
console.log(JSON.stringify({ surface: terminal ? "web-terminal" : "web", url: await openWebPreview(file, entry, page, terminal), visibility: "unconfirmed" }));
|
|
5637
5830
|
}
|
|
5638
|
-
if (process.argv[1] &&
|
|
5831
|
+
if (process.argv[1] && existsSync6(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync4(process.argv[1])).href) {
|
|
5639
5832
|
runWeb(process.argv.slice(2)).catch((error) => {
|
|
5640
5833
|
console.error(String(error));
|
|
5641
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
|
|