@sebbo2002/tgtg-ical 1.0.0-develop.0 → 1.0.0-develop.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2021 Sebastian Pekarek
3
+ Copyright (c) 2023 Sebastian Pekarek
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated
6
6
  documentation files (the "Software"), to deal in the Software without restriction, including without limitation the
package/README.md CHANGED
@@ -1,10 +1,40 @@
1
- # template
1
+ # tgtg-ical
2
2
 
3
3
  [![License](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](LICENSE)
4
4
 
5
5
  A small server that receives mails from TGTG, parses them and generates an iCal feed from them.
6
6
 
7
7
 
8
+ ## 📦 Installation
9
+
10
+ git clone https://github.com/sebbo2002/tgtg-ical.git
11
+ cd ./tgtg-ical
12
+
13
+ echo 'DATABASE_URL="mysql://root@localhost:3306/tgtg-ical"' > .env
14
+
15
+ npm install
16
+ npx prisma migrate deploy
17
+
18
+
19
+ ## 🙋 FAQ
20
+
21
+ ### How does this work?
22
+ With the help of tgtg-ical you can generate a personal email address and a corresponding calendar feed. If you store
23
+ this email address at Too Good To Go as an email address for notifications or forward the messages (e.g. via a filter
24
+ rule), collection appointments will be displayed in the corresponding calendar feed.
25
+
26
+ ### Which languages are supported?
27
+ Currently only German and English are supported. If you want to add another language, feel free to create a pull request.
28
+
29
+ ### How long are emails stored?
30
+ In the best case, the incoming e-mail can be completely analyzed and is then deleted directly. Then only the information
31
+ needed to provide the calendar is stored. If the analysis fails, the email is kept for manual analysis and deleted after
32
+ two weeks at the latest.
33
+
34
+ ### Which databases are supported?
35
+ We use [Prisma](https://www.prisma.io/) as ORM. All databases supported by Prisma should therefore work with tgtg-ical.
36
+
37
+
8
38
  ## 🙆🏼‍♂️ Copyright and license
9
39
 
10
40
  Copyright (c) Sebastian Pekarek under the [MIT license](LICENSE).
package/dist/start.js CHANGED
@@ -1,4 +1,4 @@
1
1
  #!/usr/bin/env node
2
2
  import{a as i,b as o,c as d}from"./chunk-HTOHNK4X.js";import p from"express";import y from"cookie-parser";import{randomUUID as h}from"crypto";import{generateName as f,generateNameWithNumber as g}from"@criblinc/docker-names";import{ICalAlarmType as w,ICalCalendar as E,ICalEventStatus as l}from"ical-generator";import I from"moment-timezone";import{readFile as U}from"fs/promises";var s=class{static async createUser(){let e;for(let t=0;;t++){let r=this.generatePrefix(t);try{if(e=await i.user.create({data:{prefix:r}}),e)break}catch(a){if(a.code==="P2002")continue;throw a}}if(!e)throw new Error("User not created");return e}static generatePrefix(e=0){return e>100?h():e<10?f():g()}static async getUser(e){let t=await i.user.findUniqueOrThrow({where:{id:e}});return this.updateUserLastSeen(t.id),t}static updateUserLastSeen(e){i.user.update({where:{id:e},data:{lastSeenAt:new Date}}).catch(t=>{console.log(t)})}static async generateUserPage(e){let[t,r]=await Promise.all([this.getUser(e),U(o.src("templates/user.html"),"utf-8")]);return r.replace(/\${CALENDAR_URL}/g,`${o.baseUrl}/${t.id}/calendar.ical`).replace(/\${EMAIL_ADDRESS}/g,`${t.prefix}${o.baseMail}`)}static async generateCalendar(e){let t=await i.user.findUniqueOrThrow({where:{id:e},select:{id:!0,event:{select:{id:!0,orderId:!0,from:!0,to:!0,amount:!0,price:!0,location:{select:{name:!0,address:!0,emoji:!0,latitude:!0,longitude:!0}},createdAt:!0,canceledAt:!0}}}}),r=new E({name:"TGTG",ttl:60*60,events:t.event.map(a=>{let m=new Intl.NumberFormat("de-DE",{style:"currency",currency:"EUR"}).format(a.price/100),u=l.CONFIRMED;return a.canceledAt&&(u=l.CANCELLED),{id:a.id,start:a.from,end:a.to,timestamp:a.createdAt,summary:`${a.location.emoji||d} ${a.location.name}`,description:`${a.amount}x
3
- ${m}`,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`,status:u,created:a.createdAt,location:{title:a.location.name,address:a.location.address,geo:a.location.latitude&&a.location.longitude?{lat:a.location.latitude,lon:a.location.longitude}:void 0},alarms:[{type:w.display,trigger:600}]}})});return this.updateUserLastSeen(e),r.toString()}static async isHealthy(){let e=await i.mail.count({where:{createdAt:{gte:I().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unahandled mails in the queue!`)}};import{Prisma as C}from"@prisma/client";var n=class c{static run(){new c}app;server;constructor(){this.app=p(),this.app.use(y()),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop())}setupRoutes(){this.app.get("/ping",(e,t)=>{t.send("pong")}),this.app.get("/",(e,t)=>{if("userId"in e.cookies&&e.cookies.userId){t.redirect("/"+e.cookies.userId);return}s.createUser().then(r=>{t.cookie("userId",r.id),t.redirect("/"+r.id)}).catch(r=>this.handleError(r,t))}),this.app.get("/_health",(e,t)=>{s.isHealthy().then(()=>t.sendStatus(201)).catch(r=>this.handleError(r,t))}),this.app.use(p.static(o.src("./assets"))),this.app.get("/:userId",(e,t)=>{t.format({"text/html":()=>{s.generateUserPage(e.params.userId).then(r=>{t.cookie("userId",e.params.userId),t.send(r)}).catch(r=>this.handleError(r,t))},"application/json":()=>{s.getUser(e.params.userId).then(r=>t.send(r)).catch(r=>this.handleError(r,t))}})}),this.app.get("/:userId/calendar.ical",(e,t)=>{s.generateCalendar(e.params.userId).then(r=>{t.set("Content-Type","text/calendar"),t.send(r)}).catch(r=>this.handleError(r,t))})}handleError(e,t){if(e instanceof C.PrismaClientKnownRequestError&&e.code==="P2025"){t.sendStatus(404);return}console.log(e),t.sendStatus(500)}async stop(){await new Promise(e=>this.server.close(e)),await i.$disconnect(),process.exit()}};n.run();
3
+ ${m}`,url:`https://share.toogoodtogo.com/receipts/details/${a.orderId}`,status:u,created:a.createdAt,location:{title:a.location.name,address:a.location.address,geo:a.location.latitude&&a.location.longitude?{lat:a.location.latitude,lon:a.location.longitude}:void 0},alarms:[{type:w.display,trigger:600}]}})});return this.updateUserLastSeen(e),r.toString()}static async isHealthy(){let e=await i.mail.count({where:{createdAt:{gte:I().subtract(30,"minutes").toDate()}}});if(e>0)throw new Error(`There are ${e} unahandled mails in the queue!`)}};var n=class c{static run(){new c}app;server;constructor(){this.app=p(),this.app.use(y()),this.setupRoutes(),this.server=this.app.listen(process.env.PORT||8080),process.on("SIGINT",()=>this.stop()),process.on("SIGTERM",()=>this.stop())}setupRoutes(){this.app.get("/ping",(e,t)=>{t.send("pong")}),this.app.get("/",(e,t)=>{if("userId"in e.cookies&&e.cookies.userId){t.redirect("/"+e.cookies.userId);return}s.createUser().then(r=>{t.cookie("userId",r.id),t.redirect("/"+r.id)}).catch(r=>this.handleError(r,t))}),this.app.get("/_health",(e,t)=>{s.isHealthy().then(()=>t.sendStatus(204)).catch(r=>this.handleError(r,t))}),this.app.use(p.static(o.src("./assets"))),this.app.get("/:userId",(e,t)=>{t.format({"text/html":()=>{s.generateUserPage(e.params.userId).then(r=>{t.cookie("userId",e.params.userId),t.send(r)}).catch(r=>this.handleError(r,t))},"application/json":()=>{s.getUser(e.params.userId).then(r=>t.send(r)).catch(r=>this.handleError(r,t))}})}),this.app.get("/:userId/calendar.ical",(e,t)=>{s.generateCalendar(e.params.userId).then(r=>{t.set("Content-Type","text/calendar"),t.send(r)}).catch(r=>this.handleError(r,t))})}handleError(e,t){if(typeof e=="object"&&"code"in e&&e.code==="P2025"){t.sendStatus(404);return}console.log(e),t.sendStatus(500)}async stop(){await new Promise(e=>this.server.close(e)),await i.$disconnect(),process.exit()}};n.run();
4
4
  //# sourceMappingURL=start.js.map
package/dist/start.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/bin/start.ts","../src/lib/server.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport express, { Express, Response } from 'express';\nimport cookieParser from 'cookie-parser';\nimport {Server} from 'http';\n\nimport prisma from '../lib/db.js';\nimport ServerLib from '../lib/server.js';\nimport { Prisma } from '@prisma/client';\nimport Config from '../lib/config';\n\n\nclass AppServer {\n static run() {\n new AppServer();\n }\n\n private app: Express;\n private server: Server;\n\n constructor() {\n this.app = express();\n this.app.use(cookieParser());\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n this.app.get('/', (req, res) => {\n if('userId' in req.cookies && req.cookies.userId) {\n res.redirect('/' + req.cookies.userId);\n return;\n }\n\n ServerLib.createUser()\n .then(user => {\n res.cookie('userId', user.id);\n res.redirect('/' + user.id);\n })\n .catch(error => this.handleError(error, res));\n });\n\n this.app.get('/_health', (req, res) => {\n ServerLib.isHealthy()\n .then(() => res.sendStatus(201))\n .catch(error => this.handleError(error, res));\n });\n\n this.app.use(express.static(Config.src('./assets')));\n\n this.app.get('/:userId', (req, res) => {\n res.format({\n 'text/html': () => {\n ServerLib.generateUserPage(req.params.userId)\n .then(html => {\n res.cookie('userId', req.params.userId);\n res.send(html);\n })\n .catch(error => this.handleError(error, res));\n },\n 'application/json': () => {\n ServerLib.getUser(req.params.userId)\n .then(json => res.send(json))\n .catch(error => this.handleError(error, res));\n }\n });\n });\n\n this.app.get('/:userId/calendar.ical', (req, res) => {\n ServerLib.generateCalendar(req.params.userId)\n .then(ical => {\n res.set('Content-Type', 'text/calendar');\n res.send(ical);\n })\n .catch(error => this.handleError(error, res));\n });\n }\n\n handleError(error: Prisma.PrismaClientKnownRequestError | unknown, res: Response) {\n if(error instanceof Prisma.PrismaClientKnownRequestError && error.code === 'P2025') {\n res.sendStatus(404);\n return;\n }\n\n console.log(error);\n res.sendStatus(500);\n }\n\n async stop() {\n await new Promise(cb => this.server.close(cb));\n await prisma.$disconnect();\n\n process.exit();\n }\n}\n\nAppServer.run();\n","import prisma from './db.js';\nimport { randomUUID } from 'node:crypto';\nimport { generateName, generateNameWithNumber } from '@criblinc/docker-names';\nimport { User } from '@prisma/client';\nimport { ICalAlarmType, ICalCalendar, ICalEventStatus } from 'ical-generator';\nimport { DEFAULT_EMOJI } from './emoji.js';\nimport moment from 'moment-timezone';\nimport { readFile } from 'fs/promises';\nimport Config from './config.js';\n\nexport default class ServerLib {\n static async createUser() {\n let user: User | undefined;\n for (let c = 0; true; c++) {\n const prefix = this.generatePrefix(c);\n try {\n user = await prisma.user.create({\n data: {\n prefix\n }\n });\n if(user) {\n break;\n }\n }\n catch(error) {\n if(error.code === 'P2002') {\n continue;\n }\n\n throw error;\n }\n }\n if(!user) {\n throw new Error('User not created');\n }\n\n return user;\n }\n\n static generatePrefix(c = 0) {\n if(c > 100) {\n return randomUUID();\n }\n\n if(c < 10) {\n return generateName();\n }\n\n return generateNameWithNumber();\n }\n\n static async getUser(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n }\n });\n\n this.updateUserLastSeen(user.id);\n return user;\n }\n\n static updateUserLastSeen(userId: string) {\n prisma.user.update({\n where: { id: userId },\n data: { lastSeenAt: new Date() }\n }).catch(error => {\n console.log(error);\n });\n }\n\n static async generateUserPage(userId: string) {\n const [user, html] = await Promise.all([\n this.getUser(userId),\n readFile(Config.src('templates/user.html'), 'utf-8')\n ]);\n\n return html\n .replace(/\\${CALENDAR_URL}/g, `${Config.baseUrl}/${user.id}/calendar.ical`)\n .replace(/\\${EMAIL_ADDRESS}/g, `${user.prefix}${Config.baseMail}`);\n }\n\n static async generateCalendar(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n },\n select: {\n id: true,\n event: {\n select: {\n id: true,\n orderId: true,\n from: true,\n to: true,\n amount: true,\n price: true,\n location: {\n select: {\n name: true,\n address: true,\n emoji: true,\n latitude: true,\n longitude: true\n }\n },\n createdAt: true,\n canceledAt: true\n }\n }\n }\n });\n\n const cal = new ICalCalendar({\n name: 'TGTG',\n ttl: 60 * 60,\n events: user.event.map(event => {\n const price = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })\n .format(event.price / 100);\n\n let status: ICalEventStatus = ICalEventStatus.CONFIRMED;\n if(event.canceledAt) {\n status = ICalEventStatus.CANCELLED;\n }\n\n return {\n id: event.id,\n start: event.from,\n end: event.to,\n timestamp: event.createdAt,\n summary: `${event.location.emoji || DEFAULT_EMOJI} ${event.location.name}`,\n description: `${event.amount}x\\n${price}`,\n url: `https://share.toogoodtogo.com/receipts/details/${event.orderId}`,\n status,\n created: event.createdAt,\n location: {\n title: event.location.name,\n address: event.location.address,\n geo: event.location.latitude && event.location.longitude ? {\n lat: event.location.latitude,\n lon: event.location.longitude\n } : undefined\n },\n alarms: [\n {type: ICalAlarmType.display, trigger: 600},\n ]\n };\n })\n });\n\n this.updateUserLastSeen(userId);\n return cal.toString();\n }\n\n static async isHealthy() {\n const c = await prisma.mail.count({\n where: {\n createdAt: {\n gte: moment().subtract(30, 'minutes').toDate()\n }\n }\n });\n if(c > 0) {\n throw new Error(`There are ${c} unahandled mails in the queue!`);\n }\n }\n}\n"],"mappings":"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","names":["express","cookieParser","randomUUID","generateName","generateNameWithNumber","ICalAlarmType","ICalCalendar","ICalEventStatus","moment","readFile","ServerLib","user","c","prefix","db_default","error","randomUUID","generateName","generateNameWithNumber","userId","html","readFile","config_default","cal","ICalCalendar","event","price","status","ICalEventStatus","DEFAULT_EMOJI","ICalAlarmType","moment","Prisma","AppServer","_AppServer","express","cookieParser","req","res","ServerLib","user","error","config_default","html","json","ical","Prisma","cb","db_default"]}
1
+ {"version":3,"sources":["../src/bin/start.ts","../src/lib/server.ts"],"sourcesContent":["#!/usr/bin/env node\n'use strict';\n\nimport express, { Express, Response } from 'express';\nimport cookieParser from 'cookie-parser';\nimport {Server} from 'http';\n\nimport prisma from '../lib/db.js';\nimport ServerLib from '../lib/server.js';\nimport Config from '../lib/config';\nimport type { PrismaClientKnownRequestError } from '@prisma/client/runtime/library';\n\n\nclass AppServer {\n static run() {\n new AppServer();\n }\n\n private app: Express;\n private server: Server;\n\n constructor() {\n this.app = express();\n this.app.use(cookieParser());\n\n this.setupRoutes();\n this.server = this.app.listen(process.env.PORT || 8080);\n\n process.on('SIGINT', () => this.stop());\n process.on('SIGTERM', () => this.stop());\n }\n\n setupRoutes() {\n this.app.get('/ping', (req, res) => {\n res.send('pong');\n });\n\n this.app.get('/', (req, res) => {\n if('userId' in req.cookies && req.cookies.userId) {\n res.redirect('/' + req.cookies.userId);\n return;\n }\n\n ServerLib.createUser()\n .then(user => {\n res.cookie('userId', user.id);\n res.redirect('/' + user.id);\n })\n .catch(error => this.handleError(error, res));\n });\n\n this.app.get('/_health', (req, res) => {\n ServerLib.isHealthy()\n .then(() => res.sendStatus(204))\n .catch(error => this.handleError(error, res));\n });\n\n this.app.use(express.static(Config.src('./assets')));\n\n this.app.get('/:userId', (req, res) => {\n res.format({\n 'text/html': () => {\n ServerLib.generateUserPage(req.params.userId)\n .then(html => {\n res.cookie('userId', req.params.userId);\n res.send(html);\n })\n .catch(error => this.handleError(error, res));\n },\n 'application/json': () => {\n ServerLib.getUser(req.params.userId)\n .then(json => res.send(json))\n .catch(error => this.handleError(error, res));\n }\n });\n });\n\n this.app.get('/:userId/calendar.ical', (req, res) => {\n ServerLib.generateCalendar(req.params.userId)\n .then(ical => {\n res.set('Content-Type', 'text/calendar');\n res.send(ical);\n })\n .catch(error => this.handleError(error, res));\n });\n }\n\n handleError(error: PrismaClientKnownRequestError | unknown, res: Response) {\n if(typeof error === 'object' && 'code' in error && error.code === 'P2025') {\n res.sendStatus(404);\n return;\n }\n\n console.log(error);\n res.sendStatus(500);\n }\n\n async stop() {\n await new Promise(cb => this.server.close(cb));\n await prisma.$disconnect();\n\n process.exit();\n }\n}\n\nAppServer.run();\n","import prisma from './db.js';\nimport { randomUUID } from 'node:crypto';\nimport { generateName, generateNameWithNumber } from '@criblinc/docker-names';\nimport { User } from '@prisma/client';\nimport { ICalAlarmType, ICalCalendar, ICalEventStatus } from 'ical-generator';\nimport { DEFAULT_EMOJI } from './emoji.js';\nimport moment from 'moment-timezone';\nimport { readFile } from 'fs/promises';\nimport Config from './config.js';\n\nexport default class ServerLib {\n static async createUser() {\n let user: User | undefined;\n for (let c = 0; true; c++) {\n const prefix = this.generatePrefix(c);\n try {\n user = await prisma.user.create({\n data: {\n prefix\n }\n });\n if(user) {\n break;\n }\n }\n catch(error) {\n if(error.code === 'P2002') {\n continue;\n }\n\n throw error;\n }\n }\n if(!user) {\n throw new Error('User not created');\n }\n\n return user;\n }\n\n static generatePrefix(c = 0) {\n if(c > 100) {\n return randomUUID();\n }\n\n if(c < 10) {\n return generateName();\n }\n\n return generateNameWithNumber();\n }\n\n static async getUser(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n }\n });\n\n this.updateUserLastSeen(user.id);\n return user;\n }\n\n static updateUserLastSeen(userId: string) {\n prisma.user.update({\n where: { id: userId },\n data: { lastSeenAt: new Date() }\n }).catch(error => {\n console.log(error);\n });\n }\n\n static async generateUserPage(userId: string) {\n const [user, html] = await Promise.all([\n this.getUser(userId),\n readFile(Config.src('templates/user.html'), 'utf-8')\n ]);\n\n return html\n .replace(/\\${CALENDAR_URL}/g, `${Config.baseUrl}/${user.id}/calendar.ical`)\n .replace(/\\${EMAIL_ADDRESS}/g, `${user.prefix}${Config.baseMail}`);\n }\n\n static async generateCalendar(userId: string) {\n const user = await prisma.user.findUniqueOrThrow({\n where: {\n id: userId\n },\n select: {\n id: true,\n event: {\n select: {\n id: true,\n orderId: true,\n from: true,\n to: true,\n amount: true,\n price: true,\n location: {\n select: {\n name: true,\n address: true,\n emoji: true,\n latitude: true,\n longitude: true\n }\n },\n createdAt: true,\n canceledAt: true\n }\n }\n }\n });\n\n const cal = new ICalCalendar({\n name: 'TGTG',\n ttl: 60 * 60,\n events: user.event.map(event => {\n const price = new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' })\n .format(event.price / 100);\n\n let status: ICalEventStatus = ICalEventStatus.CONFIRMED;\n if(event.canceledAt) {\n status = ICalEventStatus.CANCELLED;\n }\n\n return {\n id: event.id,\n start: event.from,\n end: event.to,\n timestamp: event.createdAt,\n summary: `${event.location.emoji || DEFAULT_EMOJI} ${event.location.name}`,\n description: `${event.amount}x\\n${price}`,\n url: `https://share.toogoodtogo.com/receipts/details/${event.orderId}`,\n status,\n created: event.createdAt,\n location: {\n title: event.location.name,\n address: event.location.address,\n geo: event.location.latitude && event.location.longitude ? {\n lat: event.location.latitude,\n lon: event.location.longitude\n } : undefined\n },\n alarms: [\n {type: ICalAlarmType.display, trigger: 600},\n ]\n };\n })\n });\n\n this.updateUserLastSeen(userId);\n return cal.toString();\n }\n\n static async isHealthy() {\n const c = await prisma.mail.count({\n where: {\n createdAt: {\n gte: moment().subtract(30, 'minutes').toDate()\n }\n }\n });\n if(c > 0) {\n throw new Error(`There are ${c} unahandled mails in the queue!`);\n }\n }\n}\n"],"mappings":"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","names":["express","cookieParser","randomUUID","generateName","generateNameWithNumber","ICalAlarmType","ICalCalendar","ICalEventStatus","moment","readFile","ServerLib","user","c","prefix","db_default","error","randomUUID","generateName","generateNameWithNumber","userId","html","readFile","config_default","cal","ICalCalendar","event","price","status","ICalEventStatus","DEFAULT_EMOJI","ICalAlarmType","moment","AppServer","_AppServer","express","cookieParser","req","res","ServerLib","user","error","config_default","html","json","ical","cb","db_default"]}
package/package.json CHANGED
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "author": "Sebastian Pekarek <mail@sebbo.net>",
3
3
  "bin": {
4
- "tgtg-ical-server": "./dist/start.js",
5
- "tgtg-ical-inhale-mail": "./dist/bin/inhale-mail.js"
4
+ "tgtg-ical-inhale-mail": "./dist/bin/inhale-mail.js",
5
+ "tgtg-ical-server": "./dist/start.js"
6
6
  },
7
7
  "bugs": {
8
8
  "url": "https://github.com/sebbo2002/tgtg-ical/issues"
@@ -68,12 +68,13 @@
68
68
  "build": "tsup",
69
69
  "build-all": "./.github/workflows/build.sh",
70
70
  "coverage": "c8 mocha",
71
+ "deploy": "./.github/workflows/deploy.sh",
71
72
  "develop": "ts-node ./src/bin/start.ts",
72
73
  "license-check": "license-checker --production --summary",
73
74
  "lint": "eslint . --ext .ts,.json",
74
- "start": "node ./dist/bin/start.js",
75
+ "start": "node ./dist/start.js",
75
76
  "test": "mocha"
76
77
  },
77
78
  "type": "module",
78
- "version": "1.0.0-develop.0"
79
+ "version": "1.0.0-develop.2"
79
80
  }