@klingis/cli 0.1.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/dist/commands/init.d.ts +1 -0
- package/dist/commands/init.js +107 -0
- package/dist/commands/login.d.ts +4 -0
- package/dist/commands/login.js +67 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +11 -0
- package/dist/commands/status.d.ts +1 -0
- package/dist/commands/status.js +17 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +31 -0
- package/dist/lib/api.d.ts +40 -0
- package/dist/lib/api.js +47 -0
- package/dist/lib/config.d.ts +16 -0
- package/dist/lib/config.js +73 -0
- package/dist/lib/detect.d.ts +7 -0
- package/dist/lib/detect.js +81 -0
- package/dist/templates/index.d.ts +6 -0
- package/dist/templates/index.js +309 -0
- package/package.json +34 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function initCommand(): Promise<void>;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import prompts from "prompts";
|
|
4
|
+
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
5
|
+
import { dirname, join } from "node:path";
|
|
6
|
+
import { loginCommand } from "./login.js";
|
|
7
|
+
import { loadUserConfig, writeEnvLocal, ensureGitignore, } from "../lib/config.js";
|
|
8
|
+
import { detectFramework, detectPackageManager, frameworkLabel, installCommand, } from "../lib/detect.js";
|
|
9
|
+
import { getTemplateFiles } from "../templates/index.js";
|
|
10
|
+
const LOGO = `
|
|
11
|
+
${chalk.hex("#A56CFE").bold("██╗ ██╗██╗ ██╗███╗ ██╗ ██████╗")}
|
|
12
|
+
${chalk.hex("#A56CFE").bold("██║ ██╔╝██║ ██║████╗ ██║██╔════╝")}
|
|
13
|
+
${chalk.hex("#A56CFE").bold("█████╔╝ ██║ ██║██╔██╗ ██║██║ ███╗")}
|
|
14
|
+
${chalk.hex("#A56CFE").bold("██╔═██╗ ██║ ██║██║╚██╗██║██║ ██║")}
|
|
15
|
+
${chalk.hex("#A56CFE").bold("██║ ██╗███████╗██║██║ ╚████║╚██████╔╝")}
|
|
16
|
+
${chalk.hex("#A56CFE").bold("╚═╝ ╚═╝╚══════╝╚═╝╚═╝ ╚═══╝ ╚═════╝")}
|
|
17
|
+
`;
|
|
18
|
+
export async function initCommand() {
|
|
19
|
+
const startTime = Date.now();
|
|
20
|
+
console.log(LOGO);
|
|
21
|
+
console.log(chalk.dim(" Payments for Iceland\n"));
|
|
22
|
+
// Step 1: Authenticate
|
|
23
|
+
let config = loadUserConfig();
|
|
24
|
+
if (config) {
|
|
25
|
+
console.log(chalk.dim(` Already connected as merchant ${config.merchant_id.slice(0, 8)}...`));
|
|
26
|
+
console.log(chalk.dim(` Using API key: ${config.api_key.slice(0, 16)}...\n`));
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
config = await loginCommand({});
|
|
30
|
+
if (!config) {
|
|
31
|
+
console.log(chalk.red("\n Could not connect. Run `kling login` to try again.\n"));
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
// Step 2: Detect environment
|
|
36
|
+
const framework = detectFramework();
|
|
37
|
+
const pm = detectPackageManager();
|
|
38
|
+
if (framework !== "unknown") {
|
|
39
|
+
console.log(chalk.dim(` Detected: ${frameworkLabel(framework)} project (${pm})\n`));
|
|
40
|
+
}
|
|
41
|
+
// Step 3: Choose what to build
|
|
42
|
+
const { useCase } = await prompts({
|
|
43
|
+
type: "select",
|
|
44
|
+
name: "useCase",
|
|
45
|
+
message: "What are you building?",
|
|
46
|
+
choices: [
|
|
47
|
+
{ title: "One-time payments (checkout)", value: "checkout" },
|
|
48
|
+
{ title: "Subscription billing", value: "subscriptions" },
|
|
49
|
+
{ title: "Payment links", value: "payment-links" },
|
|
50
|
+
{ title: "Just give me the API key", value: "key-only" },
|
|
51
|
+
],
|
|
52
|
+
});
|
|
53
|
+
if (!useCase) {
|
|
54
|
+
// User cancelled
|
|
55
|
+
process.exit(0);
|
|
56
|
+
}
|
|
57
|
+
// Step 4: Write .env.local
|
|
58
|
+
const envSpinner = ora("Writing .env.local...").start();
|
|
59
|
+
writeEnvLocal(config.api_key, config.api_url);
|
|
60
|
+
ensureGitignore();
|
|
61
|
+
envSpinner.succeed("Written .env.local");
|
|
62
|
+
if (useCase === "key-only") {
|
|
63
|
+
printSummary(config.api_key, startTime);
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
// Step 5: Suggest SDK install
|
|
67
|
+
console.log();
|
|
68
|
+
console.log(chalk.dim(` To install the Kling SDK:\n ${chalk.white(installCommand(pm, "@klingis/sdk"))}\n`));
|
|
69
|
+
// Step 6: Generate starter code
|
|
70
|
+
const templateFiles = getTemplateFiles(framework, config.api_key);
|
|
71
|
+
if (templateFiles.length > 0) {
|
|
72
|
+
const fileSpinner = ora("Creating starter files...").start();
|
|
73
|
+
const created = [];
|
|
74
|
+
for (const file of templateFiles) {
|
|
75
|
+
const fullPath = join(process.cwd(), file.path);
|
|
76
|
+
// Don't overwrite existing files
|
|
77
|
+
if (existsSync(fullPath)) {
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
mkdirSync(dirname(fullPath), { recursive: true });
|
|
81
|
+
writeFileSync(fullPath, file.content, "utf-8");
|
|
82
|
+
created.push(file.path);
|
|
83
|
+
}
|
|
84
|
+
if (created.length > 0) {
|
|
85
|
+
fileSpinner.succeed(`Created ${created.length} file${created.length > 1 ? "s" : ""}`);
|
|
86
|
+
console.log();
|
|
87
|
+
for (const f of created) {
|
|
88
|
+
console.log(chalk.dim(` ${f}`));
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
fileSpinner.info("All template files already exist, skipping");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
printSummary(config.api_key, startTime);
|
|
96
|
+
}
|
|
97
|
+
function printSummary(apiKey, startTime) {
|
|
98
|
+
const elapsed = ((Date.now() - startTime) / 1000).toFixed(0);
|
|
99
|
+
console.log();
|
|
100
|
+
console.log(chalk.bold(" Next steps:"));
|
|
101
|
+
console.log(chalk.dim(" 1. Check .env.local for your API key"));
|
|
102
|
+
console.log(chalk.dim(" 2. Read the docs: https://kling.is/docs"));
|
|
103
|
+
console.log(chalk.dim(" 3. Visit your dashboard: https://kling.is/dashboard"));
|
|
104
|
+
console.log();
|
|
105
|
+
console.log(chalk.hex("#A56CFE")(` ⚡ Setup completed in ${elapsed} seconds`));
|
|
106
|
+
console.log();
|
|
107
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import ora from "ora";
|
|
3
|
+
import open from "open";
|
|
4
|
+
import { startDeviceAuth, waitForAuth } from "../lib/api.js";
|
|
5
|
+
import { saveUserConfig, getApiUrl } from "../lib/config.js";
|
|
6
|
+
const VERSION = "0.1.0";
|
|
7
|
+
export async function loginCommand(options) {
|
|
8
|
+
// Direct API key mode (for CI/CD)
|
|
9
|
+
if (options.apiKey) {
|
|
10
|
+
const config = {
|
|
11
|
+
api_key: options.apiKey,
|
|
12
|
+
merchant_id: "",
|
|
13
|
+
merchant_name: "",
|
|
14
|
+
user_id: "",
|
|
15
|
+
api_url: getApiUrl(),
|
|
16
|
+
authorized_at: new Date().toISOString(),
|
|
17
|
+
};
|
|
18
|
+
saveUserConfig(config);
|
|
19
|
+
console.log(chalk.green(" API key saved."));
|
|
20
|
+
return config;
|
|
21
|
+
}
|
|
22
|
+
// Device auth flow
|
|
23
|
+
console.log();
|
|
24
|
+
const spinner = ora("Starting device authorization...").start();
|
|
25
|
+
let auth;
|
|
26
|
+
try {
|
|
27
|
+
auth = await startDeviceAuth(`Kling CLI v${VERSION}`);
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
spinner.fail(`Failed to connect: ${err.message}`);
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
spinner.stop();
|
|
34
|
+
console.log(chalk.bold(" Your pairing code is: ") +
|
|
35
|
+
chalk.bgWhite.black.bold(` ${auth.user_code} `));
|
|
36
|
+
console.log(chalk.dim(" Verify this code matches in your browser.\n"));
|
|
37
|
+
// Open browser
|
|
38
|
+
try {
|
|
39
|
+
await open(auth.verification_url);
|
|
40
|
+
console.log(chalk.dim(` Opened ${auth.verification_url}`));
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
console.log(chalk.yellow(` Open this URL in your browser:\n ${auth.verification_url}`));
|
|
44
|
+
}
|
|
45
|
+
console.log();
|
|
46
|
+
const pollSpinner = ora("Waiting for confirmation...").start();
|
|
47
|
+
const result = await waitForAuth(auth.device_code, auth.poll_interval, auth.expires_in);
|
|
48
|
+
if (!result) {
|
|
49
|
+
pollSpinner.fail("Authorization was denied or expired.");
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
pollSpinner.succeed("Connected!");
|
|
53
|
+
const config = {
|
|
54
|
+
api_key: result.apiKey,
|
|
55
|
+
merchant_id: result.merchantId,
|
|
56
|
+
merchant_name: "",
|
|
57
|
+
user_id: result.userId,
|
|
58
|
+
api_url: getApiUrl(),
|
|
59
|
+
authorized_at: new Date().toISOString(),
|
|
60
|
+
};
|
|
61
|
+
saveUserConfig(config);
|
|
62
|
+
console.log();
|
|
63
|
+
console.log(chalk.dim(` API Key: ${result.apiKey.slice(0, 16)}...`));
|
|
64
|
+
console.log(chalk.dim(` Merchant: ${result.merchantId}`));
|
|
65
|
+
console.log();
|
|
66
|
+
return config;
|
|
67
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function logoutCommand(): Promise<void>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { clearUserConfig } from "../lib/config.js";
|
|
3
|
+
export async function logoutCommand() {
|
|
4
|
+
const cleared = clearUserConfig();
|
|
5
|
+
if (cleared) {
|
|
6
|
+
console.log(chalk.green("\n Logged out. Credentials removed.\n"));
|
|
7
|
+
}
|
|
8
|
+
else {
|
|
9
|
+
console.log(chalk.dim("\n Not logged in.\n"));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function statusCommand(): Promise<void>;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { loadUserConfig } from "../lib/config.js";
|
|
3
|
+
export async function statusCommand() {
|
|
4
|
+
const config = loadUserConfig();
|
|
5
|
+
if (!config) {
|
|
6
|
+
console.log(chalk.yellow("\n Not connected. Run `kling login` to authenticate.\n"));
|
|
7
|
+
return;
|
|
8
|
+
}
|
|
9
|
+
console.log();
|
|
10
|
+
console.log(chalk.bold(" Kling CLI Status"));
|
|
11
|
+
console.log();
|
|
12
|
+
console.log(` API Key: ${chalk.dim(config.api_key.slice(0, 16) + "...")}`);
|
|
13
|
+
console.log(` Merchant: ${chalk.dim(config.merchant_id)}`);
|
|
14
|
+
console.log(` API URL: ${chalk.dim(config.api_url)}`);
|
|
15
|
+
console.log(` Authorized: ${chalk.dim(config.authorized_at)}`);
|
|
16
|
+
console.log();
|
|
17
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { initCommand } from "./commands/init.js";
|
|
4
|
+
import { loginCommand } from "./commands/login.js";
|
|
5
|
+
import { logoutCommand } from "./commands/logout.js";
|
|
6
|
+
import { statusCommand } from "./commands/status.js";
|
|
7
|
+
const program = new Command();
|
|
8
|
+
program
|
|
9
|
+
.name("kling")
|
|
10
|
+
.description("Kling payments CLI — set up your integration from the terminal")
|
|
11
|
+
.version("0.1.0");
|
|
12
|
+
program
|
|
13
|
+
.command("init")
|
|
14
|
+
.description("Set up Kling in your project (login + scaffold)")
|
|
15
|
+
.action(initCommand);
|
|
16
|
+
program
|
|
17
|
+
.command("login")
|
|
18
|
+
.description("Authenticate with your Kling account")
|
|
19
|
+
.option("--api-key <key>", "Use an API key directly (for CI/CD)")
|
|
20
|
+
.action(async (options) => {
|
|
21
|
+
await loginCommand(options);
|
|
22
|
+
});
|
|
23
|
+
program
|
|
24
|
+
.command("logout")
|
|
25
|
+
.description("Clear stored credentials")
|
|
26
|
+
.action(logoutCommand);
|
|
27
|
+
program
|
|
28
|
+
.command("status")
|
|
29
|
+
.description("Show current authentication state")
|
|
30
|
+
.action(statusCommand);
|
|
31
|
+
program.parse();
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
interface DeviceAuthResponse {
|
|
2
|
+
device_code: string;
|
|
3
|
+
user_code: string;
|
|
4
|
+
verification_url: string;
|
|
5
|
+
expires_in: number;
|
|
6
|
+
poll_interval: number;
|
|
7
|
+
}
|
|
8
|
+
interface TokenPendingResponse {
|
|
9
|
+
status: "pending";
|
|
10
|
+
}
|
|
11
|
+
interface TokenAuthorizedResponse {
|
|
12
|
+
status: "authorized";
|
|
13
|
+
api_key: string;
|
|
14
|
+
merchant: {
|
|
15
|
+
id: string;
|
|
16
|
+
};
|
|
17
|
+
user: {
|
|
18
|
+
id: string;
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
interface TokenExpiredResponse {
|
|
22
|
+
status: "expired";
|
|
23
|
+
}
|
|
24
|
+
interface TokenDeniedResponse {
|
|
25
|
+
status: "denied";
|
|
26
|
+
}
|
|
27
|
+
type TokenResponse = TokenPendingResponse | TokenAuthorizedResponse | TokenExpiredResponse | TokenDeniedResponse;
|
|
28
|
+
export declare function startDeviceAuth(clientName: string): Promise<DeviceAuthResponse>;
|
|
29
|
+
export declare function pollDeviceToken(deviceCode: string): Promise<TokenResponse>;
|
|
30
|
+
export interface AuthResult {
|
|
31
|
+
apiKey: string;
|
|
32
|
+
merchantId: string;
|
|
33
|
+
userId: string;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Poll until the device is authorized, denied, or expired.
|
|
37
|
+
* Returns the auth result on success, null on denial/expiry.
|
|
38
|
+
*/
|
|
39
|
+
export declare function waitForAuth(deviceCode: string, pollInterval: number, expiresIn: number): Promise<AuthResult | null>;
|
|
40
|
+
export {};
|
package/dist/lib/api.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { getApiUrl } from "./config.js";
|
|
2
|
+
export async function startDeviceAuth(clientName) {
|
|
3
|
+
const res = await fetch(`${getApiUrl()}/v1/device/authorize`, {
|
|
4
|
+
method: "POST",
|
|
5
|
+
headers: { "Content-Type": "application/json" },
|
|
6
|
+
body: JSON.stringify({ client_name: clientName }),
|
|
7
|
+
});
|
|
8
|
+
if (!res.ok) {
|
|
9
|
+
const body = await res.text();
|
|
10
|
+
throw new Error(`Failed to start device auth: ${res.status} ${body}`);
|
|
11
|
+
}
|
|
12
|
+
return res.json();
|
|
13
|
+
}
|
|
14
|
+
export async function pollDeviceToken(deviceCode) {
|
|
15
|
+
const res = await fetch(`${getApiUrl()}/v1/device/token?device_code=${encodeURIComponent(deviceCode)}`);
|
|
16
|
+
if (!res.ok) {
|
|
17
|
+
const body = await res.text();
|
|
18
|
+
throw new Error(`Failed to poll device token: ${res.status} ${body}`);
|
|
19
|
+
}
|
|
20
|
+
return res.json();
|
|
21
|
+
}
|
|
22
|
+
function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Poll until the device is authorized, denied, or expired.
|
|
27
|
+
* Returns the auth result on success, null on denial/expiry.
|
|
28
|
+
*/
|
|
29
|
+
export async function waitForAuth(deviceCode, pollInterval, expiresIn) {
|
|
30
|
+
const deadline = Date.now() + expiresIn * 1000;
|
|
31
|
+
while (Date.now() < deadline) {
|
|
32
|
+
await sleep(pollInterval * 1000);
|
|
33
|
+
const result = await pollDeviceToken(deviceCode);
|
|
34
|
+
if (result.status === "authorized") {
|
|
35
|
+
return {
|
|
36
|
+
apiKey: result.api_key,
|
|
37
|
+
merchantId: result.merchant.id,
|
|
38
|
+
userId: result.user.id,
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
if (result.status === "denied" || result.status === "expired") {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
// Still pending — continue polling
|
|
45
|
+
}
|
|
46
|
+
return null; // Timed out
|
|
47
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface UserConfig {
|
|
2
|
+
api_key: string;
|
|
3
|
+
merchant_id: string;
|
|
4
|
+
merchant_name: string;
|
|
5
|
+
user_id: string;
|
|
6
|
+
user_name?: string;
|
|
7
|
+
user_email?: string;
|
|
8
|
+
api_url: string;
|
|
9
|
+
authorized_at: string;
|
|
10
|
+
}
|
|
11
|
+
export declare function loadUserConfig(): UserConfig | null;
|
|
12
|
+
export declare function saveUserConfig(config: UserConfig): void;
|
|
13
|
+
export declare function clearUserConfig(): boolean;
|
|
14
|
+
export declare function writeEnvLocal(apiKey: string, apiUrl: string): void;
|
|
15
|
+
export declare function ensureGitignore(): void;
|
|
16
|
+
export declare function getApiUrl(): string;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, unlinkSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
function getConfigDir() {
|
|
5
|
+
return join(homedir(), ".config", "kling");
|
|
6
|
+
}
|
|
7
|
+
function getConfigPath() {
|
|
8
|
+
return join(getConfigDir(), "config.json");
|
|
9
|
+
}
|
|
10
|
+
export function loadUserConfig() {
|
|
11
|
+
const path = getConfigPath();
|
|
12
|
+
if (!existsSync(path))
|
|
13
|
+
return null;
|
|
14
|
+
try {
|
|
15
|
+
return JSON.parse(readFileSync(path, "utf-8"));
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function saveUserConfig(config) {
|
|
22
|
+
const dir = getConfigDir();
|
|
23
|
+
mkdirSync(dir, { recursive: true });
|
|
24
|
+
writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
25
|
+
}
|
|
26
|
+
export function clearUserConfig() {
|
|
27
|
+
const path = getConfigPath();
|
|
28
|
+
if (!existsSync(path))
|
|
29
|
+
return false;
|
|
30
|
+
unlinkSync(path);
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
// Project-level env (.env.local)
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
export function writeEnvLocal(apiKey, apiUrl) {
|
|
37
|
+
const envPath = join(process.cwd(), ".env.local");
|
|
38
|
+
const lines = [
|
|
39
|
+
`# Kling API — generated by kling init`,
|
|
40
|
+
`KLING_SECRET_KEY=${apiKey}`,
|
|
41
|
+
`KLING_API_URL=${apiUrl}`,
|
|
42
|
+
"",
|
|
43
|
+
];
|
|
44
|
+
// If file exists, merge rather than overwrite
|
|
45
|
+
if (existsSync(envPath)) {
|
|
46
|
+
const existing = readFileSync(envPath, "utf-8");
|
|
47
|
+
const existingLines = existing.split("\n");
|
|
48
|
+
// Remove any existing Kling keys
|
|
49
|
+
const filtered = existingLines.filter((l) => !l.startsWith("KLING_SECRET_KEY=") && !l.startsWith("KLING_API_URL=") && !l.startsWith("# Kling API"));
|
|
50
|
+
// Append our lines
|
|
51
|
+
const merged = [...filtered.filter((l) => l.trim() !== ""), "", ...lines];
|
|
52
|
+
writeFileSync(envPath, merged.join("\n"), "utf-8");
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
writeFileSync(envPath, lines.join("\n"), "utf-8");
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export function ensureGitignore() {
|
|
59
|
+
const gitignorePath = join(process.cwd(), ".gitignore");
|
|
60
|
+
if (!existsSync(gitignorePath))
|
|
61
|
+
return;
|
|
62
|
+
const content = readFileSync(gitignorePath, "utf-8");
|
|
63
|
+
if (content.includes(".env.local"))
|
|
64
|
+
return;
|
|
65
|
+
writeFileSync(gitignorePath, content.trimEnd() + "\n.env.local\n", "utf-8");
|
|
66
|
+
}
|
|
67
|
+
// ---------------------------------------------------------------------------
|
|
68
|
+
// API URL resolution
|
|
69
|
+
// ---------------------------------------------------------------------------
|
|
70
|
+
const DEFAULT_API_URL = "https://api.kling.is";
|
|
71
|
+
export function getApiUrl() {
|
|
72
|
+
return process.env.KLING_API_URL || DEFAULT_API_URL;
|
|
73
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Framework = "nextjs" | "remix" | "astro" | "express" | "elysia" | "hono" | "html" | "unknown";
|
|
2
|
+
export type PackageManager = "bun" | "npm" | "yarn" | "pnpm";
|
|
3
|
+
export declare function detectPackageManager(): PackageManager;
|
|
4
|
+
export declare function detectFramework(): Framework;
|
|
5
|
+
export declare function frameworkLabel(fw: Framework): string;
|
|
6
|
+
export declare function installCommand(pm: PackageManager, pkg: string): string;
|
|
7
|
+
export declare function runCommand(pm: PackageManager, script: string): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
export function detectPackageManager() {
|
|
4
|
+
const cwd = process.cwd();
|
|
5
|
+
if (existsSync(join(cwd, "bun.lock")) || existsSync(join(cwd, "bun.lockb")))
|
|
6
|
+
return "bun";
|
|
7
|
+
if (existsSync(join(cwd, "pnpm-lock.yaml")))
|
|
8
|
+
return "pnpm";
|
|
9
|
+
if (existsSync(join(cwd, "yarn.lock")))
|
|
10
|
+
return "yarn";
|
|
11
|
+
return "npm";
|
|
12
|
+
}
|
|
13
|
+
export function detectFramework() {
|
|
14
|
+
const cwd = process.cwd();
|
|
15
|
+
const pkgPath = join(cwd, "package.json");
|
|
16
|
+
if (!existsSync(pkgPath)) {
|
|
17
|
+
if (existsSync(join(cwd, "index.html")))
|
|
18
|
+
return "html";
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
try {
|
|
22
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
23
|
+
const allDeps = {
|
|
24
|
+
...pkg.dependencies,
|
|
25
|
+
...pkg.devDependencies,
|
|
26
|
+
};
|
|
27
|
+
if (allDeps["next"])
|
|
28
|
+
return "nextjs";
|
|
29
|
+
if (allDeps["@remix-run/node"] || allDeps["@remix-run/react"])
|
|
30
|
+
return "remix";
|
|
31
|
+
if (allDeps["astro"])
|
|
32
|
+
return "astro";
|
|
33
|
+
if (allDeps["elysia"])
|
|
34
|
+
return "elysia";
|
|
35
|
+
if (allDeps["hono"])
|
|
36
|
+
return "hono";
|
|
37
|
+
if (allDeps["express"])
|
|
38
|
+
return "express";
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
// ignore parse errors
|
|
42
|
+
}
|
|
43
|
+
return "unknown";
|
|
44
|
+
}
|
|
45
|
+
export function frameworkLabel(fw) {
|
|
46
|
+
const labels = {
|
|
47
|
+
nextjs: "Next.js",
|
|
48
|
+
remix: "Remix",
|
|
49
|
+
astro: "Astro",
|
|
50
|
+
express: "Express",
|
|
51
|
+
elysia: "Elysia",
|
|
52
|
+
hono: "Hono",
|
|
53
|
+
html: "Plain HTML",
|
|
54
|
+
unknown: "Generic",
|
|
55
|
+
};
|
|
56
|
+
return labels[fw];
|
|
57
|
+
}
|
|
58
|
+
export function installCommand(pm, pkg) {
|
|
59
|
+
switch (pm) {
|
|
60
|
+
case "bun":
|
|
61
|
+
return `bun add ${pkg}`;
|
|
62
|
+
case "pnpm":
|
|
63
|
+
return `pnpm add ${pkg}`;
|
|
64
|
+
case "yarn":
|
|
65
|
+
return `yarn add ${pkg}`;
|
|
66
|
+
default:
|
|
67
|
+
return `npm install ${pkg}`;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export function runCommand(pm, script) {
|
|
71
|
+
switch (pm) {
|
|
72
|
+
case "bun":
|
|
73
|
+
return `bun run ${script}`;
|
|
74
|
+
case "pnpm":
|
|
75
|
+
return `pnpm ${script}`;
|
|
76
|
+
case "yarn":
|
|
77
|
+
return `yarn ${script}`;
|
|
78
|
+
default:
|
|
79
|
+
return `npm run ${script}`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
export function getTemplateFiles(framework, apiKey) {
|
|
2
|
+
switch (framework) {
|
|
3
|
+
case "nextjs":
|
|
4
|
+
return nextjsTemplate(apiKey);
|
|
5
|
+
case "elysia":
|
|
6
|
+
case "hono":
|
|
7
|
+
case "express":
|
|
8
|
+
return serverTemplate(apiKey, framework);
|
|
9
|
+
case "astro":
|
|
10
|
+
return astroTemplate(apiKey);
|
|
11
|
+
default:
|
|
12
|
+
return genericTemplate(apiKey);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
function nextjsTemplate(apiKey) {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
path: "app/api/checkout/route.ts",
|
|
19
|
+
content: `// Kling Checkout — server-side session creation
|
|
20
|
+
// Docs: https://kling.is/docs/checkout
|
|
21
|
+
|
|
22
|
+
export async function POST() {
|
|
23
|
+
const res = await fetch(\`\${process.env.KLING_API_URL}/v1/checkout/sessions\`, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: \`Bearer \${process.env.KLING_SECRET_KEY}\`,
|
|
27
|
+
"Content-Type": "application/json",
|
|
28
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify({
|
|
31
|
+
amount: 5000,
|
|
32
|
+
currency: "ISK",
|
|
33
|
+
success_url: \`\${process.env.NEXT_PUBLIC_URL || "http://localhost:3000"}/success\`,
|
|
34
|
+
}),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const session = await res.json();
|
|
38
|
+
return Response.json(session);
|
|
39
|
+
}
|
|
40
|
+
`,
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
path: "app/checkout/page.tsx",
|
|
44
|
+
content: `"use client";
|
|
45
|
+
|
|
46
|
+
// Kling Checkout — client-side overlay
|
|
47
|
+
// 1. Start your dev server
|
|
48
|
+
// 2. Visit /checkout
|
|
49
|
+
// 3. Click the button to open the Kling checkout overlay
|
|
50
|
+
|
|
51
|
+
export default function CheckoutPage() {
|
|
52
|
+
const handleCheckout = async () => {
|
|
53
|
+
// Create a checkout session on the server
|
|
54
|
+
const res = await fetch("/api/checkout", { method: "POST" });
|
|
55
|
+
const session = await res.json();
|
|
56
|
+
|
|
57
|
+
// Open the Kling checkout overlay
|
|
58
|
+
const kling = (window as any).Kling?.init({
|
|
59
|
+
baseUrl: process.env.NEXT_PUBLIC_KLING_API_URL || "${process.env.KLING_API_URL || "https://api.kling.is"}",
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
if (kling) {
|
|
63
|
+
const result = await kling.checkout({ sessionId: session.id });
|
|
64
|
+
if (result.success) {
|
|
65
|
+
window.location.href = "/success";
|
|
66
|
+
}
|
|
67
|
+
} else {
|
|
68
|
+
// Fallback: redirect to hosted checkout
|
|
69
|
+
window.location.href = session.url;
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<div style={{ padding: "2rem", textAlign: "center" }}>
|
|
75
|
+
<h1>Checkout Demo</h1>
|
|
76
|
+
<p>Click below to pay 5,000 ISK</p>
|
|
77
|
+
<button
|
|
78
|
+
onClick={handleCheckout}
|
|
79
|
+
style={{
|
|
80
|
+
padding: "12px 24px",
|
|
81
|
+
fontSize: "16px",
|
|
82
|
+
background: "#A56CFE",
|
|
83
|
+
color: "white",
|
|
84
|
+
border: "2px solid #2E2211",
|
|
85
|
+
borderRadius: "8px",
|
|
86
|
+
cursor: "pointer",
|
|
87
|
+
boxShadow: "2px 2px 0 #2E2211",
|
|
88
|
+
}}
|
|
89
|
+
>
|
|
90
|
+
Pay with Kling
|
|
91
|
+
</button>
|
|
92
|
+
|
|
93
|
+
{/* Load the Kling SDK */}
|
|
94
|
+
<script src="${process.env.KLING_API_URL || "https://api.kling.is"}/v1/kling.js" />
|
|
95
|
+
</div>
|
|
96
|
+
);
|
|
97
|
+
}
|
|
98
|
+
`,
|
|
99
|
+
},
|
|
100
|
+
];
|
|
101
|
+
}
|
|
102
|
+
function serverTemplate(apiKey, framework) {
|
|
103
|
+
if (framework === "elysia") {
|
|
104
|
+
return [
|
|
105
|
+
{
|
|
106
|
+
path: "examples/kling-checkout.ts",
|
|
107
|
+
content: `// Kling Checkout — Elysia server example
|
|
108
|
+
// Run: bun run examples/kling-checkout.ts
|
|
109
|
+
// Then open: http://localhost:4321
|
|
110
|
+
|
|
111
|
+
import { Elysia } from "elysia";
|
|
112
|
+
|
|
113
|
+
const KLING_API_URL = process.env.KLING_API_URL || "https://api.kling.is";
|
|
114
|
+
const KLING_SECRET_KEY = process.env.KLING_SECRET_KEY;
|
|
115
|
+
|
|
116
|
+
const app = new Elysia()
|
|
117
|
+
.post("/api/checkout", async () => {
|
|
118
|
+
const res = await fetch(\`\${KLING_API_URL}/v1/checkout/sessions\`, {
|
|
119
|
+
method: "POST",
|
|
120
|
+
headers: {
|
|
121
|
+
Authorization: \`Bearer \${KLING_SECRET_KEY}\`,
|
|
122
|
+
"Content-Type": "application/json",
|
|
123
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
124
|
+
},
|
|
125
|
+
body: JSON.stringify({
|
|
126
|
+
amount: 5000,
|
|
127
|
+
currency: "ISK",
|
|
128
|
+
success_url: "http://localhost:4321/success",
|
|
129
|
+
}),
|
|
130
|
+
});
|
|
131
|
+
return res.json();
|
|
132
|
+
})
|
|
133
|
+
.get("/", () => {
|
|
134
|
+
return new Response(checkoutHtml(KLING_API_URL), {
|
|
135
|
+
headers: { "Content-Type": "text/html" },
|
|
136
|
+
});
|
|
137
|
+
})
|
|
138
|
+
.get("/success", () => "Payment successful!")
|
|
139
|
+
.listen(4321);
|
|
140
|
+
|
|
141
|
+
console.log("Checkout demo running at http://localhost:4321");
|
|
142
|
+
|
|
143
|
+
function checkoutHtml(apiUrl: string) {
|
|
144
|
+
return \`<!DOCTYPE html>
|
|
145
|
+
<html><head><title>Kling Checkout</title></head>
|
|
146
|
+
<body style="font-family:system-ui;text-align:center;padding:4rem">
|
|
147
|
+
<h1>Checkout Demo</h1>
|
|
148
|
+
<p>Click below to pay 5,000 ISK</p>
|
|
149
|
+
<button onclick="pay()" style="padding:12px 24px;font-size:16px;background:#A56CFE;color:white;border:2px solid #2E2211;border-radius:8px;cursor:pointer;box-shadow:2px 2px 0 #2E2211">
|
|
150
|
+
Pay with Kling
|
|
151
|
+
</button>
|
|
152
|
+
<script src="\${apiUrl}/v1/kling.js"><\\/script>
|
|
153
|
+
<script>
|
|
154
|
+
async function pay() {
|
|
155
|
+
const res = await fetch('/api/checkout', { method: 'POST' });
|
|
156
|
+
const session = await res.json();
|
|
157
|
+
const kling = Kling.init({ baseUrl: '\${apiUrl}' });
|
|
158
|
+
const result = await kling.checkout({ sessionId: session.id });
|
|
159
|
+
if (result.success) window.location.href = '/success';
|
|
160
|
+
}
|
|
161
|
+
</script>
|
|
162
|
+
</body></html>\`;
|
|
163
|
+
}
|
|
164
|
+
`,
|
|
165
|
+
},
|
|
166
|
+
];
|
|
167
|
+
}
|
|
168
|
+
// Express / Hono — generic Node server
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
path: "examples/kling-checkout.ts",
|
|
172
|
+
content: `// Kling Checkout — server example
|
|
173
|
+
// Run: npx tsx examples/kling-checkout.ts (or bun run examples/kling-checkout.ts)
|
|
174
|
+
// Then open: http://localhost:4321
|
|
175
|
+
|
|
176
|
+
const KLING_API_URL = process.env.KLING_API_URL || "https://api.kling.is";
|
|
177
|
+
const KLING_SECRET_KEY = process.env.KLING_SECRET_KEY;
|
|
178
|
+
|
|
179
|
+
const server = Bun.serve({
|
|
180
|
+
port: 4321,
|
|
181
|
+
async fetch(req) {
|
|
182
|
+
const url = new URL(req.url);
|
|
183
|
+
|
|
184
|
+
if (url.pathname === "/api/checkout" && req.method === "POST") {
|
|
185
|
+
const res = await fetch(\`\${KLING_API_URL}/v1/checkout/sessions\`, {
|
|
186
|
+
method: "POST",
|
|
187
|
+
headers: {
|
|
188
|
+
Authorization: \`Bearer \${KLING_SECRET_KEY}\`,
|
|
189
|
+
"Content-Type": "application/json",
|
|
190
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
191
|
+
},
|
|
192
|
+
body: JSON.stringify({
|
|
193
|
+
amount: 5000,
|
|
194
|
+
currency: "ISK",
|
|
195
|
+
success_url: "http://localhost:4321/success",
|
|
196
|
+
}),
|
|
197
|
+
});
|
|
198
|
+
return new Response(await res.text(), {
|
|
199
|
+
headers: { "Content-Type": "application/json" },
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (url.pathname === "/success") {
|
|
204
|
+
return new Response("Payment successful!", {
|
|
205
|
+
headers: { "Content-Type": "text/plain" },
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return new Response(\`<!DOCTYPE html>
|
|
210
|
+
<html><head><title>Kling Checkout</title></head>
|
|
211
|
+
<body style="font-family:system-ui;text-align:center;padding:4rem">
|
|
212
|
+
<h1>Checkout Demo</h1>
|
|
213
|
+
<p>Click below to pay 5,000 ISK</p>
|
|
214
|
+
<button onclick="pay()" style="padding:12px 24px;font-size:16px;background:#A56CFE;color:white;border:2px solid #2E2211;border-radius:8px;cursor:pointer;box-shadow:2px 2px 0 #2E2211">
|
|
215
|
+
Pay with Kling
|
|
216
|
+
</button>
|
|
217
|
+
<script src="\${KLING_API_URL}/v1/kling.js"></script>
|
|
218
|
+
<script>
|
|
219
|
+
async function pay() {
|
|
220
|
+
const res = await fetch('/api/checkout', { method: 'POST' });
|
|
221
|
+
const session = await res.json();
|
|
222
|
+
const kling = Kling.init({ baseUrl: '\${KLING_API_URL}' });
|
|
223
|
+
const result = await kling.checkout({ sessionId: session.id });
|
|
224
|
+
if (result.success) window.location.href = '/success';
|
|
225
|
+
}
|
|
226
|
+
</script>
|
|
227
|
+
</body></html>\`, { headers: { "Content-Type": "text/html" } });
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
console.log("Checkout demo running at http://localhost:4321");
|
|
232
|
+
`,
|
|
233
|
+
},
|
|
234
|
+
];
|
|
235
|
+
}
|
|
236
|
+
function astroTemplate(apiKey) {
|
|
237
|
+
return [
|
|
238
|
+
{
|
|
239
|
+
path: "src/pages/api/checkout.ts",
|
|
240
|
+
content: `// Kling Checkout — Astro API route
|
|
241
|
+
// Docs: https://kling.is/docs/checkout
|
|
242
|
+
|
|
243
|
+
import type { APIRoute } from "astro";
|
|
244
|
+
|
|
245
|
+
export const POST: APIRoute = async () => {
|
|
246
|
+
const res = await fetch(\`\${import.meta.env.KLING_API_URL}/v1/checkout/sessions\`, {
|
|
247
|
+
method: "POST",
|
|
248
|
+
headers: {
|
|
249
|
+
Authorization: \`Bearer \${import.meta.env.KLING_SECRET_KEY}\`,
|
|
250
|
+
"Content-Type": "application/json",
|
|
251
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
252
|
+
},
|
|
253
|
+
body: JSON.stringify({
|
|
254
|
+
amount: 5000,
|
|
255
|
+
currency: "ISK",
|
|
256
|
+
success_url: \`\${import.meta.env.SITE || "http://localhost:4321"}/success\`,
|
|
257
|
+
}),
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
const session = await res.json();
|
|
261
|
+
return new Response(JSON.stringify(session), {
|
|
262
|
+
headers: { "Content-Type": "application/json" },
|
|
263
|
+
});
|
|
264
|
+
};
|
|
265
|
+
`,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
}
|
|
269
|
+
function genericTemplate(apiKey) {
|
|
270
|
+
return [
|
|
271
|
+
{
|
|
272
|
+
path: "examples/kling-checkout.ts",
|
|
273
|
+
content: `// Kling Checkout — quick test
|
|
274
|
+
// Run: bun run examples/kling-checkout.ts (or npx tsx examples/kling-checkout.ts)
|
|
275
|
+
//
|
|
276
|
+
// This creates a checkout session and prints the payment URL.
|
|
277
|
+
|
|
278
|
+
const KLING_API_URL = process.env.KLING_API_URL || "https://api.kling.is";
|
|
279
|
+
const KLING_SECRET_KEY = process.env.KLING_SECRET_KEY;
|
|
280
|
+
|
|
281
|
+
if (!KLING_SECRET_KEY) {
|
|
282
|
+
console.error("Missing KLING_SECRET_KEY — run 'kling init' first or set it in .env.local");
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const res = await fetch(\`\${KLING_API_URL}/v1/checkout/sessions\`, {
|
|
287
|
+
method: "POST",
|
|
288
|
+
headers: {
|
|
289
|
+
Authorization: \`Bearer \${KLING_SECRET_KEY}\`,
|
|
290
|
+
"Content-Type": "application/json",
|
|
291
|
+
"Idempotency-Key": crypto.randomUUID(),
|
|
292
|
+
},
|
|
293
|
+
body: JSON.stringify({
|
|
294
|
+
amount: 5000,
|
|
295
|
+
currency: "ISK",
|
|
296
|
+
success_url: "https://example.com/success",
|
|
297
|
+
}),
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
const session = await res.json();
|
|
301
|
+
|
|
302
|
+
console.log("\\nCheckout session created!");
|
|
303
|
+
console.log(\` ID: \${session.id}\`);
|
|
304
|
+
console.log(\` URL: \${session.url}\`);
|
|
305
|
+
console.log(\`\\nOpen the URL above to complete the payment.\\n\`);
|
|
306
|
+
`,
|
|
307
|
+
},
|
|
308
|
+
];
|
|
309
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@klingis/cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Kling payments CLI — set up your integration from the terminal",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"kling": "dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"typecheck": "tsc --noEmit"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"commander": "^13.1.0",
|
|
25
|
+
"chalk": "^5.4.1",
|
|
26
|
+
"ora": "^8.2.0",
|
|
27
|
+
"open": "^10.1.0",
|
|
28
|
+
"prompts": "^2.4.2"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/prompts": "^2.4.9",
|
|
32
|
+
"typescript": "^5.9.3"
|
|
33
|
+
}
|
|
34
|
+
}
|