diff --git a/.gitignore b/.gitignore index ccfa293..f0b3199 100644 --- a/.gitignore +++ b/.gitignore @@ -34,4 +34,4 @@ report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json .DS_Store cafe_cursor.csv credits.csv -convex/_generated +# convex/_generated is now committed with stubs for CI diff --git a/README.md b/README.md index 1c22fe8..af96ee4 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ ╚═════╝╚═╝ ╚═╝╚═╝ ╚══════╝ ╚═════╝ ╚═════╝ ╚═╝ ╚═╝╚══════╝ ╚═════╝ ╚═╝ ╚═╝ ``` -# Cafe Cursor CLI +# Cafe Cursor CLI [![CI](https://github.com/Alhwyn/cafe-cursor-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/Alhwyn/cafe-cursor-cli/actions/workflows/ci.yml) A CLI tool for managing and sending Cursor credits to event attendees via email. diff --git a/build.ts b/build.ts index 3af4a58..5d50fb7 100644 --- a/build.ts +++ b/build.ts @@ -1,39 +1,28 @@ #!/usr/bin/env bun -import plugin from "bun-plugin-tailwind"; import { existsSync } from "fs"; import { rm } from "fs/promises"; import path from "path"; if (process.argv.includes("--help") || process.argv.includes("-h")) { console.log(` -🏗️ Bun Build Script +🏗️ Cafe CLI Build Script Usage: bun run build.ts [options] Common Options: --outdir Output directory (default: "dist") - --minify Enable minification (or --minify.grayspace, --minify.syntax, etc) - --sourcemap Sourcemap type: none|linked|inline|external - --target Build target: browser|bun|node - --format Output format: esm|cjs|iife - --splitting Enable code splitting - --packages Package handling: bundle|external - --public-path Public path for assets - --env Environment handling: inline|disable|prefix* - --conditions Package.json export conditions (comma separated) - --external External packages (comma separated) - --banner Add banner text to output - --footer Add footer text to output - --define Define global constants (e.g. --define.VERSION=1.0.0) + --minify Enable minification + --sourcemap Sourcemap type: none|linked|inline|external + --target Build target: bun|node --help, -h Show this help message Example: - bun run build.ts --outdir=dist --minify --sourcemap=linked --external=react,react-dom + bun run build.ts --outdir=dist --minify --sourcemap=linked `); process.exit(0); } -const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, g => g[1].toUpperCase()); +const toCamelCase = (str: string): string => str.replace(/-([a-z])/g, (_, c: string) => c.toUpperCase()); const parseValue = (value: string): any => { if (value === "true") return true; @@ -47,8 +36,8 @@ const parseValue = (value: string): any => { return value; }; -function parseArgs(): Partial { - const config: Partial = {}; +function parseArgs(): Record { + const config: Record = {}; const args = process.argv.slice(2); for (let i = 0; i < args.length; i++) { @@ -81,9 +70,9 @@ function parseArgs(): Partial { key = toCamelCase(key); if (key.includes(".")) { - const [parentKey, childKey] = key.split("."); + const [parentKey, childKey] = key.split(".", 2) as [string, string]; config[parentKey] = config[parentKey] || {}; - config[parentKey][childKey] = parseValue(value); + (config[parentKey] as Record)[childKey] = parseValue(value); } else { config[key] = parseValue(value); } @@ -108,7 +97,7 @@ const formatFileSize = (bytes: number): string => { console.log("\n🚀 Starting build process...\n"); const cliConfig = parseArgs(); -const outdir = cliConfig.outdir || path.join(process.cwd(), "dist"); +const outdir = (typeof cliConfig.outdir === "string" ? cliConfig.outdir : null) || path.join(process.cwd(), "dist"); if (existsSync(outdir)) { console.log(`🗑️ Cleaning previous build at ${outdir}`); @@ -117,18 +106,20 @@ if (existsSync(outdir)) { const start = performance.now(); -const entrypoints = [...new Bun.Glob("**.html").scanSync("src")] - .map(a => path.resolve("src", a)) - .filter(dir => !dir.includes("node_modules")); -console.log(`📄 Found ${entrypoints.length} HTML ${entrypoints.length === 1 ? "file" : "files"} to process\n`); +const entrypoints = [path.resolve("src", "cli.tsx")]; +console.log(`📄 Building CLI from ${entrypoints[0]}\n`); const result = await Bun.build({ entrypoints, outdir, - plugins: [plugin], minify: true, - target: "browser", + target: "bun", sourcemap: "linked", + external: [ + "react-devtools-core", + "electron", + "chromium-bidi", + ], define: { "process.env.NODE_ENV": JSON.stringify("production"), }, diff --git a/bun.lock b/bun.lock index 504c8c6..a16a193 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "@types/figlet": "^1.7.0", "@types/react": "^19", "@types/react-dom": "^19", + "typescript": "^5.9.3", }, }, }, @@ -313,6 +314,8 @@ "type-fest": ["type-fest@4.41.0", "", {}, "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA=="], + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], "url-parse": ["url-parse@1.5.10", "", { "dependencies": { "querystringify": "^2.1.1", "requires-port": "^1.0.0" } }, "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ=="], diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts new file mode 100644 index 0000000..abe4afe --- /dev/null +++ b/convex/_generated/api.d.ts @@ -0,0 +1,55 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type * as credits from "../credits.js"; +import type * as email from "../email.js"; +import type * as emailHelpers from "../emailHelpers.js"; +import type * as people from "../people.js"; + +import type { + ApiFromModules, + FilterApi, + FunctionReference, +} from "convex/server"; + +declare const fullApi: ApiFromModules<{ + credits: typeof credits; + email: typeof email; + emailHelpers: typeof emailHelpers; + people: typeof people; +}>; + +/** + * A utility for referencing Convex functions in your app's public API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export declare const api: FilterApi< + typeof fullApi, + FunctionReference +>; + +/** + * A utility for referencing Convex functions in your app's internal API. + * + * Usage: + * ```js + * const myFunctionReference = internal.myModule.myFunction; + * ``` + */ +export declare const internal: FilterApi< + typeof fullApi, + FunctionReference +>; + +export declare const components: {}; diff --git a/convex/_generated/api.js b/convex/_generated/api.js new file mode 100644 index 0000000..44bf985 --- /dev/null +++ b/convex/_generated/api.js @@ -0,0 +1,23 @@ +/* eslint-disable */ +/** + * Generated `api` utility. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { anyApi, componentsGeneric } from "convex/server"; + +/** + * A utility for referencing Convex functions in your app's API. + * + * Usage: + * ```js + * const myFunctionReference = api.myModule.myFunction; + * ``` + */ +export const api = anyApi; +export const internal = anyApi; +export const components = componentsGeneric(); diff --git a/convex/_generated/dataModel.d.ts b/convex/_generated/dataModel.d.ts new file mode 100644 index 0000000..f97fd19 --- /dev/null +++ b/convex/_generated/dataModel.d.ts @@ -0,0 +1,60 @@ +/* eslint-disable */ +/** + * Generated data model types. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import type { + DataModelFromSchemaDefinition, + DocumentByName, + TableNamesInDataModel, + SystemTableNames, +} from "convex/server"; +import type { GenericId } from "convex/values"; +import schema from "../schema.js"; + +/** + * The names of all of your Convex tables. + */ +export type TableNames = TableNamesInDataModel; + +/** + * The type of a document stored in Convex. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Doc = DocumentByName< + DataModel, + TableName +>; + +/** + * An identifier for a document in Convex. + * + * Convex documents are uniquely identified by their `Id`, which is accessible + * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). + * + * Documents can be loaded using `db.get(tableName, id)` in query and mutation functions. + * + * IDs are just strings at runtime, but this type can be used to distinguish them from other + * strings when type checking. + * + * @typeParam TableName - A string literal type of the table name (like "users"). + */ +export type Id = + GenericId; + +/** + * A type describing your Convex data model. + * + * This type includes information about what tables you have, the type of + * documents stored in those tables, and the indexes defined on them. + * + * This type is used to parameterize methods like `queryGeneric` and + * `mutationGeneric` to make them type-safe. + */ +export type DataModel = DataModelFromSchemaDefinition; diff --git a/convex/_generated/server.d.ts b/convex/_generated/server.d.ts new file mode 100644 index 0000000..bec05e6 --- /dev/null +++ b/convex/_generated/server.d.ts @@ -0,0 +1,143 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + ActionBuilder, + HttpActionBuilder, + MutationBuilder, + QueryBuilder, + GenericActionCtx, + GenericMutationCtx, + GenericQueryCtx, + GenericDatabaseReader, + GenericDatabaseWriter, +} from "convex/server"; +import type { DataModel } from "./dataModel.js"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const query: QueryBuilder; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export declare const internalQuery: QueryBuilder; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const mutation: MutationBuilder; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export declare const internalMutation: MutationBuilder; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export declare const action: ActionBuilder; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export declare const internalAction: ActionBuilder; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export declare const httpAction: HttpActionBuilder; + +/** + * A set of services for use within Convex query functions. + * + * The query context is passed as the first argument to any Convex query + * function run on the server. + * + * This differs from the {@link MutationCtx} because all of the services are + * read-only. + */ +export type QueryCtx = GenericQueryCtx; + +/** + * A set of services for use within Convex mutation functions. + * + * The mutation context is passed as the first argument to any Convex mutation + * function run on the server. + */ +export type MutationCtx = GenericMutationCtx; + +/** + * A set of services for use within Convex action functions. + * + * The action context is passed as the first argument to any Convex action + * function run on the server. + */ +export type ActionCtx = GenericActionCtx; + +/** + * An interface to read from the database within Convex query functions. + * + * The two entry points are {@link DatabaseReader.get}, which fetches a single + * document by its {@link Id}, or {@link DatabaseReader.query}, which starts + * building a query. + */ +export type DatabaseReader = GenericDatabaseReader; + +/** + * An interface to read from and write to the database within Convex mutation + * functions. + * + * Convex guarantees that all writes within a single mutation are + * executed atomically, so you never have to worry about partial writes leaving + * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) + * for the guarantees Convex provides your functions. + */ +export type DatabaseWriter = GenericDatabaseWriter; diff --git a/convex/_generated/server.js b/convex/_generated/server.js new file mode 100644 index 0000000..bf3d25a --- /dev/null +++ b/convex/_generated/server.js @@ -0,0 +1,93 @@ +/* eslint-disable */ +/** + * Generated utilities for implementing server-side Convex query and mutation functions. + * + * THIS CODE IS AUTOMATICALLY GENERATED. + * + * To regenerate, run `npx convex dev`. + * @module + */ + +import { + actionGeneric, + httpActionGeneric, + queryGeneric, + mutationGeneric, + internalActionGeneric, + internalMutationGeneric, + internalQueryGeneric, +} from "convex/server"; + +/** + * Define a query in this Convex app's public API. + * + * This function will be allowed to read your Convex database and will be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const query = queryGeneric; + +/** + * Define a query that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to read from your Convex database. It will not be accessible from the client. + * + * @param func - The query function. It receives a {@link QueryCtx} as its first argument. + * @returns The wrapped query. Include this as an `export` to name it and make it accessible. + */ +export const internalQuery = internalQueryGeneric; + +/** + * Define a mutation in this Convex app's public API. + * + * This function will be allowed to modify your Convex database and will be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const mutation = mutationGeneric; + +/** + * Define a mutation that is only accessible from other Convex functions (but not from the client). + * + * This function will be allowed to modify your Convex database. It will not be accessible from the client. + * + * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. + * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. + */ +export const internalMutation = internalMutationGeneric; + +/** + * Define an action in this Convex app's public API. + * + * An action is a function which can execute any JavaScript code, including non-deterministic + * code and code with side-effects, like calling third-party services. + * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. + * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. + * + * @param func - The action. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped action. Include this as an `export` to name it and make it accessible. + */ +export const action = actionGeneric; + +/** + * Define an action that is only accessible from other Convex functions (but not from the client). + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument. + * @returns The wrapped function. Include this as an `export` to name it and make it accessible. + */ +export const internalAction = internalActionGeneric; + +/** + * Define an HTTP action. + * + * The wrapped function will be used to respond to HTTP requests received + * by a Convex deployment if the requests matches the path and method where + * this action is routed. Be sure to route your httpAction in `convex/http.js`. + * + * @param func - The function. It receives an {@link ActionCtx} as its first argument + * and a Fetch API `Request` object as its second. + * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. + */ +export const httpAction = httpActionGeneric; diff --git a/package.json b/package.json index 71235af..c826bbc 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,7 @@ "ink-select-input": "^6.2.0", "ink-text-input": "^6.0.0", "node-html-parser": "^7.0.1", + "papaparse": "^5.5.3", "playwright": "^1.57.0", "react": "^19", "react-dom": "^19", @@ -31,7 +32,9 @@ "devDependencies": { "@types/bun": "latest", "@types/figlet": "^1.7.0", + "@types/papaparse": "^5.3.15", "@types/react": "^19", - "@types/react-dom": "^19" + "@types/react-dom": "^19", + "typescript": "^5.9.3" } } diff --git a/src/cli.tsx b/src/cli.tsx old mode 100644 new mode 100755 index e3b8e64..e5cd62c --- a/src/cli.tsx +++ b/src/cli.tsx @@ -70,9 +70,9 @@ const MainMenu = () => { }); const menuItems = [ - { label: "1. Send Cursor Credits", value: "send" }, - { label: "2. Upload Cursor Credits", value: "upload" }, - { label: "3. Upload Attendees", value: "attendees" }, + { label: "Send Cursor Credits", value: "send" }, + { label: "Upload Cursor Credits", value: "upload" }, + { label: "Upload Attendees", value: "attendees" }, ]; const handleSelect = (item: { label: string; value: string }) => { @@ -133,10 +133,10 @@ const MainMenu = () => { /> - 1 Send - 2 Credits - 3 Attendees - Q Quit + 1 Send + 2 Credits + 3 Attendees + Q Quit diff --git a/src/components/ContactTable.tsx b/src/components/ContactTable.tsx index b20e8df..fcf890d 100644 --- a/src/components/ContactTable.tsx +++ b/src/components/ContactTable.tsx @@ -111,7 +111,7 @@ const ContactTable = ({ contacts, onSelect, onBack, isActive = true }: ContactTa {/* Header */} - + {" "} {padRight("Name", cols.name)} {padRight("Email", cols.email)} diff --git a/src/screens/SendCredits.tsx b/src/screens/SendCredits.tsx index 1a15b29..7b76f94 100644 --- a/src/screens/SendCredits.tsx +++ b/src/screens/SendCredits.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Box, Text, useInput } from "ink"; import TextInput from "ink-text-input"; import ContactTable from "../components/ContactTable.js"; @@ -17,6 +17,7 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { const [isSearchFocused, setIsSearchFocused] = useState(false); const [selectedContact, setSelectedContact] = useState(null); const [showNotSentOnly, setShowNotSentOnly] = useState(false); + const [showSentOnly, setShowSentOnly] = useState(false); // Email sending state const [isSending, setIsSending] = useState(false); @@ -28,6 +29,19 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { const { data: creditTally, refresh: refreshCredits } = useCreditTally(); const sendCredits = useSendCredits(); + // Refresh data when entering the page + useEffect(() => { + refreshPeople(); + refreshCredits(); + }, []); + + // Handle going back - refresh data before leaving + const handleBack = () => { + refreshPeople(); + refreshCredits(); + onBack(); + }; + // Transform database people to Contact format const contacts: Contact[] = useMemo(() => { if (!people) return []; @@ -52,6 +66,8 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { // Filter by sent status if (showNotSentOnly) { filtered = filtered.filter((contact) => !contact.sent); + } else if (showSentOnly) { + filtered = filtered.filter((contact) => contact.sent); } // Filter by search query @@ -67,7 +83,7 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { } return filtered; - }, [contacts, searchQuery, showNotSentOnly]); + }, [contacts, searchQuery, showNotSentOnly, showSentOnly]); // Toggle focus between search and table useInput((input, key) => { @@ -81,6 +97,10 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { setIsSearchFocused(true); } else if (input === "f" && !isSearchFocused) { setShowNotSentOnly((prev) => !prev); + setShowSentOnly(false); + } else if (input === "s" && !isSearchFocused) { + setShowSentOnly((prev) => !prev); + setShowNotSentOnly(false); } }); @@ -181,12 +201,13 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { Send Cursor Credits ({filteredContacts.length} recipients) {showNotSentOnly && [Not Sent Only]} + {showSentOnly && [Sent Only]} @@ -220,7 +241,8 @@ const SendCredits = ({ onBack }: SendCreditsProps) => { Up/Down Navigate Enter Select Tab {isSearchFocused ? "Exit Search" : "Search"} - F Filter Not Sent + F Not Sent + S Sent Only Q Back diff --git a/src/screens/UploadAttendees.tsx b/src/screens/UploadAttendees.tsx index 4de15a1..7595dbf 100644 --- a/src/screens/UploadAttendees.tsx +++ b/src/screens/UploadAttendees.tsx @@ -143,7 +143,7 @@ const UploadAttendees = ({ onBack }: UploadAttendeesProps) => { - Expected CSV columns: + Expected CSV columns: - first_name (required) - last_name (required) - email (required) @@ -208,7 +208,7 @@ const UploadAttendees = ({ onBack }: UploadAttendeesProps) => { Processing: - {filepath} + {filepath} @@ -242,8 +242,8 @@ const UploadAttendees = ({ onBack }: UploadAttendeesProps) => { {result.imported} - Skipped (duplicates): - {result.skipped} + Skipped (duplicates): + {result.skipped} @@ -275,4 +275,4 @@ const UploadAttendees = ({ onBack }: UploadAttendeesProps) => { ); }; -export default UploadAttendees; +export default UploadAttendees; \ No newline at end of file diff --git a/src/utils/attendeeParser.ts b/src/utils/attendeeParser.ts index 490e884..98d54b6 100644 --- a/src/utils/attendeeParser.ts +++ b/src/utils/attendeeParser.ts @@ -56,7 +56,7 @@ export async function parseAttendeeCSV(filepath: string): Promise { throw new Error("CSV must have a header row and at least one data row"); } - const headers = parseCSVLine(lines[0]); + const headers = parseCSVLine(lines[0]!); const columnIndexes: Partial> = {}; // Map headers to column indexes @@ -81,7 +81,7 @@ export async function parseAttendeeCSV(filepath: string): Promise { const attendees: Attendee[] = []; for (let i = 1; i < lines.length; i++) { - const values = parseCSVLine(lines[i]); + const values = parseCSVLine(lines[i]!); const firstName = values[columnIndexes.firstName!] || ""; const lastName = values[columnIndexes.lastName!] || ""; diff --git a/test/businessLogic.test.ts b/test/businessLogic.test.ts index b30b40f..fd75584 100644 --- a/test/businessLogic.test.ts +++ b/test/businessLogic.test.ts @@ -86,11 +86,11 @@ describe("People Storage", () => { const result = addPerson(testCase.person, tempDir); - expect(result.added).toBe(testCase.expected.added); - expect(result.skipped).toBe(testCase.expected.skipped); + expect(result.added).toBe(testCase.expected.added!); + expect(result.skipped).toBe(testCase.expected.skipped!); const people = loadPeople(tempDir); - expect(people.length).toBe(testCase.expected.totalPeople); + expect(people.length).toBe(testCase.expected.totalPeople!); }); test("skips duplicate email (case insensitive)", () => { @@ -163,7 +163,7 @@ describe("Credits Storage", () => { const result = addCreditIfNotExists(testCase.credit!, tempDir); - expect(result.added).toBe(testCase.expected.added); + expect(result.added).toBe(testCase.expected.added!); const credits = loadCredits(tempDir); expect(credits.length).toBe(1);