paykitjs 0.0.1

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.
Files changed (70) hide show
  1. package/LICENSE +21 -0
  2. package/dist/_virtual/_rolldown/runtime.js +13 -0
  3. package/dist/api/define-route.d.ts +94 -0
  4. package/dist/api/define-route.js +153 -0
  5. package/dist/api/methods.d.ts +422 -0
  6. package/dist/api/methods.js +67 -0
  7. package/dist/cli/commands/check.js +92 -0
  8. package/dist/cli/commands/init.js +264 -0
  9. package/dist/cli/commands/push.js +73 -0
  10. package/dist/cli/commands/telemetry.js +16 -0
  11. package/dist/cli/index.d.ts +1 -0
  12. package/dist/cli/index.js +21 -0
  13. package/dist/cli/templates/index.js +64 -0
  14. package/dist/cli/utils/detect.js +67 -0
  15. package/dist/cli/utils/format.js +58 -0
  16. package/dist/cli/utils/get-config.js +117 -0
  17. package/dist/cli/utils/telemetry.js +103 -0
  18. package/dist/client/index.d.ts +25 -0
  19. package/dist/client/index.js +27 -0
  20. package/dist/core/context.d.ts +17 -0
  21. package/dist/core/context.js +23 -0
  22. package/dist/core/create-paykit.d.ts +7 -0
  23. package/dist/core/create-paykit.js +52 -0
  24. package/dist/core/error-codes.d.ts +12 -0
  25. package/dist/core/error-codes.js +10 -0
  26. package/dist/core/errors.d.ts +41 -0
  27. package/dist/core/errors.js +47 -0
  28. package/dist/core/logger.d.ts +11 -0
  29. package/dist/core/logger.js +51 -0
  30. package/dist/core/utils.js +21 -0
  31. package/dist/customer/customer.api.js +47 -0
  32. package/dist/customer/customer.service.js +342 -0
  33. package/dist/customer/customer.types.d.ts +31 -0
  34. package/dist/database/index.d.ts +8 -0
  35. package/dist/database/index.js +32 -0
  36. package/dist/database/migrations/0000_init.sql +157 -0
  37. package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
  38. package/dist/database/migrations/meta/_journal.json +13 -0
  39. package/dist/database/schema.d.ts +1767 -0
  40. package/dist/database/schema.js +150 -0
  41. package/dist/entitlement/entitlement.api.js +33 -0
  42. package/dist/entitlement/entitlement.service.d.ts +17 -0
  43. package/dist/entitlement/entitlement.service.js +123 -0
  44. package/dist/handlers/next.d.ts +9 -0
  45. package/dist/handlers/next.js +9 -0
  46. package/dist/index.d.ts +14 -0
  47. package/dist/index.js +6 -0
  48. package/dist/invoice/invoice.service.js +54 -0
  49. package/dist/payment/payment.service.js +49 -0
  50. package/dist/payment-method/payment-method.service.js +78 -0
  51. package/dist/product/product-sync.service.js +111 -0
  52. package/dist/product/product.service.js +127 -0
  53. package/dist/providers/provider.d.ts +159 -0
  54. package/dist/providers/stripe.js +547 -0
  55. package/dist/subscription/subscription.api.js +24 -0
  56. package/dist/subscription/subscription.service.js +896 -0
  57. package/dist/subscription/subscription.types.d.ts +18 -0
  58. package/dist/subscription/subscription.types.js +11 -0
  59. package/dist/testing/testing.api.js +29 -0
  60. package/dist/testing/testing.service.js +49 -0
  61. package/dist/types/events.d.ts +181 -0
  62. package/dist/types/instance.d.ts +88 -0
  63. package/dist/types/models.d.ts +11 -0
  64. package/dist/types/options.d.ts +32 -0
  65. package/dist/types/plugin.d.ts +11 -0
  66. package/dist/types/schema.d.ts +99 -0
  67. package/dist/types/schema.js +192 -0
  68. package/dist/webhook/webhook.api.js +29 -0
  69. package/dist/webhook/webhook.service.js +143 -0
  70. package/package.json +72 -0
@@ -0,0 +1,67 @@
1
+ import { customerPortal, deleteCustomer, getCustomer, listCustomersMethod, upsertCustomer } from "../customer/customer.api.js";
2
+ import { check, report } from "../entitlement/entitlement.api.js";
3
+ import { subscribe } from "../subscription/subscription.api.js";
4
+ import { advanceTestClock, getTestClock } from "../testing/testing.api.js";
5
+ import { receiveWebhook } from "../webhook/webhook.api.js";
6
+ import { createRouter } from "better-call";
7
+ //#region src/api/methods.ts
8
+ const baseMethods = {
9
+ subscribe,
10
+ customerPortal,
11
+ upsertCustomer,
12
+ getCustomer,
13
+ deleteCustomer,
14
+ listCustomers: listCustomersMethod,
15
+ check,
16
+ report,
17
+ handleWebhook: receiveWebhook
18
+ };
19
+ const testingMethods = {
20
+ getTestClock,
21
+ advanceTestClock
22
+ };
23
+ const methods = {
24
+ ...baseMethods,
25
+ ...testingMethods
26
+ };
27
+ pickMethods(baseMethods);
28
+ pickMethods(methods);
29
+ function pickMethods(source) {
30
+ return Object.fromEntries(Object.entries(source).filter(([, method]) => method.client === true));
31
+ }
32
+ function wrapMethods(source, ctx) {
33
+ return Object.fromEntries(Object.entries(source).map(([key, method]) => {
34
+ const fn = async (input) => {
35
+ return method(await ctx, input);
36
+ };
37
+ if (method.endpoint) Object.assign(fn, {
38
+ options: method.endpoint.options,
39
+ path: method.endpoint.path
40
+ });
41
+ return [key, fn];
42
+ }));
43
+ }
44
+ function getRouteEndpoints(source) {
45
+ return Object.fromEntries(Object.entries(source).flatMap(([key, method]) => method.endpoint ? [[key, method.endpoint]] : []));
46
+ }
47
+ function isTestingEnabled(options) {
48
+ return options.testing?.enabled === true;
49
+ }
50
+ function getApi(ctx, options) {
51
+ return wrapMethods(isTestingEnabled(options) ? methods : baseMethods, ctx);
52
+ }
53
+ function createPayKitRouter(ctx, options) {
54
+ const pluginEndpoints = Object.assign({}, ...(options.plugins ?? []).map((plugin) => plugin.endpoints ?? {}));
55
+ return createRouter({
56
+ ...getRouteEndpoints(isTestingEnabled(options) ? methods : baseMethods),
57
+ ...pluginEndpoints
58
+ }, {
59
+ basePath: options.basePath ?? "/paykit/api",
60
+ routerContext: ctx,
61
+ onError(error) {
62
+ ctx.logger.error({ err: error }, "API error");
63
+ }
64
+ });
65
+ }
66
+ //#endregion
67
+ export { createPayKitRouter, getApi };
@@ -0,0 +1,92 @@
1
+ import { dryRunSyncProducts } from "../../product/product-sync.service.js";
2
+ import { getPendingMigrationCount } from "../../database/index.js";
3
+ import { createContext } from "../../core/context.js";
4
+ import { formatPlanLine, formatPrice, getConnectionString, getStripeAccountInfo } from "../utils/format.js";
5
+ import { getPayKitConfig } from "../utils/get-config.js";
6
+ import { capture } from "../utils/telemetry.js";
7
+ import { Pool } from "pg";
8
+ import path from "node:path";
9
+ import StripeSdk from "stripe";
10
+ import { Command } from "commander";
11
+ import * as p from "@clack/prompts";
12
+ import picocolors from "picocolors";
13
+ //#region src/cli/commands/check.ts
14
+ async function checkAction(options) {
15
+ const cwd = path.resolve(options.cwd);
16
+ p.intro("paykit check");
17
+ let config;
18
+ try {
19
+ config = await getPayKitConfig({
20
+ configPath: options.config,
21
+ cwd
22
+ });
23
+ } catch (error) {
24
+ const message = error instanceof Error ? error.message : String(error);
25
+ p.log.error(`Config\n ${picocolors.red("✖")} ${message}`);
26
+ p.outro("Fix config issues before continuing");
27
+ process.exit(1);
28
+ }
29
+ const planCount = config.options.plans ? Object.values(config.options.plans).length : 0;
30
+ const hasProvider = Boolean(config.options.provider);
31
+ p.log.info(`Config\n ${picocolors.green("✔")} ${picocolors.dim(config.path)}\n ${picocolors.green("✔")} ${String(planCount)} product${planCount === 1 ? "" : "s"} defined\n ${hasProvider ? picocolors.green("✔") : picocolors.red("✖")} ${hasProvider ? "Stripe provider configured" : "No provider configured"}`);
32
+ if (!hasProvider) {
33
+ p.outro("Fix config issues before continuing");
34
+ process.exit(1);
35
+ }
36
+ const database = typeof config.options.database === "string" ? new Pool({ connectionString: config.options.database }) : config.options.database;
37
+ const connStr = getConnectionString(database);
38
+ let pendingMigrations = 0;
39
+ try {
40
+ await database.query("SELECT 1");
41
+ pendingMigrations = await getPendingMigrationCount(database);
42
+ } catch (error) {
43
+ const message = error instanceof Error ? error.message : String(error);
44
+ p.log.error(`Database\n ${picocolors.red("✖")} ${connStr}\n ${message}`);
45
+ p.outro("Fix database issues before continuing");
46
+ await database.end();
47
+ process.exit(1);
48
+ }
49
+ const migrationStatus = pendingMigrations > 0 ? `${picocolors.red("✖")} Schema needs migration` : `${picocolors.green("✔")} Schema up to date`;
50
+ p.log.info(`Database\n ${picocolors.green("✔")} ${connStr}\n ${migrationStatus}`);
51
+ const stripeAccount = await getStripeAccountInfo(config.options.provider.secretKey);
52
+ let webhookStatus;
53
+ try {
54
+ const activeEndpoints = (await new StripeSdk(config.options.provider.secretKey).webhookEndpoints.list({ limit: 100 })).data.filter((ep) => ep.status === "enabled");
55
+ if (activeEndpoints.length > 0) webhookStatus = activeEndpoints.map((ep) => picocolors.dim(`- Webhook endpoint registered (${ep.url})`)).join("\n ");
56
+ else webhookStatus = picocolors.dim("- No webhook endpoint (use Stripe CLI for local testing)");
57
+ } catch {
58
+ webhookStatus = `${picocolors.dim("?")} Could not check webhook status`;
59
+ }
60
+ p.log.info(`Stripe\n ${picocolors.green("✔")} ${stripeAccount.displayName} (${stripeAccount.mode})\n ${webhookStatus}`);
61
+ let hasIssues = pendingMigrations > 0;
62
+ if (pendingMigrations > 0) p.log.info(`Products\n ${picocolors.dim("?")} Cannot check sync status until migrations are applied`);
63
+ else {
64
+ const ctx = await createContext(config.options);
65
+ const diffs = await dryRunSyncProducts(ctx);
66
+ if (diffs.length === 0) p.log.info(`Products\n ${picocolors.dim("No products defined")}`);
67
+ else {
68
+ const allSynced = diffs.every((d) => d.action === "unchanged");
69
+ if (!allSynced) hasIssues = true;
70
+ const header = allSynced ? `${picocolors.green("✔")} All synced` : `${picocolors.red("✖")} Not synced (run ${picocolors.bold("paykitjs push")})`;
71
+ const planLines = diffs.map((diff) => {
72
+ const plan = ctx.plans.plans.find((pl) => pl.id === diff.id);
73
+ const price = plan ? formatPrice(plan.priceAmount ?? 0, plan.priceInterval) : "$0";
74
+ return formatPlanLine(diff.action, diff.id, price);
75
+ });
76
+ p.log.info(`Products\n ${header}\n${planLines.join("\n")}`);
77
+ }
78
+ }
79
+ capture("cli_command", {
80
+ command: "check",
81
+ needsMigration: pendingMigrations > 0
82
+ });
83
+ await database.end();
84
+ if (hasIssues) {
85
+ p.outro(`Run ${picocolors.bold("paykitjs push")} to apply migrations and sync plans`);
86
+ process.exit(1);
87
+ }
88
+ p.outro("Everything looks good");
89
+ }
90
+ const checkCommand = new Command("check").description("Check PayKit configuration and sync status").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the PayKit configuration file to load.").action(checkAction);
91
+ //#endregion
92
+ export { checkCommand };
@@ -0,0 +1,264 @@
1
+ import { capture } from "../utils/telemetry.js";
2
+ import { templates } from "../templates/index.js";
3
+ import { defaultConfigPath, defaultRoutePath, detectPackageManager, getInstallCommand, isPackageInstalled, resolveImportPath } from "../utils/detect.js";
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { Command } from "commander";
7
+ import * as p from "@clack/prompts";
8
+ import picocolors from "picocolors";
9
+ import { execSync } from "node:child_process";
10
+ //#region src/cli/commands/init.ts
11
+ function ensureDir(filePath) {
12
+ const dir = path.dirname(filePath);
13
+ if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
14
+ }
15
+ function generateConfigFile() {
16
+ return `import { stripe } from "@paykitjs/stripe";
17
+ import { createPayKit } from "paykitjs";
18
+ import { Pool } from "pg";
19
+
20
+ import * as plans from "./paykit.plans";
21
+
22
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL });
23
+
24
+ export const paykit = createPayKit({
25
+ database: pool,
26
+ provider: stripe({
27
+ secretKey: process.env.STRIPE_SECRET_KEY!,
28
+ webhookSecret: process.env.STRIPE_WEBHOOK_SECRET!,
29
+ }),
30
+ plans,
31
+ });
32
+ `;
33
+ }
34
+ function generateRouteHandler(configPath, routePath, cwd) {
35
+ const configFullPath = path.join(cwd, configPath);
36
+ return `import { paykitHandler } from "paykitjs/handlers/next";
37
+
38
+ import { paykit } from "${resolveImportPath(path.join(cwd, routePath), configFullPath, cwd)}";
39
+
40
+ export const { GET, POST } = paykitHandler(paykit);
41
+ `;
42
+ }
43
+ function generateClientFile(configPath, clientPath, cwd) {
44
+ const configFullPath = path.join(cwd, configPath);
45
+ return `import { createPayKitClient } from "paykitjs/client";
46
+
47
+ import type { paykit } from "${resolveImportPath(path.join(cwd, clientPath), configFullPath, cwd)}";
48
+
49
+ export const paykitClient = createPayKitClient<typeof paykit>();
50
+ `;
51
+ }
52
+ function getEnvVarsToAdd(cwd) {
53
+ const envPath = path.join(cwd, ".env");
54
+ const existing = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
55
+ return [
56
+ {
57
+ key: "DATABASE_URL",
58
+ comment: "# PostgreSQL connection string"
59
+ },
60
+ {
61
+ key: "STRIPE_SECRET_KEY",
62
+ comment: "# Stripe secret key (sk_test_... or sk_live_...)"
63
+ },
64
+ {
65
+ key: "STRIPE_WEBHOOK_SECRET",
66
+ comment: "# Stripe webhook secret (whsec_...)"
67
+ }
68
+ ].filter((v) => !existing.includes(`${v.key}=`));
69
+ }
70
+ function writeEnvVars(cwd, vars) {
71
+ const envPath = path.join(cwd, ".env");
72
+ let content = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf-8") : "";
73
+ for (const v of vars) {
74
+ if (content.length > 0 && !content.endsWith("\n")) content += "\n";
75
+ content += `${v.comment}\n${v.key}=\n`;
76
+ }
77
+ fs.writeFileSync(envPath, content);
78
+ }
79
+ async function initAction(options) {
80
+ const cwd = path.resolve(options.cwd);
81
+ p.intro("paykit init");
82
+ p.log.info("Welcome to PayKit! Let's set up billing.");
83
+ const provider = await p.select({
84
+ message: "Select a payment provider",
85
+ options: [
86
+ {
87
+ value: "stripe",
88
+ label: "Stripe"
89
+ },
90
+ {
91
+ value: "polar",
92
+ label: "Polar",
93
+ hint: "coming soon",
94
+ disabled: true
95
+ },
96
+ {
97
+ value: "creem",
98
+ label: "Creem",
99
+ hint: "coming soon",
100
+ disabled: true
101
+ }
102
+ ]
103
+ });
104
+ if (p.isCancel(provider)) {
105
+ p.cancel("Aborted");
106
+ process.exit(0);
107
+ }
108
+ const configDefault = defaultConfigPath(cwd);
109
+ const configPath = await p.text({
110
+ message: "Where should we create the PayKit config?",
111
+ defaultValue: configDefault,
112
+ placeholder: configDefault
113
+ });
114
+ if (p.isCancel(configPath)) {
115
+ p.cancel("Aborted");
116
+ process.exit(0);
117
+ }
118
+ const framework = await p.select({
119
+ message: "Framework",
120
+ options: [{
121
+ value: "nextjs",
122
+ label: "Next.js (App Router)"
123
+ }, {
124
+ value: "other",
125
+ label: "Other",
126
+ hint: "not yet supported"
127
+ }]
128
+ });
129
+ if (p.isCancel(framework)) {
130
+ p.cancel("Aborted");
131
+ process.exit(0);
132
+ }
133
+ let routePath = null;
134
+ if (framework === "nextjs") {
135
+ const routeDefault = defaultRoutePath(cwd);
136
+ const result = await p.text({
137
+ message: "Route handler path",
138
+ defaultValue: routeDefault,
139
+ placeholder: routeDefault
140
+ });
141
+ if (p.isCancel(result)) {
142
+ p.cancel("Aborted");
143
+ process.exit(0);
144
+ }
145
+ routePath = result;
146
+ }
147
+ const generateClient = await p.confirm({ message: "Generate a PayKit client?" });
148
+ if (p.isCancel(generateClient)) {
149
+ p.cancel("Aborted");
150
+ process.exit(0);
151
+ }
152
+ let clientPath = null;
153
+ if (generateClient) {
154
+ const clientDefault = "src/lib/paykit-client.ts";
155
+ const result = await p.text({
156
+ message: "Client file path",
157
+ defaultValue: clientDefault,
158
+ placeholder: clientDefault
159
+ });
160
+ if (p.isCancel(result)) {
161
+ p.cancel("Aborted");
162
+ process.exit(0);
163
+ }
164
+ clientPath = result;
165
+ }
166
+ const templateId = await p.select({
167
+ message: "Select a pricing template",
168
+ options: templates.map((t) => ({
169
+ value: t.id,
170
+ label: t.name,
171
+ hint: t.hint
172
+ }))
173
+ });
174
+ if (p.isCancel(templateId)) {
175
+ p.cancel("Aborted");
176
+ process.exit(0);
177
+ }
178
+ const toInstall = ["paykitjs", "@paykitjs/stripe"].filter((pkg) => !isPackageInstalled(cwd, pkg));
179
+ if (toInstall.length > 0) {
180
+ const installCmd = getInstallCommand(detectPackageManager(cwd), toInstall);
181
+ const spinner = p.spinner();
182
+ spinner.start(`Installing ${toInstall.join(", ")}`);
183
+ try {
184
+ execSync(installCmd, {
185
+ cwd,
186
+ stdio: "pipe",
187
+ env: {
188
+ ...process.env,
189
+ NODE_ENV: ""
190
+ }
191
+ });
192
+ spinner.stop(`Installed ${toInstall.join(", ")}`);
193
+ } catch {
194
+ spinner.stop("Could not install automatically");
195
+ p.log.step(`Add to your package.json manually:\n ${picocolors.dim(installCmd)}`);
196
+ }
197
+ } else p.log.step("Dependencies already installed");
198
+ const files = [];
199
+ const skipped = [];
200
+ const configFullPath = path.join(cwd, configPath);
201
+ if (fs.existsSync(configFullPath)) skipped.push(configPath);
202
+ else files.push({
203
+ path: configPath,
204
+ content: generateConfigFile()
205
+ });
206
+ const template = templates.find((t) => t.id === templateId);
207
+ if (template) {
208
+ const plansPath = configPath.replace(/paykit\.ts$/, "paykit.plans.ts");
209
+ const plansFullPath = path.join(cwd, plansPath);
210
+ if (fs.existsSync(plansFullPath)) skipped.push(plansPath);
211
+ else files.push({
212
+ path: plansPath,
213
+ content: template.content
214
+ });
215
+ }
216
+ if (routePath) {
217
+ const routeFullPath = path.join(cwd, routePath);
218
+ if (fs.existsSync(routeFullPath)) skipped.push(routePath);
219
+ else files.push({
220
+ path: routePath,
221
+ content: generateRouteHandler(configPath, routePath, cwd)
222
+ });
223
+ }
224
+ if (clientPath) {
225
+ const clientFullPath = path.join(cwd, clientPath);
226
+ if (fs.existsSync(clientFullPath)) skipped.push(clientPath);
227
+ else files.push({
228
+ path: clientPath,
229
+ content: generateClientFile(configPath, clientPath, cwd)
230
+ });
231
+ }
232
+ for (const file of files) {
233
+ const fullPath = path.join(cwd, file.path);
234
+ ensureDir(fullPath);
235
+ fs.writeFileSync(fullPath, file.content);
236
+ }
237
+ if (files.length > 0) {
238
+ const fileList = files.map((f) => ` ${picocolors.dim(f.path)}`).join("\n");
239
+ p.log.success(`Created ${String(files.length)} file${files.length === 1 ? "" : "s"}:\n${fileList}`);
240
+ }
241
+ if (skipped.length > 0) {
242
+ const skipList = skipped.map((f) => ` ${picocolors.dim(f)}`).join("\n");
243
+ p.log.step(`Skipped (already exist):\n${skipList}`);
244
+ }
245
+ const envVars = getEnvVarsToAdd(cwd);
246
+ if (envVars.length > 0) {
247
+ writeEnvVars(cwd, envVars);
248
+ const varList = envVars.map((v) => ` ${picocolors.dim(`${v.key}=`)}`).join("\n");
249
+ p.log.success(`Added to .env:\n${varList}`);
250
+ } else p.log.step("Environment variables already configured");
251
+ if (framework === "other") p.note("See the docs for manual route handler setup:\nhttps://paykitjs.com/docs/setup", "Manual Setup");
252
+ capture("cli_command", {
253
+ command: "init",
254
+ provider,
255
+ framework,
256
+ template: templateId,
257
+ filesCreated: files.length
258
+ });
259
+ p.note(`Fill in the variables in .env, then run:\n ${picocolors.bold("paykitjs push")}\n\nFor local webhooks:\n ${picocolors.dim("stripe listen --forward-to localhost:3000/api/paykit")}`, "Almost there!");
260
+ p.outro("Done");
261
+ }
262
+ const initCommand = new Command("init").description("Initialize PayKit in your project").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).action(initAction);
263
+ //#endregion
264
+ export { initCommand };
@@ -0,0 +1,73 @@
1
+ import { dryRunSyncProducts, syncProducts } from "../../product/product-sync.service.js";
2
+ import { getPendingMigrationCount, migrateDatabase } from "../../database/index.js";
3
+ import { createContext } from "../../core/context.js";
4
+ import { formatPlanLine, formatPrice, getConnectionString, getStripeAccountInfo } from "../utils/format.js";
5
+ import { getPayKitConfig } from "../utils/get-config.js";
6
+ import { capture } from "../utils/telemetry.js";
7
+ import { Pool } from "pg";
8
+ import path from "node:path";
9
+ import { Command } from "commander";
10
+ import * as p from "@clack/prompts";
11
+ import picocolors from "picocolors";
12
+ //#region src/cli/commands/push.ts
13
+ async function pushAction(options) {
14
+ const cwd = path.resolve(options.cwd);
15
+ p.intro("paykit push");
16
+ const config = await getPayKitConfig({
17
+ configPath: options.config,
18
+ cwd
19
+ });
20
+ const database = typeof config.options.database === "string" ? new Pool({ connectionString: config.options.database }) : config.options.database;
21
+ try {
22
+ const connStr = getConnectionString(database);
23
+ const stripeAccount = await getStripeAccountInfo(config.options.provider.secretKey);
24
+ p.log.info(`Connected\n Database ${picocolors.dim("·")} ${connStr}\n Stripe ${picocolors.dim("·")} ${stripeAccount.displayName} (${stripeAccount.mode})`);
25
+ const pendingMigrations = await getPendingMigrationCount(database);
26
+ if (pendingMigrations > 0) {
27
+ await migrateDatabase(database);
28
+ p.log.success(`Schema ${picocolors.dim("·")} migrated`);
29
+ } else p.log.step(`Schema ${picocolors.dim("·")} up to date`);
30
+ const ctx = await createContext(config.options);
31
+ const planDiffs = await dryRunSyncProducts(ctx);
32
+ const hasPlanChanges = planDiffs.some((d) => d.action !== "unchanged");
33
+ if (planDiffs.length > 0) {
34
+ const planLines = planDiffs.map((diff) => {
35
+ const plan = ctx.plans.plans.find((pl) => pl.id === diff.id);
36
+ const price = plan ? formatPrice(plan.priceAmount ?? 0, plan.priceInterval) : "$0";
37
+ return formatPlanLine(diff.action, diff.id, price);
38
+ });
39
+ p.log.step(`Plan changes\n${planLines.join("\n")}`);
40
+ }
41
+ if (!hasPlanChanges && pendingMigrations === 0) {
42
+ p.outro("Nothing to do");
43
+ return;
44
+ }
45
+ if (hasPlanChanges && !options.yes) {
46
+ const shouldContinue = await p.confirm({ message: "Sync plans?" });
47
+ if (p.isCancel(shouldContinue) || !shouldContinue) {
48
+ p.cancel("Aborted");
49
+ process.exit(0);
50
+ }
51
+ }
52
+ const results = await syncProducts(ctx);
53
+ const syncedCount = results.filter((r) => r.action !== "unchanged").length;
54
+ if (syncedCount > 0) p.log.success("Plans synced");
55
+ capture("cli_command", {
56
+ command: "push",
57
+ migrated: pendingMigrations > 0,
58
+ planCount: results.length,
59
+ plansSynced: syncedCount
60
+ });
61
+ p.outro(`Done ${picocolors.dim("·")} ${String(results.length)} plan${results.length === 1 ? "" : "s"} synced`);
62
+ } catch (error) {
63
+ const message = error instanceof Error ? error.message : String(error);
64
+ p.log.error(message);
65
+ p.cancel("Push failed");
66
+ process.exit(1);
67
+ } finally {
68
+ await database.end();
69
+ }
70
+ }
71
+ const pushCommand = new Command("push").description("Apply migrations and sync plans to database and payment provider").option("-c, --cwd <cwd>", "the working directory. defaults to the current directory.", process.cwd()).option("--config <config>", "the path to the PayKit configuration file to load.").option("-y, --yes", "skip confirmation prompt").action(pushAction);
72
+ //#endregion
73
+ export { pushCommand };
@@ -0,0 +1,16 @@
1
+ import { setEnabled } from "../utils/telemetry.js";
2
+ import { Command } from "commander";
3
+ import * as p from "@clack/prompts";
4
+ //#region src/cli/commands/telemetry.ts
5
+ const telemetryCommand = new Command("telemetry").description("Manage anonymous telemetry").addCommand(new Command("enable").description("Enable anonymous telemetry").action(() => {
6
+ setEnabled(true);
7
+ p.log.success("Telemetry enabled");
8
+ })).addCommand(new Command("disable").description("Disable anonymous telemetry").action(() => {
9
+ setEnabled(false);
10
+ p.log.success("Telemetry disabled");
11
+ })).addCommand(new Command("status").description("Check telemetry status").action(() => {
12
+ if (process.env.PAYKIT_TELEMETRY_DISABLED === "1" || process.env.DO_NOT_TRACK === "1") p.log.info("Telemetry is disabled (via environment variable)");
13
+ else p.log.info("Run `paykitjs telemetry disable` to opt out");
14
+ }));
15
+ //#endregion
16
+ export { telemetryCommand };
@@ -0,0 +1 @@
1
+ export { };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ import { captureError, flush } from "./utils/telemetry.js";
3
+ import { checkCommand } from "./commands/check.js";
4
+ import { initCommand } from "./commands/init.js";
5
+ import { pushCommand } from "./commands/push.js";
6
+ import { telemetryCommand } from "./commands/telemetry.js";
7
+ import { Command } from "commander";
8
+ //#region src/cli/index.ts
9
+ const program = new Command().name("paykitjs").description("CLI for PayKit").addCommand(checkCommand).addCommand(initCommand).addCommand(pushCommand).addCommand(telemetryCommand);
10
+ try {
11
+ await program.parseAsync(process.argv);
12
+ } catch (error) {
13
+ captureError(process.argv[2] ?? "unknown", error);
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ console.error(`\n error: ${message}\n`);
16
+ await flush();
17
+ process.exit(1);
18
+ }
19
+ await flush();
20
+ //#endregion
21
+ export {};
@@ -0,0 +1,64 @@
1
+ //#region src/cli/templates/index.ts
2
+ const templates = [
3
+ {
4
+ id: "saas-starter",
5
+ name: "SaaS Starter",
6
+ hint: "free + pro monthly",
7
+ content: `import { plan } from "paykitjs";
8
+
9
+ export const free = plan({
10
+ id: "free",
11
+ name: "Free",
12
+ group: "base",
13
+ default: true,
14
+ });
15
+
16
+ export const pro = plan({
17
+ id: "pro",
18
+ name: "Pro",
19
+ group: "base",
20
+ price: { amount: 29, interval: "month" },
21
+ });
22
+ `
23
+ },
24
+ {
25
+ id: "usage-based",
26
+ name: "Usage Based",
27
+ hint: "metered with limits",
28
+ content: `import { feature, plan } from "paykitjs";
29
+
30
+ const messages = feature({ id: "messages", type: "metered" });
31
+
32
+ export const free = plan({
33
+ id: "free",
34
+ name: "Free",
35
+ group: "base",
36
+ default: true,
37
+ includes: [messages({ limit: 100, reset: "month" })],
38
+ });
39
+
40
+ export const pro = plan({
41
+ id: "pro",
42
+ name: "Pro",
43
+ group: "base",
44
+ price: { amount: 29, interval: "month" },
45
+ includes: [messages({ limit: 5000, reset: "month" })],
46
+ });
47
+ `
48
+ },
49
+ {
50
+ id: "empty",
51
+ name: "Empty",
52
+ hint: "start from scratch",
53
+ content: `import { plan } from "paykitjs";
54
+
55
+ // export const myPlan = plan({
56
+ // id: "my-plan",
57
+ // name: "My Plan",
58
+ // price: { amount: 29, interval: "month" },
59
+ // });
60
+ `
61
+ }
62
+ ];
63
+ //#endregion
64
+ export { templates };
@@ -0,0 +1,67 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ //#region src/cli/utils/detect.ts
4
+ function detectPackageManager(cwd) {
5
+ const userAgent = process.env.npm_config_user_agent ?? "";
6
+ if (userAgent.startsWith("pnpm")) return "pnpm";
7
+ if (userAgent.startsWith("yarn")) return "yarn";
8
+ if (userAgent.startsWith("bun")) return "bun";
9
+ let dir = cwd;
10
+ while (true) {
11
+ if (fs.existsSync(path.join(dir, "pnpm-lock.yaml"))) return "pnpm";
12
+ if (fs.existsSync(path.join(dir, "yarn.lock"))) return "yarn";
13
+ if (fs.existsSync(path.join(dir, "bun.lockb")) || fs.existsSync(path.join(dir, "bun.lock"))) return "bun";
14
+ if (fs.existsSync(path.join(dir, "package-lock.json"))) return "npm";
15
+ const parent = path.dirname(dir);
16
+ if (parent === dir) break;
17
+ dir = parent;
18
+ }
19
+ return "npm";
20
+ }
21
+ function getInstallCommand(pm, packages) {
22
+ return `${pm === "npm" ? "npm install" : `${pm} add`} ${packages.join(" ")}`;
23
+ }
24
+ function isPackageInstalled(cwd, name) {
25
+ const pkgPath = path.join(cwd, "package.json");
26
+ if (!fs.existsSync(pkgPath)) return false;
27
+ try {
28
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
29
+ return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
30
+ } catch {
31
+ return false;
32
+ }
33
+ }
34
+ function hasTsconfigPathAlias(cwd) {
35
+ const tsconfigPath = path.join(cwd, "tsconfig.json");
36
+ if (!fs.existsSync(tsconfigPath)) return false;
37
+ try {
38
+ const stripped = fs.readFileSync(tsconfigPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
39
+ const tsconfig = JSON.parse(stripped);
40
+ return Boolean(tsconfig.compilerOptions?.paths?.["@/*"]);
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+ function resolveImportPath(fromFile, toFile, cwd) {
46
+ if (hasTsconfigPathAlias(cwd)) {
47
+ const srcRelative = path.relative(path.join(cwd, "src"), toFile);
48
+ if (!srcRelative.startsWith("..")) return "@/" + srcRelative.replace(/\.tsx?$/, "");
49
+ }
50
+ const fromDir = path.dirname(fromFile);
51
+ let relative = path.relative(fromDir, toFile).replace(/\.tsx?$/, "");
52
+ if (!relative.startsWith(".")) relative = "./" + relative;
53
+ return relative;
54
+ }
55
+ function defaultConfigPath(cwd) {
56
+ if (fs.existsSync(path.join(cwd, "src", "server"))) return "src/server/paykit.ts";
57
+ if (fs.existsSync(path.join(cwd, "src", "lib"))) return "src/lib/paykit.ts";
58
+ if (fs.existsSync(path.join(cwd, "src"))) return "src/paykit.ts";
59
+ return "paykit.ts";
60
+ }
61
+ function defaultRoutePath(cwd) {
62
+ if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app/paykit/api/[...slug]/route.ts";
63
+ if (fs.existsSync(path.join(cwd, "app"))) return "app/paykit/api/[...slug]/route.ts";
64
+ return "src/app/paykit/api/[...slug]/route.ts";
65
+ }
66
+ //#endregion
67
+ export { defaultConfigPath, defaultRoutePath, detectPackageManager, getInstallCommand, isPackageInstalled, resolveImportPath };