glyphpay 0.3.0 → 0.4.0

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/README.md CHANGED
@@ -16,7 +16,8 @@ The CLI covers setup, diagnostics, and credential lifecycle:
16
16
  | `glyphpay doctor` | Check env, token, Stripe readiness, webhook URL, and an optional signed test event. |
17
17
  | `glyphpay rotate` | Re-pair the site and replace the credential. |
18
18
  | `glyphpay disconnect` | Revoke the credential and remove the local keys. |
19
- | `glyphpay init` | Scaffold the Next.js App Router routes and env placeholders. |
19
+ | `glyphpay deliveries` | List recent webhook deliveries and retry one with `--retry <id>`. |
20
+ | `glyphpay init` | Scaffold routes for Next.js, Remix, Astro, SvelteKit, Hono, or Express. |
20
21
 
21
22
  ## Connect a site from the CLI
22
23
 
@@ -58,13 +59,31 @@ The test event has type `webhook.test` and a zero-dollar `data` payload. Ignore
58
59
 
59
60
  `rotate` re-runs browser approval and reuses the registered callback URL when the current token is still valid. `disconnect` revokes the credential immediately and removes `GLYPHPAY_TOKEN` and `GLYPHPAY_CALLBACK_SECRET` from the env file; pass `--yes` in scripts or `--keep-env` to leave the local values in place.
60
61
 
61
- ## Scaffold a Next.js site
62
+ ## Inspect and retry webhook deliveries
63
+
64
+ ```sh
65
+ npx glyphpay deliveries
66
+ npx glyphpay deliveries --retry evt_...
67
+ ```
68
+
69
+ `deliveries` lists the most recent callback outbox records for the credential's client and environment: event type, delivery state, attempt count, timestamp, and the last error. A retry sends the exact stored signed event again and persists the result. Retry is one attempt; fix the endpoint first. The admin **Connections** tab exposes the same operations.
70
+
71
+ ## Scaffold a site
62
72
 
63
73
  ```sh
64
74
  npx glyphpay init
65
75
  ```
66
76
 
67
- The command detects `app` or `src/app`, creates `lib/glyphpay.ts` (or `.js`), adds the checkout and webhook route handlers, and appends placeholders to `.env.example` without changing existing values. It refuses to overwrite existing files unless you pass `--force`; use `--dry-run` to preview. Then run `glyphpay connect` and wire `priceOrder()` and `onEvent()` to your order database.
77
+ The command detects the project framework from `package.json`. It creates a server connector plus the framework-native checkout and webhook entry points, and appends placeholders to `.env.example` without changing existing values. Supported targets:
78
+
79
+ - **Next.js App Router** — `app/api/glyphpay/*` or `src/app/api/glyphpay/*` route handlers.
80
+ - **SvelteKit** — `src/routes/api/glyphpay/*/+server.ts` with `src/lib/server/glyphpay.ts`.
81
+ - **Astro** — `src/pages/api/glyphpay/*.ts`; use server/SSR output and an adapter.
82
+ - **Remix** — `app/routes/api.glyphpay.*.ts` with `app/lib/glyphpay.server.ts`.
83
+ - **Hono** — `src/glyphpay-routes.ts`; mount it with `app.route("/api/glyphpay", glyphpayRoutes)`.
84
+ - **Express** — `src/glyphpay-routes.ts`; mount it with `app.use("/api/glyphpay", glyphpayRoutes)`. The webhook route uses `express.raw` so signatures verify against the original bytes.
85
+
86
+ It refuses to overwrite existing files unless you pass `--force`; use `--dry-run` to preview. Then run `glyphpay connect` and wire `priceOrder()` and `onEvent()` to your order database.
68
87
 
69
88
  ## Manual setup: connect the client in Design Glyph admin
70
89
 
@@ -143,7 +162,7 @@ Without React:
143
162
  </script>
144
163
  ```
145
164
 
146
- The route handlers take a standard `Request` and return a `Response`, so the same two lines work in Remix, Astro, Hono, SvelteKit, and Cloudflare Workers. In Express, use `createPayment` and `verifyCallback` directly.
165
+ The route handlers take a standard `Request` and return a `Response`, so the same two lines work in Remix, Astro, Hono, SvelteKit, and Cloudflare Workers. In Express, `glyphpay init` generates a small adapter that preserves the raw webhook body.
147
166
 
148
167
  ## How a payment flows
149
168
 
package/dist/cli.d.ts CHANGED
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * `doctor` verifies the local setup and, optionally, delivers a signed test
11
11
  * webhook. `disconnect` revokes the credential. `rotate` re-pairs and replaces
12
- * it. `init` scaffolds the Next.js App Router integration.
12
+ * it. `init` scaffolds a framework integration. `deliveries` lists callback
13
+ * attempts and replays one on demand.
13
14
  *
14
15
  * Zero runtime dependencies: Node 18+ built-ins and global fetch only.
15
16
  */
@@ -48,6 +49,8 @@ export interface CliOptions {
48
49
  dryRun?: boolean;
49
50
  /** Init: project directory. Defaults to the current directory. */
50
51
  dir?: string;
52
+ /** Deliveries: replay one failed or delivered callback by id. */
53
+ retry?: string;
51
54
  }
52
55
  export interface ConnectResult {
53
56
  businessName: string;
@@ -61,7 +64,28 @@ export interface DisconnectResult {
61
64
  alreadyRevoked: boolean;
62
65
  canceled: boolean;
63
66
  }
64
- type Command = "connect" | "rotate" | "disconnect" | "doctor" | "init" | "help" | "version";
67
+ export interface CallbackDeliveryView {
68
+ id: string;
69
+ paymentId: string;
70
+ type: string;
71
+ state: "pending" | "delivered" | "failed";
72
+ attempts: number;
73
+ lastError: string | null;
74
+ createdAt: string;
75
+ updatedAt: string;
76
+ }
77
+ export interface CallbackDeliveriesResponse {
78
+ businessId: string;
79
+ businessName: string;
80
+ environment: "test" | "live";
81
+ deliveries: CallbackDeliveryView[];
82
+ }
83
+ export interface CallbackReplayResponse {
84
+ outcome: "delivered" | "retry" | "failed";
85
+ detail?: string;
86
+ delivery: CallbackDeliveryView;
87
+ }
88
+ type Command = "connect" | "rotate" | "disconnect" | "doctor" | "init" | "deliveries" | "help" | "version";
65
89
  interface ParsedCommand {
66
90
  command: Command;
67
91
  options: CliOptions;
@@ -107,6 +131,7 @@ export interface DoctorReport {
107
131
  checks: DoctorCheck[];
108
132
  }
109
133
  export declare function runDoctor(options?: CliOptions): Promise<DoctorReport>;
134
+ export declare function runDeliveries(options?: CliOptions): Promise<CallbackDeliveriesResponse | CallbackReplayResponse>;
110
135
  export declare function runInit(options?: CliOptions): Promise<void>;
111
136
  export declare function main(argv?: string[]): Promise<number>;
112
137
  export {};
package/dist/cli.js CHANGED
@@ -9,7 +9,8 @@
9
9
  *
10
10
  * `doctor` verifies the local setup and, optionally, delivers a signed test
11
11
  * webhook. `disconnect` revokes the credential. `rotate` re-pairs and replaces
12
- * it. `init` scaffolds the Next.js App Router integration.
12
+ * it. `init` scaffolds a framework integration. `deliveries` lists callback
13
+ * attempts and replays one on demand.
13
14
  *
14
15
  * Zero runtime dependencies: Node 18+ built-ins and global fetch only.
15
16
  */
@@ -19,7 +20,7 @@ import { access, chmod, mkdir, readFile, stat, writeFile } from "node:fs/promise
19
20
  import { dirname, join, relative, resolve } from "node:path";
20
21
  import { createInterface } from "node:readline/promises";
21
22
  import { pathToFileURL } from "node:url";
22
- const VERSION = "0.3.0";
23
+ const VERSION = "0.4.0";
23
24
  const DEFAULT_BASE_URL = "https://designglyph.web.app";
24
25
  const DEFAULT_ENV_FILE = ".env.local";
25
26
  const DEFAULT_TIMEOUT_SECONDS = 600;
@@ -72,6 +73,8 @@ function commandFor(value) {
72
73
  return "doctor";
73
74
  if (value === "init")
74
75
  return "init";
76
+ if (value === "deliveries" || value === "webhooks")
77
+ return "deliveries";
75
78
  return null;
76
79
  }
77
80
  export function parseArgs(argv) {
@@ -164,6 +167,19 @@ export function parseArgs(argv) {
164
167
  }
165
168
  continue;
166
169
  }
170
+ if (flag === "--retry") {
171
+ if (inlineValue !== undefined) {
172
+ if (!inlineValue)
173
+ throw new CliUsageError("--retry needs a delivery id.");
174
+ options.retry = inlineValue;
175
+ }
176
+ else {
177
+ const [value, next] = takeValue(argv, index, flag);
178
+ options.retry = value;
179
+ index = next;
180
+ }
181
+ continue;
182
+ }
167
183
  if (flag === "--dir") {
168
184
  if (inlineValue !== undefined) {
169
185
  if (!inlineValue)
@@ -202,7 +218,8 @@ Usage
202
218
  glyphpay rotate [options] Re-pair and rotate the current credential
203
219
  glyphpay disconnect [options] Revoke the credential and remove local keys
204
220
  glyphpay doctor [options] Check env, token, Stripe and webhook setup
205
- glyphpay init [options] Scaffold a Next.js App Router integration
221
+ glyphpay deliveries [options] List recent webhook deliveries
222
+ glyphpay init [options] Scaffold an integration for your framework
206
223
  glyphpay --version
207
224
 
208
225
  Connect / rotate
@@ -222,6 +239,10 @@ Disconnect
222
239
  --yes, -y Do not ask for confirmation
223
240
  --keep-env Keep the local env values after revocation
224
241
 
242
+ Deliveries
243
+ --retry <id> Send one stored webhook again (alias: webhooks)
244
+ --json Print a machine-readable response
245
+
225
246
  Init
226
247
  --dir <path> Project directory (default: current directory)
227
248
  --dry-run Show planned files without writing them
@@ -838,6 +859,56 @@ export async function runDoctor(options = {}) {
838
859
  }
839
860
  return report;
840
861
  }
862
+ /* ------------------------------------------------------------------ */
863
+ /* Webhook deliveries */
864
+ /* ------------------------------------------------------------------ */
865
+ function printDelivery(delivery) {
866
+ say(` ${delivery.id} ${delivery.type} ${delivery.state.toUpperCase()} ` +
867
+ `attempt ${delivery.attempts} ${delivery.updatedAt}`);
868
+ if (delivery.lastError)
869
+ say(` ${delivery.lastError}`);
870
+ }
871
+ export async function runDeliveries(options = {}) {
872
+ const baseUrl = baseUrlFor(options);
873
+ const envFile = resolve(options.envFile ?? DEFAULT_ENV_FILE);
874
+ const contents = await readEnvFile(envFile);
875
+ const token = readEnvValue(contents, TOKEN_KEY);
876
+ if (!token) {
877
+ throw new CliUsageError(`No ${TOKEN_KEY} found in ${relative(process.cwd(), envFile) || envFile}. Run glyphpay connect first.`);
878
+ }
879
+ if (options.retry) {
880
+ const result = await api(baseUrl, `/credential/deliveries/${encodeURIComponent(options.retry)}/replay`, {}, token);
881
+ if (options.json) {
882
+ say(JSON.stringify(result, null, 2));
883
+ }
884
+ else {
885
+ say();
886
+ say(style(1, `Webhook ${result.outcome === "delivered" ? "delivered" : "not delivered"}.`));
887
+ printDelivery(result.delivery);
888
+ if (result.detail)
889
+ say(` ${result.detail}`);
890
+ }
891
+ return result;
892
+ }
893
+ const result = await api(baseUrl, "/credential/deliveries", undefined, token);
894
+ if (options.json) {
895
+ say(JSON.stringify(result, null, 2));
896
+ return result;
897
+ }
898
+ say();
899
+ say(style(1, `Webhook deliveries - ${result.businessName} (${result.environment} mode)`));
900
+ say();
901
+ if (result.deliveries.length === 0) {
902
+ say("No deliveries recorded yet.");
903
+ }
904
+ else {
905
+ for (const delivery of result.deliveries)
906
+ printDelivery(delivery);
907
+ say();
908
+ say("Retry one with `glyphpay deliveries --retry <id>`.");
909
+ }
910
+ return result;
911
+ }
841
912
  function libTemplate(ts) {
842
913
  const cartId = ts
843
914
  ? `const cartId = (body as { cartId?: string } | null)?.cartId;`
@@ -866,17 +937,150 @@ export const glyphpay = glyphPay({
866
937
  });
867
938
  `;
868
939
  }
869
- function routeTemplate() {
870
- return `import { glyphpay } from "../../../../lib/glyphpay";
871
-
872
- export const POST = glyphpay.checkout;
873
- `;
940
+ function routeTemplate(libImport) {
941
+ return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.checkout;\n`;
874
942
  }
875
- function webhookRouteTemplate() {
876
- return `import { glyphpay } from "../../../../lib/glyphpay";
877
-
878
- export const POST = glyphpay.webhook;
879
- `;
943
+ function webhookRouteTemplate(libImport) {
944
+ return `import { glyphpay } from "${libImport}";\n\nexport const POST = glyphpay.webhook;\n`;
945
+ }
946
+ function detectFramework(dependencies) {
947
+ if (dependencies.next)
948
+ return "next";
949
+ if (dependencies["@sveltejs/kit"])
950
+ return "sveltekit";
951
+ if (dependencies.astro)
952
+ return "astro";
953
+ if (dependencies["@remix-run/node"] || dependencies["@remix-run/react"])
954
+ return "remix";
955
+ if (dependencies.hono)
956
+ return "hono";
957
+ if (dependencies.express)
958
+ return "express";
959
+ return null;
960
+ }
961
+ function svelteRoute(kind, isTs) {
962
+ const typeImport = isTs ? `import type { RequestHandler } from "./$types";\n\n` : "\n";
963
+ const annotation = isTs ? ": RequestHandler" : "";
964
+ return `import { glyphpay } from "$lib/server/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
965
+ }
966
+ function astroRoute(kind, isTs) {
967
+ const typeImport = isTs ? `import type { APIRoute } from "astro";\n\n` : "\n";
968
+ const annotation = isTs ? ": APIRoute" : "";
969
+ return `import { glyphpay } from "../../../lib/glyphpay";\n${typeImport}export const POST${annotation} = ({ request }) => glyphpay.${kind}(request);\n`;
970
+ }
971
+ function remixRoute(kind, isTs) {
972
+ const typeImport = isTs ? `import type { ActionFunctionArgs } from "@remix-run/node";\n\n` : "\n";
973
+ const signature = isTs ? "{ request }: ActionFunctionArgs" : "{ request }";
974
+ return `import { glyphpay } from "~/lib/glyphpay.server";\n${typeImport}export async function action(${signature}) {\n return glyphpay.${kind}(request);\n}\n`;
975
+ }
976
+ function honoRoutes() {
977
+ return `import { Hono } from "hono";\nimport { glyphpay } from "./lib/glyphpay";\n\nexport const glyphpayRoutes = new Hono();\n\nglyphpayRoutes.post("/checkout", (c) => glyphpay.checkout(c.req.raw));\nglyphpayRoutes.post("/webhook", (c) => glyphpay.webhook(c.req.raw));\n`;
978
+ }
979
+ function expressRoutes(isTs) {
980
+ const typeImport = isTs
981
+ ? `import type { Request as ExpressRequest, Response as ExpressResponse } from "express";\n`
982
+ : "";
983
+ const signature = isTs ? ": ExpressRequest" : "";
984
+ const responseType = isTs ? ": ExpressResponse" : "";
985
+ const handlerType = isTs ? "(request: Request) => Promise<Response>" : "handler";
986
+ return `import express from "express";\nimport { glyphpay } from "./lib/glyphpay";\n${typeImport}\nexport const glyphpayRoutes = express.Router();\n\nasync function adapt(handler${isTs ? `: ${handlerType}` : ""}, req${signature}, res${responseType}) {\n const body =\n req.method === "GET" || req.method === "HEAD"\n ? undefined\n : Buffer.isBuffer(req.body)\n ? req.body\n : typeof req.body === "string"\n ? req.body\n : JSON.stringify(req.body ?? {});\n const protocol = req.protocol || "http";\n const host = req.get("host") || "localhost";\n const request = new Request(\`\${protocol}://\${host}\${req.originalUrl}\`, {\n method: req.method,\n headers: req.headers as unknown as HeadersInit,\n body,\n });\n const response = await handler(request);\n res.status(response.status);\n response.headers.forEach((value, key) => res.setHeader(key, value));\n res.send(Buffer.from(await response.arrayBuffer()));\n}\n\nglyphpayRoutes.post("/checkout", express.json(), (req, res) => adapt(glyphpay.checkout, req, res));\nglyphpayRoutes.post("/webhook", express.raw({ type: "application/json" }), (req, res) => adapt(glyphpay.webhook, req, res));\n`;
987
+ }
988
+ async function frameworkPlan(projectRoot, framework) {
989
+ const isTs = await fileExists(join(projectRoot, "tsconfig.json"));
990
+ const extension = isTs ? "ts" : "js";
991
+ const envComment = "# Glyph Pay — server-side only. Keep these in the server environment.\n";
992
+ if (framework === "next") {
993
+ const hasSrcApp = await directoryExists(join(projectRoot, "src", "app"));
994
+ const hasRootApp = await directoryExists(join(projectRoot, "app"));
995
+ const hasPages = (await directoryExists(join(projectRoot, "pages"))) ||
996
+ (await directoryExists(join(projectRoot, "src", "pages")));
997
+ if (!hasSrcApp && !hasRootApp && hasPages) {
998
+ throw new CliUsageError("This project uses the Next.js Pages Router. glyphpay init supports the App Router; see the README for the two route files.");
999
+ }
1000
+ const appDir = hasSrcApp ? join("src", "app") : join("app");
1001
+ const libDir = hasSrcApp ? join("src", "lib") : join("lib");
1002
+ const importPath = "../../../../lib/glyphpay";
1003
+ return {
1004
+ framework,
1005
+ libPath: join(projectRoot, libDir, `glyphpay.${extension}`),
1006
+ files: [
1007
+ { path: join(projectRoot, libDir, `glyphpay.${extension}`), contents: libTemplate(isTs) },
1008
+ { path: join(projectRoot, appDir, "api", "glyphpay", "checkout", `route.${extension}`), contents: routeTemplate(importPath) },
1009
+ { path: join(projectRoot, appDir, "api", "glyphpay", "webhook", `route.${extension}`), contents: webhookRouteTemplate(importPath) },
1010
+ ],
1011
+ envComment,
1012
+ notes: ["The generated routes live at /api/glyphpay/checkout and /api/glyphpay/webhook."],
1013
+ };
1014
+ }
1015
+ if (framework === "sveltekit") {
1016
+ const libPath = join(projectRoot, "src", "lib", "server", `glyphpay.${extension}`);
1017
+ return {
1018
+ framework,
1019
+ libPath,
1020
+ files: [
1021
+ { path: libPath, contents: libTemplate(isTs) },
1022
+ { path: join(projectRoot, "src", "routes", "api", "glyphpay", "checkout", `+server.${extension}`), contents: svelteRoute("checkout", isTs) },
1023
+ { path: join(projectRoot, "src", "routes", "api", "glyphpay", "webhook", `+server.${extension}`), contents: svelteRoute("webhook", isTs) },
1024
+ ],
1025
+ envComment,
1026
+ notes: ["Use adapter-node or another server adapter; static SvelteKit builds cannot run checkout."],
1027
+ };
1028
+ }
1029
+ if (framework === "astro") {
1030
+ const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
1031
+ return {
1032
+ framework,
1033
+ libPath,
1034
+ files: [
1035
+ { path: libPath, contents: libTemplate(isTs) },
1036
+ { path: join(projectRoot, "src", "pages", "api", "glyphpay", `checkout.${extension}`), contents: astroRoute("checkout", isTs) },
1037
+ { path: join(projectRoot, "src", "pages", "api", "glyphpay", `webhook.${extension}`), contents: astroRoute("webhook", isTs) },
1038
+ ],
1039
+ envComment,
1040
+ notes: ["Set output to server/SSR in astro.config and install a server adapter before deploying."],
1041
+ };
1042
+ }
1043
+ if (framework === "remix") {
1044
+ const libPath = join(projectRoot, "app", "lib", `glyphpay.server.${extension}`);
1045
+ return {
1046
+ framework,
1047
+ libPath,
1048
+ files: [
1049
+ { path: libPath, contents: libTemplate(isTs) },
1050
+ { path: join(projectRoot, "app", "routes", `api.glyphpay.checkout.${extension}`), contents: remixRoute("checkout", isTs) },
1051
+ { path: join(projectRoot, "app", "routes", `api.glyphpay.webhook.${extension}`), contents: remixRoute("webhook", isTs) },
1052
+ ],
1053
+ envComment,
1054
+ notes: ["Routes assume the standard ~ alias; adjust the import if your tsconfig uses another alias."],
1055
+ };
1056
+ }
1057
+ if (framework === "hono") {
1058
+ const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
1059
+ return {
1060
+ framework,
1061
+ libPath,
1062
+ files: [
1063
+ { path: libPath, contents: libTemplate(isTs) },
1064
+ { path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: honoRoutes() },
1065
+ ],
1066
+ envComment,
1067
+ notes: ["Mount the router in your app: app.route(\"/api/glyphpay\", glyphpayRoutes)."],
1068
+ };
1069
+ }
1070
+ const libPath = join(projectRoot, "src", "lib", `glyphpay.${extension}`);
1071
+ return {
1072
+ framework: "express",
1073
+ libPath,
1074
+ files: [
1075
+ { path: libPath, contents: libTemplate(isTs) },
1076
+ { path: join(projectRoot, "src", `glyphpay-routes.${extension}`), contents: expressRoutes(isTs) },
1077
+ ],
1078
+ envComment,
1079
+ notes: [
1080
+ "Mount the router with app.use(\"/api/glyphpay\", glyphpayRoutes).",
1081
+ "The webhook route uses express.raw so the signature is verified against the original bytes.",
1082
+ ],
1083
+ };
880
1084
  }
881
1085
  function installCommand(hasPnpmLock, hasYarnLock, hasBunLock) {
882
1086
  if (hasPnpmLock)
@@ -891,7 +1095,7 @@ export async function runInit(options = {}) {
891
1095
  const projectRoot = resolve(options.dir ?? process.cwd());
892
1096
  const packagePath = join(projectRoot, "package.json");
893
1097
  if (!(await fileExists(packagePath))) {
894
- throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a Next.js project.`);
1098
+ throw new CliUsageError(`No package.json found in ${projectRoot}. Run init in a web project.`);
895
1099
  }
896
1100
  let pkg;
897
1101
  try {
@@ -901,29 +1105,13 @@ export async function runInit(options = {}) {
901
1105
  throw new CliUsageError("package.json is not valid JSON.");
902
1106
  }
903
1107
  const dependencies = { ...(pkg.dependencies ?? {}), ...(pkg.devDependencies ?? {}) };
904
- if (!dependencies.next) {
905
- throw new CliUsageError("glyphpay init currently supports Next.js projects only.");
906
- }
907
- const hasSrcApp = await directoryExists(join(projectRoot, "src", "app"));
908
- const hasRootApp = await directoryExists(join(projectRoot, "app"));
909
- const hasPages = (await directoryExists(join(projectRoot, "pages"))) ||
910
- (await directoryExists(join(projectRoot, "src", "pages")));
911
- if (!hasSrcApp && !hasRootApp && hasPages) {
912
- throw new CliUsageError("This project uses the Next.js Pages Router. glyphpay init supports the App Router; see the README for the two route files.");
913
- }
914
- const appDir = hasSrcApp ? join("src", "app") : join("app");
915
- const libDir = hasSrcApp ? join("src", "lib") : join("lib");
916
- const isTs = await fileExists(join(projectRoot, "tsconfig.json"));
917
- const extension = isTs ? "ts" : "js";
918
- const libPath = join(projectRoot, libDir, `glyphpay.${extension}`);
919
- const checkoutPath = join(projectRoot, appDir, "api", "glyphpay", "checkout", `route.${extension}`);
920
- const webhookPath = join(projectRoot, appDir, "api", "glyphpay", "webhook", `route.${extension}`);
1108
+ const framework = detectFramework(dependencies);
1109
+ if (!framework) {
1110
+ throw new CliUsageError("glyphpay init supports Next.js, Remix, Astro, SvelteKit, Express, and Hono. Install one of those frameworks first.");
1111
+ }
1112
+ const plan = await frameworkPlan(projectRoot, framework);
921
1113
  const envPath = join(projectRoot, ".env.example");
922
- const generated = [
923
- { path: libPath, contents: libTemplate(isTs) },
924
- { path: checkoutPath, contents: routeTemplate() },
925
- { path: webhookPath, contents: webhookRouteTemplate() },
926
- ];
1114
+ const generated = plan.files;
927
1115
  const existing = [];
928
1116
  for (const file of generated) {
929
1117
  if (await fileExists(file.path))
@@ -933,9 +1121,7 @@ export async function runInit(options = {}) {
933
1121
  throw new CliUsageError(`Refusing to overwrite existing files:\n ${existing.join("\n ")}\nRe-run with --force to overwrite them.`);
934
1122
  }
935
1123
  const envExisting = await readEnvFile(envPath);
936
- const envBase = envExisting.trim().length > 0
937
- ? envExisting
938
- : "# Glyph Pay — server-side only. Never prefix these with NEXT_PUBLIC_.\n";
1124
+ const envBase = envExisting.trim().length > 0 ? envExisting : plan.envComment;
939
1125
  const envNext = ensureEnvKeys(envBase, [TOKEN_KEY, CALLBACK_SECRET_KEY]);
940
1126
  const envChanged = envNext !== envExisting;
941
1127
  const display = (path) => relative(projectRoot, path).replace(/\\/g, "/");
@@ -949,6 +1135,7 @@ export async function runInit(options = {}) {
949
1135
  }
950
1136
  say();
951
1137
  say(style(1, "Glyph Pay init"));
1138
+ say(`Framework: ${plan.framework}`);
952
1139
  say();
953
1140
  for (const file of generated) {
954
1141
  const verb = existing.includes(display(file.path)) ? "Updated" : options.dryRun ? "Would create" : "Created";
@@ -962,7 +1149,9 @@ export async function runInit(options = {}) {
962
1149
  say("Next steps:");
963
1150
  say(` 1. ${installCommand(pnpm, yarn, bun)}`);
964
1151
  say(" 2. npx glyphpay connect --url https://your-site.com");
965
- say(` 3. Wire priceOrder() and onEvent() in ${display(libPath)}.`);
1152
+ say(` 3. Wire priceOrder() and onEvent() in ${display(plan.libPath)}.`);
1153
+ for (const note of plan.notes)
1154
+ say(` ${note}`);
966
1155
  if (options.dryRun) {
967
1156
  say();
968
1157
  say("Dry run: no files were written.");
@@ -990,6 +1179,10 @@ export async function main(argv = process.argv.slice(2)) {
990
1179
  await runInit(parsed.options);
991
1180
  return 0;
992
1181
  }
1182
+ if (parsed.command === "deliveries") {
1183
+ const result = await runDeliveries(parsed.options);
1184
+ return "outcome" in result && result.outcome !== "delivered" ? 1 : 0;
1185
+ }
993
1186
  if (parsed.command === "disconnect") {
994
1187
  await runDisconnect(parsed.options);
995
1188
  return 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glyphpay",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "Drop-in GlyphPay checkout for websites, with a CLI for setup, diagnostics, and credential lifecycle.",
5
5
  "homepage": "https://glyphpay.net",
6
6
  "keywords": [