diff --git a/.gitignore b/.gitignore index 5449cac..2b223a7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ node_modules dist -.env* \ No newline at end of file +.env* + +app +polar.ts \ No newline at end of file diff --git a/package.json b/package.json index 20e82b7..2f7d062 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,8 @@ "dependencies": { "@inkjs/ui": "^2.0.0", "@polar-sh/sdk": "^0.13.3", + "@types/cross-spawn": "^6.0.6", + "cross-spawn": "^7.0.3", "ink": "^4.4.1", "listr": "^0.14.3", "meow": "^11.0.0", @@ -27,6 +29,7 @@ "open": "^10.1.0", "prompts": "^2.4.2", "react": "^18.2.0", + "standardwebhooks": "1.0.0", "zod": "^3.23.8" }, "devDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7efe9d7..d401495 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,6 +14,12 @@ importers: '@polar-sh/sdk': specifier: ^0.13.3 version: 0.13.3(zod@3.23.8) + '@types/cross-spawn': + specifier: ^6.0.6 + version: 6.0.6 + cross-spawn: + specifier: ^7.0.3 + version: 7.0.3 ink: specifier: ^4.4.1 version: 4.4.1(@types/react@18.3.11)(react@18.3.1) @@ -35,6 +41,9 @@ importers: react: specifier: ^18.2.0 version: 18.3.1 + standardwebhooks: + specifier: 1.0.0 + version: 1.0.0 zod: specifier: ^3.23.8 version: 3.23.8 @@ -423,6 +432,9 @@ packages: resolution: {integrity: sha512-0/gtPNTY3++0J2BZM5nHHULg0BIMw886gqdn8vWN+Av6bgF5ZU2qIcHubAn+Z9KNvJhO8WFE+9kDOU3n6OcKtA==} engines: {node: '>=14'} + '@stablelib/base64@1.0.1': + resolution: {integrity: sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==} + '@swc/counter@0.1.3': resolution: {integrity: sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==} @@ -441,6 +453,9 @@ packages: '@tsconfig/node16@1.0.4': resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/cross-spawn@6.0.6': + resolution: {integrity: sha512-fXRhhUkG4H3TQk5dBhQ7m/JDdSNHKwR2BBia62lhwEIq9xGiQKLxd6LymNhn47SjXhsUEPmxi+PKw2OkW4LLjA==} + '@types/eslint@7.29.0': resolution: {integrity: sha512-VNcvioYDH8/FxaeTKkM4/TiTwt6pBV9E3OfGmvaw8tPl0rrHCJ4Ll15HRT+pMiFAf/MLQvAzC+6RzUMEL9Ceng==} @@ -1322,6 +1337,9 @@ packages: fast-levenshtein@2.0.6: resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + fast-sha256@1.3.0: + resolution: {integrity: sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==} + fastq@1.17.1: resolution: {integrity: sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==} @@ -2593,6 +2611,9 @@ packages: resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} engines: {node: '>=10'} + standardwebhooks@1.0.0: + resolution: {integrity: sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg==} + streamsearch@1.1.0: resolution: {integrity: sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==} engines: {node: '>=10.0.0'} @@ -3287,6 +3308,8 @@ snapshots: '@sindresorhus/tsconfig@3.0.1': {} + '@stablelib/base64@1.0.1': {} + '@swc/counter@0.1.3': {} '@swc/helpers@0.5.13': @@ -3301,6 +3324,10 @@ snapshots: '@tsconfig/node16@1.0.4': {} + '@types/cross-spawn@6.0.6': + dependencies: + '@types/node': 22.7.8 + '@types/eslint@7.29.0': dependencies: '@types/estree': 1.0.6 @@ -4378,6 +4405,8 @@ snapshots: fast-levenshtein@2.0.6: {} + fast-sha256@1.3.0: {} + fastq@1.17.1: dependencies: reusify: 1.0.4 @@ -5639,6 +5668,11 @@ snapshots: dependencies: escape-string-regexp: 2.0.0 + standardwebhooks@1.0.0: + dependencies: + '@stablelib/base64': 1.0.1 + fast-sha256: 1.3.0 + streamsearch@1.1.0: {} string-width@1.0.2: diff --git a/src/cli.tsx b/src/cli.tsx index ad50d0e..3236898 100644 --- a/src/cli.tsx +++ b/src/cli.tsx @@ -1,6 +1,6 @@ import { Polar } from "@polar-sh/sdk"; import meow from "meow"; -import { installDependencies } from "./dependencies.js"; +import { installDependencies } from "./install.js"; import { login } from "./oauth.js"; import { resolveOrganization } from "./organization.js"; import { resolvePackageName } from "./package.js"; @@ -15,6 +15,8 @@ import { import { authenticationDisclaimer } from "./ui/authentication.js"; import { installDisclaimer } from "./ui/install.js"; import { precheckDisclaimer } from "./ui/precheck.js"; +import { environmentDisclaimer } from "./ui/environment.js"; +import { appendEnvironmentVariables } from "./env.js"; const cli = meow( ` @@ -30,10 +32,6 @@ const cli = meow( { importMeta: import.meta, flags: { - skipProduct: { - type: "boolean", - default: false, - }, skipPrecheck: { type: "boolean", default: false, @@ -51,34 +49,35 @@ const cli = meow( await precheckDisclaimer(); } - const packageName = await resolvePackageName(); + const product = await productPrompt(); - if (!cli.flags.skipProduct) { - const product = await productPrompt(); + await authenticationDisclaimer(); + const code = await login(); - await authenticationDisclaimer(); - const code = await login(); + const api = new Polar({ + accessToken: code, + server: "sandbox", + }); - const api = new Polar({ - accessToken: code, - server: "sandbox", - }); + const packageName = await resolvePackageName(); + const organization = await resolveOrganization(api, packageName); - const organization = await resolveOrganization(api, packageName); + await createProduct(api, organization, product); - await createProduct(api, organization, product); - } if (!cli.flags.skipTemplate) { const templates = await templatePrompt(); await copyPolarClientTemplate(); - if (templates.includes("checkout")) { + const shouldCopyCheckout = templates.includes("checkout"); + const shouldCopyWebhooks = templates.includes("webhooks"); + + if (shouldCopyCheckout) { await copyCheckoutTemplate(); } - if (templates.includes("webhooks")) { + if (shouldCopyWebhooks) { await copyWebhooksTemplate(); } @@ -87,10 +86,18 @@ const cli = meow( await installDisclaimer( installDependencies( - templates.includes("webhooks") + shouldCopyWebhooks ? [...baseDependencies, ...webhooksDependencies] : baseDependencies, ), ); + + // Handle environment variables + await environmentDisclaimer(appendEnvironmentVariables( shouldCopyWebhooks ? { + // POLAR_ACCESS_TOKEN: accessToken, + POLAR_ORGANIZATION_ID: organization.id + } : { + POLAR_ORGANIZATION_ID: organization.id + })); } })(); diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..83e44d4 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,41 @@ +import path from 'node:path'; +import fs from 'node:fs/promises'; + +const envFiles = ['.env', '.env.local']; + +const resolveEnvPaths = async () => { + const cwd = process.cwd(); + const paths = envFiles.map(file => path.join(cwd, file)); + + const existingFiles = await Promise.all( + paths.map(async (filePath) => { + try { + await fs.access(filePath); + return filePath; + } catch { + return null; + } + }) + ); + + return existingFiles.filter(Boolean); +}; + +export const appendEnvironmentVariables = async (variables: Record) => { + const envPaths = await resolveEnvPaths(); + + for (const envPath of envPaths) { + if (!envPath) continue; + + const envFile = await fs.readFile(envPath, 'utf8'); + + const newEnvFile = Object.entries(variables).reduce((acc, [key, value]) => { + if (!acc.includes(`${key}=`)) { + return `${acc}\n${key}=${value}`; + } + return acc; + }, envFile); + + await fs.writeFile(envPath, newEnvFile); + } +} \ No newline at end of file diff --git a/src/dependencies.ts b/src/install.ts similarity index 89% rename from src/dependencies.ts rename to src/install.ts index ca66121..bef88ba 100644 --- a/src/dependencies.ts +++ b/src/install.ts @@ -1,11 +1,8 @@ -// @ts-ignore -import spawn from "next/dist/compiled/cross-spawn/index.js"; +import spawn from 'cross-spawn' import { getPkgManager } from "next/dist/lib/helpers/get-pkg-manager.js"; /** * Spawn a package manager installation with either npm, pnpm, or yarn. - * - * @returns A Promise that resolves once the installation is finished. */ const install = ( root: string, @@ -43,6 +40,7 @@ const install = ( NODE_ENV: "development", }, }); + child.on("close", (code: number) => { if (code !== 0) { reject({ command: `${packageManager} ${args.join(" ")}` }); diff --git a/src/prompts/template.ts b/src/prompts/template.ts index ec10c97..e5d9dea 100644 --- a/src/prompts/template.ts +++ b/src/prompts/template.ts @@ -4,11 +4,10 @@ export const templatePrompt = async () => { const { templates } = await prompts({ type: "multiselect", name: "templates", - message: "Features to setup with your Next.js project", + message: "Polar Features", instructions: false, choices: [ - { title: "Checkout Route", value: "checkout", selected: true }, - { title: "Confirmation Page", value: "confirmation", selected: true }, + { title: "Checkout Routes", value: "checkout", selected: true }, { title: "Webhook Handler", value: "webhooks", selected: true }, ], }); diff --git a/src/ui/environment.tsx b/src/ui/environment.tsx new file mode 100644 index 0000000..ca1a298 --- /dev/null +++ b/src/ui/environment.tsx @@ -0,0 +1,16 @@ +import { Spinner } from "@inkjs/ui"; +import { render } from "ink"; +import React from "react"; + +export const environmentDisclaimer = async (promise: Promise) => { + const { unmount, clear, waitUntilExit } = render( + , + ); + + promise.then(() => { + clear(); + unmount(); + }); + + await waitUntilExit(); +};