permamind
Version:
An MCP server that provides an immortal memory layer for AI agents and clients
322 lines (321 loc) • 13.3 kB
JavaScript
import { exec } from "child_process";
import { constants } from "fs";
import { access, readFile } from "fs/promises";
import { promisify } from "util";
import { getKeyFromMnemonic } from "../mnemonic.js";
const execAsync = promisify(exec);
export class PermawebDeployService {
constructor() { }
async checkPrerequisites(params) {
const checks = [];
// Check CLI installation
const cliCheck = await this.checkCliInstallation();
checks.push(cliCheck);
// Check directory
const dirCheck = await this.checkDirectory(params.directoryPath);
checks.push(dirCheck);
// Check wallet setup
const walletCheck = await this.checkWalletSetup(params);
checks.push(walletCheck);
const allPassed = checks.every((check) => check.status === "pass");
if (!allPassed) {
const failedChecks = checks.filter((check) => check.status === "fail");
const primaryFailure = failedChecks[0];
return {
allPassed: false,
checks,
error: {
code: `PREREQUISITE_${primaryFailure.name.toUpperCase().replace(/\s+/g, "_")}_FAILED`,
message: `Prerequisites not met: ${primaryFailure.message}`,
solutions: primaryFailure.solutions || [],
},
};
}
return {
allPassed: true,
checks,
};
}
async deployDirectory(params) {
try {
// Check prerequisites first
const prereqResult = await this.checkPrerequisites(params);
if (!prereqResult.allPassed) {
return {
error: prereqResult.error,
success: false,
};
}
// Setup wallet
const walletData = await this.setupWallet(params);
// Set DEPLOY_KEY environment variable
process.env.DEPLOY_KEY = walletData;
// Execute permaweb-deploy CLI
const result = await this.executeDeployment(params);
return {
arnsUrl: this.constructArnsUrl(params.arnsName, params.undername),
success: true,
transactionId: result.transactionId,
};
}
catch (error) {
return {
error: {
code: "DEPLOYMENT_FAILED",
details: error,
message: error instanceof Error ? error.message : "Unknown error occurred",
},
success: false,
};
}
}
async checkCliInstallation() {
try {
await execAsync("permaweb-deploy --version");
return {
message: "permaweb-deploy CLI is installed and accessible",
name: "CLI Installation",
status: "pass",
};
}
catch {
return {
message: "permaweb-deploy CLI is not installed or not in PATH",
name: "CLI Installation",
solutions: [
"Install permaweb-deploy globally: npm install -g permaweb-deploy",
"Or install locally in your project: npm install permaweb-deploy",
"Verify installation: permaweb-deploy --version",
"Make sure npm global bin directory is in your PATH",
"Restart your terminal after installation",
],
status: "fail",
};
}
}
async checkDirectory(directoryPath) {
try {
await access(directoryPath, constants.F_OK);
await access(directoryPath, constants.R_OK);
// Check if directory contains files
const { stdout } = await execAsync(`find "${directoryPath}" -type f | head -1`);
if (!stdout.trim()) {
return {
message: "Directory exists but appears to be empty",
name: "Directory Content",
solutions: [
"Ensure your build process has completed successfully",
"Check that files are in the correct directory",
"Common build directories: ./dist, ./build, ./public, ./out",
],
status: "warning",
};
}
return {
message: `Directory ${directoryPath} exists and contains files`,
name: "Directory",
status: "pass",
};
}
catch {
return {
message: `Directory ${directoryPath} not found or not readable`,
name: "Directory",
solutions: [
`Create the directory: mkdir -p "${directoryPath}"`,
"Run your build process to generate the deployment files",
"Ensure the correct path is specified",
"Common build directories: ./dist, ./build, ./public, ./out",
"Check directory permissions",
],
status: "fail",
};
}
}
async checkWalletSetup(params) {
try {
if (params.walletPath) {
// Check custom wallet file
await access(params.walletPath, constants.F_OK);
const walletJson = await readFile(params.walletPath, "utf8");
const walletData = JSON.parse(walletJson);
if (!walletData.kty || !walletData.n || !walletData.d) {
return {
message: "Wallet file exists but is not a valid Arweave wallet",
name: "Wallet File",
solutions: [
"Ensure the wallet file is a valid Arweave JWK (JSON Web Key)",
"The wallet should contain required fields: kty, n, d, e, p, q, dp, dq, qi",
"Export your wallet from ArConnect, Arweave.app, or generate a new one",
"Check the file is not corrupted",
],
status: "fail",
};
}
return {
message: `Custom wallet file ${params.walletPath} is valid`,
name: "Wallet File",
status: "pass",
};
}
else {
// Check SEED_PHRASE
const seedPhrase = process.env.SEED_PHRASE;
if (!seedPhrase) {
return {
message: "SEED_PHRASE environment variable not found",
name: "Wallet Setup",
solutions: [
"Set SEED_PHRASE environment variable with your 12-word mnemonic",
"Example: export SEED_PHRASE='word1 word2 word3 ... word12'",
"Or create a .env file with SEED_PHRASE=your_mnemonic_here",
"Alternatively, use --wallet-path flag to specify a wallet file",
"Generate a new mnemonic if you don't have one",
],
status: "fail",
};
}
// Validate mnemonic format
const words = seedPhrase.trim().split(/\s+/);
if (words.length !== 12) {
return {
message: "SEED_PHRASE must be exactly 12 words",
name: "Wallet Setup",
solutions: [
"Ensure your seed phrase contains exactly 12 words",
"Check for extra spaces or missing words",
"Verify the seed phrase is valid BIP39 mnemonic",
"Example format: 'word1 word2 word3 word4 word5 word6 word7 word8 word9 word10 word11 word12'",
],
status: "fail",
};
}
return {
message: "SEED_PHRASE is configured and appears valid",
name: "Wallet Setup",
status: "pass",
};
}
}
catch {
if (params.walletPath) {
return {
message: `Cannot access wallet file: ${params.walletPath}`,
name: "Wallet File",
solutions: [
`Ensure the file exists: ${params.walletPath}`,
"Check file permissions (should be readable)",
"Verify the file path is correct",
"Use absolute path if relative path is not working",
],
status: "fail",
};
}
else {
return {
message: "Failed to validate wallet configuration",
name: "Wallet Setup",
solutions: [
"Check SEED_PHRASE environment variable is set correctly",
"Ensure the seed phrase is a valid 12-word mnemonic",
"Try using a custom wallet file instead",
"Restart your terminal/application to reload environment variables",
],
status: "fail",
};
}
}
}
constructArnsUrl(arnsName, undername) {
const baseUrl = undername ? `${undername}_${arnsName}` : arnsName;
return `https://${baseUrl}.arweave.net`;
}
async executeDeployment(params) {
const { arnsName, directoryPath, network = "mainnet", undername } = params;
// Build permaweb-deploy command
let command = `permaweb-deploy --deploy-folder "${directoryPath}" --arns-name "${arnsName}"`;
if (undername) {
command += ` --undername "${undername}"`;
}
if (network === "testnet") {
command += ` --ario-process testnet`;
}
try {
const { stderr, stdout } = await execAsync(command);
if (stderr) {
console.warn("Deployment warnings:", stderr);
}
// Parse output to extract transaction ID
const transactionId = this.parseTransactionId(stdout);
return { transactionId };
}
catch (error) {
throw new Error(`Deployment failed: ${error}`);
}
}
async generateWalletFromSeed() {
const seedPhrase = process.env.SEED_PHRASE;
if (!seedPhrase) {
throw new Error("SEED_PHRASE environment variable not found");
}
try {
// Generate deterministic wallet from seed phrase using existing utility
const wallet = await getKeyFromMnemonic(seedPhrase);
// Convert wallet to JSON string
const walletJson = JSON.stringify(wallet);
// Base64 encode the raw JSON
return Buffer.from(walletJson).toString("base64");
}
catch (error) {
throw new Error(`Failed to generate wallet from seed: ${error}`);
}
}
async loadWalletFromFile(walletPath) {
try {
const walletJson = await readFile(walletPath, "utf8");
// Validate JSON format
const walletData = JSON.parse(walletJson);
// Validate it's a proper Arweave wallet
if (!walletData.kty || !walletData.n || !walletData.d) {
throw new Error("Invalid Arweave wallet format");
}
// Base64 encode the raw JSON
return Buffer.from(walletJson).toString("base64");
}
catch (error) {
throw new Error(`Failed to load wallet from ${walletPath}: ${error}`);
}
}
parseTransactionId(output) {
// Common patterns for transaction IDs in permaweb-deploy output
const txIdMatch = output.match(/(?:Transaction ID|txId|tx):\s*([a-zA-Z0-9_-]{43})/i);
if (txIdMatch) {
return txIdMatch[1];
}
// Alternative pattern
const altMatch = output.match(/([a-zA-Z0-9_-]{43})/);
if (altMatch) {
return altMatch[1];
}
throw new Error("Could not parse transaction ID from deployment output");
}
async setupWallet(params) {
if (params.walletPath) {
// Use custom wallet from file
return await this.loadWalletFromFile(params.walletPath);
}
else {
// Generate wallet from SEED_PHRASE
return await this.generateWalletFromSeed();
}
}
async validateDirectory(directoryPath) {
try {
await access(directoryPath, constants.F_OK);
await access(directoryPath, constants.R_OK);
}
catch {
throw new Error(`Directory not found or not readable: ${directoryPath}`);
}
}
}