Files
2026-07-13 12:30:28 +08:00

275 lines
8.2 KiB
TypeScript

import path from 'path';
import chalk from 'chalk';
import inquirer from 'inquirer';
import {
checkDomainAvailable,
bindPinmeDomain,
bindDnsDomainV4,
getRootDomain,
getWalletBalance,
} from './utils/pinmeApi';
import { printCliError, printRechargeUrl } from './utils/cliError';
import { getWalletRechargeUrl } from './utils/config';
import { getAuthConfig } from './utils/webLogin';
import {
isDnsDomain,
normalizeDomain,
validateDnsDomain,
} from './utils/domainValidator';
import { uploadPath } from './services/uploadService';
import tracker, {
getPathKind,
getTrackErrorReason,
} from './utils/tracker';
import {
TRACK_EVENTS,
TRACK_PAGES,
resolveTrackAction,
} from './utils/trackerEvents';
interface Args {
domain?: string;
targetPath?: string;
dns?: boolean;
}
function parseArgs(): Args {
// Usage: pinme bind <path> --domain <name> [--dns]
const args = process.argv.slice(2);
const res: Args = {};
const idx = args.indexOf('bind');
if (idx >= 0) {
const maybePath = args[idx + 1];
if (maybePath && !maybePath.startsWith('-')) res.targetPath = maybePath;
}
const dIdx = args.findIndex((a) => a === '--domain' || a === '-d');
if (dIdx >= 0 && args[dIdx + 1]) {
res.domain = args[dIdx + 1];
}
const dnsIdx = args.findIndex((a) => a === '--dns' || a === '-D');
if (dnsIdx >= 0) {
res.dns = true;
}
return res;
}
async function checkWalletBalanceStatus(authConfig: {
address: string;
token: string;
}): Promise<boolean> {
console.log(chalk.blue('Checking wallet balance...'));
try {
const balanceResult = await getWalletBalance(
authConfig.address,
authConfig.token,
);
const balance = Number(balanceResult.data?.wallet_balance_usd ?? 0);
if (!Number.isFinite(balance) || balance <= 0) {
return false;
}
console.log(
chalk.green(`Wallet balance available: $${balance.toFixed(2)}`),
);
return true;
} catch (e: any) {
if (e.message === 'Token expired' || e?.name === 'CliError') {
throw e;
}
console.log(chalk.yellow('Failed to check wallet balance, continuing...'));
return true;
}
}
export default async function bindCmd(): Promise<void> {
try {
let { domain, targetPath, dns } = parseArgs();
// Check auth
const authConfig = getAuthConfig();
if (!authConfig) {
console.log(
chalk.red('Please login first. Run: pinme set-appkey <AppKey>'),
);
return;
}
if (!targetPath) {
const ans = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter the path to upload and bind: ',
},
]);
targetPath = ans.path;
}
if (!domain) {
const ans = await inquirer.prompt([
{
type: 'input',
name: 'domain',
message: 'Enter the domain to bind (e.g., my-site or example.com): ',
},
]);
domain = ans.domain?.trim();
}
if (!targetPath || !domain) {
console.log(
chalk.red('Missing parameters. Path and domain are required.'),
);
return;
}
// Auto-detect domain type if not explicitly specified
const isDns = dns || isDnsDomain(domain);
const displayDomain = normalizeDomain(domain);
const pathKind = getPathKind(path.resolve(targetPath));
const domainType = isDns ? 'dns' : 'pinme_subdomain';
// Validate DNS domain format
if (isDns) {
const validation = validateDnsDomain(domain);
if (!validation.valid) {
console.log(chalk.red(validation.message!));
return;
}
}
// Domain binding now uses wallet balance instead of VIP.
try {
const hasWalletBalance = await checkWalletBalanceStatus(authConfig);
if (!hasWalletBalance) {
console.log(
chalk.red(
'Insufficient wallet balance. Please recharge your wallet first.',
),
);
printRechargeUrl(getWalletRechargeUrl());
return;
}
} catch (e: any) {
if (e.message === 'Token expired') {
return; // Token expired hint already shown in API
}
throw e;
}
// Pre-check domain availability
try {
const check = await checkDomainAvailable(displayDomain);
if (!check.is_valid) {
console.log(
chalk.red(`Domain not available: ${check.error || 'unknown reason'}`),
);
return;
}
console.log(chalk.green(`Domain available: ${displayDomain}`));
} catch (e: any) {
if (e.message === 'Token expired') {
return; // Token expired hint already shown in API
}
throw e;
}
// Upload
const absolutePath = path.resolve(targetPath);
console.log(chalk.blue(`Uploading: ${absolutePath}`));
const up = await uploadPath(absolutePath, {
action: 'bind',
uid: authConfig.address,
});
if (!up?.contentHash) {
void tracker.trackEvent(TRACK_EVENTS.uploadFailed, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadFailed),
path_kind: pathKind,
has_domain: true,
reason: 'no_content_hash',
});
console.log(chalk.red('Upload failed, binding aborted.'));
return;
}
void tracker.trackEvent(TRACK_EVENTS.uploadSuccess, TRACK_PAGES.upload, {
a: resolveTrackAction(TRACK_EVENTS.uploadSuccess),
path_kind: pathKind,
has_domain: true,
});
console.log(chalk.green(`Upload success, CID: ${up.contentHash}`));
// Bind domain
try {
if (isDns) {
// DNS domain binding
console.log(chalk.blue('Binding DNS domain...'));
const dnsResult = await bindDnsDomainV4(
displayDomain,
up.contentHash,
authConfig.address,
authConfig.token,
);
if (dnsResult.code !== 200) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
reason: dnsResult.msg || 'dns_bind_failed',
});
console.log(chalk.red(`DNS binding failed: ${dnsResult.msg}`));
return;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
});
console.log(chalk.green(`DNS bind success: ${displayDomain}`));
console.log(chalk.white(`Visit: https://${displayDomain}`));
console.log(
chalk.cyan(
'\n📚 DNS Setup Guide: https://pinme.eth.limo/#/docs?id=custom-domain',
),
);
} else {
// Pinme subdomain binding
console.log(chalk.blue('Binding Pinme subdomain...'));
const ok = await bindPinmeDomain(displayDomain, up.contentHash);
if (!ok) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
reason: 'pinme_bind_failed',
});
console.log(chalk.red('Binding failed. Please try again later.'));
return;
}
void tracker.trackEvent(TRACK_EVENTS.domainBindSuccess, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindSuccess),
domain_type: domainType,
domain_name: displayDomain,
bind_source: 'bind',
});
console.log(chalk.green(`Bind success: ${displayDomain}`));
const rootDomain = await getRootDomain();
console.log(
chalk.white(`Visit: https://${displayDomain}.${rootDomain}`),
);
}
} catch (e: any) {
if (e.message === 'Token expired') {
return; // Token expired hint already shown in API
}
throw e;
}
} catch (e: any) {
void tracker.trackEvent(TRACK_EVENTS.domainBindFailed, TRACK_PAGES.domain, {
a: resolveTrackAction(TRACK_EVENTS.domainBindFailed),
bind_source: 'bind',
reason: getTrackErrorReason(e),
});
printCliError(e, 'Bind failed.');
}
}