glotfile 0.8.0 → 0.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/server/cli.js
CHANGED
|
@@ -1579,7 +1579,8 @@ function attrEscape(s) {
|
|
|
1579
1579
|
return xmlEscape2(s).replace(/"/g, """);
|
|
1580
1580
|
}
|
|
1581
1581
|
function angularXMeta(placeholders, name) {
|
|
1582
|
-
|
|
1582
|
+
const meta = placeholders?.[name];
|
|
1583
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
|
|
1583
1584
|
}
|
|
1584
1585
|
function renderInterpolations(text, ids, placeholders) {
|
|
1585
1586
|
let out = "";
|
|
@@ -4823,18 +4824,20 @@ function decodeInline(raw, addMeta) {
|
|
|
4823
4824
|
const meta = {};
|
|
4824
4825
|
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4825
4826
|
if (equiv !== void 0) meta.example = equiv;
|
|
4827
|
+
if (!ANGULAR_CONVENTION_ID.test(id)) meta.origin = "x";
|
|
4826
4828
|
addMeta(id, meta);
|
|
4827
4829
|
}
|
|
4828
4830
|
last = m.index + m[0].length;
|
|
4829
4831
|
}
|
|
4830
4832
|
return out + decodeEntities(raw.slice(last));
|
|
4831
4833
|
}
|
|
4832
|
-
var LOCALE_RE5, FILE_RE, angularXliff2;
|
|
4834
|
+
var LOCALE_RE5, FILE_RE, ANGULAR_CONVENTION_ID, angularXliff2;
|
|
4833
4835
|
var init_angular_xliff2 = __esm({
|
|
4834
4836
|
"src/server/import/parsers/angular-xliff.ts"() {
|
|
4835
4837
|
"use strict";
|
|
4836
4838
|
LOCALE_RE5 = /^[a-z]{2,3}([_-][A-Za-z]{2,4}){0,2}$/;
|
|
4837
4839
|
FILE_RE = /^messages(?:\.(.+))?\.xlf$/;
|
|
4840
|
+
ANGULAR_CONVENTION_ID = /^[A-Z][A-Z0-9_]*$/;
|
|
4838
4841
|
angularXliff2 = {
|
|
4839
4842
|
name: "angular-xliff",
|
|
4840
4843
|
parse(localeRoot, opts) {
|
|
@@ -6619,10 +6622,139 @@ var init_ui_prefs = __esm({
|
|
|
6619
6622
|
}
|
|
6620
6623
|
});
|
|
6621
6624
|
|
|
6625
|
+
// src/server/events.ts
|
|
6626
|
+
function createEventHub() {
|
|
6627
|
+
const senders = /* @__PURE__ */ new Set();
|
|
6628
|
+
return {
|
|
6629
|
+
subscribe(send) {
|
|
6630
|
+
senders.add(send);
|
|
6631
|
+
return () => senders.delete(send);
|
|
6632
|
+
},
|
|
6633
|
+
broadcast(event, data) {
|
|
6634
|
+
for (const send of [...senders]) {
|
|
6635
|
+
try {
|
|
6636
|
+
send(event, data);
|
|
6637
|
+
} catch {
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
},
|
|
6641
|
+
size() {
|
|
6642
|
+
return senders.size;
|
|
6643
|
+
}
|
|
6644
|
+
};
|
|
6645
|
+
}
|
|
6646
|
+
var init_events = __esm({
|
|
6647
|
+
"src/server/events.ts"() {
|
|
6648
|
+
"use strict";
|
|
6649
|
+
}
|
|
6650
|
+
});
|
|
6651
|
+
|
|
6652
|
+
// src/server/watch.ts
|
|
6653
|
+
import { statSync as statSync8, readdirSync as readdirSync14 } from "fs";
|
|
6654
|
+
import { join as join17 } from "path";
|
|
6655
|
+
import { createHash as createHash2 } from "crypto";
|
|
6656
|
+
function hashState(state) {
|
|
6657
|
+
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
6658
|
+
}
|
|
6659
|
+
function signature(statePath) {
|
|
6660
|
+
const fmt = detectFormat(statePath);
|
|
6661
|
+
if (fmt === "none") return "none";
|
|
6662
|
+
if (fmt === "single") {
|
|
6663
|
+
const s = statSync8(statePath);
|
|
6664
|
+
return `single:${s.size}:${s.mtimeMs}`;
|
|
6665
|
+
}
|
|
6666
|
+
const dir = splitDirFor(statePath);
|
|
6667
|
+
const parts = [];
|
|
6668
|
+
for (const rel of ["config.json", "keys.json"]) {
|
|
6669
|
+
try {
|
|
6670
|
+
const s = statSync8(join17(dir, rel));
|
|
6671
|
+
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6672
|
+
} catch {
|
|
6673
|
+
}
|
|
6674
|
+
}
|
|
6675
|
+
try {
|
|
6676
|
+
for (const name of readdirSync14(join17(dir, "locales")).sort()) {
|
|
6677
|
+
if (!name.endsWith(".json")) continue;
|
|
6678
|
+
const s = statSync8(join17(dir, "locales", name));
|
|
6679
|
+
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6680
|
+
}
|
|
6681
|
+
} catch {
|
|
6682
|
+
}
|
|
6683
|
+
return `split:${parts.join("|")}`;
|
|
6684
|
+
}
|
|
6685
|
+
function createStateWatcher(opts) {
|
|
6686
|
+
const intervalMs = opts.intervalMs ?? 750;
|
|
6687
|
+
let statePath = opts.statePath;
|
|
6688
|
+
let lastSig = "";
|
|
6689
|
+
let lastHash = "";
|
|
6690
|
+
let timer;
|
|
6691
|
+
function baseline() {
|
|
6692
|
+
try {
|
|
6693
|
+
lastSig = signature(statePath);
|
|
6694
|
+
lastHash = hashState(loadState(statePath));
|
|
6695
|
+
} catch {
|
|
6696
|
+
lastSig = "";
|
|
6697
|
+
lastHash = "";
|
|
6698
|
+
}
|
|
6699
|
+
}
|
|
6700
|
+
function check() {
|
|
6701
|
+
let sig;
|
|
6702
|
+
try {
|
|
6703
|
+
sig = signature(statePath);
|
|
6704
|
+
} catch {
|
|
6705
|
+
return;
|
|
6706
|
+
}
|
|
6707
|
+
if (sig === lastSig) return;
|
|
6708
|
+
let hash;
|
|
6709
|
+
try {
|
|
6710
|
+
hash = hashState(loadState(statePath));
|
|
6711
|
+
} catch {
|
|
6712
|
+
lastSig = sig;
|
|
6713
|
+
return;
|
|
6714
|
+
}
|
|
6715
|
+
lastSig = sig;
|
|
6716
|
+
if (hash !== lastHash) {
|
|
6717
|
+
lastHash = hash;
|
|
6718
|
+
opts.onChange();
|
|
6719
|
+
}
|
|
6720
|
+
}
|
|
6721
|
+
function noteWrite(state) {
|
|
6722
|
+
try {
|
|
6723
|
+
lastSig = signature(statePath);
|
|
6724
|
+
} catch {
|
|
6725
|
+
lastSig = "";
|
|
6726
|
+
}
|
|
6727
|
+
lastHash = hashState(state);
|
|
6728
|
+
}
|
|
6729
|
+
function retarget(next) {
|
|
6730
|
+
statePath = next;
|
|
6731
|
+
baseline();
|
|
6732
|
+
}
|
|
6733
|
+
function start() {
|
|
6734
|
+
if (timer) return;
|
|
6735
|
+
timer = setInterval(check, intervalMs);
|
|
6736
|
+
timer.unref?.();
|
|
6737
|
+
}
|
|
6738
|
+
function stop() {
|
|
6739
|
+
if (timer) clearInterval(timer);
|
|
6740
|
+
timer = void 0;
|
|
6741
|
+
}
|
|
6742
|
+
baseline();
|
|
6743
|
+
return { check, noteWrite, retarget, start, stop };
|
|
6744
|
+
}
|
|
6745
|
+
var init_watch = __esm({
|
|
6746
|
+
"src/server/watch.ts"() {
|
|
6747
|
+
"use strict";
|
|
6748
|
+
init_state();
|
|
6749
|
+
init_format();
|
|
6750
|
+
init_storage();
|
|
6751
|
+
}
|
|
6752
|
+
});
|
|
6753
|
+
|
|
6622
6754
|
// src/server/api.ts
|
|
6623
6755
|
import { Hono } from "hono";
|
|
6624
6756
|
import { streamSSE } from "hono/streaming";
|
|
6625
|
-
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as
|
|
6757
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync9, rmSync as rmSync6 } from "fs";
|
|
6626
6758
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
6627
6759
|
function projectName(root) {
|
|
6628
6760
|
const nameFile = resolve9(root, ".idea", ".name");
|
|
@@ -6654,6 +6786,14 @@ function createApi(deps) {
|
|
|
6654
6786
|
const app = new Hono();
|
|
6655
6787
|
const load = () => loadState(deps.statePath);
|
|
6656
6788
|
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
6789
|
+
const hub = deps.eventHub ?? createEventHub();
|
|
6790
|
+
const watcher = createStateWatcher({
|
|
6791
|
+
statePath: deps.statePath,
|
|
6792
|
+
intervalMs: deps.watchIntervalMs,
|
|
6793
|
+
onChange: () => hub.broadcast("state-changed", JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
6794
|
+
});
|
|
6795
|
+
deps.onWatcher?.(watcher);
|
|
6796
|
+
if (deps.watch) watcher.start();
|
|
6657
6797
|
let translateQueue = Promise.resolve();
|
|
6658
6798
|
const withTranslateLock = (fn) => {
|
|
6659
6799
|
const next = translateQueue.then(fn, fn);
|
|
@@ -6675,12 +6815,30 @@ function createApi(deps) {
|
|
|
6675
6815
|
};
|
|
6676
6816
|
const persist = (s) => {
|
|
6677
6817
|
saveState(deps.statePath, s);
|
|
6818
|
+
watcher.noteWrite(s);
|
|
6678
6819
|
scheduleAutoExport(s);
|
|
6679
6820
|
};
|
|
6680
6821
|
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6681
6822
|
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
6682
6823
|
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
6683
6824
|
app.get("/state", (c) => c.json(load()));
|
|
6825
|
+
app.get("/events", (c) => streamSSE(c, async (stream) => {
|
|
6826
|
+
const send = (event, data) => {
|
|
6827
|
+
void stream.writeSSE({ event, data });
|
|
6828
|
+
};
|
|
6829
|
+
const unsubscribe = hub.subscribe(send);
|
|
6830
|
+
stream.onAbort(unsubscribe);
|
|
6831
|
+
await stream.writeSSE({ event: "ready", data: "" });
|
|
6832
|
+
try {
|
|
6833
|
+
while (!stream.aborted) {
|
|
6834
|
+
await stream.sleep(3e4);
|
|
6835
|
+
if (stream.aborted) break;
|
|
6836
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
6837
|
+
}
|
|
6838
|
+
} finally {
|
|
6839
|
+
unsubscribe();
|
|
6840
|
+
}
|
|
6841
|
+
}));
|
|
6684
6842
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
6685
6843
|
app.put("/ui-prefs", async (c) => {
|
|
6686
6844
|
const body = await c.req.json();
|
|
@@ -6768,7 +6926,7 @@ function createApi(deps) {
|
|
|
6768
6926
|
if (depth > 4) return;
|
|
6769
6927
|
let entries = [];
|
|
6770
6928
|
try {
|
|
6771
|
-
entries =
|
|
6929
|
+
entries = readdirSync15(dir);
|
|
6772
6930
|
} catch {
|
|
6773
6931
|
return;
|
|
6774
6932
|
}
|
|
@@ -6782,7 +6940,7 @@ function createApi(deps) {
|
|
|
6782
6940
|
filePath = abs;
|
|
6783
6941
|
} else {
|
|
6784
6942
|
try {
|
|
6785
|
-
if (
|
|
6943
|
+
if (statSync9(abs).isDirectory()) walk(abs, depth + 1);
|
|
6786
6944
|
} catch {
|
|
6787
6945
|
}
|
|
6788
6946
|
continue;
|
|
@@ -6813,6 +6971,7 @@ function createApi(deps) {
|
|
|
6813
6971
|
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6814
6972
|
loadState(resolved);
|
|
6815
6973
|
deps.statePath = resolved;
|
|
6974
|
+
watcher.retarget(resolved);
|
|
6816
6975
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
6817
6976
|
});
|
|
6818
6977
|
app.post("/keys", async (c) => {
|
|
@@ -7816,6 +7975,8 @@ var init_api = __esm({
|
|
|
7816
7975
|
init_ui_prefs();
|
|
7817
7976
|
init_local_settings();
|
|
7818
7977
|
init_atomic_write();
|
|
7978
|
+
init_events();
|
|
7979
|
+
init_watch();
|
|
7819
7980
|
sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
7820
7981
|
screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
7821
7982
|
}
|
|
@@ -7830,7 +7991,7 @@ __export(server_exports, {
|
|
|
7830
7991
|
import { Hono as Hono2 } from "hono";
|
|
7831
7992
|
import { serve } from "@hono/node-server";
|
|
7832
7993
|
import { fileURLToPath } from "url";
|
|
7833
|
-
import { dirname as dirname4, join as
|
|
7994
|
+
import { dirname as dirname4, join as join18, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
7834
7995
|
import { readFile, stat } from "fs/promises";
|
|
7835
7996
|
import { createServer } from "net";
|
|
7836
7997
|
import open from "open";
|
|
@@ -7847,7 +8008,12 @@ async function readFileResponse(absPath) {
|
|
|
7847
8008
|
}
|
|
7848
8009
|
function buildApp(opts) {
|
|
7849
8010
|
const app = new Hono2();
|
|
7850
|
-
const apiDeps = {
|
|
8011
|
+
const apiDeps = {
|
|
8012
|
+
statePath: opts.statePath,
|
|
8013
|
+
autoExport: true,
|
|
8014
|
+
watch: opts.watch,
|
|
8015
|
+
onWatcher: opts.onWatcher
|
|
8016
|
+
};
|
|
7851
8017
|
app.route("/api", createApi(apiDeps));
|
|
7852
8018
|
app.get("/:dir/*", async (c, next) => {
|
|
7853
8019
|
const dirSeg = c.req.param("dir");
|
|
@@ -7873,7 +8039,7 @@ function buildApp(opts) {
|
|
|
7873
8039
|
const file = await readFileResponse(target);
|
|
7874
8040
|
if (file) return file;
|
|
7875
8041
|
}
|
|
7876
|
-
const index = await readFileResponse(
|
|
8042
|
+
const index = await readFileResponse(join18(root, "index.html"));
|
|
7877
8043
|
if (index) return index;
|
|
7878
8044
|
return c.notFound();
|
|
7879
8045
|
});
|
|
@@ -7898,13 +8064,19 @@ function findAvailablePort(start) {
|
|
|
7898
8064
|
});
|
|
7899
8065
|
}
|
|
7900
8066
|
async function startServer(opts) {
|
|
7901
|
-
|
|
8067
|
+
let watcher;
|
|
8068
|
+
const app = buildApp({ ...opts, watch: opts.watch ?? true, onWatcher: (w) => {
|
|
8069
|
+
watcher = w;
|
|
8070
|
+
} });
|
|
7902
8071
|
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
7903
8072
|
return new Promise((resolveP) => {
|
|
7904
8073
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
7905
8074
|
const url = `http://127.0.0.1:${info.port}`;
|
|
7906
8075
|
if (opts.open !== false && !opts.dev) void open(url);
|
|
7907
|
-
resolveP({ url, close: () =>
|
|
8076
|
+
resolveP({ url, close: () => {
|
|
8077
|
+
watcher?.stop();
|
|
8078
|
+
server.close();
|
|
8079
|
+
} });
|
|
7908
8080
|
backgroundScan(opts.statePath);
|
|
7909
8081
|
});
|
|
7910
8082
|
});
|
|
@@ -7937,7 +8109,7 @@ var init_server = __esm({
|
|
|
7937
8109
|
init_scanner();
|
|
7938
8110
|
init_usage();
|
|
7939
8111
|
here = dirname4(fileURLToPath(import.meta.url));
|
|
7940
|
-
DEFAULT_UI_DIR =
|
|
8112
|
+
DEFAULT_UI_DIR = join18(here, "..", "ui");
|
|
7941
8113
|
MIME = {
|
|
7942
8114
|
".html": "text/html; charset=utf-8",
|
|
7943
8115
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7987,7 +8159,7 @@ init_usage();
|
|
|
7987
8159
|
init_context();
|
|
7988
8160
|
init_run2();
|
|
7989
8161
|
init_outputs();
|
|
7990
|
-
import { resolve as resolve11, dirname as dirname5, join as
|
|
8162
|
+
import { resolve as resolve11, dirname as dirname5, join as join19 } from "path";
|
|
7991
8163
|
import { readFileSync as readFileSync24, existsSync as existsSync14, mkdirSync as mkdirSync6, cpSync } from "fs";
|
|
7992
8164
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7993
8165
|
|
|
@@ -8751,10 +8923,10 @@ function runSplit(args) {
|
|
|
8751
8923
|
`Split catalog into ${splitDirFor(args.statePath)}/ (config.json, keys.json, locales/ \u2014 up to ${state.config.locales.length} locale files). Removed ${args.statePath}.`
|
|
8752
8924
|
);
|
|
8753
8925
|
}
|
|
8754
|
-
var SKILL_SRC =
|
|
8926
|
+
var SKILL_SRC = join19(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "skill");
|
|
8755
8927
|
function runSkill(args) {
|
|
8756
8928
|
if (args.print) {
|
|
8757
|
-
console.log(readFileSync24(
|
|
8929
|
+
console.log(readFileSync24(join19(SKILL_SRC, "SKILL.md"), "utf8").trimEnd());
|
|
8758
8930
|
return;
|
|
8759
8931
|
}
|
|
8760
8932
|
const dest = resolve11(process.cwd(), ".claude", "skills", "glotfile");
|
|
@@ -8916,7 +9088,7 @@ ${formatOpts([...options, ...GLOBAL_OPTS])}`);
|
|
|
8916
9088
|
);
|
|
8917
9089
|
}
|
|
8918
9090
|
function printVersion() {
|
|
8919
|
-
const pkgPath =
|
|
9091
|
+
const pkgPath = join19(dirname5(fileURLToPath2(import.meta.url)), "..", "..", "package.json");
|
|
8920
9092
|
console.log(JSON.parse(readFileSync24(pkgPath, "utf8")).version);
|
|
8921
9093
|
}
|
|
8922
9094
|
async function main(argv) {
|
package/dist/server/server.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Hono as Hono2 } from "hono";
|
|
3
3
|
import { serve } from "@hono/node-server";
|
|
4
4
|
import { fileURLToPath } from "url";
|
|
5
|
-
import { dirname as dirname4, join as
|
|
5
|
+
import { dirname as dirname4, join as join18, resolve as resolve10, extname as extname3, sep as sep3 } from "path";
|
|
6
6
|
import { readFile, stat } from "fs/promises";
|
|
7
7
|
import { createServer } from "net";
|
|
8
8
|
import open from "open";
|
|
@@ -2594,7 +2594,8 @@ function attrEscape(s) {
|
|
|
2594
2594
|
return xmlEscape2(s).replace(/"/g, """);
|
|
2595
2595
|
}
|
|
2596
2596
|
function angularXMeta(placeholders, name) {
|
|
2597
|
-
|
|
2597
|
+
const meta = placeholders?.[name];
|
|
2598
|
+
return /^[A-Z][A-Z0-9_]*$/.test(name) || meta?.origin === "x" ? meta : void 0;
|
|
2598
2599
|
}
|
|
2599
2600
|
function renderInterpolations(text, ids, placeholders) {
|
|
2600
2601
|
let out = "";
|
|
@@ -2841,7 +2842,7 @@ function checkOutputs(state, root) {
|
|
|
2841
2842
|
}
|
|
2842
2843
|
|
|
2843
2844
|
// src/server/api.ts
|
|
2844
|
-
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as
|
|
2845
|
+
import { readFileSync as readFileSync23, existsSync as existsSync13, readdirSync as readdirSync15, statSync as statSync9, rmSync as rmSync6 } from "fs";
|
|
2845
2846
|
import { dirname as dirname3, resolve as resolve9, basename, relative as relative4, sep as sep2 } from "path";
|
|
2846
2847
|
|
|
2847
2848
|
// src/server/ai/anthropic.ts
|
|
@@ -4788,6 +4789,7 @@ function parseAttrs(s) {
|
|
|
4788
4789
|
for (const m of s.matchAll(/([\w-]+)="([^"]*)"/g)) out[m[1]] = decodeEntities(m[2]);
|
|
4789
4790
|
return out;
|
|
4790
4791
|
}
|
|
4792
|
+
var ANGULAR_CONVENTION_ID = /^[A-Z][A-Z0-9_]*$/;
|
|
4791
4793
|
function decodeLocations(body) {
|
|
4792
4794
|
const out = [];
|
|
4793
4795
|
const seen = /* @__PURE__ */ new Set();
|
|
@@ -4821,6 +4823,7 @@ function decodeInline(raw, addMeta) {
|
|
|
4821
4823
|
const meta = {};
|
|
4822
4824
|
if (attrs["ctype"]) meta.type = attrs["ctype"];
|
|
4823
4825
|
if (equiv !== void 0) meta.example = equiv;
|
|
4826
|
+
if (!ANGULAR_CONVENTION_ID.test(id)) meta.origin = "x";
|
|
4824
4827
|
addMeta(id, meta);
|
|
4825
4828
|
}
|
|
4826
4829
|
last = m.index + m[0].length;
|
|
@@ -6043,6 +6046,122 @@ function aiConfigError(ai) {
|
|
|
6043
6046
|
return null;
|
|
6044
6047
|
}
|
|
6045
6048
|
|
|
6049
|
+
// src/server/events.ts
|
|
6050
|
+
function createEventHub() {
|
|
6051
|
+
const senders = /* @__PURE__ */ new Set();
|
|
6052
|
+
return {
|
|
6053
|
+
subscribe(send) {
|
|
6054
|
+
senders.add(send);
|
|
6055
|
+
return () => senders.delete(send);
|
|
6056
|
+
},
|
|
6057
|
+
broadcast(event, data) {
|
|
6058
|
+
for (const send of [...senders]) {
|
|
6059
|
+
try {
|
|
6060
|
+
send(event, data);
|
|
6061
|
+
} catch {
|
|
6062
|
+
}
|
|
6063
|
+
}
|
|
6064
|
+
},
|
|
6065
|
+
size() {
|
|
6066
|
+
return senders.size;
|
|
6067
|
+
}
|
|
6068
|
+
};
|
|
6069
|
+
}
|
|
6070
|
+
|
|
6071
|
+
// src/server/watch.ts
|
|
6072
|
+
import { statSync as statSync8, readdirSync as readdirSync14 } from "fs";
|
|
6073
|
+
import { join as join17 } from "path";
|
|
6074
|
+
import { createHash as createHash2 } from "crypto";
|
|
6075
|
+
function hashState(state) {
|
|
6076
|
+
return createHash2("sha1").update(serializeJson(state, state.config.format)).digest("hex");
|
|
6077
|
+
}
|
|
6078
|
+
function signature(statePath) {
|
|
6079
|
+
const fmt = detectFormat(statePath);
|
|
6080
|
+
if (fmt === "none") return "none";
|
|
6081
|
+
if (fmt === "single") {
|
|
6082
|
+
const s = statSync8(statePath);
|
|
6083
|
+
return `single:${s.size}:${s.mtimeMs}`;
|
|
6084
|
+
}
|
|
6085
|
+
const dir = splitDirFor(statePath);
|
|
6086
|
+
const parts = [];
|
|
6087
|
+
for (const rel of ["config.json", "keys.json"]) {
|
|
6088
|
+
try {
|
|
6089
|
+
const s = statSync8(join17(dir, rel));
|
|
6090
|
+
parts.push(`${rel}:${s.size}:${s.mtimeMs}`);
|
|
6091
|
+
} catch {
|
|
6092
|
+
}
|
|
6093
|
+
}
|
|
6094
|
+
try {
|
|
6095
|
+
for (const name of readdirSync14(join17(dir, "locales")).sort()) {
|
|
6096
|
+
if (!name.endsWith(".json")) continue;
|
|
6097
|
+
const s = statSync8(join17(dir, "locales", name));
|
|
6098
|
+
parts.push(`${name}:${s.size}:${s.mtimeMs}`);
|
|
6099
|
+
}
|
|
6100
|
+
} catch {
|
|
6101
|
+
}
|
|
6102
|
+
return `split:${parts.join("|")}`;
|
|
6103
|
+
}
|
|
6104
|
+
function createStateWatcher(opts) {
|
|
6105
|
+
const intervalMs = opts.intervalMs ?? 750;
|
|
6106
|
+
let statePath = opts.statePath;
|
|
6107
|
+
let lastSig = "";
|
|
6108
|
+
let lastHash = "";
|
|
6109
|
+
let timer;
|
|
6110
|
+
function baseline() {
|
|
6111
|
+
try {
|
|
6112
|
+
lastSig = signature(statePath);
|
|
6113
|
+
lastHash = hashState(loadState(statePath));
|
|
6114
|
+
} catch {
|
|
6115
|
+
lastSig = "";
|
|
6116
|
+
lastHash = "";
|
|
6117
|
+
}
|
|
6118
|
+
}
|
|
6119
|
+
function check() {
|
|
6120
|
+
let sig;
|
|
6121
|
+
try {
|
|
6122
|
+
sig = signature(statePath);
|
|
6123
|
+
} catch {
|
|
6124
|
+
return;
|
|
6125
|
+
}
|
|
6126
|
+
if (sig === lastSig) return;
|
|
6127
|
+
let hash;
|
|
6128
|
+
try {
|
|
6129
|
+
hash = hashState(loadState(statePath));
|
|
6130
|
+
} catch {
|
|
6131
|
+
lastSig = sig;
|
|
6132
|
+
return;
|
|
6133
|
+
}
|
|
6134
|
+
lastSig = sig;
|
|
6135
|
+
if (hash !== lastHash) {
|
|
6136
|
+
lastHash = hash;
|
|
6137
|
+
opts.onChange();
|
|
6138
|
+
}
|
|
6139
|
+
}
|
|
6140
|
+
function noteWrite(state) {
|
|
6141
|
+
try {
|
|
6142
|
+
lastSig = signature(statePath);
|
|
6143
|
+
} catch {
|
|
6144
|
+
lastSig = "";
|
|
6145
|
+
}
|
|
6146
|
+
lastHash = hashState(state);
|
|
6147
|
+
}
|
|
6148
|
+
function retarget(next) {
|
|
6149
|
+
statePath = next;
|
|
6150
|
+
baseline();
|
|
6151
|
+
}
|
|
6152
|
+
function start() {
|
|
6153
|
+
if (timer) return;
|
|
6154
|
+
timer = setInterval(check, intervalMs);
|
|
6155
|
+
timer.unref?.();
|
|
6156
|
+
}
|
|
6157
|
+
function stop() {
|
|
6158
|
+
if (timer) clearInterval(timer);
|
|
6159
|
+
timer = void 0;
|
|
6160
|
+
}
|
|
6161
|
+
baseline();
|
|
6162
|
+
return { check, noteWrite, retarget, start, stop };
|
|
6163
|
+
}
|
|
6164
|
+
|
|
6046
6165
|
// src/server/api.ts
|
|
6047
6166
|
var sanitize = (s) => s.replace(/[^\w.\-]+/g, "_");
|
|
6048
6167
|
var screenshotDirName = (statePath) => basename(statePath).replace(/\.[^.]+$/, "") + "-screenshots";
|
|
@@ -6076,6 +6195,14 @@ function createApi(deps) {
|
|
|
6076
6195
|
const app = new Hono();
|
|
6077
6196
|
const load = () => loadState(deps.statePath);
|
|
6078
6197
|
const projectRoot = dirname3(resolve9(deps.statePath));
|
|
6198
|
+
const hub = deps.eventHub ?? createEventHub();
|
|
6199
|
+
const watcher = createStateWatcher({
|
|
6200
|
+
statePath: deps.statePath,
|
|
6201
|
+
intervalMs: deps.watchIntervalMs,
|
|
6202
|
+
onChange: () => hub.broadcast("state-changed", JSON.stringify({ at: (/* @__PURE__ */ new Date()).toISOString() }))
|
|
6203
|
+
});
|
|
6204
|
+
deps.onWatcher?.(watcher);
|
|
6205
|
+
if (deps.watch) watcher.start();
|
|
6079
6206
|
let translateQueue = Promise.resolve();
|
|
6080
6207
|
const withTranslateLock = (fn) => {
|
|
6081
6208
|
const next = translateQueue.then(fn, fn);
|
|
@@ -6097,12 +6224,30 @@ function createApi(deps) {
|
|
|
6097
6224
|
};
|
|
6098
6225
|
const persist = (s) => {
|
|
6099
6226
|
saveState(deps.statePath, s);
|
|
6227
|
+
watcher.noteWrite(s);
|
|
6100
6228
|
scheduleAutoExport(s);
|
|
6101
6229
|
};
|
|
6102
6230
|
const logChange = (entry) => appendLog(projectRoot, { ...entry, at: (/* @__PURE__ */ new Date()).toISOString() });
|
|
6103
6231
|
const valueText = (s, key, locale) => s.keys[key]?.values[locale]?.value;
|
|
6104
6232
|
const uiPrefsPath = deps.uiPrefsPath ?? defaultUiPrefsPath();
|
|
6105
6233
|
app.get("/state", (c) => c.json(load()));
|
|
6234
|
+
app.get("/events", (c) => streamSSE(c, async (stream) => {
|
|
6235
|
+
const send = (event, data) => {
|
|
6236
|
+
void stream.writeSSE({ event, data });
|
|
6237
|
+
};
|
|
6238
|
+
const unsubscribe = hub.subscribe(send);
|
|
6239
|
+
stream.onAbort(unsubscribe);
|
|
6240
|
+
await stream.writeSSE({ event: "ready", data: "" });
|
|
6241
|
+
try {
|
|
6242
|
+
while (!stream.aborted) {
|
|
6243
|
+
await stream.sleep(3e4);
|
|
6244
|
+
if (stream.aborted) break;
|
|
6245
|
+
await stream.writeSSE({ event: "ping", data: "" });
|
|
6246
|
+
}
|
|
6247
|
+
} finally {
|
|
6248
|
+
unsubscribe();
|
|
6249
|
+
}
|
|
6250
|
+
}));
|
|
6106
6251
|
app.get("/ui-prefs", (c) => c.json(loadUiPrefs(uiPrefsPath)));
|
|
6107
6252
|
app.put("/ui-prefs", async (c) => {
|
|
6108
6253
|
const body = await c.req.json();
|
|
@@ -6190,7 +6335,7 @@ function createApi(deps) {
|
|
|
6190
6335
|
if (depth > 4) return;
|
|
6191
6336
|
let entries = [];
|
|
6192
6337
|
try {
|
|
6193
|
-
entries =
|
|
6338
|
+
entries = readdirSync15(dir);
|
|
6194
6339
|
} catch {
|
|
6195
6340
|
return;
|
|
6196
6341
|
}
|
|
@@ -6204,7 +6349,7 @@ function createApi(deps) {
|
|
|
6204
6349
|
filePath = abs;
|
|
6205
6350
|
} else {
|
|
6206
6351
|
try {
|
|
6207
|
-
if (
|
|
6352
|
+
if (statSync9(abs).isDirectory()) walk(abs, depth + 1);
|
|
6208
6353
|
} catch {
|
|
6209
6354
|
}
|
|
6210
6355
|
continue;
|
|
@@ -6235,6 +6380,7 @@ function createApi(deps) {
|
|
|
6235
6380
|
if (!existsSync13(resolved)) return c.json({ error: "file not found" }, 400);
|
|
6236
6381
|
loadState(resolved);
|
|
6237
6382
|
deps.statePath = resolved;
|
|
6383
|
+
watcher.retarget(resolved);
|
|
6238
6384
|
return c.json({ ok: true, path: resolved, name: basename(resolved), dir: projectRoot, project: basename(projectRoot) });
|
|
6239
6385
|
});
|
|
6240
6386
|
app.post("/keys", async (c) => {
|
|
@@ -7209,7 +7355,7 @@ function createApi(deps) {
|
|
|
7209
7355
|
|
|
7210
7356
|
// src/server/server.ts
|
|
7211
7357
|
var here = dirname4(fileURLToPath(import.meta.url));
|
|
7212
|
-
var DEFAULT_UI_DIR =
|
|
7358
|
+
var DEFAULT_UI_DIR = join18(here, "..", "ui");
|
|
7213
7359
|
var MIME = {
|
|
7214
7360
|
".html": "text/html; charset=utf-8",
|
|
7215
7361
|
".js": "text/javascript; charset=utf-8",
|
|
@@ -7237,7 +7383,12 @@ async function readFileResponse(absPath) {
|
|
|
7237
7383
|
}
|
|
7238
7384
|
function buildApp(opts) {
|
|
7239
7385
|
const app = new Hono2();
|
|
7240
|
-
const apiDeps = {
|
|
7386
|
+
const apiDeps = {
|
|
7387
|
+
statePath: opts.statePath,
|
|
7388
|
+
autoExport: true,
|
|
7389
|
+
watch: opts.watch,
|
|
7390
|
+
onWatcher: opts.onWatcher
|
|
7391
|
+
};
|
|
7241
7392
|
app.route("/api", createApi(apiDeps));
|
|
7242
7393
|
app.get("/:dir/*", async (c, next) => {
|
|
7243
7394
|
const dirSeg = c.req.param("dir");
|
|
@@ -7263,7 +7414,7 @@ function buildApp(opts) {
|
|
|
7263
7414
|
const file = await readFileResponse(target);
|
|
7264
7415
|
if (file) return file;
|
|
7265
7416
|
}
|
|
7266
|
-
const index = await readFileResponse(
|
|
7417
|
+
const index = await readFileResponse(join18(root, "index.html"));
|
|
7267
7418
|
if (index) return index;
|
|
7268
7419
|
return c.notFound();
|
|
7269
7420
|
});
|
|
@@ -7299,13 +7450,19 @@ function findAvailablePort(start) {
|
|
|
7299
7450
|
});
|
|
7300
7451
|
}
|
|
7301
7452
|
async function startServer(opts) {
|
|
7302
|
-
|
|
7453
|
+
let watcher;
|
|
7454
|
+
const app = buildApp({ ...opts, watch: opts.watch ?? true, onWatcher: (w) => {
|
|
7455
|
+
watcher = w;
|
|
7456
|
+
} });
|
|
7303
7457
|
const port = await findAvailablePort(opts.dev ? DEV_PORT : DEFAULT_PORT);
|
|
7304
7458
|
return new Promise((resolveP) => {
|
|
7305
7459
|
const server = serve({ fetch: app.fetch, hostname: "127.0.0.1", port }, (info) => {
|
|
7306
7460
|
const url = `http://127.0.0.1:${info.port}`;
|
|
7307
7461
|
if (opts.open !== false && !opts.dev) void open(url);
|
|
7308
|
-
resolveP({ url, close: () =>
|
|
7462
|
+
resolveP({ url, close: () => {
|
|
7463
|
+
watcher?.stop();
|
|
7464
|
+
server.close();
|
|
7465
|
+
} });
|
|
7309
7466
|
backgroundScan(opts.statePath);
|
|
7310
7467
|
});
|
|
7311
7468
|
});
|