paykitjs 0.0.1-alpha.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.
- package/LICENSE +21 -0
- package/dist/_virtual/_rolldown/runtime.js +14 -0
- package/dist/api/define-route.d.ts +94 -0
- package/dist/api/define-route.js +153 -0
- package/dist/api/methods.d.ts +422 -0
- package/dist/api/methods.js +67 -0
- package/dist/cli/commands/init.js +264 -0
- package/dist/cli/commands/push.js +71 -0
- package/dist/cli/commands/status.js +84 -0
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +45 -0
- package/dist/cli/templates/index.js +64 -0
- package/dist/cli/utils/detect.js +73 -0
- package/dist/cli/utils/format.js +58 -0
- package/dist/cli/utils/get-config.js +117 -0
- package/dist/cli/utils/shared.js +81 -0
- package/dist/cli/utils/telemetry.js +63 -0
- package/dist/client/index.d.ts +25 -0
- package/dist/client/index.js +27 -0
- package/dist/core/context.d.ts +17 -0
- package/dist/core/context.js +23 -0
- package/dist/core/create-paykit.d.ts +7 -0
- package/dist/core/create-paykit.js +67 -0
- package/dist/core/error-codes.d.ts +12 -0
- package/dist/core/error-codes.js +10 -0
- package/dist/core/errors.d.ts +41 -0
- package/dist/core/errors.js +47 -0
- package/dist/core/logger.d.ts +11 -0
- package/dist/core/logger.js +51 -0
- package/dist/core/utils.js +21 -0
- package/dist/customer/customer.api.js +47 -0
- package/dist/customer/customer.service.js +342 -0
- package/dist/customer/customer.types.d.ts +31 -0
- package/dist/database/index.d.ts +8 -0
- package/dist/database/index.js +32 -0
- package/dist/database/migrations/0000_init.sql +157 -0
- package/dist/database/migrations/meta/0000_snapshot.json +1222 -0
- package/dist/database/migrations/meta/_journal.json +13 -0
- package/dist/database/schema.d.ts +1767 -0
- package/dist/database/schema.js +150 -0
- package/dist/entitlement/entitlement.api.js +33 -0
- package/dist/entitlement/entitlement.service.d.ts +17 -0
- package/dist/entitlement/entitlement.service.js +123 -0
- package/dist/handlers/next.d.ts +9 -0
- package/dist/handlers/next.js +9 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +6 -0
- package/dist/invoice/invoice.service.js +54 -0
- package/dist/payment/payment.service.js +49 -0
- package/dist/payment-method/payment-method.service.js +78 -0
- package/dist/product/product-sync.service.js +111 -0
- package/dist/product/product.service.js +127 -0
- package/dist/providers/provider.d.ts +159 -0
- package/dist/providers/stripe.js +547 -0
- package/dist/subscription/subscription.api.js +24 -0
- package/dist/subscription/subscription.service.js +896 -0
- package/dist/subscription/subscription.types.d.ts +18 -0
- package/dist/subscription/subscription.types.js +11 -0
- package/dist/testing/testing.api.js +29 -0
- package/dist/testing/testing.service.js +49 -0
- package/dist/types/events.d.ts +181 -0
- package/dist/types/instance.d.ts +88 -0
- package/dist/types/models.d.ts +11 -0
- package/dist/types/options.d.ts +32 -0
- package/dist/types/plugin.d.ts +11 -0
- package/dist/types/schema.d.ts +99 -0
- package/dist/types/schema.js +192 -0
- package/dist/utilities/dependencies/check-dependencies.js +16 -0
- package/dist/utilities/dependencies/get-dependencies.js +68 -0
- package/dist/utilities/dependencies/index.js +8 -0
- package/dist/utilities/dependencies/paykit-package-list.js +8 -0
- package/dist/webhook/webhook.api.js +29 -0
- package/dist/webhook/webhook.service.js +143 -0
- package/package.json +76 -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,264 @@
|
|
|
1
|
+
import { templates } from "../templates/index.js";
|
|
2
|
+
import { defaultConfigPath, defaultRoutePath, detectPackageManager, getInstallCommand, isPackageInstalled, resolveImportPath } from "../utils/detect.js";
|
|
3
|
+
import { capture } from "../utils/telemetry.js";
|
|
4
|
+
import picocolors from "picocolors";
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
import { Command } from "commander";
|
|
8
|
+
import { execSync } from "node:child_process";
|
|
9
|
+
import * as p from "@clack/prompts";
|
|
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,71 @@
|
|
|
1
|
+
import { checkStripe, createPool, formatProductDiffs, loadCliDeps, loadProductDiffs } from "../utils/shared.js";
|
|
2
|
+
import picocolors from "picocolors";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
//#region src/cli/commands/push.ts
|
|
7
|
+
async function pushAction(options) {
|
|
8
|
+
const cwd = path.resolve(options.cwd);
|
|
9
|
+
const s = p.spinner();
|
|
10
|
+
s.start("Connecting");
|
|
11
|
+
const deps = await loadCliDeps();
|
|
12
|
+
deps.capture("cli_command", { command: "push" });
|
|
13
|
+
const config = await deps.getPayKitConfig({
|
|
14
|
+
configPath: options.config,
|
|
15
|
+
cwd
|
|
16
|
+
});
|
|
17
|
+
const database = createPool(deps, config.options.database);
|
|
18
|
+
try {
|
|
19
|
+
const connStr = deps.getConnectionString(database);
|
|
20
|
+
const [stripeResult, pendingMigrations] = await Promise.all([checkStripe(deps, config.options.provider.secretKey), deps.getPendingMigrationCount(database)]);
|
|
21
|
+
if (!stripeResult.account.ok) {
|
|
22
|
+
s.stop("");
|
|
23
|
+
p.log.error(`Stripe\n ${picocolors.red("✖")} ${stripeResult.account.message}`);
|
|
24
|
+
p.cancel("Push failed");
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
if (pendingMigrations > 0) {
|
|
28
|
+
s.message("Applying migrations");
|
|
29
|
+
await deps.migrateDatabase(database);
|
|
30
|
+
}
|
|
31
|
+
s.message("Checking products");
|
|
32
|
+
const { ctx, diffs } = await loadProductDiffs(config, deps);
|
|
33
|
+
const hasChanges = diffs.some((d) => d.action !== "unchanged");
|
|
34
|
+
s.stop("");
|
|
35
|
+
const migrationStatus = pendingMigrations > 0 ? `${picocolors.green("✔")} Migrated (${String(pendingMigrations)} applied)` : `${picocolors.green("✔")} Schema up to date`;
|
|
36
|
+
p.log.info(`Database\n ${picocolors.green("✔")} ${connStr}\n ${migrationStatus}`);
|
|
37
|
+
p.log.info(`Stripe\n ${picocolors.green("✔")} ${stripeResult.account.displayName} (${stripeResult.account.mode})`);
|
|
38
|
+
if (diffs.length > 0) {
|
|
39
|
+
const planLines = formatProductDiffs(diffs, ctx.plans.plans, deps);
|
|
40
|
+
p.log.info(`Products\n${planLines.join("\n")}`);
|
|
41
|
+
}
|
|
42
|
+
if (!hasChanges && pendingMigrations === 0) {
|
|
43
|
+
p.outro("Nothing to do");
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
if (pendingMigrations > 0 && !hasChanges) {
|
|
47
|
+
p.outro("Done");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const changeCount = diffs.filter((d) => d.action !== "unchanged").length;
|
|
51
|
+
if (!options.yes) {
|
|
52
|
+
const shouldContinue = await p.confirm({ message: `Push ${String(changeCount)} product change${changeCount === 1 ? "" : "s"} to Stripe?` });
|
|
53
|
+
if (p.isCancel(shouldContinue) || !shouldContinue) {
|
|
54
|
+
p.cancel("Aborted");
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
const syncedCount = (await deps.syncProducts(ctx)).filter((r) => r.action !== "unchanged").length;
|
|
59
|
+
p.outro(`Done ${picocolors.dim("·")} synced ${String(syncedCount)} product${syncedCount === 1 ? "" : "s"}`);
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
62
|
+
p.log.error(message);
|
|
63
|
+
p.cancel("Push failed");
|
|
64
|
+
process.exit(1);
|
|
65
|
+
} finally {
|
|
66
|
+
await database.end();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
const pushCommand = new Command("push").description("Apply migrations and sync products 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);
|
|
70
|
+
//#endregion
|
|
71
|
+
export { pushCommand };
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { checkDatabase, checkStripe, createPool, formatProductDiffs, loadCliDeps, loadProductDiffs } from "../utils/shared.js";
|
|
2
|
+
import picocolors from "picocolors";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Command } from "commander";
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
//#region src/cli/commands/status.ts
|
|
7
|
+
async function statusAction(options) {
|
|
8
|
+
const cwd = path.resolve(options.cwd);
|
|
9
|
+
const s = p.spinner();
|
|
10
|
+
s.start("Checking");
|
|
11
|
+
const deps = await loadCliDeps();
|
|
12
|
+
deps.capture("cli_command", { command: "status" });
|
|
13
|
+
const pm = deps.detectPackageManager(cwd);
|
|
14
|
+
const pushCmd = deps.getRunCommand(pm, "paykitjs push");
|
|
15
|
+
let config;
|
|
16
|
+
try {
|
|
17
|
+
config = await deps.getPayKitConfig({
|
|
18
|
+
configPath: options.config,
|
|
19
|
+
cwd
|
|
20
|
+
});
|
|
21
|
+
} catch (error) {
|
|
22
|
+
s.stop("");
|
|
23
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
24
|
+
p.log.error(`Config\n ${picocolors.red("✖")} ${message}`);
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
const planCount = config.options.plans ? Object.values(config.options.plans).length : 0;
|
|
28
|
+
if (!Boolean(config.options.provider)) {
|
|
29
|
+
s.stop("");
|
|
30
|
+
p.log.error(`Config\n ${picocolors.green("✔")} ${picocolors.dim(config.path)}\n ${picocolors.green("✔")} ${String(planCount)} plan${planCount === 1 ? "" : "s"} defined\n ${picocolors.red("✖")} No provider configured`);
|
|
31
|
+
p.outro("Fix config issues before continuing");
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
const database = createPool(deps, config.options.database);
|
|
35
|
+
const connStr = deps.getConnectionString(database);
|
|
36
|
+
const [dbResult, stripeResult] = await Promise.all([checkDatabase(database, deps), checkStripe(deps, config.options.provider.secretKey)]);
|
|
37
|
+
if (!dbResult.ok) {
|
|
38
|
+
s.stop("");
|
|
39
|
+
p.log.error(`Database\n ${picocolors.red("✖")} ${connStr}\n ${dbResult.message}`);
|
|
40
|
+
p.outro("Fix database issues before continuing");
|
|
41
|
+
await database.end();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
if (!stripeResult.account.ok) {
|
|
45
|
+
s.stop("");
|
|
46
|
+
p.log.error(`Stripe\n ${picocolors.red("✖")} ${stripeResult.account.message}`);
|
|
47
|
+
p.outro("Fix Stripe issues before continuing");
|
|
48
|
+
await database.end();
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
const pendingMigrations = dbResult.pendingMigrations;
|
|
52
|
+
let webhookStatus;
|
|
53
|
+
if (stripeResult.webhooks === null) webhookStatus = `${picocolors.dim("?")} Could not check webhook status`;
|
|
54
|
+
else if (stripeResult.webhooks.length > 0) webhookStatus = stripeResult.webhooks.map((ep) => picocolors.dim(`· Webhook endpoint registered (${ep.url})`)).join("\n ");
|
|
55
|
+
else webhookStatus = picocolors.dim("· No webhook endpoint (use Stripe CLI for local testing)");
|
|
56
|
+
let needsSync = false;
|
|
57
|
+
let productsBlock;
|
|
58
|
+
if (pendingMigrations > 0) productsBlock = `Products\n ${picocolors.dim("?")} Cannot check sync status until migrations are applied`;
|
|
59
|
+
else {
|
|
60
|
+
const { ctx, diffs } = await loadProductDiffs(config, deps);
|
|
61
|
+
if (diffs.length === 0) productsBlock = `Products\n ${picocolors.dim("No products defined")}`;
|
|
62
|
+
else {
|
|
63
|
+
const allSynced = diffs.every((d) => d.action === "unchanged");
|
|
64
|
+
if (!allSynced) needsSync = true;
|
|
65
|
+
productsBlock = `Products\n ${allSynced ? `${picocolors.green("✔")} All synced` : `${picocolors.red("✖")} Not synced (run ${picocolors.bold(pushCmd)})`}\n${formatProductDiffs(diffs, ctx.plans.plans, deps).join("\n")}`;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
await database.end();
|
|
69
|
+
const migrationStatus = pendingMigrations > 0 ? `${picocolors.red("✖")} Schema needs migration` : `${picocolors.green("✔")} Schema up to date`;
|
|
70
|
+
s.stop("");
|
|
71
|
+
p.log.info(`Config\n ${picocolors.green("✔")} ${picocolors.dim(config.path)}\n ${picocolors.green("✔")} ${String(planCount)} plan${planCount === 1 ? "" : "s"} defined\n ${picocolors.green("✔")} Stripe provider configured`);
|
|
72
|
+
p.log.info(`Database\n ${picocolors.green("✔")} ${connStr}\n ${migrationStatus}`);
|
|
73
|
+
p.log.info(`Stripe\n ${picocolors.green("✔")} ${stripeResult.account.displayName} (${stripeResult.account.mode})\n ${webhookStatus}`);
|
|
74
|
+
p.log.info(productsBlock);
|
|
75
|
+
const needsMigration = pendingMigrations > 0;
|
|
76
|
+
if (needsMigration || needsSync) {
|
|
77
|
+
const action = needsMigration && needsSync ? "apply migrations and sync products" : needsMigration ? "apply migrations" : "sync products";
|
|
78
|
+
p.outro(`Run ${picocolors.bold(pushCmd)} to ${action}`);
|
|
79
|
+
if (options.throw) process.exit(1);
|
|
80
|
+
} else p.outro("Everything looks good");
|
|
81
|
+
}
|
|
82
|
+
const statusCommand = new Command("status").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.").option("--throw", "exit with code 1 if there are issues").action(statusAction);
|
|
83
|
+
//#endregion
|
|
84
|
+
export { statusCommand };
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
//#region src/cli/index.ts
|
|
4
|
+
process.env.PAYKIT_CLI = "1";
|
|
5
|
+
const program = new Command().name("paykitjs").description("CLI for PayKit");
|
|
6
|
+
const commandName = process.argv[2];
|
|
7
|
+
switch (commandName) {
|
|
8
|
+
case "status": {
|
|
9
|
+
const { statusCommand } = await import("./commands/status.js");
|
|
10
|
+
program.addCommand(statusCommand);
|
|
11
|
+
break;
|
|
12
|
+
}
|
|
13
|
+
case "init": {
|
|
14
|
+
const { initCommand } = await import("./commands/init.js");
|
|
15
|
+
program.addCommand(initCommand);
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
case "push": {
|
|
19
|
+
const { pushCommand } = await import("./commands/push.js");
|
|
20
|
+
program.addCommand(pushCommand);
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
default: {
|
|
24
|
+
const [{ statusCommand }, { initCommand }, { pushCommand }] = await Promise.all([
|
|
25
|
+
import("./commands/status.js"),
|
|
26
|
+
import("./commands/init.js"),
|
|
27
|
+
import("./commands/push.js")
|
|
28
|
+
]);
|
|
29
|
+
program.addCommand(statusCommand);
|
|
30
|
+
program.addCommand(initCommand);
|
|
31
|
+
program.addCommand(pushCommand);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
await program.parseAsync(process.argv);
|
|
36
|
+
} catch (error) {
|
|
37
|
+
const { captureError, flush } = await import("./utils/telemetry.js");
|
|
38
|
+
captureError(commandName ?? "unknown", error);
|
|
39
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
40
|
+
console.error(`\n error: ${message}\n`);
|
|
41
|
+
await flush();
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
//#endregion
|
|
45
|
+
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,73 @@
|
|
|
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 getRunCommand(pm, script) {
|
|
25
|
+
if (pm === "npm") return `npx ${script}`;
|
|
26
|
+
if (pm === "bun") return `bunx ${script}`;
|
|
27
|
+
if (pm === "yarn") return `yarn dlx ${script}`;
|
|
28
|
+
return `pnpm dlx ${script}`;
|
|
29
|
+
}
|
|
30
|
+
function isPackageInstalled(cwd, name) {
|
|
31
|
+
const pkgPath = path.join(cwd, "package.json");
|
|
32
|
+
if (!fs.existsSync(pkgPath)) return false;
|
|
33
|
+
try {
|
|
34
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
|
|
35
|
+
return Boolean(pkg.dependencies?.[name] || pkg.devDependencies?.[name]);
|
|
36
|
+
} catch {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function hasTsconfigPathAlias(cwd) {
|
|
41
|
+
const tsconfigPath = path.join(cwd, "tsconfig.json");
|
|
42
|
+
if (!fs.existsSync(tsconfigPath)) return false;
|
|
43
|
+
try {
|
|
44
|
+
const stripped = fs.readFileSync(tsconfigPath, "utf-8").replace(/\/\/.*$/gm, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
45
|
+
const tsconfig = JSON.parse(stripped);
|
|
46
|
+
return Boolean(tsconfig.compilerOptions?.paths?.["@/*"]);
|
|
47
|
+
} catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveImportPath(fromFile, toFile, cwd) {
|
|
52
|
+
if (hasTsconfigPathAlias(cwd)) {
|
|
53
|
+
const srcRelative = path.relative(path.join(cwd, "src"), toFile);
|
|
54
|
+
if (!srcRelative.startsWith("..")) return "@/" + srcRelative.replace(/\.tsx?$/, "");
|
|
55
|
+
}
|
|
56
|
+
const fromDir = path.dirname(fromFile);
|
|
57
|
+
let relative = path.relative(fromDir, toFile).replace(/\.tsx?$/, "");
|
|
58
|
+
if (!relative.startsWith(".")) relative = "./" + relative;
|
|
59
|
+
return relative;
|
|
60
|
+
}
|
|
61
|
+
function defaultConfigPath(cwd) {
|
|
62
|
+
if (fs.existsSync(path.join(cwd, "src", "server"))) return "src/server/paykit.ts";
|
|
63
|
+
if (fs.existsSync(path.join(cwd, "src", "lib"))) return "src/lib/paykit.ts";
|
|
64
|
+
if (fs.existsSync(path.join(cwd, "src"))) return "src/paykit.ts";
|
|
65
|
+
return "paykit.ts";
|
|
66
|
+
}
|
|
67
|
+
function defaultRoutePath(cwd) {
|
|
68
|
+
if (fs.existsSync(path.join(cwd, "src", "app"))) return "src/app/paykit/api/[...slug]/route.ts";
|
|
69
|
+
if (fs.existsSync(path.join(cwd, "app"))) return "app/paykit/api/[...slug]/route.ts";
|
|
70
|
+
return "src/app/paykit/api/[...slug]/route.ts";
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { defaultConfigPath, defaultRoutePath, detectPackageManager, getInstallCommand, getRunCommand, hasTsconfigPathAlias, isPackageInstalled, resolveImportPath };
|