#!/usr/bin/env node /** * First-time bootstrap: create Store "OpsXShell" and one-time Pro product, * then write IDs into .env (never logs the private key). */ import { createRequire } from 'node:module' import { existsSync, readFileSync, writeFileSync } from 'node:fs' import { dirname, join } from 'node:path' import { fileURLToPath } from 'node:url' const __dirname = dirname(fileURLToPath(import.meta.url)) const ROOT = join(__dirname, '..') const ENV_PATH = join(ROOT, '.env') function loadEnv() { if (!existsSync(ENV_PATH)) { console.error('缺少 .env — 请先 cp .env.example .env 并填入 WAFFO_PRIVATE_KEY') process.exit(1) } const raw = readFileSync(ENV_PATH, 'utf8') for (const line of raw.split('\n')) { const trimmed = line.trim() if (!trimmed || trimmed.startsWith('#')) continue const eq = trimmed.indexOf('=') if (eq <= 0) continue const key = trimmed.slice(0, eq).trim() let value = trimmed.slice(eq + 1).trim() if ( (value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'")) ) { value = value.slice(1, -1) } if (!(key in process.env)) process.env[key] = value } } function upsertEnv(key, value) { let raw = existsSync(ENV_PATH) ? readFileSync(ENV_PATH, 'utf8') : '' const line = `${key}=${value}` const re = new RegExp(`^${key}=.*$`, 'm') if (re.test(raw)) raw = raw.replace(re, line) else raw = `${raw.trimEnd()}\n${line}\n` writeFileSync(ENV_PATH, raw) } loadEnv() const merchantId = process.env.WAFFO_MERCHANT_ID const privateKey = process.env.WAFFO_PRIVATE_KEY if (!merchantId || !privateKey) { console.error('需要 WAFFO_MERCHANT_ID 与 WAFFO_PRIVATE_KEY') process.exit(1) } const require = createRequire(import.meta.url) const { WaffoPancake } = require('@waffo/pancake-ts') const client = new WaffoPancake({ merchantId, privateKey, ...(process.env.WAFFO_ENVIRONMENT ? { environment: process.env.WAFFO_ENVIRONMENT } : {}), }) async function main() { let storeId = process.env.WAFFO_STORE_ID if (!storeId) { console.log('Creating store OpsXShell…') const store = await client.stores.create({ name: 'OpsXShell', slug: `opsxshell-${Date.now().toString(36)}`, }) storeId = store.id || store.storeId if (!storeId) throw new Error(`stores.create 未返回 id: ${JSON.stringify(store)}`) upsertEnv('WAFFO_STORE_ID', storeId) console.log('Store:', storeId) } else { console.log('Using existing store:', storeId) } if (process.env.WAFFO_PRODUCT_ID) { console.log(`Using WAFFO_PRODUCT_ID=${process.env.WAFFO_PRODUCT_ID}`) console.log('\nBootstrap complete.') return } const price = process.env.WAFFO_PRODUCT_PRICE || '29.00' console.log(`Creating one-time product OpsXShell Pro (USD ${price})…`) const { product } = await client.onetimeProducts.create({ storeId, name: 'OpsXShell Pro', description: 'One-time unlock for OpsXShell professional features', prices: { USD: { amount: price, taxCategory: 'software' }, }, successUrl: process.env.SUCCESS_URL || 'opsxshell://pro/activated', metadata: { app: 'opsxshell', license: 'lifetime' }, }) const id = product?.id if (!id) throw new Error(`create onetime product failed: ${JSON.stringify(product)}`) if (client.onetimeProducts.publish) { await client.onetimeProducts.publish({ id }).catch(() => undefined) } upsertEnv('WAFFO_PRODUCT_ID', id) console.log('WAFFO_PRODUCT_ID', id) console.log('\nBootstrap complete. Product ID written to .env') console.log('Next: npm start') } main().catch((error) => { console.error(error) process.exit(1) })