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/dist/web.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { createRequire } from 'node:module'; const require = createRequire(import.meta.url);
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: createHash4 } = __require("crypto");
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 = createHash4("sha1").update(key + GUID).digest("base64");
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: createHash4 } = __require("crypto");
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 = createHash4("sha1").update(key + GUID).digest("base64");
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 existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync5, realpathSync as realpathSync2, rmSync as rmSync4, statSync as statSync2 } from "node:fs";
3707
- import { dirname as dirname8, join as join6, resolve as resolve2 } from "node:path";
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 dirname3, join as join2 } from "node:path";
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/maps.ts
4305
- import { readFileSync } from "node:fs";
4306
- function loadMapFile(path) {
4307
- let text;
4308
- try {
4309
- text = readFileSync(path, "utf8");
4310
- } catch (e) {
4311
- const code = e.code;
4312
- if (code === "ENOENT") return err({ kind: "not-found", path });
4313
- throw e;
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
- let raw;
4316
- try {
4317
- raw = JSON.parse(stripBom(text));
4318
- } catch (e) {
4319
- return err({ kind: "malformed-json", path, detail: e.message });
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
- return parseMap(raw, path);
4328
+ loaded = backend;
4329
+ return backend;
4322
4330
  }
4323
4331
 
4324
4332
  // src/store/transaction.ts
4325
- import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync3, writeFileSync as writeFileSync2 } from "node:fs";
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 = dirname4(file);
4337
- return dir.endsWith("/pages") || dir.endsWith("\\pages") ? dirname4(dir) : dir;
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
- const owner = JSON.parse(readFileSync2(join3(lock, "owner.json"), "utf8"));
4342
- if (!Number.isSafeInteger(owner.pid) || owner.pid <= 0) return false;
4343
- try {
4344
- process.kill(owner.pid, 0);
4345
- return false;
4346
- } catch (e) {
4347
- return e.code === "ESRCH";
4348
- }
4349
- } catch {
4350
- return false;
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 = join3(dir, ".write-lock");
4357
- const owner = join3(lock, "owner.json");
4358
- const token = randomUUID();
4374
+ const lock = join2(dir, ".write-lock");
4375
+ checkLockPath(lock);
4376
+ let fd;
4359
4377
  try {
4360
- mkdirSync2(lock);
4378
+ fd = openSync(lock, "a+", 384);
4361
4379
  } catch (error) {
4362
- if (error.code !== "EEXIST") throw error;
4363
- if (!deadOwner(lock)) throw new LedgerError("BUSY", `Another writer owns ${lock}; retry after it completes. An orphan without owner metadata needs manual inspection.`);
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 initialized = false;
4383
+ let acquired = false;
4381
4384
  try {
4382
- writeFileSync2(owner, JSON.stringify({ pid: process.pid, token }), { flag: "wx" });
4383
- initialized = true;
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 (!initialized || JSON.parse(readFileSync2(owner, "utf8")).token === token) rmSync3(lock, { recursive: true });
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 existsSync2, readFileSync as readFileSync3 } from "node:fs";
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 as createHash2 } from "node:crypto";
4445
+ import { createHash } from "node:crypto";
4400
4446
  import { statSync } from "node:fs";
4401
- import { basename as basename2, dirname as dirname5 } from "node:path";
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: basename2(dirname5(dirname5(defaultFile))), pages };
4417
- return { revision: createHash2("sha256").update(JSON.stringify(value)).digest("hex"), value };
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 runningWebUrl(defaultFile) {
4468
+ async function runningWebRuntime(defaultFile) {
4423
4469
  try {
4424
- const info = JSON.parse(readFileSync3(webRuntimeFile(defaultFile), "utf8"));
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 && (await response.json()).file === defaultFile) return url;
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 url = await runningWebUrl(defaultFile);
4436
- if (url) {
4437
- const health = await fetch(`${url}api/health`, { signal: AbortSignal.timeout(2e3) });
4438
- const info = await health.json();
4439
- if (!info.formats?.includes(2) || terminal && !info.surfaces?.includes("web-terminal")) {
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
- url = void 0;
4497
+ runtime = void 0;
4447
4498
  }
4448
4499
  }
4449
- if (!url) {
4450
- if (!existsSync2(entry)) throw new Error(`Web runtime missing: ${entry}. Run npm run build or reinstall the plugin.`);
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 (!url && Date.now() < deadline) {
4509
+ while (!runtime && Date.now() < deadline) {
4459
4510
  if (failure) throw failure;
4460
4511
  await new Promise((resolve3) => setTimeout(resolve3, 100));
4461
- url = await runningWebUrl(defaultFile);
4512
+ runtime = await runningWebRuntime(defaultFile);
4462
4513
  }
4463
- if (!url) throw new Error("Web preview did not start. Run the web CLI directly to inspect the error.");
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 createHash3 } from "node:crypto";
4477
- import { existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, readdirSync as readdirSync2, realpathSync, rmdirSync } from "node:fs";
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 (readFileSync4(path, "utf8") === contents) return;
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(realpathSync(dirname7(path)), path.slice(dirname7(path).length + 1));
5213
- const actual = realpathSync(path);
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 = () => existsSync3(enabledFile);
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 loaded = loadMapFile(source);
5247
- if (!loaded.ok) throw new Error(describeStoreError(loaded.error));
5248
- pages.push({ page: slug, map: loaded.value });
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 = createHash3("sha256").update(svg).digest("hex");
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 = fileURLToPath(import.meta.url);
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 = dirname8(webRuntimeFile(file2));
5778
+ const directory = dirname9(webRuntimeFile(file2));
5586
5779
  mkdirSync4(directory, { recursive: true });
5587
- if (realpathSync2(directory).toLowerCase() !== join6(realpathSync2(dirname8(directory)), "web").toLowerCase()) throw new Error("Redirected web runtime directory");
5588
- const assets = join6(dirname8(entry), "web");
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(join6(assets, "index.html"), "utf8"),
5592
- javascript: readFileSync5(join6(assets, "app.js"), "utf8"),
5593
- css: readFileSync5(join6(assets, "app.css"), "utf8"),
5594
- terminal: { html: readFileSync5(join6(assets, "terminal.html"), "utf8"), javascript: readFileSync5(join6(assets, "terminal.js"), "utf8"), css: readFileSync5(join6(assets, "terminal.css"), "utf8"), xtermCss: readFileSync5(join6(assets, "xterm.css"), "utf8") }
5595
- }, { terminalWorker: join6(dirname8(entry), "terminal-worker.mjs"), onClose: () => {
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) rmSync4(webRuntimeFile(file2));
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 (!existsSync4(project) || !statSync2(project).isDirectory()) throw new Error(`Project directory does not exist: ${project}`);
5629
- const file = join6(project, STATE_FILE_RELATIVE_PATH);
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] && existsSync4(process.argv[1]) && import.meta.url === pathToFileURL(realpathSync2(process.argv[1])).href) {
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 commands (tested with 0.153.4).
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