updating open source launcher
This commit was merged in pull request #3.
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
FROM node:22-alpine AS build
|
||||
|
||||
WORKDIR /app
|
||||
COPY server/package.json server/package-lock.json* ./
|
||||
RUN npm install --omit=dev --no-audit --no-fund
|
||||
|
||||
COPY server/src/ ./src/
|
||||
|
||||
FROM node:22-alpine AS runtime
|
||||
|
||||
RUN adduser -D -H -s /sbin/nologin manifest
|
||||
|
||||
WORKDIR /app
|
||||
COPY --from=build --chown=manifest:manifest /app /app
|
||||
|
||||
RUN npm install --no-save --no-audit --no-fund tsx@^4.21.0
|
||||
|
||||
USER manifest
|
||||
EXPOSE 5000
|
||||
|
||||
ENV SOURCE_DIR=/srv/source
|
||||
|
||||
ENTRYPOINT ["node", "--import", "tsx/esm", "src/index.ts"]
|
||||
Generated
+545
@@ -14,6 +14,9 @@
|
||||
"fs-extra": "^11.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^5.2.2"
|
||||
},
|
||||
"devDependencies": {
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@cspotcode/source-map-support": {
|
||||
@@ -27,6 +30,448 @@
|
||||
"node": ">=12"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/aix-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"aix"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/android-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"android"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/darwin-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/freebsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"freebsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz",
|
||||
"integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==",
|
||||
"cpu": [
|
||||
"arm"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-loong64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz",
|
||||
"integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==",
|
||||
"cpu": [
|
||||
"loong64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-mips64el": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz",
|
||||
"integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==",
|
||||
"cpu": [
|
||||
"mips64el"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-ppc64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz",
|
||||
"integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==",
|
||||
"cpu": [
|
||||
"ppc64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-riscv64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz",
|
||||
"integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==",
|
||||
"cpu": [
|
||||
"riscv64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-s390x": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz",
|
||||
"integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==",
|
||||
"cpu": [
|
||||
"s390x"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/linux-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/netbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"netbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openbsd-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openbsd"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/openharmony-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"openharmony"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/sunos-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"sunos"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-arm64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz",
|
||||
"integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-ia32": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz",
|
||||
"integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==",
|
||||
"cpu": [
|
||||
"ia32"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@esbuild/win32-x64": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz",
|
||||
"integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
}
|
||||
},
|
||||
"node_modules/@jridgewell/resolve-uri": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.1.tgz",
|
||||
@@ -336,6 +781,48 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/esbuild": {
|
||||
"version": "0.27.7",
|
||||
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz",
|
||||
"integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"bin": {
|
||||
"esbuild": "bin/esbuild"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@esbuild/aix-ppc64": "0.27.7",
|
||||
"@esbuild/android-arm": "0.27.7",
|
||||
"@esbuild/android-arm64": "0.27.7",
|
||||
"@esbuild/android-x64": "0.27.7",
|
||||
"@esbuild/darwin-arm64": "0.27.7",
|
||||
"@esbuild/darwin-x64": "0.27.7",
|
||||
"@esbuild/freebsd-arm64": "0.27.7",
|
||||
"@esbuild/freebsd-x64": "0.27.7",
|
||||
"@esbuild/linux-arm": "0.27.7",
|
||||
"@esbuild/linux-arm64": "0.27.7",
|
||||
"@esbuild/linux-ia32": "0.27.7",
|
||||
"@esbuild/linux-loong64": "0.27.7",
|
||||
"@esbuild/linux-mips64el": "0.27.7",
|
||||
"@esbuild/linux-ppc64": "0.27.7",
|
||||
"@esbuild/linux-riscv64": "0.27.7",
|
||||
"@esbuild/linux-s390x": "0.27.7",
|
||||
"@esbuild/linux-x64": "0.27.7",
|
||||
"@esbuild/netbsd-arm64": "0.27.7",
|
||||
"@esbuild/netbsd-x64": "0.27.7",
|
||||
"@esbuild/openbsd-arm64": "0.27.7",
|
||||
"@esbuild/openbsd-x64": "0.27.7",
|
||||
"@esbuild/openharmony-arm64": "0.27.7",
|
||||
"@esbuild/sunos-x64": "0.27.7",
|
||||
"@esbuild/win32-arm64": "0.27.7",
|
||||
"@esbuild/win32-ia32": "0.27.7",
|
||||
"@esbuild/win32-x64": "0.27.7"
|
||||
}
|
||||
},
|
||||
"node_modules/escape-html": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
|
||||
@@ -436,6 +923,21 @@
|
||||
"node": ">=14.14"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.3",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
|
||||
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"license": "MIT",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/function-bind": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -458,6 +960,19 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/get-tsconfig": {
|
||||
"version": "4.14.0",
|
||||
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.14.0.tgz",
|
||||
"integrity": "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"resolve-pkg-maps": "^1.0.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/gopd": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
|
||||
@@ -717,6 +1232,16 @@
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/resolve-pkg-maps": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
|
||||
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
|
||||
}
|
||||
},
|
||||
"node_modules/safe-buffer": {
|
||||
"version": "5.2.1",
|
||||
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
|
||||
@@ -873,6 +1398,26 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"node_modules/tsx": {
|
||||
"version": "4.21.0",
|
||||
"resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
|
||||
"integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"esbuild": "~0.27.0",
|
||||
"get-tsconfig": "^4.7.5"
|
||||
},
|
||||
"bin": {
|
||||
"tsx": "dist/cli.mjs"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18.0.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.3"
|
||||
}
|
||||
},
|
||||
"node_modules/type-is": {
|
||||
"version": "1.6.18",
|
||||
"resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
|
||||
|
||||
+5
-3
@@ -4,12 +4,11 @@
|
||||
"main": "index.ts",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "node --loader ts-node/esm src/index.ts"
|
||||
"dev": "tsx src/index.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/express": "^4.17.21",
|
||||
"@types/node": "^20.9.0",
|
||||
"dotenv": "^16.0.0",
|
||||
"express": "^4.18.2",
|
||||
"fs-extra": "^11.1.1",
|
||||
"ts-node": "^10.9.1",
|
||||
@@ -18,5 +17,8 @@
|
||||
"eslintConfig": {
|
||||
"extends": "@haaxor1689/eslint-config"
|
||||
},
|
||||
"prettier": "@haaxor1689/prettier-config"
|
||||
"prettier": "@haaxor1689/prettier-config",
|
||||
"devDependencies": {
|
||||
"tsx": "^4.21.0"
|
||||
}
|
||||
}
|
||||
|
||||
+114
-40
@@ -26,6 +26,9 @@ type CacheEntry = { at: number; data: ResolvedAddon[] };
|
||||
let cache: CacheEntry | undefined;
|
||||
let inFlight: Promise<ResolvedAddon[]> | undefined;
|
||||
|
||||
const normalizeColorCodes = (s: string): string =>
|
||||
s.replace(/\|C(?=[0-9a-fA-F]{8})/g, '|c').replace(/\|R/g, '|r');
|
||||
|
||||
const parseToc = (content: string): TocData =>
|
||||
content
|
||||
.split('\n')
|
||||
@@ -38,7 +41,7 @@ const parseToc = (content: string): TocData =>
|
||||
})
|
||||
.filter((e): e is readonly [string, string] => !!e)
|
||||
.reduce<TocData>((acc, [k, v]) => {
|
||||
acc[k] = v;
|
||||
acc[k] = normalizeColorCodes(v);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
@@ -52,57 +55,129 @@ const fetchWithTimeout = async (url: string, init?: RequestInit) => {
|
||||
}
|
||||
};
|
||||
|
||||
const parseGitUrl = (git: string) => {
|
||||
// https://github.com/{owner}/{repo}.git
|
||||
const m = git.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (!m || !m[1] || !m[2]) throw Error(`Unsupported git URL: ${git}`);
|
||||
return { owner: m[1], repo: m[2] };
|
||||
type RepoMeta = {
|
||||
description?: string;
|
||||
defaultBranch?: string;
|
||||
lastUpdated?: string;
|
||||
stars?: number;
|
||||
};
|
||||
|
||||
const resolveOne = async (src: AddonSource): Promise<ResolvedAddon | null> => {
|
||||
try {
|
||||
const { owner, repo } = parseGitUrl(src.git);
|
||||
const name = src.name ?? repo;
|
||||
const branch = src.branch ?? 'master';
|
||||
const tocRef = src.ref ?? branch;
|
||||
type RawMeta = {
|
||||
description?: string | null;
|
||||
default_branch?: string;
|
||||
pushed_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
stargazers_count?: number | null;
|
||||
stars_count?: number | null;
|
||||
};
|
||||
|
||||
const tocUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${tocRef}/${name}.toc`;
|
||||
const apiUrl = `https://api.github.com/repos/${owner}/${repo}`;
|
||||
type Provider = {
|
||||
apiUrl: (owner: string, repo: string) => string;
|
||||
apiHeaders: () => Record<string, string>;
|
||||
mapMeta: (json: RawMeta) => RepoMeta;
|
||||
tocUrl: (owner: string, repo: string, ref: string, name: string) => string;
|
||||
};
|
||||
|
||||
const [tocRes, apiRes] = await Promise.all([
|
||||
fetchWithTimeout(tocUrl).catch(() => null),
|
||||
fetchWithTimeout(apiUrl, {
|
||||
headers: {
|
||||
const githubProvider: Provider = {
|
||||
apiUrl: (o, r) => `https://api.github.com/repos/${o}/${r}`,
|
||||
apiHeaders: () => ({
|
||||
Accept: 'application/vnd.github+json',
|
||||
...(process.env.GITHUB_TOKEN && {
|
||||
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`
|
||||
})
|
||||
}),
|
||||
mapMeta: j => ({
|
||||
description: j.description ?? undefined,
|
||||
defaultBranch: j.default_branch,
|
||||
lastUpdated: j.pushed_at ?? undefined,
|
||||
stars: j.stargazers_count ?? undefined
|
||||
}),
|
||||
tocUrl: (o, r, ref, name) =>
|
||||
`https://raw.githubusercontent.com/${o}/${r}/${ref}/${name}.toc`
|
||||
};
|
||||
|
||||
const GITEA_API = 'https://octowow.st/git/api/v1';
|
||||
const giteaProvider: Provider = {
|
||||
apiUrl: (o, r) => `${GITEA_API}/repos/${o}/${r}`,
|
||||
apiHeaders: () => ({ Accept: 'application/json' }),
|
||||
mapMeta: j => ({
|
||||
description: j.description ?? undefined,
|
||||
defaultBranch: j.default_branch,
|
||||
lastUpdated: j.updated_at ?? undefined,
|
||||
stars: j.stars_count ?? undefined
|
||||
}),
|
||||
tocUrl: (o, r, ref, name) =>
|
||||
`${GITEA_API}/repos/${o}/${r}/raw/${name}.toc?ref=${encodeURIComponent(ref)}`
|
||||
};
|
||||
|
||||
const parseGitUrl = (
|
||||
git: string
|
||||
): { owner: string; repo: string; provider: Provider } => {
|
||||
const gh = git.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (gh && gh[1] && gh[2]) {
|
||||
return { owner: gh[1], repo: gh[2], provider: githubProvider };
|
||||
}
|
||||
}).catch(() => null)
|
||||
]);
|
||||
const gitea = git.match(/octowow\.st\/git\/([^/]+)\/([^/]+?)(?:\.git)?$/);
|
||||
if (gitea && gitea[1] && gitea[2]) {
|
||||
return { owner: gitea[1], repo: gitea[2], provider: giteaProvider };
|
||||
}
|
||||
throw Error(`Unsupported git URL: ${git}`);
|
||||
};
|
||||
|
||||
const REQUIRED_TOC_KEYS = ['Interface'];
|
||||
|
||||
const tryFetchToc = async (
|
||||
provider: Provider,
|
||||
owner: string,
|
||||
repo: string,
|
||||
name: string,
|
||||
ref: string
|
||||
): Promise<TocData | undefined> => {
|
||||
const res = await fetchWithTimeout(
|
||||
provider.tocUrl(owner, repo, ref, name)
|
||||
).catch(() => null);
|
||||
if (!res?.ok) return undefined;
|
||||
const parsed = parseToc(await res.text());
|
||||
return REQUIRED_TOC_KEYS.every(k => typeof parsed[k] === 'string')
|
||||
? parsed
|
||||
: undefined;
|
||||
};
|
||||
|
||||
const resolveOne = async (src: AddonSource): Promise<ResolvedAddon | null> => {
|
||||
try {
|
||||
const { owner, repo, provider } = parseGitUrl(src.git);
|
||||
const name = src.name ?? repo;
|
||||
|
||||
const apiRes = await fetchWithTimeout(provider.apiUrl(owner, repo), {
|
||||
headers: provider.apiHeaders()
|
||||
}).catch(() => null);
|
||||
|
||||
let meta: RepoMeta | undefined;
|
||||
if (apiRes?.ok) meta = provider.mapMeta((await apiRes.json()) as RawMeta);
|
||||
|
||||
const candidates = src.ref
|
||||
? [src.ref]
|
||||
: src.branch
|
||||
? [src.branch]
|
||||
: [...new Set([meta?.defaultBranch, 'master', 'main'].filter((b): b is string => !!b))];
|
||||
|
||||
let toc: TocData | undefined;
|
||||
if (tocRes?.ok) {
|
||||
const parsed = parseToc(await tocRes.text());
|
||||
const required = ['Interface', 'Title', 'Author', 'Notes', 'Version'];
|
||||
if (required.every(k => typeof parsed[k] === 'string')) {
|
||||
toc = parsed;
|
||||
let resolvedRef: string | undefined;
|
||||
for (const ref of candidates) {
|
||||
toc = await tryFetchToc(provider, owner, repo, name, ref);
|
||||
if (toc) {
|
||||
resolvedRef = ref;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let description: string | undefined;
|
||||
let lastUpdated: string | undefined;
|
||||
let stars: number | undefined;
|
||||
if (apiRes?.ok) {
|
||||
const meta = (await apiRes.json()) as {
|
||||
description?: string;
|
||||
pushed_at?: string;
|
||||
stargazers_count?: number;
|
||||
};
|
||||
description = meta.description ?? undefined;
|
||||
lastUpdated = meta.pushed_at;
|
||||
stars = meta.stargazers_count;
|
||||
}
|
||||
const effectiveBranch = src.ref
|
||||
? undefined
|
||||
: src.branch ?? resolvedRef ?? meta?.defaultBranch;
|
||||
|
||||
let description = meta?.description ?? undefined;
|
||||
const lastUpdated = meta?.lastUpdated;
|
||||
const stars = meta?.stars;
|
||||
|
||||
if (src.description) {
|
||||
description = src.description;
|
||||
@@ -110,7 +185,7 @@ const resolveOne = async (src: AddonSource): Promise<ResolvedAddon | null> => {
|
||||
}
|
||||
|
||||
const result: ResolvedAddon = { name, owner, git: src.git };
|
||||
if (src.branch !== undefined) result.branch = src.branch;
|
||||
if (effectiveBranch !== undefined) result.branch = effectiveBranch;
|
||||
if (src.ref !== undefined) result.ref = src.ref;
|
||||
if (toc !== undefined) result.toc = toc;
|
||||
if (description !== undefined) result.description = description;
|
||||
@@ -174,7 +249,6 @@ export const getAddons = async (force = false): Promise<ResolvedAddon[]> => {
|
||||
if (!force && cache && Date.now() - cache.at < CACHE_TTL_MS) {
|
||||
return cache.data;
|
||||
}
|
||||
// Deduplicate concurrent callers — only one scrape in flight at a time.
|
||||
if (inFlight) return inFlight;
|
||||
inFlight = buildList()
|
||||
.then(data => {
|
||||
|
||||
@@ -7,28 +7,50 @@ export type AddonSource = {
|
||||
};
|
||||
|
||||
export const defaultSources: AddonSource[] = [
|
||||
{
|
||||
git: 'https://github.com/Alukarho/AI_VoiceOver.git',
|
||||
description: 'Adds AI-generated voice acting to NPC quest and gossip dialogue'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/McPewPew/ArcHUD2.git',
|
||||
description: 'Combat HUD showing health and power as arcs around your character'
|
||||
},
|
||||
{ git: 'https://github.com/CosminPOP/AtlasLoot.git', name: 'AtlasLoot' },
|
||||
{
|
||||
git: 'https://github.com/byCFM2/Atlas-TW.git',
|
||||
branch: 'main',
|
||||
ref: 'pre-rewrite-backup'
|
||||
name: 'Atlas-CFM'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/Road-block/AuldLangSyne.git',
|
||||
description: 'Adds personal notes to friends, ignore, and guild lists, remembered while offline'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/FSuhas/AutoLFM.git',
|
||||
description: 'Automated "Looking For More" broadcaster for Turtle WoW dungeons and raids'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/shirsig/aux-addon-vanilla.git',
|
||||
name: 'aux-addon',
|
||||
description: 'Auction House replacement with advanced filtering and search'
|
||||
},
|
||||
{ git: 'https://github.com/absir/Bagshui.git', branch: 'main' },
|
||||
{ git: 'https://github.com/absir/Bagshui.git' },
|
||||
{ git: 'https://github.com/pepopo978/BetterCharacterStats.git', branch: 'main' },
|
||||
{ git: 'https://github.com/pepopo978/BigWigs.git' },
|
||||
{
|
||||
git: 'https://github.com/DBFBlackbull/BitesCookBook.git',
|
||||
description: 'Tracks which items are used in cooking and what they create'
|
||||
},
|
||||
{ git: 'https://github.com/bhhandley/CleveRoidMacros.git', branch: 'main' },
|
||||
{
|
||||
git: 'https://github.com/brotalnia/BlizzPlates.git',
|
||||
description: 'Adds castbars, debuffs, and class icons to the default Blizzard nameplates'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/MDGitHubRepo/CallOfElements.git',
|
||||
description: 'All-in-one Shaman totem bar and totem/healing manager'
|
||||
},
|
||||
{ git: 'https://github.com/bhhandley/CleveRoidMacros.git' },
|
||||
{
|
||||
git: 'https://github.com/Cinecom/ConsumesManager.git',
|
||||
branch: 'main',
|
||||
description: 'Tracks consumables and food buffs across alts, bank, and mail'
|
||||
},
|
||||
{
|
||||
@@ -36,26 +58,37 @@ export const defaultSources: AddonSource[] = [
|
||||
name: 'Cursive-Raid',
|
||||
description: 'Raid debuff tracker with profiles and multi-curse assist (SuperWoW)'
|
||||
},
|
||||
{ git: 'https://github.com/Player-Doite/DoiteAuras.git', branch: 'main' },
|
||||
{ git: 'https://github.com/Stormhand-dev/DragonflightUI-Reforged.git', branch: 'main' },
|
||||
{
|
||||
git: 'https://github.com/Zerf/Decursive.git',
|
||||
description: 'Raid/party debuff-cleaning helper that dispels whoever needs it'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/DeterminedPanda/DifficultBulletinBoard.git',
|
||||
description: 'Organizes LFG, profession, and hardcore chat announcements into a bulletin board'
|
||||
},
|
||||
{ git: 'https://github.com/Player-Doite/DoiteAuras.git' },
|
||||
{ git: 'https://github.com/Stormhand-dev/DragonflightUI-Reforged.git' },
|
||||
{
|
||||
git: 'https://github.com/Fiurs-Hearth/ExtraResourceBars.git',
|
||||
description: 'Adds extra resource bars (mana, energy, rage) to the UI'
|
||||
},
|
||||
{ git: 'https://github.com/tilare/FlightTracker.git', branch: 'main' },
|
||||
{ git: 'https://github.com/lookino/Flyout.git', branch: 'main' },
|
||||
{
|
||||
git: 'https://github.com/SeVeN7000/FishingBuddy.git',
|
||||
description: 'Auto-equips fishing gear and tracks catches, fish, and zone info'
|
||||
},
|
||||
{ git: 'https://github.com/tilare/FlightTracker.git' },
|
||||
{ git: 'https://github.com/lookino/Flyout.git' },
|
||||
{
|
||||
git: 'https://github.com/trumpetx/GetHead.git',
|
||||
description: 'Recovers Onyxia and Nefarian heads from disenchant grief'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/zanthor/GNS.git',
|
||||
branch: 'main',
|
||||
description: 'Custom naming for Goblin Brainwashing Device specializations'
|
||||
},
|
||||
{ git: 'https://github.com/vatichild/guda.git', name: 'Guda', branch: 'main' },
|
||||
{ git: 'https://github.com/vatichild/GudaPlates.git', branch: 'main' },
|
||||
{ git: 'https://github.com/andresuarezschou/HCDeaths.git', branch: 'main' },
|
||||
{ git: 'https://github.com/vatichild/guda.git', name: 'Guda' },
|
||||
{ git: 'https://github.com/vatichild/GudaPlates.git' },
|
||||
{ git: 'https://github.com/andresuarezschou/HCDeaths.git' },
|
||||
{
|
||||
git: 'https://github.com/Arthur-Helias/InstanceJournal.git',
|
||||
description: "Encounter Journal reimagined for Turtle WoW"
|
||||
@@ -69,43 +102,49 @@ export const defaultSources: AddonSource[] = [
|
||||
name: '_LazyPig',
|
||||
description: 'Auto-dismount, auto-accept, auto-roll, and chat spam filter. /lp to configure'
|
||||
},
|
||||
{ git: 'https://github.com/Spartelfant/LevelRange-Turtle.git', branch: 'main' },
|
||||
{ git: 'https://github.com/tilare/MessageBox.git', branch: 'main' },
|
||||
{ git: 'https://github.com/Spartelfant/LevelRange-Turtle.git' },
|
||||
{ git: 'https://github.com/tilare/MessageBox.git' },
|
||||
{
|
||||
git: 'https://github.com/tdymel/ModifiedPowerAuras.git',
|
||||
description: "Advanced version of Sinesther's Power Auras"
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/tilare/ModernMapMarkers.git',
|
||||
branch: 'main',
|
||||
description: 'Shows dungeons, raids, world bosses, and travel routes on the world map'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/vegeta1k95/ModernSpellBook.git',
|
||||
description: 'Retail-style spellbook UI for vanilla'
|
||||
},
|
||||
{ git: 'https://github.com/tilare/MovementTracker.git', branch: 'main' },
|
||||
{ git: 'https://github.com/tilare/MovementTracker.git' },
|
||||
{
|
||||
git: 'https://github.com/pepopo978/NampowerSettings.git',
|
||||
git: 'https://github.com/Dusk-92/NampowerSettings.git',
|
||||
description: 'Settings panel for the Nampower spellqueue addon'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/BlackHobbiT/necrosis-twow.git',
|
||||
branch: 'main',
|
||||
name: 'Necrosis',
|
||||
description: 'Warlock helper: pets, soul shards, summoning, demon timers'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/gnwl/NotGrid.git',
|
||||
name: 'notgrid',
|
||||
description: 'Grid-like compact party/raid frames with buff/debuff, aggro, and proximity tracking'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/zanthor/OG-RaidHelper.git',
|
||||
branch: 'main',
|
||||
description: 'Raid management: roles, trade distribution, soft-reserve validation'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/sica42/Outfitter.git',
|
||||
description: 'Equipment set manager to save and quickly swap gear outfits, with Turtle mount fixes'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/CosminPOP/PallyPower.git',
|
||||
description: 'Paladin buff and assignment manager for raids and parties'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/Cliencer/pfExtend.git',
|
||||
branch: 'main',
|
||||
description: 'pfQuest extension showing all monster drops and quest chains. /pfex'
|
||||
},
|
||||
{ git: 'https://github.com/shagu/pfQuest.git' },
|
||||
@@ -113,12 +152,10 @@ export const defaultSources: AddonSource[] = [
|
||||
{ git: 'https://github.com/shagu/pfUI.git' },
|
||||
{
|
||||
git: 'https://github.com/jrc13245/pfUI-addonskinner.git',
|
||||
branch: 'main',
|
||||
description: 'pfUI module that re-skins other addons to match the pfUI theme'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/Bombg/pfUI-bettertotems.git',
|
||||
branch: 'main',
|
||||
description: 'pfUI module with improved Shaman totem timers'
|
||||
},
|
||||
{
|
||||
@@ -126,13 +163,12 @@ export const defaultSources: AddonSource[] = [
|
||||
name: 'pfUI-locplus',
|
||||
description: 'Adds a location panel and zone info to pfUI'
|
||||
},
|
||||
{ git: 'https://github.com/acid9000/PizzaWorldBuffs.git', branch: 'main' },
|
||||
{ git: 'https://github.com/acid9000/PizzaWorldBuffs.git' },
|
||||
{
|
||||
git: 'https://github.com/npfs666/ProcDoc.git',
|
||||
branch: 'main',
|
||||
description: 'Visual proc alerts with pulsing images so you never miss them'
|
||||
},
|
||||
{ git: 'https://github.com/SabineWren/Quiver.git', branch: 'main' },
|
||||
{ git: 'https://github.com/SabineWren/Quiver.git' },
|
||||
{
|
||||
git: 'https://github.com/hazlema/Rested.git',
|
||||
description: 'Progress bar showing your rested XP while resting'
|
||||
@@ -153,10 +189,18 @@ export const defaultSources: AddonSource[] = [
|
||||
description: 'Extras module for ShaguTweaks (additional UI tweaks)'
|
||||
},
|
||||
{ git: 'https://github.com/pepopo978/SimpleActionSets.git' },
|
||||
{
|
||||
git: 'https://github.com/balakethelock/SuperAPI.git',
|
||||
description: 'Companion compatibility addon bridging the SuperWoW client mod\'s expanded Lua API'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/jrc13245/SuperMacro-turtle-SuperWoW.git',
|
||||
name: 'SuperMacro',
|
||||
description: 'Extended macros with long macros, keybind execution, item links, and a code editor'
|
||||
},
|
||||
{ git: 'https://github.com/Siventt/AttackBar.git' },
|
||||
{
|
||||
git: 'https://github.com/Player-Doite/Tactica.git',
|
||||
branch: 'main',
|
||||
description: 'Auto-build raids: invite/gearcheck, tactics, masterloot, role sync'
|
||||
},
|
||||
{
|
||||
@@ -165,25 +209,31 @@ export const defaultSources: AddonSource[] = [
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/whtmst/T-RestedXP.git',
|
||||
branch: 'main',
|
||||
description: 'Tracks 0% and 100% rested XP thresholds'
|
||||
},
|
||||
{ git: 'https://github.com/sica42/TurtleCalendar.git', branch: 'main' },
|
||||
{ git: 'https://github.com/sica42/TurtleCalendar.git' },
|
||||
{
|
||||
git: 'https://github.com/sica42/TurtleMail.git',
|
||||
description: 'Mailbox UI enhancement: bulk send, search, multi-mail'
|
||||
},
|
||||
{ git: 'https://github.com/tempranova/turtlerp.git', name: 'TurtleRP', branch: 'main' },
|
||||
{ git: 'https://github.com/tempranova/turtlerp.git', name: 'TurtleRP' },
|
||||
{ git: 'https://github.com/CosminPOP/TWThreat.git' },
|
||||
{
|
||||
git: 'https://github.com/RetroCro/unitscan-turtle-hc.git',
|
||||
description: 'Hardcore unitscan fork for Turtle WoW that alerts on rares, elites, and dangerous mobs'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/whtmst/UnitXP_SP3_Addon.git',
|
||||
branch: 'main',
|
||||
description: 'Settings UI for the UnitXP SuperWoW client patch'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/tdymel/VCB.git',
|
||||
description: 'Smart consolidated buff frames with extensive customization'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/acolis/VitalWatch.git',
|
||||
description: 'Customizable health, mana, and aggro alert system for solo, group, and raid play'
|
||||
},
|
||||
{
|
||||
git: 'https://github.com/Fiurs-Hearth/WIIIUI.git',
|
||||
description: 'Compact custom UI replacement for Turtle WoW'
|
||||
|
||||
+220
-10
@@ -16,7 +16,29 @@ const allowedExtra = [
|
||||
|
||||
const vanillaFixes = ['VfPatcher.dll', 'd3d9.dll', 'dxvk.conf'];
|
||||
|
||||
const skipFiles = new Set(['manifest.json', 'wow-client.zip', '.gitkeep']);
|
||||
const skipFiles = new Set([
|
||||
'manifest.json',
|
||||
'manifest.json.tmp',
|
||||
'wow-client.zip',
|
||||
'.gitkeep',
|
||||
'.manifest-overrides.json'
|
||||
]);
|
||||
|
||||
const skipPatterns: RegExp[] = [/\.bak(\.|$)/, /\.crashing(\.|$)/];
|
||||
const isSkipPattern = (file: string) =>
|
||||
skipPatterns.some(p => p.test(file));
|
||||
|
||||
const skipDirsPosix = new Set([
|
||||
'Interface/GlueXML',
|
||||
'Interface/FrameXML',
|
||||
'Errors',
|
||||
'Logs',
|
||||
'Screenshots',
|
||||
'WDB',
|
||||
'WTF/Account'
|
||||
]);
|
||||
const isSkipDir = (...filePath: string[]) =>
|
||||
skipDirsPosix.has(filePath.join('/'));
|
||||
|
||||
type FolderTags = 'allowExtra';
|
||||
type FileTags = 'vanillaFixes';
|
||||
@@ -31,8 +53,21 @@ type FileManifest = { name: string } & (
|
||||
size: number;
|
||||
tags?: FileTags[];
|
||||
}
|
||||
| { type: 'del' }
|
||||
);
|
||||
|
||||
export type BuildProgress = {
|
||||
state: 'idle' | 'building' | 'ready' | 'failed';
|
||||
done: number;
|
||||
total: number;
|
||||
currentFile: string;
|
||||
startedAt: number | null;
|
||||
finishedAt: number | null;
|
||||
error: string | null;
|
||||
};
|
||||
|
||||
export type ProgressCallback = (p: Pick<BuildProgress, 'done' | 'total' | 'currentFile'>) => void;
|
||||
|
||||
const getHash = (...filePath: string[]): Promise<string> =>
|
||||
new Promise((resolve, reject) => {
|
||||
const hash = crypto.createHash('sha1');
|
||||
@@ -42,9 +77,96 @@ const getHash = (...filePath: string[]): Promise<string> =>
|
||||
stream.on('end', () => resolve(hash.digest('hex').toLocaleUpperCase()));
|
||||
});
|
||||
|
||||
export const buildCache = async (clientPath: string) => {
|
||||
const countFiles = async (clientPath: string, ...filePath: string[]): Promise<number> => {
|
||||
let total = 0;
|
||||
const dir = path.join(clientPath, ...filePath);
|
||||
const files = await fs.readdir(dir);
|
||||
for (const file of files.sort()) {
|
||||
if (skipFiles.has(file)) continue;
|
||||
if (isSkipPattern(file)) continue;
|
||||
const stats = await fs.stat(path.join(dir, file));
|
||||
if (stats.isDirectory()) {
|
||||
if (isSkipDir(...filePath, file)) continue;
|
||||
if (file.match(/patch-./)) {
|
||||
const mpqPath = path.join(dir, `${file}.mpq`);
|
||||
if (await fs.pathExists(mpqPath)) total += 1;
|
||||
}
|
||||
total += await countFiles(clientPath, ...filePath, file);
|
||||
} else {
|
||||
total += 1;
|
||||
}
|
||||
}
|
||||
return total;
|
||||
};
|
||||
|
||||
export const buildCache = async (
|
||||
clientPath: string,
|
||||
onProgress?: ProgressCallback
|
||||
) => {
|
||||
console.log('Building cache...');
|
||||
|
||||
const prevManifestPath = path.join(clientPath, 'manifest.json');
|
||||
let prevManifestMtimeMs = 0;
|
||||
const prevHashByPath = new Map<string, string>();
|
||||
const prevVersionByPath = new Map<string, number | undefined>();
|
||||
const prevSizeByPath = new Map<string, number>();
|
||||
try {
|
||||
const prevStat = await fs.stat(prevManifestPath);
|
||||
prevManifestMtimeMs = prevStat.mtimeMs;
|
||||
const prev = await fs.readJSON(prevManifestPath);
|
||||
const walk = (node: FileManifest, prefix: string[]) => {
|
||||
if (node.type === 'dir' || node.type === 'mpq') {
|
||||
const newPrefix = node.name ? [...prefix, node.name] : prefix;
|
||||
if (node.type === 'mpq') {
|
||||
const mpqKey = [...newPrefix.slice(0, -1), node.name + '.mpq']
|
||||
.join('/');
|
||||
prevHashByPath.set(mpqKey, node.hash);
|
||||
prevSizeByPath.set(mpqKey, node.size);
|
||||
}
|
||||
for (const child of node.files) walk(child, newPrefix);
|
||||
} else {
|
||||
const key = [...prefix, node.name].join('/');
|
||||
prevHashByPath.set(key, node.hash);
|
||||
prevVersionByPath.set(key, node.version);
|
||||
prevSizeByPath.set(key, node.size);
|
||||
}
|
||||
};
|
||||
walk(prev.root, []);
|
||||
console.log(
|
||||
`mtime-skip: loaded ${prevHashByPath.size} cached hashes from ` +
|
||||
`prior manifest (mtime=${new Date(prevManifestMtimeMs).toISOString()})`
|
||||
);
|
||||
} catch (e) {
|
||||
console.log(
|
||||
'mtime-skip: no usable prior manifest, full rebuild ' +
|
||||
`(${(e as Error).message})`
|
||||
);
|
||||
}
|
||||
|
||||
const total = await countFiles(clientPath);
|
||||
let done = 0;
|
||||
let reused = 0;
|
||||
const tick = (currentFile: string) => {
|
||||
done += 1;
|
||||
onProgress?.({ done, total, currentFile });
|
||||
};
|
||||
console.log(`Building cache: ${total} files to hash...`);
|
||||
|
||||
const getHashCached = async (
|
||||
relPath: string,
|
||||
mtimeMs: number,
|
||||
...filePath: string[]
|
||||
): Promise<string> => {
|
||||
if (prevManifestMtimeMs > 0 && mtimeMs <= prevManifestMtimeMs) {
|
||||
const cached = prevHashByPath.get(relPath);
|
||||
if (cached) {
|
||||
reused++;
|
||||
return cached;
|
||||
}
|
||||
}
|
||||
return getHash(clientPath, ...filePath);
|
||||
};
|
||||
|
||||
const buildTree = async (...filePath: string[]): Promise<FileManifest[]> => {
|
||||
const files = await fs.readdir(path.join(clientPath, ...filePath));
|
||||
|
||||
@@ -52,21 +174,34 @@ export const buildCache = async (clientPath: string) => {
|
||||
const tree: FileManifest[] = [];
|
||||
for (const file of files.sort()) {
|
||||
if (skipFiles.has(file)) continue;
|
||||
if (isSkipPattern(file)) continue;
|
||||
|
||||
const stats = await fs.stat(path.join(clientPath, ...filePath, file));
|
||||
|
||||
if (stats.isDirectory()) {
|
||||
if (isSkipDir(...filePath, file)) continue;
|
||||
if (file.match(/patch-./)) {
|
||||
patches.push(file);
|
||||
const mpqRelPath = path
|
||||
.join(...filePath, `${file}.mpq`)
|
||||
.split(path.sep)
|
||||
.join('/');
|
||||
const mpqStat = await fs.stat(
|
||||
path.join(clientPath, ...filePath, `${file}.mpq`)
|
||||
);
|
||||
tree.push({
|
||||
type: 'mpq',
|
||||
name: file,
|
||||
files: await buildTree(...filePath, file),
|
||||
size: (
|
||||
await fs.stat(path.join(clientPath, ...filePath, `${file}.mpq`))
|
||||
).size,
|
||||
hash: await getHash(clientPath, ...filePath, `${file}.mpq`)
|
||||
size: mpqStat.size,
|
||||
hash: await getHashCached(
|
||||
mpqRelPath,
|
||||
mpqStat.mtimeMs,
|
||||
...filePath,
|
||||
`${file}.mpq`
|
||||
)
|
||||
});
|
||||
tick(mpqRelPath);
|
||||
} else {
|
||||
const tags: FolderTags[] = [];
|
||||
allowedExtra.includes(path.join(...filePath, file)) &&
|
||||
@@ -81,8 +216,8 @@ export const buildCache = async (clientPath: string) => {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if extracted mpq patch
|
||||
if (patches.find(v => file.match(v))) continue;
|
||||
|
||||
const allowModifiedPaths = new Set([
|
||||
'WTF/Config.wtf',
|
||||
'Data/fonts.MPQ',
|
||||
@@ -102,22 +237,97 @@ export const buildCache = async (clientPath: string) => {
|
||||
tree.push({
|
||||
type: 'file',
|
||||
name: file,
|
||||
hash: await getHash(clientPath, ...filePath, file),
|
||||
hash: await getHashCached(
|
||||
fullPath,
|
||||
stats.mtimeMs,
|
||||
...filePath,
|
||||
file
|
||||
),
|
||||
version: allowModified ? stats.mtimeMs : undefined,
|
||||
size: stats.size,
|
||||
tags: tags.length ? tags : undefined
|
||||
});
|
||||
tick(fullPath);
|
||||
}
|
||||
return tree;
|
||||
};
|
||||
|
||||
await fs.writeJSON(path.join(clientPath, 'manifest.json'), {
|
||||
const rootFiles = await buildTree();
|
||||
|
||||
const overridesPath = path.join(clientPath, '.manifest-overrides.json');
|
||||
try {
|
||||
if (await fs.pathExists(overridesPath)) {
|
||||
const ov = await fs.readJSON(overridesPath);
|
||||
const dels: string[] = Array.isArray(ov.del) ? ov.del : [];
|
||||
for (const relPath of dels) {
|
||||
const parts = relPath.split('/').filter(Boolean);
|
||||
if (parts.length === 0) continue;
|
||||
const fileName = parts.pop()!;
|
||||
let dirNode: FileManifest = {
|
||||
type: 'dir',
|
||||
name: '',
|
||||
files: rootFiles
|
||||
} as FileManifest;
|
||||
let ok = true;
|
||||
for (const seg of parts) {
|
||||
if (dirNode.type !== 'dir' && dirNode.type !== 'mpq') {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
let child = dirNode.files.find(f => f.name === seg);
|
||||
if (!child) {
|
||||
child = {
|
||||
type: 'dir',
|
||||
name: seg,
|
||||
files: [],
|
||||
tags: ['allowExtra']
|
||||
};
|
||||
dirNode.files.push(child);
|
||||
}
|
||||
dirNode = child;
|
||||
}
|
||||
if (!ok || (dirNode.type !== 'dir' && dirNode.type !== 'mpq')) {
|
||||
console.warn(
|
||||
`manifest-overrides: del path "${relPath}" hit a non-dir ` +
|
||||
`node, skipping`
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const exists = dirNode.files.some(
|
||||
f => f.name === fileName && f.type === 'del'
|
||||
);
|
||||
if (!exists) {
|
||||
dirNode.files.push({ type: 'del', name: fileName } as FileManifest);
|
||||
console.log(
|
||||
`manifest-overrides: inserted {type:'del', name:'${fileName}'} ` +
|
||||
`under ${parts.join('/') || '<root>'}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn(
|
||||
`manifest-overrides: failed to apply ${overridesPath} -- continuing ` +
|
||||
`without overrides (${(e as Error).message})`
|
||||
);
|
||||
}
|
||||
|
||||
const finalPath = path.join(clientPath, 'manifest.json');
|
||||
const tmpPath = path.join(clientPath, 'manifest.json.tmp');
|
||||
await fs.writeJSON(tmpPath, {
|
||||
build: 3,
|
||||
buildName: '3',
|
||||
root: {
|
||||
type: 'dir',
|
||||
name: '',
|
||||
files: await buildTree()
|
||||
files: rootFiles
|
||||
}
|
||||
});
|
||||
await fs.rename(tmpPath, finalPath);
|
||||
if (prevManifestMtimeMs > 0) {
|
||||
console.log(
|
||||
`mtime-skip: reused ${reused}/${total} cached hashes ` +
|
||||
`(re-hashed ${total - reused})`
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
+100
-29
@@ -1,61 +1,86 @@
|
||||
import path from 'path';
|
||||
|
||||
import { config as loadEnv } from 'dotenv';
|
||||
import express from 'express';
|
||||
|
||||
loadEnv();
|
||||
|
||||
import fs from 'fs-extra';
|
||||
import { buildCache } from './cache.js';
|
||||
import { buildCache, type BuildProgress } from './cache.js';
|
||||
import { getAddons, warmUp as warmUpAddons } from './addons-resolver.js';
|
||||
|
||||
// Set SOURCE_DIR to your local WoW client directory (see server/.env.example).
|
||||
const SourceDir: string = (() => {
|
||||
const dir = process.env.SOURCE_DIR;
|
||||
if (!dir) {
|
||||
console.error(
|
||||
'ERROR: SOURCE_DIR is not set.\n' +
|
||||
'Set it to your local WoW client directory.\n' +
|
||||
'Example: SOURCE_DIR="C:\\\\WoW\\\\client" npm run dev\n' +
|
||||
'Or create server/.env — see server/.env.example.'
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
return dir;
|
||||
})();
|
||||
const SourceDir = process.env.SOURCE_DIR || 'C:\\WoW\\TurtleFresh';
|
||||
|
||||
const app = express();
|
||||
const port = 5000;
|
||||
|
||||
const buildProgress: BuildProgress = {
|
||||
state: 'idle',
|
||||
done: 0,
|
||||
total: 0,
|
||||
currentFile: '',
|
||||
startedAt: null,
|
||||
finishedAt: null,
|
||||
error: null
|
||||
};
|
||||
|
||||
let buildInFlight: Promise<void> | null = null;
|
||||
const ensureManifestBuilt = (): Promise<void> => {
|
||||
if (buildInFlight) return buildInFlight;
|
||||
buildInFlight = buildCache(SourceDir).catch(e => {
|
||||
buildProgress.state = 'building';
|
||||
buildProgress.done = 0;
|
||||
buildProgress.total = 0;
|
||||
buildProgress.currentFile = '';
|
||||
buildProgress.startedAt = Date.now();
|
||||
buildProgress.finishedAt = null;
|
||||
buildProgress.error = null;
|
||||
buildInFlight = buildCache(SourceDir, p => {
|
||||
buildProgress.done = p.done;
|
||||
buildProgress.total = p.total;
|
||||
buildProgress.currentFile = p.currentFile;
|
||||
})
|
||||
.then(() => {
|
||||
buildProgress.state = 'ready';
|
||||
buildProgress.finishedAt = Date.now();
|
||||
})
|
||||
.catch(e => {
|
||||
buildProgress.state = 'failed';
|
||||
buildProgress.error = e instanceof Error ? e.message : String(e);
|
||||
buildProgress.finishedAt = Date.now();
|
||||
buildInFlight = null;
|
||||
throw e;
|
||||
});
|
||||
return buildInFlight;
|
||||
};
|
||||
|
||||
app.get('/api/build-status', (_req, res) => {
|
||||
res.json(buildProgress);
|
||||
});
|
||||
|
||||
app.get('/api/file/:version/manifest.json', async (_req, res) => {
|
||||
console.log(`Fetching manifest`);
|
||||
const filePath = path.join(SourceDir, 'manifest.json');
|
||||
if (!fs.existsSync(filePath)) await ensureManifestBuilt();
|
||||
|
||||
if (await fs.pathExists(filePath)) {
|
||||
res.json(await fs.readJSON(filePath));
|
||||
return;
|
||||
}
|
||||
|
||||
void ensureManifestBuilt().catch(() => {
|
||||
});
|
||||
res.setHeader('Retry-After', '5');
|
||||
res.status(503).json({
|
||||
error: 'manifest_building',
|
||||
message:
|
||||
'Manifest is being built for the first time on this server. ' +
|
||||
'Poll /api/build-status for progress; retry this endpoint when ready.',
|
||||
buildProgress
|
||||
});
|
||||
});
|
||||
|
||||
app.get(
|
||||
'/api/file/:version/*',
|
||||
async (req: express.Request<{ 0: string }>, res) => {
|
||||
const filePath = req.params[0];
|
||||
const resolved = path.resolve(SourceDir, filePath);
|
||||
if (!resolved.startsWith(path.resolve(SourceDir) + path.sep)) {
|
||||
res.status(403).send('Forbidden');
|
||||
return;
|
||||
}
|
||||
console.log(`Fetching file: ${filePath}`);
|
||||
res.sendFile(resolved);
|
||||
|
||||
res.sendFile(path.join(SourceDir, filePath));
|
||||
}
|
||||
);
|
||||
|
||||
@@ -70,19 +95,65 @@ app.get('/api/addons.json', async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
const newestSourceMtime = async (dir: string): Promise<number> => {
|
||||
let newest = 0;
|
||||
const entries = await fs.readdir(dir);
|
||||
for (const name of entries) {
|
||||
if (name === 'manifest.json' || name === 'manifest.json.tmp') continue;
|
||||
const full = path.join(dir, name);
|
||||
const stat = await fs.stat(full);
|
||||
if (stat.isDirectory()) {
|
||||
const inner = await newestSourceMtime(full);
|
||||
if (inner > newest) newest = inner;
|
||||
} else if (stat.mtimeMs > newest) {
|
||||
newest = stat.mtimeMs;
|
||||
}
|
||||
}
|
||||
return newest;
|
||||
};
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server listening on port ${port}`);
|
||||
warmUpAddons();
|
||||
|
||||
void (async () => {
|
||||
const manifestPath = path.join(SourceDir, 'manifest.json');
|
||||
if (fs.existsSync(manifestPath)) return;
|
||||
if (!fs.existsSync(manifestPath)) {
|
||||
console.log(`Pre-warming manifest cache for ${SourceDir}...`);
|
||||
try {
|
||||
await ensureManifestBuilt();
|
||||
console.log(`Manifest cache pre-warm complete.`);
|
||||
} catch (e) {
|
||||
console.error('Manifest pre-warm failed:', e);
|
||||
console.error(
|
||||
'Manifest pre-warm failed (will fall back to lazy build on first request):',
|
||||
e
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
buildProgress.state = 'ready';
|
||||
buildProgress.finishedAt = Date.now();
|
||||
|
||||
try {
|
||||
const manifestStat = await fs.stat(manifestPath);
|
||||
const newest = await newestSourceMtime(SourceDir);
|
||||
if (newest > manifestStat.mtimeMs) {
|
||||
console.log(
|
||||
`Manifest is stale (newest source mtime ${new Date(newest).toISOString()} > manifest ${new Date(manifestStat.mtimeMs).toISOString()}); rebuilding in background.`
|
||||
);
|
||||
ensureManifestBuilt()
|
||||
.then(() => console.log('Background manifest rebuild complete.'))
|
||||
.catch(e =>
|
||||
console.error('Background manifest rebuild failed:', e)
|
||||
);
|
||||
} else {
|
||||
console.log(
|
||||
`Manifest cache already on disk at ${manifestPath} and up to date; no rebuild needed.`
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Manifest staleness check failed:', e);
|
||||
}
|
||||
})();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user