From fe3363cf7446331888a4851385ea0ec2ddf86019 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 17:48:24 +0000 Subject: [PATCH 1/4] Initial plan From 4bf464fb77972eeb3fa7ae49d783b3ff8caf1e36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 19 Mar 2026 18:02:18 +0000 Subject: [PATCH 2/4] feat: v1.0.1 improvements - exception filter, DTO validation, dashboard spend fix, API consolidation, test fixes Co-authored-by: ejames-dev <180847219+ejames-dev@users.noreply.github.com> --- apps/api/package.json | 2 +- apps/api/src/common/http-exception.filter.ts | 52 +++++++++++++++++++ apps/api/src/dashboard/dashboard.service.ts | 9 +++- .../api/src/ingest/email-ingest.controller.ts | 1 + apps/api/src/main.ts | 2 + .../src/settings/dto/update-settings.dto.ts | 3 +- .../dto/create-subscription.dto.ts | 8 ++- .../subscriptions.service.spec.ts | 16 ++++-- apps/web/package.json | 2 +- apps/web/src/components/subscription-form.tsx | 52 +++++++------------ apps/web/src/lib/api.ts | 10 ++++ docs/release-notes-v1.0.1.md | 29 +++++++++++ package-lock.json | 10 ++-- package.json | 2 +- packages/types/package.json | 11 ++-- 15 files changed, 159 insertions(+), 50 deletions(-) create mode 100644 apps/api/src/common/http-exception.filter.ts create mode 100644 docs/release-notes-v1.0.1.md diff --git a/apps/api/package.json b/apps/api/package.json index 82fcd83..11e4f91 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -1,6 +1,6 @@ { "name": "api", - "version": "1.0.0", + "version": "1.0.1", "description": "", "author": "", "private": true, diff --git a/apps/api/src/common/http-exception.filter.ts b/apps/api/src/common/http-exception.filter.ts new file mode 100644 index 0000000..4f5017d --- /dev/null +++ b/apps/api/src/common/http-exception.filter.ts @@ -0,0 +1,52 @@ +import { + ArgumentsHost, + Catch, + ExceptionFilter, + HttpException, + HttpStatus, + Logger, +} from '@nestjs/common'; +import { Response } from 'express'; + +@Catch() +export class HttpExceptionFilter implements ExceptionFilter { + private readonly logger = new Logger(HttpExceptionFilter.name); + + catch(exception: unknown, host: ArgumentsHost): void { + const ctx = host.switchToHttp(); + const response = ctx.getResponse(); + + let status = HttpStatus.INTERNAL_SERVER_ERROR; + let message: string | string[] = 'Internal server error'; + let error = 'Internal Server Error'; + + if (exception instanceof HttpException) { + status = exception.getStatus(); + const exceptionResponse = exception.getResponse(); + if (typeof exceptionResponse === 'string') { + message = exceptionResponse; + error = exceptionResponse; + } else if ( + typeof exceptionResponse === 'object' && + exceptionResponse !== null + ) { + const resp = exceptionResponse as Record; + if (resp.message !== undefined) { + message = resp.message as string | string[]; + } + if (typeof resp.error === 'string') { + error = resp.error; + } + } + } else { + const err = exception instanceof Error ? exception : new Error(String(exception)); + this.logger.error(err.message, err.stack); + } + + response.status(status).json({ + statusCode: status, + message, + error, + }); + } +} diff --git a/apps/api/src/dashboard/dashboard.service.ts b/apps/api/src/dashboard/dashboard.service.ts index 8379a3c..438c325 100644 --- a/apps/api/src/dashboard/dashboard.service.ts +++ b/apps/api/src/dashboard/dashboard.service.ts @@ -21,6 +21,11 @@ export class DashboardService { const ordered = subscriptions .slice() .sort((a, b) => a.nextRenewal.localeCompare(b.nextRenewal)); + + const billableSubscriptions = subscriptions.filter( + (s) => s.status !== 'canceled_pending', + ); + const sourceBreakdown: DashboardSummary['sourceBreakdown'] = { manual: 0, email: 0, @@ -33,7 +38,7 @@ export class DashboardService { } const spendByCategoryMap = new Map(); - for (const subscription of subscriptions) { + for (const subscription of billableSubscriptions) { const category = servicesById[subscription.serviceId]?.category ?? 'other'; spendByCategoryMap.set( @@ -53,7 +58,7 @@ export class DashboardService { return { monthlyEquivalentSpend: this.roundCurrency( - subscriptions.reduce( + billableSubscriptions.reduce( (sum, subscription) => sum + this.toMonthlyEquivalent(subscription), 0, ), diff --git a/apps/api/src/ingest/email-ingest.controller.ts b/apps/api/src/ingest/email-ingest.controller.ts index 323a0c3..2ae97ff 100644 --- a/apps/api/src/ingest/email-ingest.controller.ts +++ b/apps/api/src/ingest/email-ingest.controller.ts @@ -25,6 +25,7 @@ export class EmailIngestPayload { @IsOptional() @IsString() + @MaxLength(50000) body?: string; } diff --git a/apps/api/src/main.ts b/apps/api/src/main.ts index 9191a46..e22f6a5 100644 --- a/apps/api/src/main.ts +++ b/apps/api/src/main.ts @@ -2,6 +2,7 @@ import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import { ValidationPipe } from '@nestjs/common'; import { LoggingInterceptor } from './common/logging.interceptor'; +import { HttpExceptionFilter } from './common/http-exception.filter'; async function bootstrap() { const app = await NestFactory.create(AppModule); @@ -12,6 +13,7 @@ async function bootstrap() { transformOptions: { enableImplicitConversion: true }, }), ); + app.useGlobalFilters(new HttpExceptionFilter()); app.useGlobalInterceptors(new LoggingInterceptor()); app.setGlobalPrefix('api'); diff --git a/apps/api/src/settings/dto/update-settings.dto.ts b/apps/api/src/settings/dto/update-settings.dto.ts index 4fd8a98..1470f1f 100644 --- a/apps/api/src/settings/dto/update-settings.dto.ts +++ b/apps/api/src/settings/dto/update-settings.dto.ts @@ -1,9 +1,10 @@ -import { IsArray, IsIn, IsInt, Min } from 'class-validator'; +import { IsArray, IsIn, IsInt, Max, Min } from 'class-validator'; import { NotificationPreference } from '@subscription-tracker/types'; export class UpdateSettingsDto { @IsInt() @Min(0) + @Max(365) leadTimeDays!: number; @IsArray() diff --git a/apps/api/src/subscriptions/dto/create-subscription.dto.ts b/apps/api/src/subscriptions/dto/create-subscription.dto.ts index 58831da..e1f5a93 100644 --- a/apps/api/src/subscriptions/dto/create-subscription.dto.ts +++ b/apps/api/src/subscriptions/dto/create-subscription.dto.ts @@ -5,14 +5,17 @@ import { IsISO8601, Min, IsIn, + MaxLength, } from 'class-validator'; import { BillingInterval, Subscription } from '@subscription-tracker/types'; export class CreateSubscriptionDto { @IsString() + @MaxLength(100) serviceId!: string; @IsString() + @MaxLength(150) planName!: string; @IsNumber({ maxDecimalPlaces: 2 }) @@ -20,6 +23,7 @@ export class CreateSubscriptionDto { billingAmount!: number; @IsString() + @MaxLength(3) billingCurrency!: string; @IsIn(['monthly', 'yearly', 'quarterly', 'custom']) @@ -29,11 +33,12 @@ export class CreateSubscriptionDto { nextRenewal!: string; @IsOptional() - @IsString() + @IsIn(['card', 'paypal', 'gift', 'other']) paymentSource?: 'card' | 'paypal' | 'gift' | 'other'; @IsOptional() @IsString() + @MaxLength(4) paymentLast4?: string; @IsOptional() @@ -42,5 +47,6 @@ export class CreateSubscriptionDto { @IsOptional() @IsString() + @MaxLength(1000) notes?: string; } diff --git a/apps/api/src/subscriptions/subscriptions.service.spec.ts b/apps/api/src/subscriptions/subscriptions.service.spec.ts index 06808ea..b82e922 100644 --- a/apps/api/src/subscriptions/subscriptions.service.spec.ts +++ b/apps/api/src/subscriptions/subscriptions.service.spec.ts @@ -1,7 +1,7 @@ import { NotFoundException } from '@nestjs/common'; -import { Prisma } from '../../prisma/generated/client'; import { SubscriptionsService } from './subscriptions.service'; import { PrismaService } from '../prisma/prisma.service'; +import { ServiceCatalogService } from '../service-catalog/service-catalog.service'; type PrismaMock = { subscription: { @@ -20,6 +20,7 @@ type PrismaMock = { describe('SubscriptionsService', () => { let service: SubscriptionsService; let prisma: PrismaMock; + let serviceCatalog: jest.Mocked>; beforeEach(() => { prisma = { @@ -36,7 +37,14 @@ describe('SubscriptionsService', () => { }, }; - service = new SubscriptionsService(prisma as unknown as PrismaService); + serviceCatalog = { + ensureExists: jest.fn().mockResolvedValue({ id: 'svc_spotify', name: 'Spotify' }), + }; + + service = new SubscriptionsService( + prisma as unknown as PrismaService, + serviceCatalog as unknown as ServiceCatalogService, + ); }); const subscriptionEntity = () => ({ @@ -44,7 +52,7 @@ describe('SubscriptionsService', () => { serviceId: 'svc_spotify', planName: 'Premium', status: 'active', - billingAmount: new Prisma.Decimal(15), + billingAmountCents: 1500, billingCurrency: 'USD', billingInterval: 'monthly', nextRenewal: new Date('2026-04-01T00:00:00.000Z'), @@ -88,6 +96,7 @@ describe('SubscriptionsService', () => { }); expect(result.id).toBe('sub_1'); expect(result.status).toBe('active'); + expect(result.billingAmount).toBe(15); }); it('records a status change event during update', async () => { @@ -185,3 +194,4 @@ describe('SubscriptionsService', () => { await expect(service.findOne('missing')).rejects.toThrow(NotFoundException); }); }); + diff --git a/apps/web/package.json b/apps/web/package.json index 256ad66..1ec94b2 100644 --- a/apps/web/package.json +++ b/apps/web/package.json @@ -1,6 +1,6 @@ { "name": "web", - "version": "1.0.0", + "version": "1.0.1", "private": true, "scripts": { "dev": "next dev", diff --git a/apps/web/src/components/subscription-form.tsx b/apps/web/src/components/subscription-form.tsx index 4361f31..6d4450a 100644 --- a/apps/web/src/components/subscription-form.tsx +++ b/apps/web/src/components/subscription-form.tsx @@ -4,11 +4,11 @@ import { ServiceProvider, Subscription } from '@subscription-tracker/types'; import { FormEvent, useState } from 'react'; import { useRouter } from 'next/navigation'; import { Button } from './ui/button'; - -const API_BASE = - process.env.NEXT_PUBLIC_API_BASE_URL ?? - process.env.NEXT_PUBLIC_API_URL ?? - 'http://127.0.0.1:43100/api'; +import { + createSubscription, + deleteSubscription, + updateSubscription, +} from '../lib/api'; interface Props { services: ServiceProvider[]; @@ -30,31 +30,23 @@ export function SubscriptionForm({ services, mode, initial }: Props) { const formData = new FormData(event.currentTarget); const payload = { - serviceId: formData.get('serviceId'), - planName: formData.get('planName'), + serviceId: formData.get('serviceId') as string, + planName: formData.get('planName') as string, billingAmount: Number(formData.get('billingAmount')), - billingCurrency: formData.get('billingCurrency'), - billingInterval: formData.get('billingInterval'), - nextRenewal: formData.get('nextRenewal'), - paymentSource: formData.get('paymentSource') || undefined, - paymentLast4: formData.get('paymentLast4') || undefined, - notes: formData.get('notes') || undefined, - status: formData.get('status') || undefined, + billingCurrency: formData.get('billingCurrency') as string, + billingInterval: formData.get('billingInterval') as Subscription['billingInterval'], + nextRenewal: `${formData.get('nextRenewal') as string}T00:00:00.000Z`, + paymentSource: (formData.get('paymentSource') as Subscription['paymentSource']) || undefined, + paymentLast4: (formData.get('paymentLast4') as string) || undefined, + notes: (formData.get('notes') as string) || undefined, + status: (formData.get('status') as Subscription['status']) || undefined, }; - const url = - mode === 'edit' && initial - ? `${API_BASE}/subscriptions/${initial.id}` - : `${API_BASE}/subscriptions`; - try { - const response = await fetch(url, { - method: mode === 'edit' ? 'PATCH' : 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify(payload), - }); - if (!response.ok) { - throw new Error(await response.text()); + if (mode === 'edit' && initial) { + await updateSubscription(initial.id, payload); + } else { + await createSubscription(payload); } setStatus('success'); router.push('/dashboard'); @@ -70,12 +62,7 @@ export function SubscriptionForm({ services, mode, initial }: Props) { if (!confirm('Delete this subscription?')) return; try { - const response = await fetch(`${API_BASE}/subscriptions/${initial.id}`, { - method: 'DELETE', - }); - if (!response.ok) { - throw new Error(await response.text()); - } + await deleteSubscription(initial.id); router.push('/dashboard'); router.refresh(); } catch (err) { @@ -133,6 +120,7 @@ export function SubscriptionForm({ services, mode, initial }: Props) { name="billingCurrency" defaultValue={initial?.billingCurrency ?? 'USD'} required + maxLength={3} className="mt-1 w-full rounded-md border border-slate-300 px-3 py-2 text-sm" /> diff --git a/apps/web/src/lib/api.ts b/apps/web/src/lib/api.ts index 4cff0bd..daffded 100644 --- a/apps/web/src/lib/api.ts +++ b/apps/web/src/lib/api.ts @@ -87,6 +87,16 @@ export function createSubscription(payload: CreateSubscriptionPayload) { }); } +export function updateSubscription( + id: string, + payload: Partial, +) { + return apiRequest(`/subscriptions/${id}`, { + method: 'PATCH', + body: JSON.stringify(payload), + }); +} + export function deleteSubscription(id: string) { return apiRequest(`/subscriptions/${id}`, { method: 'DELETE', diff --git a/docs/release-notes-v1.0.1.md b/docs/release-notes-v1.0.1.md new file mode 100644 index 0000000..eeedbe6 --- /dev/null +++ b/docs/release-notes-v1.0.1.md @@ -0,0 +1,29 @@ +# SubSync v1.0.1 + +## What's changed + +### Bug fixes +- **Dashboard monthly spend** now correctly excludes `canceled_pending` subscriptions from the monthly equivalent spend calculation and spend-by-category breakdown. Subscriptions in the process of being canceled are no longer counted toward your ongoing costs. +- **Unit tests** (`subscriptions.service.spec.ts`) corrected: the `ServiceCatalogService` dependency is now properly mocked in the constructor, and the subscription entity fixture uses the correct `billingAmountCents` integer field instead of the wrong `billingAmount` Decimal. All five test cases now pass. + +### Improvements +- **Global HTTP exception filter** added to the API (`common/http-exception.filter.ts`). All error responses now return a consistent JSON envelope `{ statusCode, message, error }` regardless of whether the error originated from a `HttpException`, a validation pipe failure, or an unexpected runtime error. Unexpected server errors are also logged with a stack trace via NestJS `Logger`. +- **Stronger DTO validation** across the API: + - `CreateSubscriptionDto`: `planName` capped at 150 characters, `billingCurrency` capped at 3 characters, `serviceId` capped at 100 characters, `notes` capped at 1,000 characters, `paymentLast4` capped at 4 characters, and `paymentSource` now validated with `@IsIn` instead of the looser `@IsString`. + - `UpdateSettingsDto`: `leadTimeDays` now has an upper bound of 365 days. + - `EmailIngestPayload`: `body` field now capped at 50 000 characters. +- **Web API layer consolidated**: `subscription-form.tsx` previously issued raw `fetch` calls and duplicated the API base-URL resolution logic. It now uses the typed helpers `createSubscription`, `updateSubscription`, and `deleteSubscription` from `lib/api.ts`. A new `updateSubscription` export was added to `lib/api.ts`, and the `billingCurrency` field enforces a `maxLength={3}` HTML attribute in the form. + +## Upgrade notes +No database migrations required. Drop in the new executable and restart; all existing data remains compatible. + +## Validation +``` +npm run lint +npm run test --workspace api +npm run build:desktop +``` + +## Known limitations +- Provider sync is not yet a full live OAuth integration. +- The portable executable is unsigned; Windows SmartScreen may warn on first launch. diff --git a/package-lock.json b/package-lock.json index 7833219..6ff909e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "subscription-tracker", - "version": "1.0.0", + "version": "1.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "subscription-tracker", - "version": "1.0.0", + "version": "1.0.1", "workspaces": [ "apps/*", "packages/*" @@ -30,7 +30,7 @@ } }, "apps/api": { - "version": "1.0.0", + "version": "1.0.1", "license": "UNLICENSED", "dependencies": { "@nestjs/common": "^10.0.0", @@ -146,7 +146,7 @@ } }, "apps/web": { - "version": "1.0.0", + "version": "1.0.1", "dependencies": { "@radix-ui/react-slot": "^1.0.2", "@subscription-tracker/types": "file:../../packages/types", @@ -13938,7 +13938,7 @@ }, "packages/types": { "name": "@subscription-tracker/types", - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "devDependencies": { "prettier": "^3.4.2", diff --git a/package.json b/package.json index 8a79a0b..83bba35 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "subscription-tracker", - "version": "1.0.0", + "version": "1.0.1", "private": true, "description": "SubSync desktop-packaged subscription tracker", "author": "Evan Newman", diff --git a/packages/types/package.json b/packages/types/package.json index 65545e1..7fd1f05 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@subscription-tracker/types", - "version": "1.0.0", + "version": "1.0.1", "description": "Shared TypeScript interfaces for the subscription tracker", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -12,11 +12,16 @@ "test": "echo 'no tests yet' && exit 0", "prepare": "npm run build" }, - "keywords": ["subscriptions", "types"], + "keywords": [ + "subscriptions", + "types" + ], "author": "", "license": "MIT", "type": "commonjs", - "files": ["dist"], + "files": [ + "dist" + ], "devDependencies": { "prettier": "^3.4.2", "rimraf": "^6.0.1", From 72da03a7c1d77aa045fb7cdfb62a575aa219c98e Mon Sep 17 00:00:00 2001 From: Evan Newman Date: Sun, 17 May 2026 14:48:03 -0230 Subject: [PATCH 3/4] docs: add CHANGELOG and template version in release checklist - Seed CHANGELOG.md with 1.0.0 and 1.0.1 entries plus 1.1.0 candidates - Replace hardcoded SubSync 1.0.0.exe references with ${VERSION} so the checklist stays valid across releases Deferred: cross-env fix for the Windows-only `set VAR=...&&` dev scripts. Requires a fresh npm install to refresh package-lock.json; tracked for a follow-up commit before tagging v1.0.1. Co-Authored-By: Claude Opus 4.7 --- CHANGELOG.md | 47 +++++++++++++++++++++++++++++++++++++++ docs/release-checklist.md | 5 +++-- 2 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..c26c303 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,47 @@ +# Changelog + +All notable changes to SubSync are documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Planned for v1.1.0 +- Real OAuth integration for at least one provider (Gmail billing import is the highest-leverage target). +- Auto-update via `electron-updater` so portable users have a real upgrade path. +- macOS and Linux desktop builds. +- CSV / JSON export of subscriptions. +- Backup and restore of the local SQLite database. +- Notification worker wired up to the existing reminder preferences. + +## [1.0.1] - TBD + +See [`docs/release-notes-v1.0.1.md`](docs/release-notes-v1.0.1.md) for the full notes. + +### Added +- Global `HttpExceptionFilter` so API errors return consistent JSON payloads instead of leaking stack traces. +- DTO validation on subscription, settings, and email-ingest endpoints. + +### Fixed +- Dashboard monthly spend and spend-by-category now exclude `canceled_pending` subscriptions. +- `subscriptions.service.spec.ts` mocks the `ServiceCatalogService` dependency correctly and uses the `billingAmountCents` integer field. +- Cross-platform `dev:api` / `dev:web` scripts (previous `set VAR=...&&` form only worked on Windows). + +### Changed +- Consolidated duplicated fetch helpers in the web client's `lib/api.ts`. + +## [1.0.0] - 2026-05 (prior release) + +See [`docs/release-notes-v1.0.0.md`](docs/release-notes-v1.0.0.md). + +### Added +- Windows portable desktop executable bundling the NestJS API and Next.js web client. +- Local SQLite persistence for subscriptions, integrations, and settings. +- Dashboard summary metrics, renewal stack, and status-change feed. +- Manual subscription CRUD with `SubscriptionEvent` logging. +- Billing email import endpoint that creates or updates subscriptions. + +### Known limitations +- Provider `Connect` actions persist local state only — no real third-party OAuth. +- The portable executable is unsigned, so Windows SmartScreen may warn on first launch. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 8fcbeee..c1bdb7b 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -5,10 +5,11 @@ - Run `npm run test:e2e --workspace api` - Run `npm run build:desktop` - Run `npm run dist:desktop` -- Smoke-test the generated `release/SubSync 1.0.0.exe` +- Smoke-test the generated `release/SubSync ${VERSION}.exe` +- Update `CHANGELOG.md` with the new version's entry and move planned items out of `[Unreleased]` ## Release contents -- Upload `release/SubSync 1.0.0.exe` to GitHub Releases +- Upload `release/SubSync ${VERSION}.exe` to GitHub Releases - Include release notes that mention: - local SQLite storage - dashboard summary metrics From c5be9c946f12b1d6e98814a27d64f62535109703 Mon Sep 17 00:00:00 2001 From: Evan Newman Date: Sun, 17 May 2026 15:07:51 -0230 Subject: [PATCH 4/4] chore: use cross-env for dev scripts so they work on macOS/Linux The previous `set VAR=...&&` syntax only ran on Windows cmd. Switching to cross-env keeps the same env-var semantics across all platforms. Co-Authored-By: Claude Opus 4.7 --- package-lock.json | 20 ++++++++++++++++++++ package.json | 5 +++-- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6ff909e..df3d50b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25,6 +25,7 @@ "uuid": "^11.0.3" }, "devDependencies": { + "cross-env": "^7.0.3", "electron": "^41.0.2", "electron-builder": "^26.8.1" } @@ -5114,6 +5115,25 @@ "optional": true, "peer": true }, + "node_modules/cross-env": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz", + "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.1" + }, + "bin": { + "cross-env": "src/bin/cross-env.js", + "cross-env-shell": "src/bin/cross-env-shell.js" + }, + "engines": { + "node": ">=10.14", + "npm": ">=6", + "yarn": ">=1" + } + }, "node_modules/cross-spawn": { "version": "7.0.6", "dev": true, diff --git a/package.json b/package.json index 83bba35..c457037 100644 --- a/package.json +++ b/package.json @@ -11,8 +11,8 @@ "packages/*" ], "scripts": { - "dev:api": "set API_PORT=43100&& npm run start:dev --workspace api", - "dev:web": "set NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:43100/api&& npm run dev --workspace web", + "dev:api": "cross-env API_PORT=43100 npm run start:dev --workspace api", + "dev:web": "cross-env NEXT_PUBLIC_API_BASE_URL=http://127.0.0.1:43100/api npm run dev --workspace web", "dev:desktop": "electron .", "build": "npm run build --workspace @subscription-tracker/types && npm run build --workspace api && npm run build --workspace web", "build:desktop": "npm run build && node ./desktop/prepare-dist.mjs", @@ -75,6 +75,7 @@ } }, "devDependencies": { + "cross-env": "^7.0.3", "electron": "^41.0.2", "electron-builder": "^26.8.1" }