Compare commits

..

47 Commits

Author SHA1 Message Date
Lino Silva d2010f5d3a fix: Capitalization
Build and publish / build (push) Successful in 45s
2026-05-13 00:15:00 +01:00
Lino Silva 730353baa7 feat: Financial Times tracking
Build and publish / build (push) Successful in 44s
2026-05-13 00:10:00 +01:00
Lino Silva 5a851a15ae feat: Novos símbolos
Build and publish / build (push) Successful in 46s
2026-05-12 23:32:45 +01:00
Lino Silva e6f7b2270b chore: Refactor
Build and publish / build (push) Successful in 1m50s
2026-05-12 23:26:48 +01:00
Lino Silva 405a959691 feat: Deco proteste
Build and publish / build (push) Successful in 1m42s
2026-05-12 22:59:12 +01:00
Lino Silva 1ce14628de feat: Add currency
Build and publish / build (push) Successful in 6m37s
2026-04-10 23:50:37 +01:00
Lino Silva 6234f33ebb fix: Multiarch
Build and publish / build (push) Successful in 3m45s
2026-04-10 23:36:26 +01:00
Lino Silva eab6aa4e25 fix: Metadata cache
Build and publish / build (push) Successful in 1m15s
2026-04-10 23:28:20 +01:00
Lino Silva b0504e88f6 fix: Gitea host
Build and publish / build (push) Failing after 4m18s
2026-04-10 23:09:28 +01:00
Lino Silva ef264dff2a fix: Ports
Build and publish / build (push) Failing after 11s
2026-04-10 23:02:39 +01:00
Lino Silva f81c39153a fix: New password
Build and publish / build (push) Failing after 11s
2026-04-10 17:43:42 +01:00
Lino Silva dfe18a62dd fix: Wrong ports
Build and publish / build (push) Failing after 12s
2026-04-10 17:40:52 +01:00
Lino Silva 7841804cd1 fix: New approach
Build and publish / build (push) Failing after 11s
2026-04-10 17:35:59 +01:00
Lino Silva dd626dc272 fix: Remove insecure=true
Build and publish / build (push) Failing after 13s
2026-04-10 17:34:17 +01:00
Lino Silva ee4487f914 fix: Buildx gambiarra
Build and publish / build (push) Failing after 11s
2026-04-10 17:32:48 +01:00
Lino Silva 0744ccdf6a fix: Tentative of reverse proxy
Build and publish / build (push) Failing after 12s
2026-04-10 17:31:07 +01:00
Lino Silva e4a7143f75 fix: Repo endpoint
Build and publish / build (push) Failing after 12s
2026-04-10 17:29:57 +01:00
Lino Silva f7382487d3 fix: Var name
Build and publish / build (push) Failing after 12s
2026-04-10 17:28:01 +01:00
Lino Silva fe4c54ebcb feat: Initial move to docker image
Build and publish / build (push) Failing after 2m37s
2026-04-10 17:24:44 +01:00
Lino Silva be8ff08f94 updated symbols
Build and publish / build (push) Successful in 37s
2026-03-20 15:41:47 +00:00
Lino Silva 1e7dc2baca updated endpoint
Build and publish / build (push) Successful in 48s
2026-03-20 13:42:33 +00:00
Lino Silva 199d729802 libasound2t64
Build and publish / build (push) Failing after 50s
2026-03-20 12:23:20 +00:00
Lino Silva a9122ecab1 added libasound2
Build and publish / build (push) Failing after 9s
2026-03-20 12:22:19 +00:00
Lino Silva 1feab44ee4 dependencies
Build and publish / build (push) Failing after 48s
2026-03-20 12:20:23 +00:00
Lino Silva 4006719950 updated image
Build and publish / build (push) Failing after 11s
2026-03-20 12:16:08 +00:00
Lino Silva 00e61c3fea Changed cron
Build and publish / build (push) Failing after 10s
2024-11-06 23:40:07 +00:00
Lino Silva 211d18e606 Changed cron
Build and publish / build (push) Successful in 49s
2024-11-06 23:33:11 +00:00
Lino Silva 552a75f13d Changed cron
Build and publish / build (push) Successful in 51s
2024-11-06 23:31:51 +00:00
Lino Silva 554bc62199 Changed cron
Build and publish / build (push) Has been cancelled
2024-11-06 23:31:08 +00:00
lino-authelia 438f19812b Update .gitea/workflows/sync-prices.yaml 2024-10-16 20:20:19 +00:00
Lino Silva dbd7f0f9e7 Working version
Build and publish / build (push) Successful in 1m18s
2024-10-14 23:21:51 +01:00
Lino Silva 3594e33786 Fixed actions
Build and publish / build (push) Successful in 1m6s
2024-10-14 23:19:54 +01:00
Lino Silva bb777f002b Fixed actions
Build and publish / build (push) Failing after 1m4s
2024-10-14 23:17:59 +01:00
Lino Silva 6603df380c Fixed actions
Build and publish / build (push) Failing after 51s
2024-10-14 23:16:44 +01:00
Lino Silva cdbadd5c92 Fixed actions
Build and publish / build (push) Failing after 17s
2024-10-14 23:15:58 +01:00
Lino Silva b2d20fac5c Fixed actions
Build and publish / build (push) Failing after 5s
2024-10-14 23:15:33 +01:00
Lino Silva 4ddcfd33cf Fixed actions
Build and publish / build (push) Failing after 46s
2024-10-14 23:13:52 +01:00
Lino Silva 59f6f90664 Fixed actions
Build and publish / build (push) Failing after 14s
2024-10-14 23:11:51 +01:00
Lino Silva b4c37b218a Fixed actions
Build and publish / build (push) Failing after 45s
2024-10-14 23:05:48 +01:00
Lino Silva d1761800c0 Fixed actions
Build and publish / build (push) Failing after 16s
2024-10-14 23:03:49 +01:00
Lino Silva 9716c25377 Fixed actions
Build and publish / build (push) Has been cancelled
2024-10-14 23:03:34 +01:00
Lino Silva 46ebcc041f Fixed actions
Build and publish / build (push) Successful in 10s
Run price sync / build (push) Failing after 26s
2024-10-14 22:59:21 +01:00
Lino Silva fe7e197480 Added sync action
Build and publish / build (push) Successful in 9s
Run price sync / build (push) Failing after 6s
2024-10-14 22:58:21 +01:00
Lino Silva f27c03dbaf Fixed actions
Build and publish / build (push) Successful in 55s
2024-10-14 22:55:29 +01:00
Lino Silva cfe7eb0454 Fixed dockerfile
Build and publish / build (push) Failing after 2m0s
2024-10-14 22:51:05 +01:00
Lino Silva 06400d276e actions
Build and publish / build (push) Failing after 1m17s
2024-10-14 22:47:34 +01:00
Lino Silva 13d55bc800 actions
Build and publish / build (push) Waiting to run
Gitea Actions Demo / run (push) Successful in 35s
2024-10-13 22:03:55 +01:00
16 changed files with 5701 additions and 1486 deletions
+12
View File
@@ -0,0 +1,12 @@
node_modules
npm-debug.log
.git
.gitignore
.env
.DS_Store
*.md
!README.md
.vscode
.idea
coverage
.eslintcache
+2 -2
View File
@@ -1,2 +1,2 @@
GHOSTFOLIO_SECURITY_TOKEN=
GHOSTFOLIO_HOST=
# Optional: Port to run the server on (defaults to 3000)
PORT=3000
+54
View File
@@ -0,0 +1,54 @@
name: Build and publish
run-name: Build docker image and publish to registry
on:
push:
branches:
- 'main'
tags:
- 'v*'
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: https://github.com/actions/checkout@v4
- name: Set up Docker Buildx
uses: https://github.com/docker/setup-buildx-action@v3
with:
config-inline: |
[registry."gitea.lino.cooking"]
http = true
- name: Log in to Gitea Container Registry
uses: https://github.com/docker/login-action@v3
with:
registry: gitea.lino.cooking
username: lino
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Extract metadata
id: meta
uses: https://github.com/docker/metadata-action@v5
with:
images: gitea.lino.cooking/${{ gitea.repository }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,prefix={{branch}}-
type=raw,value=latest,enable={{is_default_branch}}
- name: Build and push Docker image
uses: https://github.com/docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=registry,ref=gitea.lino.cooking/${{ gitea.repository }}:buildcache
cache-to: type=registry,ref=gitea.lino.cooking/${{ gitea.repository }}:buildcache,mode=max
+1 -1
View File
@@ -129,4 +129,4 @@ dist
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
.DS_Store
+34 -19
View File
@@ -1,25 +1,40 @@
FROM ghcr.io/puppeteer/puppeteer:22
# Use Node.js LTS with Debian (needed for Puppeteer dependencies)
FROM node:20-bookworm-slim
USER root
# Install Chromium dependencies for Puppeteer
RUN apt-get update && apt-get install -y \
chromium \
fonts-ipafont-gothic fonts-wqy-zenhei fonts-thai-tlwg fonts-khmeros fonts-kacst fonts-freefont-ttf \
libxss1 \
--no-install-recommends \
&& rm -rf /var/lib/apt/lists/*
# Add user so we don't need --no-sandbox.
RUN mkdir -p /home/pptruser/Downloads /app \
&& chown -R pptruser:pptruser /home/pptruser \
&& chown -R pptruser:pptruser /app
# Set Puppeteer to use installed Chromium
ENV PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true
ENV PUPPETEER_EXECUTABLE_PATH=/usr/bin/chromium
# Run everything after as non-privileged user.
USER pptruser
# Create app directory
WORKDIR /usr/src/app
# Install Puppeteer under /node_modules so it's available system-wide
COPY package.json /app/
COPY yarn.lock /app/
RUN cd /app/ && yarn
COPY .env /app/
COPY index.mjs /app/
# Copy package files
COPY package*.json ./
COPY pnpm-lock.yaml ./
ARG GHOSTFOLIO_SECURITY_TOKEN
ARG GHOSTFOLIO_HOST
ENV GHOSTFOLIO_SECURITY_TOKEN=$GHOSTFOLIO_SECURITY_TOKEN
ENV GHOSTFOLIO_HOST=$GHOSTFOLIO_HOST
# Install pnpm
RUN npm install -g pnpm
ENTRYPOINT ["/usr/local/bin/node", "/app/index.mjs"]
# Install dependencies
RUN pnpm install --frozen-lockfile
# Copy application files
COPY . .
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=40s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"
# Run the application
CMD ["npm", "start"]
+87 -11
View File
@@ -1,21 +1,97 @@
# bpi-stock-price-scraper
Extrair preço de PPRs para introduzir no Ghostfolio
Web server API to fetch latest BPI PPR stock prices on demand.
# Utilização
## Features
Criar ficheiro `.env` a partir do `.env.example`
- REST API endpoints to get latest prices for BPI Destino PPR funds
- Returns price data as JSON
- Dockerized for easy deployment
- Health check endpoint included
## Docker
## Available Symbols
```
docker compose build
docker compose up
- `BPIDEST2040` - BPI Destino PPR 2040
- `BPIDEST2050` - BPI Destino PPR 2050
## API Endpoints
- `GET /` - API information and usage examples
- `GET /health` - Health check with available symbols
- `GET /price/:symbol` - Get latest price for a specific symbol
### Example Response
```json
{
"symbol": "BPIDEST2040",
"name": "BPI Destino PPR 2040",
"price": 7.73313,
"date": "2026-04-09",
"timestamp": "2026-04-10T16:11:25.850Z"
}
```
## Node
## Usage
### Docker (Recommended)
Build and run with docker-compose:
```bash
docker compose up -d
```
Or build and run manually:
```bash
docker build -t bpi-stock-price-scraper .
docker run -p 3000:3000 bpi-stock-price-scraper
```
### Node.js
Install dependencies and run:
```bash
npm install
npm start
```
For development with auto-reload:
```bash
npm run dev
```
## Configuration
The server runs on port 3000 by default. You can change it via the `PORT` environment variable:
```bash
PORT=8080 npm start
```
Or in docker-compose.yml:
```yaml
environment:
- PORT=8080
ports:
- "8080:8080"
```
## Testing
Test the API:
```bash
# Get API info
curl http://localhost:3000/
# Check health
curl http://localhost:3000/health
# Get latest price
curl http://localhost:3000/price/BPIDEST2040
```
yarn
node index.mjs
```
+37
View File
@@ -0,0 +1,37 @@
import { AvailableSymbol, ProviderType } from "./interfaces";
export const availableSymbols: Record<string, AvailableSymbol> = {
// DECO Proteste
"0P0001P80D": {
mappedSymbol: "bpi-impacto-clima-moderado",
provider: ProviderType.DECO,
},
IE00BFMXXD54: {
mappedSymbol: "vanguard-sp-500-ucits-etf-usd-acc",
provider: ProviderType.DECO,
},
IE00BK5BQT80: {
mappedSymbol: "vanguard-ftse-all-world-ucits-etf-usd-acc",
provider: ProviderType.DECO,
},
// BPI
BPIDEST2040: {
mappedSymbol: "bpi-destino-ppr-2040",
provider: ProviderType.BPI,
},
BPIDEST2050: {
mappedSymbol: "bpi-destino-ppr-2050",
provider: ProviderType.BPI,
},
// Financial times
"0P0001P80D-FI": {
mappedSymbol: "PTBG2EHM0006",
provider: ProviderType.FT,
},
LU0203975437: {
mappedSymbol: "lu0203975437",
provider: ProviderType.FT,
},
};
+14 -7
View File
@@ -1,9 +1,16 @@
version: '3.8'
services:
scraper:
image: bpi-stock-price-scraper
bpi-scraper:
build: .
ports:
- "3000:3000"
environment:
- GHOSTFOLIO_SECURITY_TOKEN=${GHOSTFOLIO_SECURITY_TOKEN} # here it is!
- GHOSTFOLIO_HOST=${GHOSTFOLIO_HOST} # here it is!
build:
context: .
dockerfile: Dockerfile
- PORT=3000
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/health', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
-100
View File
@@ -1,100 +0,0 @@
import puppeteer from "puppeteer";
import process from "node:process";
import axios from "axios";
import "dotenv/config";
const contas = {
bpi2040: {
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2040",
ghostfolioName: "BPI%20Destino%20PPR%202040",
},
bpi2050: {
url: "https://www.bancobpi.pt/particulares/poupar-investir/ppr/bpi-destino-ppr-2050",
ghostfolioName: "BPI%20Destino%20PPR%202050",
},
};
async function parseLogRocketBlogHome(conta) {
// Launch the browser
const browser = await puppeteer.launch();
// Open a new tab
const page = await browser.newPage();
// Visit the page and wait until network connections are completed
await page.goto(conta.url, { waitUntil: "networkidle2" });
// Interact with the DOM to retrieve the titles
const [price, date] = await page.evaluate(() => {
const spanTags = document.getElementsByTagName("span");
const priceSearchText = "ÚLTIMA COTAÇÃO:";
const dateSearchText = "DATA COTAÇÃO:";
let priceElementFound;
let dateElementFound;
for (let i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent.trim() == priceSearchText) {
priceElementFound = spanTags[i];
console.log(`Found ${priceSearchText}.`);
if (priceElementFound && dateElementFound) break;
}
if (spanTags[i].textContent.trim() == dateSearchText) {
dateElementFound = spanTags[i];
console.log(`Found ${dateSearchText}.`);
if (priceElementFound && dateElementFound) break;
}
}
return [
priceElementFound.nextSibling.innerHTML,
dateElementFound.nextSibling.innerHTML,
];
});
// Don't forget to close the browser instance to clean up the memory
await browser.close();
const marketPrice = parseFloat(
price.replace("€", "").trim().replace(",", ".")
);
// Print the results
console.log(decodeURIComponent(conta.ghostfolioName));
console.log(`Current price: € ${marketPrice}`);
console.log(`Date: ${date}`);
const bearerTokenResponse = await axios.post(
`${process.env.GHOSTFOLIO_HOST}/api/v1/auth/anonymous`,
{
accessToken: process.env.GHOSTFOLIO_SECURITY_TOKEN,
}
);
const response = await axios
.post(
`${process.env.GHOSTFOLIO_HOST}/api/v1/admin/market-data/MANUAL/${conta.ghostfolioName}`,
{
marketData: [
{
date,
marketPrice,
},
],
},
{
headers: {
Authorization: `Bearer ${bearerTokenResponse.data.authToken}`,
},
}
)
.catch(console.error);
console.log(response.status);
}
await Promise.all([
parseLogRocketBlogHome(contas.bpi2040),
parseLogRocketBlogHome(contas.bpi2050),
]);
process.exit(0);
+243
View File
@@ -0,0 +1,243 @@
import puppeteer, { Page } from "puppeteer";
import express from "express";
import "dotenv/config";
import { Provider, ProviderType } from "./interfaces";
import { availableSymbols } from "./availableSymbols";
const app = express();
const PORT = process.env.PORT || 3000;
const providers: Record<ProviderType, Provider> = {
[ProviderType.BPI]: {
type: ProviderType.BPI,
baseUrl:
"https://www.bancobpi.pt/particulares/poupar-investir/ppr/${symbol}",
},
[ProviderType.DECO]: {
type: ProviderType.DECO,
baseUrl:
"https://www.deco.proteste.pt/investe/investimentos/fundos/${symbol}",
},
[ProviderType.FT]: {
type: ProviderType.FT,
baseUrl: "https://markets.ft.com/data/funds/tearsheet/summary?s=${symbol}",
},
};
interface SymbolConfig {
name: string;
type: ProviderType;
url: string;
}
function getConfigForSymbol(symbol: string): SymbolConfig {
if (!availableSymbols[symbol]) {
throw new Error(`Symbol ${symbol} is not supported`);
}
return {
name: symbol,
type: availableSymbols[symbol].provider,
url: providers[availableSymbols[symbol].provider].baseUrl.replace(
"${symbol}",
availableSymbols[symbol].mappedSymbol,
),
};
}
// Function to scrape price for a given symbol
async function getLatestPrice(symbol: string) {
const config: SymbolConfig = getConfigForSymbol(symbol);
console.log(
`Scraping ${config.url} for symbol ${symbol} using provider ${config.type}`,
);
// Launch the browser
const browser = await puppeteer.launch({
args: ["--no-sandbox"],
timeout: 10000,
});
try {
// Open a new tab
const page = await browser.newPage();
// Visit the page and wait until network connections are completed
await page.goto(config.url, { waitUntil: "networkidle2" });
switch (config.type) {
case ProviderType.BPI:
return await scrapeBPI(page, symbol, config);
case ProviderType.DECO:
return await scrapeDeco(page, symbol, config);
case ProviderType.FT:
return await scrapeFT(page, symbol, config);
default:
throw new Error(`Unsupported type: ${config.type}`);
}
} finally {
// Always close the browser
await browser.close();
}
}
async function scrapeDeco(page: Page, symbol: string, config: SymbolConfig) {
// Interact with the DOM to retrieve the price and date
const [price, date] = await page.evaluate(() => {
const priceElement = document.querySelector(
".product-points-data__current-value",
);
const dateElement = document.querySelector(
".product-points-data__current-value + span",
);
if (!priceElement || !dateElement) {
throw new Error("Could not find price or date elements");
}
const price = priceElement.textContent
.replace("€", "")
.replace("EUR", "")
.replace(",", ".")
.trim();
const date = dateElement.textContent.trim();
return [price, date];
});
const marketPrice = parseFloat(price);
return {
symbol,
name: config.name,
currency: "EUR",
price: marketPrice,
date,
timestamp: new Date().toISOString(),
};
}
async function scrapeBPI(page: Page, symbol: string, config: SymbolConfig) {
// Interact with the DOM to retrieve the price and date
const [price, date] = await page.evaluate(() => {
const spanTags = document.getElementsByTagName("span");
const priceSearchText = "ÚLTIMA COTAÇÃO:";
const dateSearchText = "DATA COTAÇÃO:";
let priceElementFound;
let dateElementFound;
for (let i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent.trim() == priceSearchText) {
priceElementFound = spanTags[i];
if (priceElementFound && dateElementFound) break;
}
if (spanTags[i].textContent.trim() == dateSearchText) {
dateElementFound = spanTags[i];
if (priceElementFound && dateElementFound) break;
}
}
if (!priceElementFound || !dateElementFound) {
throw new Error("Could not find price or date elements");
}
return [
priceElementFound?.nextSibling?.textContent,
dateElementFound?.nextSibling?.textContent,
];
});
const marketPrice = parseFloat(
price?.replace("€", "").trim().replace(",", ".") || "0",
);
return {
symbol,
name: config.name,
currency: "EUR",
price: marketPrice,
date: date?.trim() || "",
timestamp: new Date().toISOString(),
};
}
async function scrapeFT(page: Page, symbol: string, config: SymbolConfig) {
// Interact with the DOM to retrieve the price and date
const price = await page.evaluate(() => {
const spanTags = document.getElementsByTagName("span");
const priceSearchText = "Price (EUR)";
let priceElementFound;
for (let i = 0; i < spanTags.length; i++) {
if (spanTags[i].textContent.trim() == priceSearchText) {
priceElementFound = spanTags[i];
if (priceElementFound) break;
}
}
if (!priceElementFound) {
throw new Error("Could not find price element");
}
return priceElementFound?.nextSibling?.textContent;
});
const marketPrice = parseFloat(
price?.replace("€", "").trim().replace(",", ".") || "0",
);
return {
symbol,
name: config.name,
currency: "EUR",
price: marketPrice,
// date in FT is not available, so we use the current date as a placeholder
// yyyy-mm-dd format
date: new Date().toISOString().split("T")[0],
timestamp: new Date().toISOString(),
};
}
// API endpoint to get latest price for a symbol
app.get("/price/:symbol", async (req, res) => {
const { symbol } = req.params;
try {
console.log(`Fetching price for symbol: ${symbol}`);
const priceData = await getLatestPrice(symbol);
console.log(`Success: ${symbol} - ${priceData.price}`);
res.json(priceData);
} catch (error: any) {
console.error(`Error fetching price for ${symbol}:`, error.message);
res.status(400).json({
error: error.message,
symbol,
});
}
});
// Health check endpoint
app.get("/health", (req, res) => {
res.json({ status: "ok" });
});
// Root endpoint with usage instructions
app.get("/", (req, res) => {
res.json({
message: "BPI Stock Price Scraper API",
endpoints: {
health: "/health",
"price-bpi": "/price/bpi/:symbol",
"price-deco": "/price/deco/:symbol",
"price-ft": "/price/ft/:symbol",
},
example: `/price/bpi/BPIDEST2040`,
});
});
// Start the server
app.listen(PORT, () => {
console.log(`Server running on http://localhost:${PORT}`);
console.log("Available symbols:", Object.keys(availableSymbols).join(", "));
console.log(`Example: http://localhost:${PORT}/price/bpi/BPIDEST2040`);
});
+15
View File
@@ -0,0 +1,15 @@
export enum ProviderType {
BPI = "bpi",
DECO = "deco",
FT = "ft",
}
export interface Provider {
type: ProviderType;
baseUrl: string;
}
export interface AvailableSymbol {
mappedSymbol: string;
provider: ProviderType;
}
+2786
View File
File diff suppressed because it is too large Load Diff
+12 -3
View File
@@ -1,18 +1,27 @@
{
"name": "bpi-stock-price-scraper",
"version": "1.0.0",
"main": "index.js",
"main": "index.ts",
"type": "module",
"repository": "ssh://git@10.0.2.28:222/lino-authelia/bpi-stock-price-scraper.git",
"author": "Lino Silva <me@lino.cooking>",
"license": "MIT",
"scripts": {
"start": "tsx index.ts",
"dev": "tsx watch index.ts"
},
"dependencies": {
"axios": "^1.7.7",
"dotenv": "^16.4.5",
"express": "^5.2.1",
"puppeteer": "^23.5.3"
},
"devDependencies": {
"@eslint/js": "^9.12.0",
"@types/express": "^5.0.6",
"@types/node": "^25.7.0",
"eslint": "^9.12.0",
"globals": "^15.11.0"
"globals": "^15.11.0",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
}
}
+2385
View File
File diff suppressed because it is too large Load Diff
+19
View File
@@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"lib": ["ES2022", "DOM"],
"esModuleInterop": true,
"skipLibCheck": true,
"strict": true,
"resolveJsonModule": true,
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
"outDir": "./dist",
"rootDir": "."
},
"include": ["**/*.ts"],
"exclude": ["node_modules", "dist"]
}
-1343
View File
File diff suppressed because it is too large Load Diff